@reusable-ui/disableable
Version:
A capability of UI to be disabled.
164 lines (163 loc) • 6.34 kB
JavaScript
// cssfn:
import {
// writes css in javascript:
rule, states, style, vars, cssVars, } from '@cssfn/core'; // writes css in javascript
// reusable-ui utilities:
import { useSemantic, } from '@reusable-ui/semantics'; // a semantic management system for react web components
import {
// hooks:
usePropEnabled, } from '@reusable-ui/accessibilities'; // an accessibility management system
import {
// hooks:
useAnimatingState, } from '@reusable-ui/animating-state'; // a hook for creating animating state
// reusable-ui features:
import {
// hooks:
usesAnimation, } from '@reusable-ui/animation'; // animation stuff of UI
const [disableableVars] = cssVars({ prefix: 'di', minify: false }); // shared variables: ensures the server-side & client-side have the same generated css variable names
{
const { animationRegistry: { registerFilter, registerAnim } } = usesAnimation();
registerFilter(disableableVars.filter);
registerAnim(disableableVars.anim);
}
// if all below are not set => enabled:
const selectorIfEnabled = ':not(:is(.enabling, .disabling, [aria-disabled]:not([aria-disabled="false"]), :disabled, .disabled))';
// .enabling will be added after loosing disable and will be removed after enabling-animation done:
const selectorIfEnabling = '.enabling';
// .disabling, [aria-disabled] = styled disable, :disabled = native disable:
const selectorIfDisabling = ':is(.disabling, [aria-disabled]:not([aria-disabled="false"]), :disabled):not(:is(.enabling, .disabled))';
// .disabled will be added after disabling-animation done:
const selectorIfDisabled = '.disabled';
export const ifEnabled = (styles) => rule(selectorIfEnabled, styles);
export const ifEnabling = (styles) => rule(selectorIfEnabling, styles);
export const ifDisabling = (styles) => rule(selectorIfDisabling, styles);
export const ifDisabled = (styles) => rule(selectorIfDisabled, styles);
export const ifEnable = (styles) => rule([selectorIfEnabling, selectorIfEnabled], styles);
export const ifDisable = (styles) => rule([selectorIfDisabling, selectorIfDisabled], styles);
export const ifEnablingDisable = (styles) => rule([selectorIfEnabling, selectorIfDisabling, selectorIfDisabled], styles);
/**
* Adds a capability of UI to be disabled.
* @param config A configuration of `disableableRule`.
* @returns A `DisableableStuff` represents a disableable state.
*/
export const usesDisableable = (config) => {
return {
disableableRule: () => style({
// animation states:
...states([
ifEnabling({
...vars({
[disableableVars.filter]: config?.filterDisable,
[disableableVars.anim]: config?.animEnable,
}),
}),
ifDisabling({
...vars({
[disableableVars.filter]: config?.filterDisable,
[disableableVars.anim]: config?.animDisable,
}),
}),
ifDisabled({
...vars({
[disableableVars.filter]: config?.filterDisable,
}),
}),
]),
}),
disableableVars,
};
};
const htmlCtrls = [
'button',
'fieldset',
'input',
'select',
'optgroup',
'option',
'textarea',
];
export const useDisableable = (props) => {
// fn props:
const propEnabled = usePropEnabled(props);
const { tag } = useSemantic(props);
// fn states:
/*
* state is enabled/disabled based on [controllable enabled]
* [uncontrollable enabled] is not supported
*/
const enabledFn = propEnabled /*controllable*/;
// states:
const [enabled, setEnabled, animation, { handleAnimationStart, handleAnimationEnd, handleAnimationCancel }] = useAnimatingState({
initialState: enabledFn,
animationName: /((^|[^a-z])(enable|disable)|([a-z])(Enable|Disable))(?![a-z])/,
});
// update state:
if (enabled !== enabledFn) { // change detected => apply the change & start animating
setEnabled(enabledFn); // remember the last change
} // if
// fn props:
const state = (() => {
// enabling:
if (animation === true)
return 2 /* DisableableState.Enabling */;
// disabling:
if (animation === false)
return 1 /* DisableableState.Disabling */;
// fully disabled:
if (!enabled)
return 0 /* DisableableState.Disabled */;
// fully enabled:
return 3 /* DisableableState.Enabled */;
})();
const stateClass = (() => {
switch (state) {
// enabling:
case 2 /* DisableableState.Enabling */:
{
return 'enabling';
}
;
// disabling:
case 1 /* DisableableState.Disabling */:
{
// [enabled] but *still* animating of disabling => force to keep disabling using class .disabling
if (enabled)
return 'disabling';
return null; // uses :disabled or [aria-disabled]
}
;
// fully disabled:
case 0 /* DisableableState.Disabled */:
{
return 'disabled';
}
;
// fully enabled:
case 3 /* DisableableState.Enabled */:
{
return null;
}
;
} // switch
})();
// api:
return {
enabled: enabled,
disabled: !enabled,
state: state,
class: stateClass,
props: (() => {
if (enabled)
return null;
// use :disabled if <control>:
if (tag && htmlCtrls.includes(tag))
return { disabled: true };
// else, use [aria-disabled]:
return { 'aria-disabled': true };
})(),
handleAnimationStart,
handleAnimationEnd,
handleAnimationCancel,
};
};
//#endregion disableable