@reusable-ui/groupable
Version:
Groups a list of UIs into a single UI.
459 lines (376 loc) • 19.5 kB
text/typescript
// cssfn:
import {
// cssfn general types:
Factory,
// cssfn css specific types:
CssRule,
CssStyleCollection,
CssSelectorCollection,
// writes css in javascript:
rule,
children,
nextSiblings,
style,
vars,
iif,
// strongly typed of css variables:
CssVars,
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 {
// hooks:
OrientationableOptions,
defaultBlockOrientationableOptions,
usesOrientationable,
} from '@reusable-ui/orientationable' // a capability of UI to rotate its layout
// defaults:
export const defaultOrientationableOptions = defaultBlockOrientationableOptions;
// hooks:
// capabilities:
//#region groupable
export interface GroupableVars {
// borders:
borderWidth : any
borderStartStartRadius : any
borderStartEndRadius : any
borderEndStartRadius : any
borderEndEndRadius : any
// spacings:
paddingInline : any
paddingBlock : any
}
const [groupableVars] = cssVars<GroupableVars>({ 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: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign))' , styles);
export const ifFirstVisibleChild = (styles: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign)):where(:first-child, .first-visible-child)' , styles);
export const ifLastVisibleChild = (styles: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign)):where( :last-child, .last-visible-child)' , styles);
export const ifNotFirstVisibleChild = (styles: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign, :first-child, .first-visible-child))' , styles);
export const ifNotLastVisibleChild = (styles: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign, :last-child, .last-visible-child))' , styles);
export const ifSecondVisibleChild = (styles: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign)):where(:nth-child(2))' , styles);
export const ifNotSecondVisibleChild = (styles: CssStyleCollection): CssRule => rule(':not(:where(.overlay, .foreign, :nth-child(2)))' , styles);
export interface GroupableStuff { groupableRule: Factory<CssRule>, separatorRule: Factory<CssRule>, groupableVars: CssVars<GroupableVars> }
export interface GroupableOptions extends OrientationableOptions {
itemsSelector ?: CssSelectorCollection
swapFirstItem ?: boolean
}
const groupableChildrenRuleOfOrientationInline = memoizeStyle((): CssRule => {
// 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'], // reads parent's prop
/*
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'], // reads parent's prop
[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'], // reads parent's prop
/*
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'], // reads parent's prop
[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((): CssRule => {
// 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'], // reads parent's prop
/*
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'], // reads parent's prop
[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'], // reads parent's prop
/*
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'], // reads parent's prop
[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?: GroupableOptions): GroupableStuff => {
// 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: boolean, options?: GroupableOptions): CssRule => {
// 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 (left|right)-border
// 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({ // *note : *special case* => move the first separator to the second child
// 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?: GroupableOptions): CssRule => createSeparatorRuleOf(false, options);
const createSeparatorRuleBlock = (options?: GroupableOptions): CssRule => createSeparatorRuleOf(true , options);
const getDefaultSeparatorRuleInline = memoizeStyle(() => createSeparatorRuleInline());
const getDefaultSeparatorRuleBlock = memoizeStyle(() => createSeparatorRuleBlock());
//#endregion groupable