UNPKG

@reusable-ui/groupable

Version:

Groups a list of UIs into a single UI.

459 lines (376 loc) 19.5 kB
// 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