@reusable-ui/modal
Version:
Overlays a dialog to the entire site's page.
309 lines (235 loc) • 11.4 kB
text/typescript
// cssfn:
import {
// writes css in javascript:
rule,
variants,
states,
fallback,
style,
vars,
// reads/writes css variables configuration:
usesCssProps,
// writes complex stylesheets in simpler way:
watchChanges,
memoizeStyle,
} from '@cssfn/core' // writes css in javascript
// reusable-ui core:
import {
// border (stroke) stuff of UI:
usesBorder,
// animation stuff of UI:
usesAnimation,
// a capability of UI to stack on top-most of another UI(s) regardless of DOM's stacking context:
globalStacks,
// groups a list of UIs into a single UI:
usesGroupable,
// a capability of UI to expand/reduce its size or toggle the visibility:
ifCollapsed,
usesCollapsible,
} from '@reusable-ui/core' // a set of reusable-ui packages which are responsible for building any component
// internals:
import {
// features:
usesBackdrop,
} from '../features/backdrop.js'
import {
// utilities:
ifGlobalModal,
} from '../utilities.js'
// import {
// // elements:
// noParentScrollElm,
// noParentScrollDummyChildElm,
// } from './elements.js'
import {
// configs:
modals,
cssModalConfig,
} from './config.js'
// styles:
export const onBackdropStylesChange = watchChanges(cssModalConfig.onChange);
export const usesBackdropLayout = memoizeStyle(() => {
// dependencies:
// features:
const {borderRule , borderVars } = usesBorder({
borderWidth : 'inherit',
borderRadius : 'inherit',
});
const {animationRule , animationVars} = usesAnimation();
const {backdropRule , backdropVars } = usesBackdrop();
// capabilities:
const { groupableVars} = usesGroupable();
return style({
// layouts:
...style({
// layouts:
display : 'grid',
// child default sizes:
justifyItems : 'center', // center horizontally
alignItems : 'center', // center vertically
// positions:
position : 'absolute', // local <Modal>: absolute position
...ifGlobalModal({
position : 'fixed', // global <Modal>: directly inside `body > portal` => fixed position
}),
inset : 0, // span the <Modal> to the edges of <container>
zIndex : globalStacks.modalBackdrop,
translate : `${backdropVars.scrollLeft} ${backdropVars.scrollTop}`,
willChange : 'translate', // Forces Chrome to update the translate when we update the corresponding variables.
// sizes:
// global <Modal>: fills the entire screen height:
...ifGlobalModal({
boxSizing : 'border-box', // the final size is including borders & paddings
minBlockSize : '100svh', // for modern browsers
...fallback({
minBlockSize : '100dvh', // for semi-modern browsers
}),
...fallback({
minBlockSize : '100vh', // for old browsers
}),
}),
// // children:
// ...children(noParentScrollElm, {
// // layouts:
// display : 'grid',
// gridTemplate : [[
// '"content"', '1fr',
// '/',
// '1fr'
// ]],
//
//
//
// // positions:
// gridArea : '1/1/-1/-1', // fills the entire <Backdrop>'s gridArea(s), inclding it's (virtual) paddings
// position : 'absolute', // supports for ::before pseudo_element of `position: relative`
// inset : 0, // span the <NoParentScroll> to the edges of <Backdrop>
// zIndex : -1, // below the <ModalUi>
//
//
//
// // appearances:
// opacity : 0, // visually_hidden but still processing the *hack* of scrolling
//
//
//
// // sizes:
// justifySelf : 'stretch', // span the <NoParentScroll> to the width of <Backdrop>
// alignSelf : 'stretch', // span the <NoParentScroll> to the height of <Backdrop>
//
//
//
// // scrolls:
// overflow : 'scroll', // here how the *hack* works
// overscrollBehavior : 'none', // here how the *hack* works
//
//
//
// // accessibilities:
// pointerEvents : 'auto', // cancel out *inherited* ghost layer from <Backdrop>, *force-enabling* mouse_event on the <NoParentScroll>
//
//
//
// // children:
// ...children(noParentScrollDummyChildElm, {
// // layouts:
// display : 'inline-block', // use inline-block, so it takes the width & height as we set
// content : '""', // activate the pseudo_element
//
//
//
// // positions:
// gridArea : 'content',
// position : 'relative', // supports for adjusting the `insetBlockEnd`
// insetInlineEnd : '-1px', // triggers the horz_scrollbar of the <NoParentScroll>
// insetBlockEnd : '-1px', // triggers the vert_scrollbar of the <NoParentScroll>
//
//
//
// // sizes:
// justifySelf : 'end', // put the <DummyChild> to the right of <NoParentScroll>
// alignSelf : 'end', // put the <DummyChild> to the bottom of <NoParentScroll>
// inlineSize : '1px', // triggers the horz_scrollbar of the <NoParentScroll>
// blockSize : '1px', // triggers the vert_scrollbar of the <NoParentScroll>
// }),
// }, { specificityWeight : 2 }), // ensures not overwritten by `* as <CardDialog>`
// customize:
...usesCssProps(modals), // apply config's cssProps
// animations:
anim : animationVars.anim,
}),
// features:
...borderRule(), // must be placed at the last
...animationRule(), // must be placed at the last
...backdropRule(),
// configs:
...vars({
// borders:
// makes the backdrop as a <group> by calculating <parent>'s border & borderRadius:
[groupableVars.borderWidth ] : '0px',
[groupableVars.borderStartStartRadius] : `calc(${borderVars.borderStartStartRadius} - ${borderVars.borderWidth} - min(${borderVars.borderWidth}, 0.5px))`,
[groupableVars.borderStartEndRadius ] : `calc(${borderVars.borderStartEndRadius } - ${borderVars.borderWidth} - min(${borderVars.borderWidth}, 0.5px))`,
[groupableVars.borderEndStartRadius ] : `calc(${borderVars.borderEndStartRadius } - ${borderVars.borderWidth} - min(${borderVars.borderWidth}, 0.5px))`,
[groupableVars.borderEndEndRadius ] : `calc(${borderVars.borderEndEndRadius } - ${borderVars.borderWidth} - min(${borderVars.borderWidth}, 0.5px))`,
// spacings:
[groupableVars.paddingInline ] : '0px',
[groupableVars.paddingBlock ] : '0px',
}),
...style({
// borders:
// fit the backdrop borderRadius to <container>'s borderRadius:
borderWidth : groupableVars.borderWidth,
borderStartStartRadius : groupableVars.borderStartStartRadius,
borderStartEndRadius : groupableVars.borderStartEndRadius,
borderEndStartRadius : groupableVars.borderEndStartRadius,
borderEndEndRadius : groupableVars.borderEndEndRadius,
// spacings:
paddingInline : groupableVars.paddingInline,
paddingBlock : groupableVars.paddingBlock,
}),
});
}, onBackdropStylesChange);
export const usesBackdropVariants = memoizeStyle(() => {
return style({
// variants:
...variants([
rule('.hidden', {
// backgrounds:
background : 'none',
}),
rule(['.hidden', '.interactive'], {
// accessibilities:
pointerEvents : 'none', // just an overlay element (ghost), no mouse interaction, clicking on it will focus on the parent
// children:
...rule(':where(&)>*', { // <ModalUi>, zeroed the specificity to be easily overriden
// accessibilities:
pointerEvents : 'auto', // cancel out *inherited* ghost layer from <Backdrop>, *re-enabling* mouse_event on the <ModalUi>
}),
}),
]),
});
}, onBackdropStylesChange);
export const usesBackdropStates = memoizeStyle(() => {
// dependencies:
// states:
const {collapsibleRule} = usesCollapsible(modals);
return style({
// states:
...collapsibleRule(),
...states([
ifCollapsed({
// appearances:
// display : 'none', // hide the <Modal> -- causes the <Children>'s animation interrupted prematurely
visibility : 'hidden', // hide the <Modal> without causing the <Children>'s animation interrupted prematurely
}),
]),
});
}, onBackdropStylesChange);
export default memoizeStyle(() => style({
// layouts:
...usesBackdropLayout(),
// variants:
...usesBackdropVariants(),
// states:
...usesBackdropStates(),
}), onBackdropStylesChange);