UNPKG

@reusable-ui/hamburger-menu-button

Version:

A toggleable hamburger button for showing/hiding menu in Navbar.

350 lines (260 loc) 12 kB
// cssfn: import { // cssfn general types: Factory, // cssfn css specific types: CssKnownProps, CssRule, CssStyleCollection, // writes css in javascript: rule, states, fallback, style, vars, // strongly typed of css variables: CssVars, 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 // hooks: // states: //#region hamburgerable export interface HamburgerableVars { topTransformIn : any midTransformIn : any btmTransformIn : any topTransformOut : any midTransformOut : any btmTransformOut : any /** * final transform for the hamburger top. */ topTransform : any /** * final transform for the hamburger middle. */ midTransform : any /** * final transform for the hamburger bottom. */ btmTransform : any /** * final animation for the hamburger top. */ topAnim : any /** * final animation for the hamburger middle. */ midAnim : any /** * final animation for the hamburger bottom. */ btmAnim : any } const [hamburgerableVars] = cssVars<HamburgerableVars>(); // 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: CssStyleCollection): CssRule => rule(selectorIfCrossed , styles); export const ifCrossing = (styles: CssStyleCollection): CssRule => rule(selectorIfCrossing , styles); export const ifHamburgering = (styles: CssStyleCollection): CssRule => rule(selectorIfHamburgering, styles); export const ifHamburgered = (styles: CssStyleCollection): CssRule => rule(selectorIfHamburgered , styles); export const ifCross = (styles: CssStyleCollection): CssRule => rule([selectorIfCrossing , selectorIfCrossed ], styles); export const ifHamburger = (styles: CssStyleCollection): CssRule => rule([ selectorIfHamburgering, selectorIfHamburgered], styles); export const ifCrossHamburgering = (styles: CssStyleCollection): CssRule => rule([selectorIfCrossing , selectorIfCrossed, selectorIfHamburgering ], styles); export interface HamburgerableStuff { hamburgerableRule: Factory<CssRule>, hamburgerableVars: CssVars<HamburgerableVars> } export interface HamburgerableConfig { hamburgerTopTransformIn ?: CssKnownProps['transform'] hamburgerMidTransformIn ?: CssKnownProps['transform'] hamburgerBtmTransformIn ?: CssKnownProps['transform'] hamburgerTopTransformOut ?: CssKnownProps['transform'] hamburgerMidTransformOut ?: CssKnownProps['transform'] hamburgerBtmTransformOut ?: CssKnownProps['transform'] hamburgerTopAnimIn ?: CssKnownProps['animation'] hamburgerMidAnimIn ?: CssKnownProps['animation'] hamburgerBtmAnimIn ?: CssKnownProps['animation'] hamburgerTopAnimOut ?: CssKnownProps['animation'] hamburgerMidAnimOut ?: CssKnownProps['animation'] hamburgerBtmAnimOut ?: CssKnownProps['animation'] } /** * Uses hamburger animation. * @param config A configuration of `hamburgerableRule`. * @returns A `HamburgerableStuff` represents a hamburgerable state. */ export const usesHamburgerable = (config?: HamburgerableConfig): HamburgerableStuff => { // 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 enum HamburgerableState { Hamburgered = 0, Hamburgering = 1, Crossing = 2, Crossed = 3, } export interface HamburgerableApi<TElement extends Element = HTMLElement> { active : boolean state : HamburgerableState class : string|null handleAnimationStart : React.AnimationEventHandler<TElement> handleAnimationEnd : React.AnimationEventHandler<TElement> handleAnimationCancel : React.AnimationEventHandler<TElement> } export const useHamburgerable = <TElement extends Element = HTMLElement>(isActive: boolean): HamburgerableApi<TElement> => { // fn states: /* * state is hamburgered/crossed based on [controllable crossed = isActive] * [uncontrollable crossed] is not supported */ const crossedFn : boolean = isActive /*controllable*/; // states: const [crossed, setCrossed, animation, {handleAnimationStart, handleAnimationEnd, handleAnimationCancel}] = useAnimatingState<boolean, TElement>({ 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 = ((): HamburgerableState => { // crossing: if (animation === true ) return HamburgerableState.Crossing; // hamburgering: if (animation === false) return HamburgerableState.Hamburgering; // fully crossed: if (crossed) return HamburgerableState.Crossed; // fully hamburgered: return HamburgerableState.Hamburgered; })(); const stateClass = ((): string|null => { switch (state) { // crossing: case HamburgerableState.Crossing: { return 'crossing'; }; // hamburgering: case HamburgerableState.Hamburgering: { return 'hamburgering'; }; // fully crossed: case HamburgerableState.Crossed: { return 'crossed'; }; // fully hamburgered: case HamburgerableState.Hamburgered: { return null; // discard all classes above }; } // switch })(); // api: return { active : crossed, state : state, class : stateClass, handleAnimationStart, handleAnimationEnd, handleAnimationCancel, }; }; //#endregion hamburgerable