@reusable-ui/hamburger-menu-button
Version:
A toggleable hamburger button for showing/hiding menu in Navbar.
193 lines (192 loc) • 8.3 kB
JavaScript
// cssfn:
import {
// writes css in javascript:
rule, states, fallback, style, vars, cssVars, } from '@cssfn/core'; // writes css in javascript
// reusable-ui features:
import {
// hooks:
useAnimatingState,
// animation stuff of UI:
usesAnimation, } from '@reusable-ui/core'; // a set of reusable-ui packages which are responsible for building any component
const [hamburgerableVars] = cssVars(); // no need to have SSR support because the variables are not shared externally (outside <HamburgerMenuButton>)
// .crossed will be added after crossing-animation done:
const selectorIfCrossed = '.crossed';
// .crossing = styled crossing:
const selectorIfCrossing = '.crossing';
// .hamburgering will be added after loosing cross(ing|ed) and will be removed after hamburgering-animation done:
const selectorIfHamburgering = '.hamburgering';
// if all above are not set => hamburgered:
const selectorIfHamburgered = ':not(:is(.crossed, .crossing, .hamburgering))';
export const ifCrossed = (styles) => rule(selectorIfCrossed, styles);
export const ifCrossing = (styles) => rule(selectorIfCrossing, styles);
export const ifHamburgering = (styles) => rule(selectorIfHamburgering, styles);
export const ifHamburgered = (styles) => rule(selectorIfHamburgered, styles);
export const ifCross = (styles) => rule([selectorIfCrossing, selectorIfCrossed], styles);
export const ifHamburger = (styles) => rule([selectorIfHamburgering, selectorIfHamburgered], styles);
export const ifCrossHamburgering = (styles) => rule([selectorIfCrossing, selectorIfCrossed, selectorIfHamburgering], styles);
/**
* Uses hamburger animation.
* @param config A configuration of `hamburgerableRule`.
* @returns A `HamburgerableStuff` represents a hamburgerable state.
*/
export const usesHamburgerable = (config) => {
// dependencies:
// features:
const { animationRule, animationVars } = usesAnimation();
// css vars:
const transformNoneVars = () => vars({
[hamburgerableVars.topTransformIn]: animationVars.transformNone,
[hamburgerableVars.midTransformIn]: animationVars.transformNone,
[hamburgerableVars.btmTransformIn]: animationVars.transformNone,
[hamburgerableVars.topTransformOut]: animationVars.transformNone,
[hamburgerableVars.midTransformOut]: animationVars.transformNone,
[hamburgerableVars.btmTransformOut]: animationVars.transformNone,
});
const transformInVars = () => vars({
[hamburgerableVars.topTransformIn]: config?.hamburgerTopTransformIn,
[hamburgerableVars.midTransformIn]: config?.hamburgerMidTransformIn,
[hamburgerableVars.btmTransformIn]: config?.hamburgerBtmTransformIn,
});
const transformOutVars = () => vars({
[hamburgerableVars.topTransformOut]: config?.hamburgerTopTransformOut,
[hamburgerableVars.midTransformOut]: config?.hamburgerMidTransformOut,
[hamburgerableVars.btmTransformOut]: config?.hamburgerBtmTransformOut,
});
const animNoneVars = () => vars({
[hamburgerableVars.topAnim]: animationVars.animNone,
[hamburgerableVars.midAnim]: animationVars.animNone,
[hamburgerableVars.btmAnim]: animationVars.animNone,
});
const animInVars = () => vars({
[hamburgerableVars.topAnim]: config?.hamburgerTopAnimIn,
[hamburgerableVars.midAnim]: config?.hamburgerMidAnimIn,
[hamburgerableVars.btmAnim]: config?.hamburgerBtmAnimIn,
});
const animOutVars = () => vars({
[hamburgerableVars.topAnim]: config?.hamburgerTopAnimOut,
[hamburgerableVars.midAnim]: config?.hamburgerMidAnimOut,
[hamburgerableVars.btmAnim]: config?.hamburgerBtmAnimOut,
});
return {
hamburgerableRule: () => style({
// features:
...animationRule(),
// reset functions:
// declare default values at lowest specificity:
...fallback({
...transformNoneVars(),
...animNoneVars(),
}),
// animation states:
...states([
ifCrossed({
...transformInVars(),
}),
ifCrossing({
...transformInVars(),
...transformOutVars(),
...animInVars(),
}),
ifHamburgering({
...transformInVars(),
...transformOutVars(),
...animOutVars(),
}),
ifHamburgered({
...transformOutVars(),
}),
]),
// compositions:
...vars({
[hamburgerableVars.topTransform]: [[
// combining: transform1 * transform2 * transform3 ...
// back-to-front order, the last is processed first, the first is processed last
hamburgerableVars.topTransformIn,
hamburgerableVars.topTransformOut,
]],
[hamburgerableVars.midTransform]: [[
// combining: transform1 * transform2 * transform3 ...
// back-to-front order, the last is processed first, the first is processed last
hamburgerableVars.midTransformIn,
hamburgerableVars.midTransformOut,
]],
[hamburgerableVars.btmTransform]: [[
// combining: transform1 * transform2 * transform3 ...
// back-to-front order, the last is processed first, the first is processed last
hamburgerableVars.btmTransformIn,
hamburgerableVars.btmTransformOut,
]],
}),
}),
hamburgerableVars,
};
};
export const useHamburgerable = (isActive) => {
// fn states:
/*
* state is hamburgered/crossed based on [controllable crossed = isActive]
* [uncontrollable crossed] is not supported
*/
const crossedFn = isActive /*controllable*/;
// states:
const [crossed, setCrossed, animation, { handleAnimationStart, handleAnimationEnd, handleAnimationCancel }] = useAnimatingState({
initialState: crossedFn,
animationName: /((^|[^a-z])(hamburgerout|hamburgerin)|([a-z])(Hamburgerout|Hamburgerin))(?![a-z])/,
});
// update state:
if (crossed !== crossedFn) { // change detected => apply the change & start animating
setCrossed(crossedFn); // remember the last change
} // if
// fn props:
const state = (() => {
// crossing:
if (animation === true)
return 2 /* HamburgerableState.Crossing */;
// hamburgering:
if (animation === false)
return 1 /* HamburgerableState.Hamburgering */;
// fully crossed:
if (crossed)
return 3 /* HamburgerableState.Crossed */;
// fully hamburgered:
return 0 /* HamburgerableState.Hamburgered */;
})();
const stateClass = (() => {
switch (state) {
// crossing:
case 2 /* HamburgerableState.Crossing */:
{
return 'crossing';
}
;
// hamburgering:
case 1 /* HamburgerableState.Hamburgering */:
{
return 'hamburgering';
}
;
// fully crossed:
case 3 /* HamburgerableState.Crossed */:
{
return 'crossed';
}
;
// fully hamburgered:
case 0 /* HamburgerableState.Hamburgered */:
{
return null; // discard all classes above
}
;
} // switch
})();
// api:
return {
active: crossed,
state: state,
class: stateClass,
handleAnimationStart,
handleAnimationEnd,
handleAnimationCancel,
};
};
//#endregion hamburgerable