@reusable-ui/groupable
Version:
Groups a list of UIs into a single UI.
320 lines (314 loc) • 16.6 kB
JavaScript
// cssfn:
import {
// writes css in javascript:
rule, children, nextSiblings, style, vars, iif, cssVars,
// writes complex stylesheets in simpler way:
memoizeStyle, } from '@cssfn/core'; // writes css in javascript
// reusable-ui features:
import {
// hooks:
usesBorder, } from '@reusable-ui/border'; // border (stroke) stuff of UI
import {
// hooks:
usesPadding, } from '@reusable-ui/padding'; // padding (inner spacing) stuff of UI
// reusable-ui variants:
import { defaultBlockOrientationableOptions, usesOrientationable, } from '@reusable-ui/orientationable'; // a capability of UI to rotate its layout
// defaults:
export const defaultOrientationableOptions = defaultBlockOrientationableOptions;
const [groupableVars] = cssVars({ prefix: 'gr', minify: false }); // shared variables: ensures the server-side & client-side have the same generated css variable names
// rules:
// :where(...) => zero specificity => easy to overwrite further:
export const ifVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign))', styles);
export const ifFirstVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign)):where(:first-child, .first-visible-child)', styles);
export const ifLastVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign)):where( :last-child, .last-visible-child)', styles);
export const ifNotFirstVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign, :first-child, .first-visible-child))', styles);
export const ifNotLastVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign, :last-child, .last-visible-child))', styles);
export const ifSecondVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign)):where(:nth-child(2))', styles);
export const ifNotSecondVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign, :nth-child(2)))', styles);
const groupableChildrenRuleOfOrientationInline = memoizeStyle(() => {
// dependencies:
// features:
const { borderVars } = usesBorder();
return style({
...ifFirstVisibleChild({
...vars({
/*
if the_current_element is a_child_of_container and also a_separator,
the deleted `groupableVars.borderWidth` in separator must be pointed to container,
so we can calculate the correct inner_borderRadius.
that's why we set `!important` to the `groupableVars.borderWidth`.
*/
[groupableVars.borderWidth]: ['inherit', '!important'],
/*
the two props below using `!important` to overwrite `groupableRule()`,
if the_current_element is a_child_of_container and also a_separator.
*/
[groupableVars.borderStartStartRadius]: ['inherit', '!important'],
[groupableVars.borderEndStartRadius]: ['inherit', '!important'], // reads parent's prop
}),
...style({
// borders:
// add rounded corners on left:
/*
the two props below using `!important` to overwrite `borderRule()`,
if the_current_element defines `...borderRule()`.
*/
[borderVars.borderStartStartRadius]: [`calc(${groupableVars.borderStartStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
[borderVars.borderEndStartRadius]: [`calc(${groupableVars.borderEndStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
/* recursive calculation of borderRadius is not supported yet */
}),
}),
...ifLastVisibleChild({
...vars({
/*
if the_current_element is a_child_of_container and also a_separator,
the deleted `groupableVars.borderWidth` in separator must be pointed to container,
so we can calculate the correct inner_borderRadius.
that's why we set `!important` to the `groupableVars.borderWidth`.
*/
[groupableVars.borderWidth]: ['inherit', '!important'],
/*
the two props below using `!important` to overwrite `groupableRule()`,
if the_current_element is a_child_of_container and also a_separator.
*/
[groupableVars.borderStartEndRadius]: ['inherit', '!important'],
[groupableVars.borderEndEndRadius]: ['inherit', '!important'], // reads parent's prop
}),
...style({
// borders:
// add rounded corners on right:
/*
the two props below using `!important` to overwrite `borderRule()`,
if the_current_element defines `...borderRule()`.
*/
[borderVars.borderStartEndRadius]: [`calc(${groupableVars.borderStartEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
[borderVars.borderEndEndRadius]: [`calc(${groupableVars.borderEndEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
/* recursive calculation of borderRadius is not supported yet */
}),
}),
});
});
const groupableChildrenRuleOfOrientationBlock = memoizeStyle(() => {
// dependencies:
// features:
const { borderVars } = usesBorder();
return style({
...ifFirstVisibleChild({
...vars({
/*
if the_current_element is a_child_of_container and also a_separator,
the deleted `groupableVars.borderWidth` in separator must be pointed to container,
so we can calculate the correct inner_borderRadius.
that's why we set `!important` to the `groupableVars.borderWidth`.
*/
[groupableVars.borderWidth]: ['inherit', '!important'],
/*
the two props below using `!important` to overwrite `groupableRule()`,
if the_current_element is a_child_of_container and also a_separator.
*/
[groupableVars.borderStartStartRadius]: ['inherit', '!important'],
[groupableVars.borderStartEndRadius]: ['inherit', '!important'], // reads parent's prop
}),
...style({
// borders:
// add rounded corners on top:
/*
the two props below using `!important` to overwrite `borderRule()`,
if the_current_element defines `...borderRule()`.
*/
[borderVars.borderStartStartRadius]: [`calc(${groupableVars.borderStartStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
[borderVars.borderStartEndRadius]: [`calc(${groupableVars.borderStartEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
/* recursive calculation of borderRadius is not supported yet */
}),
}),
...ifLastVisibleChild({
...vars({
/*
if the_current_element is a_child_of_container and also a_separator,
the deleted `groupableVars.borderWidth` in separator must be pointed to container,
so we can calculate the correct inner_borderRadius.
that's why we set `!important` to the `groupableVars.borderWidth`.
*/
[groupableVars.borderWidth]: ['inherit', '!important'],
/*
the two props below using `!important` to overwrite `groupableRule()`,
if the_current_element is a_child_of_container and also a_separator.
*/
[groupableVars.borderEndStartRadius]: ['inherit', '!important'],
[groupableVars.borderEndEndRadius]: ['inherit', '!important'], // reads parent's prop
}),
...style({
// borders:
// add rounded corners on bottom:
/*
the two props below using `!important` to overwrite `borderRule()`,
if the_current_element defines `...borderRule()`.
*/
[borderVars.borderEndStartRadius]: [`calc(${groupableVars.borderEndStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
[borderVars.borderEndEndRadius]: [`calc(${groupableVars.borderEndEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'],
/* recursive calculation of borderRadius is not supported yet */
}),
}),
});
});
/**
* Groups a list of UIs into a single UI.
* @param options Options of `groupableRule`.
* @returns A `GroupableStuff` represents the group rules.
*/
export const usesGroupable = (options) => {
// options:
const orientationableStuff = usesOrientationable(options, defaultOrientationableOptions);
const { ifOrientationInline, ifOrientationBlock } = orientationableStuff;
options = orientationableStuff;
const { itemsSelector = '*', } = options;
// dependencies:
// features:
const { borderVars } = usesBorder();
const { paddingVars } = usesPadding();
return {
groupableRule: () => style({
...vars({
// borders:
[groupableVars.borderWidth]: borderVars.borderWidth,
[groupableVars.borderStartStartRadius]: borderVars.borderStartStartRadius,
[groupableVars.borderStartEndRadius]: borderVars.borderStartEndRadius,
[groupableVars.borderEndStartRadius]: borderVars.borderEndStartRadius,
[groupableVars.borderEndEndRadius]: borderVars.borderEndEndRadius,
// spacings:
[groupableVars.paddingInline]: paddingVars.paddingInline,
[groupableVars.paddingBlock]: paddingVars.paddingBlock,
}),
// ...style({
// // borders:
// overflow : 'hidden', // clip the children at the rounded corners // bad idea, causing child's focus boxShadow to be clipped off
// }),
...ifOrientationInline(() => // inline
children(itemsSelector, groupableChildrenRuleOfOrientationInline)),
...ifOrientationBlock(() => // block
children(itemsSelector, groupableChildrenRuleOfOrientationBlock)),
}),
separatorRule: () => style({
...ifOrientationInline(() => // inline
((options === undefined)
||
((options.itemsSelector === '*')
&&
(options.swapFirstItem === false)))
?
getDefaultSeparatorRuleInline()
:
createSeparatorRuleInline(options)),
...ifOrientationBlock(() => // block
((options === undefined)
||
((options.itemsSelector === '*')
&&
(options.swapFirstItem === false)))
?
getDefaultSeparatorRuleBlock()
:
createSeparatorRuleBlock(options)),
}),
groupableVars,
};
};
const createSeparatorRuleOf = (block, options) => {
// options:
const { itemsSelector = '*', swapFirstItem = false, } = options ?? {};
// dependencies:
// features:
const { borderVars } = usesBorder();
return style({
...vars({
/*
if the_current_element is a container,
the `groupableVars.borderWidth` will be deleted (not follow `borderWidth` again),
because the_current_element becomes a separator.
use `0px` instead of 0,
because the value will be calculated in `calc()` expression.
*/
[groupableVars.borderWidth]: '0px', // remove border
}),
...style({
// borders:
boxShadow: null, // remove shadow
}),
// removes unecessary border stroke:
...style({
// borders:
[`border${block ? 'Inline' : 'Block'}Width`]: 0,
// remove top-border at the first-child, so that it wouldn't collide with the container's top-border
...ifFirstVisibleChild({
// borders:
[`border${block ? 'Block' : 'Inline'}StartWidth`]: 0, // remove top-border
}),
// remove bottom-border at the last-child, so that it wouldn't collide with the container's bottom-border
// *note : *special case* => move the first separator to the second child
...ifLastVisibleChild({
// borders:
[`border${block ? 'Block' : 'Inline'}EndWidth`]: 0, // remove bottom-border
}),
...iif(swapFirstItem, ifFirstVisibleChild({
// borders:
[`border${block ? 'Block' : 'Inline'}EndWidth`]: 0, // remove bottom-border
})),
// children:
// remove double border by removing top-border at the subsequent sibling(s)
...nextSiblings(itemsSelector, {
...ifVisibleChild({
...iif(!swapFirstItem, style({
// borders:
[`border${block ? 'Block' : 'Inline'}StartWidth`]: 0, // remove top-border
})),
...iif(swapFirstItem, ifNotSecondVisibleChild({
// borders:
[`border${block ? 'Block' : 'Inline'}StartWidth`]: 0, // remove top-border
})),
}),
}),
}),
// removes unecessary border radius:
// although the border stroke was/not removed, it *affects* the children's border radius
// do not remove border radius at the parent's corners (:first-child & :last-child)
...ifNotFirstVisibleChild({
...(block
?
style({
// borders:
// remove rounded corners on top:
[borderVars.borderStartStartRadius]: '0px',
[borderVars.borderStartEndRadius]: '0px',
})
:
style({
// borders:
// remove rounded corners on left:
[borderVars.borderStartStartRadius]: '0px',
[borderVars.borderEndStartRadius]: '0px',
})),
}),
...ifNotLastVisibleChild({
...(block
?
style({
// borders:
// remove rounded corners on bottom:
[borderVars.borderEndStartRadius]: '0px',
[borderVars.borderEndEndRadius]: '0px',
})
:
style({
// borders:
// remove rounded corners on right:
[borderVars.borderStartEndRadius]: '0px',
[borderVars.borderEndEndRadius]: '0px',
})),
}),
});
};
const createSeparatorRuleInline = (options) => createSeparatorRuleOf(false, options);
const createSeparatorRuleBlock = (options) => createSeparatorRuleOf(true, options);
const getDefaultSeparatorRuleInline = memoizeStyle(() => createSeparatorRuleInline());
const getDefaultSeparatorRuleBlock = memoizeStyle(() => createSeparatorRuleBlock());
//#endregion groupable