// react: import { // react: default as React, // hooks: useRef, useMemo, } from 'react' // cssfn: import { // style sheets: dynamicStyleSheet, } from '@cssfn/cssfn-react' // writes css in react hook // reusable-ui core: import { // a set of React node utility functions: flattenChildren, // react helper hooks: useIsomorphicLayoutEffect, useEvent, useMergeEvents, useMergeRefs, useMergeClasses, // focusing functions: setFocusFirst, setFocusLast, setFocusPrev, setFocusNext, // a capability of UI to rotate its layout: useOrientationable, // a capability of UI to expand/reduce its size or toggle the visibility: CollapsibleState, useCollapsible, } from '@reusable-ui/core' // a set of reusable-ui packages which are responsible for building any component // reusable-ui components: import { // react components: DropdownActionType, DropdownExpandedChangeEvent, DropdownProps, Dropdown, DropdownComponentProps, } from '@reusable-ui/dropdown' // a base component import { // variants: ListStyle, ListVariant, // react components: ListItemProps, ListItem, ListSeparatorItemProps, ListSeparatorItem, ListProps, List, ListComponentProps, ListItemComponentProps, } from '@reusable-ui/list' // represents a series of content // internals: import { // defaults: listDefaultOrientationableOptions, } from './defaults.js' import { // utilities: calculateSemanticRole, } from './utilities.js' // defaults: const _defaultTabIndex : number = -1 // makes the programatically focusable const _defaultActionCtrl : boolean|undefined = true // the default for (s) is clickable const _defaultListStyle : ListStyle = 'flat' // styles: export const useDropdownListStyleSheet = dynamicStyleSheet( () => import(/* webpackPrefetch: true */ './styles/styles.js') , { id: 'jkrdb7z3wo' }); // a unique salt for SSR support, ensures the server-side & client-side have the same generated class names // react components: export { ListItemProps, ListItem, ListSeparatorItemProps, ListSeparatorItem, } export type DropdownListActionType = DropdownActionType|number export interface DropdownListExpandedChangeEvent extends DropdownExpandedChangeEvent { actionType : DropdownListActionType } export interface DropdownListProps = DropdownListExpandedChangeEvent> extends // bases: ListProps, // additional bases: Omit, // refs: |'elmRef'|'outerRef' // all (elm|outer)Ref are for // DOMs: |Exclude, 'children'> // all DOM [attributes] are for // children: |'children' // we redefined `children` prop as (s) >, // components: Omit, // we don't need these extra properties because the is sub |'listRef' |'listOrientation' |'listStyle' // children: |'listItems' // we redefined `children` prop as (s) >, DropdownComponentProps { // behaviors: scrollToActiveItem ?: boolean } const DropdownList = = DropdownListExpandedChangeEvent>(props: DropdownListProps): JSX.Element|null => { // styles: const styleSheet = useDropdownListStyleSheet(); // rest props: const { // behaviors: scrollToActiveItem = true, lazy, // states: expanded, // take, to be handled by onExpandedChange, // take, to be handled by onExpandStart, // take, to be handled by onCollapseStart, // take, to be handled by onExpandEnd, // take, to be handled by onCollapseEnd, // take, to be handled by // floatable: floatingRef, floatingOn, floatingFriends, floatingPlacement, floatingMiddleware, floatingStrategy, floatingAutoFlip, floatingAutoShift, floatingOffset, floatingShift, onFloatingUpdate, // global stackable: viewport, // auto focusable: autoFocusOn, restoreFocusOn, autoFocus, restoreFocus, autoFocusScroll, restoreFocusScroll, // components: listComponent = ( /> as React.ReactComponentElement>), children : listItems, dropdownRef, dropdownOrientation, dropdownComponent = ( >{listComponent} as React.ReactComponentElement>), // behaviors: actionCtrl : defaultActionCtrl = listComponent.props.actionCtrl ?? _defaultActionCtrl, ...restListProps} = props; // states: const collapsibleState = useCollapsible(props); const isFullyExpanded = (collapsibleState.state === CollapsibleState.Expanded); // variants: const listOrientationableVariant = useOrientationable(props, listDefaultOrientationableOptions); const listIsOrientationVertical = listOrientationableVariant.isOrientationVertical; // classes: const mergedListClasses = useMergeClasses( // preserves the original `classes` from `listComponent`: listComponent.props.classes, // classes: styleSheet.main, ); // refs: const listRefInternal = useRef(null); const mergedListRef = useMergeRefs( // preserves the original `ref` from `listComponent`: listComponent.props.elmRef, // preserves the original `elmRef` from `props`: props.elmRef, listRefInternal, ); const mergedDropdownRef = useMergeRefs( // preserves the original `outerRef` from `dropdownComponent`: dropdownComponent.props.outerRef, // preserves the original `dropdownRef` from `props`: dropdownRef, // preserves the original `outerRef` from `props`: props.outerRef, ); // handlers: const handleKeyDownInternal = useEvent>((event) => { // conditions: if (event.defaultPrevented) return; // the event was already handled by user => nothing to do /* note: the `code` may `undefined` on autoComplete */ const keyCode = (event.code as string|undefined)?.toLowerCase(); if (!keyCode) return; // ignores [unidentified] key if (((): boolean => { const isRtl = (getComputedStyle(event.currentTarget).direction === 'rtl'); if ( (keyCode === 'tab' )) setFocusNext(event.currentTarget); else if ( (keyCode === 'pagedown' )) setFocusNext(event.currentTarget); else if ( (keyCode === 'pageup' )) setFocusPrev(event.currentTarget); else if ( (keyCode === 'home' )) setFocusFirst(event.currentTarget); else if ( (keyCode === 'end' )) setFocusLast(event.currentTarget); else if ( listIsOrientationVertical && (keyCode === 'arrowdown' )) setFocusNext(event.currentTarget); else if ( listIsOrientationVertical && (keyCode === 'arrowup' )) setFocusPrev(event.currentTarget); else if (!listIsOrientationVertical && !isRtl && (keyCode === 'arrowleft' )) setFocusNext(event.currentTarget); else if (!listIsOrientationVertical && !isRtl && (keyCode === 'arrowright')) setFocusPrev(event.currentTarget); else if (!listIsOrientationVertical && isRtl && (keyCode === 'arrowright')) setFocusNext(event.currentTarget); else if (!listIsOrientationVertical && isRtl && (keyCode === 'arrowleft' )) setFocusPrev(event.currentTarget); else return false; // not handled return true; // handled })()) { event.preventDefault(); // prevents the whole page from scrolling when the user press the [up],[down],[left],[right],[pg up],[pg down],[home],[end] } // if }); const handleListKeyDown = useMergeEvents( // preserves the original `onKeyDown` from `listComponent`: listComponent.props.onKeyDown, // actions: handleKeyDownInternal, ); const handleDropdownExpandedChange = useMergeEvents( // preserves the original `onExpandedChange` from `dropdownComponent`: dropdownComponent.props.onExpandedChange, // actions: onExpandedChange, ); const handleDropdownFloatingUpdate = useMergeEvents( // preserves the original `onFloatingUpdate` from `dropdownComponent`: dropdownComponent.props.onFloatingUpdate, // actions: onFloatingUpdate, ); const handleDropdownAnimationStart = useMergeEvents( // preserves the original `onAnimationStart` from `dropdownComponent`: dropdownComponent.props.onAnimationStart, // states: collapsibleState.handleAnimationStart, ); const handleDropdownAnimationEnd = useMergeEvents( // preserves the original `onAnimationEnd` from `dropdownComponent`: dropdownComponent.props.onAnimationEnd, // states: collapsibleState.handleAnimationEnd, ); const handleExpandStart = useMergeEvents( // preserves the original `onExpandStart` from `dropdownComponent`: dropdownComponent.props.onExpandStart, // preserves the original `onExpandStart` from `props`: onExpandStart, ); const handleCollapseStart = useMergeEvents( // preserves the original `onCollapseStart` from `dropdownComponent`: dropdownComponent.props.onCollapseStart, // preserves the original `onCollapseStart` from `props`: onCollapseStart, ); const handleExpandEnd = useMergeEvents( // preserves the original `onExpandEnd` from `dropdownComponent`: dropdownComponent.props.onExpandEnd, // preserves the original `onExpandEnd` from `props`: onExpandEnd, ); const handleCollapseEnd = useMergeEvents( // preserves the original `onCollapseEnd` from `dropdownComponent`: dropdownComponent.props.onCollapseEnd, // preserves the original `onCollapseEnd` from `props`: onCollapseEnd, ); // dom effects: /* scrolls the first active when the is expading or fully expanded */ useIsomorphicLayoutEffect(() => { // conditions: if (!isFullyExpanded) return; // only if fully_expanded if (!scrollToActiveItem) return; // only if the `scrollToActiveItem` feature is enabled const listElm = listRefInternal.current; if (!listElm) return; // only if the corresponding has mounted to DOM // setups: const activeItem = listElm.querySelector(':is(:scope>*, :scope>*>*):is(.activating, .activated, [aria-checked]:not([aria-checked="false"]), [aria-pressed]:not([aria-pressed="false"]), [aria-selected]:not([aria-selected="false"]), :checked):not(.passivating)'); if (!activeItem) return; // no active item found => abort activeItem.scrollIntoView({ behavior : 'smooth', inline : 'nearest', block : 'nearest', }); }, [isFullyExpanded, scrollToActiveItem]); // children: const listComponentChildren = listComponent.props.children; const wrappedChildren = useMemo(() => { let listIndex = -1; return ( listComponentChildren ?? flattenChildren(listItems) .map((listItem, childIndex) => { // conditions: if (!onExpandedChange) return listItem; // [onExpandedChange] was not set => place it anyway if (!React.isValidElement>(listItem)) return listItem; // not a => place it anyway if (!(listItem.props.actionCtrl ?? defaultActionCtrl)) return listItem; // => place it anyway if (listItem.type === ListSeparatorItem) return listItem; // => place it anyway if (listItem.props.classes?.includes?.('void')) return listItem; // a foreign => place it anyway if (listItem.props.className?.split?.(' ')?.includes?.('void')) return listItem; // a foreign => place it anyway // if or or is disabled => the will take care for us // a valid listItem counter: listIndex++; // only count of s, ignores of foreign nodes // props: const listItemProps = listItem.props; // jsx: return ( /* wrap child with */ // other props: {...listItemProps} // steals all listItem's props, so the can recognize the as // identifiers: key={listItem.key ?? childIndex} // positions: listIndex={listIndex} // states: onExpandedChange={onExpandedChange} // components: listItemComponent={ // clone listItem element with (almost) blank props: } /> ); }) ); }, [listComponentChildren, listItems]); // jsx: /* */ return React.cloneElement>(dropdownComponent, // props: { // refs: outerRef : mergedDropdownRef, // semantics: semanticTag : dropdownComponent.props.semanticTag ?? props.semanticTag ?? '', // no corresponding semantic tag => defaults to
// NOTE: we don't use as the default semantic tag because our is similar to ::backdrop, not the itself semanticRole : dropdownComponent.props.semanticRole ?? props.semanticRole ?? calculateSemanticRole(props, defaultActionCtrl), // calculates the default semantic // variants: orientation : dropdownComponent.props.orientation ?? dropdownOrientation, // behaviors: lazy : dropdownComponent.props.lazy ?? lazy, // states: expanded : dropdownComponent.props.expanded ?? expanded, onExpandedChange : handleDropdownExpandedChange, onExpandStart : handleExpandStart, onCollapseStart : handleCollapseStart, onExpandEnd : handleExpandEnd, onCollapseEnd : handleCollapseEnd, onAnimationStart : handleDropdownAnimationStart, onAnimationEnd : handleDropdownAnimationEnd, // floatable: floatingRef, floatingOn, floatingFriends, floatingPlacement, floatingMiddleware, floatingStrategy, floatingAutoFlip, floatingAutoShift, floatingOffset, floatingShift, onFloatingUpdate : handleDropdownFloatingUpdate, // global stackable: viewport, // auto focusable: autoFocusOn : dropdownComponent.props.autoFocusOn ?? autoFocusOn, restoreFocusOn : dropdownComponent.props.restoreFocusOn ?? restoreFocusOn, autoFocus : dropdownComponent.props.autoFocus ?? autoFocus, restoreFocus : dropdownComponent.props.restoreFocus ?? restoreFocus, autoFocusScroll : dropdownComponent.props.autoFocusScroll ?? autoFocusScroll, restoreFocusScroll : dropdownComponent.props.restoreFocusScroll ?? restoreFocusScroll, }, // children: /* */ ((dropdownComponent.props.children !== listComponent) ? dropdownComponent.props.children : React.cloneElement>(listComponent, // props: { // other props: ...restListProps, ...listComponent.props, // overwrites restListProps (if any conflics) // refs: elmRef : mergedListRef, // variants: listStyle : listComponent.props.listStyle ?? props.listStyle ?? _defaultListStyle, // classes: classes : mergedListClasses, // accessibilities: tabIndex : listComponent.props.tabIndex ?? _defaultTabIndex, // behaviors: actionCtrl : listComponent.props.actionCtrl ?? defaultActionCtrl, // handlers: onKeyDown : handleListKeyDown, }, // children: wrappedChildren, )), ); }; export { DropdownList, DropdownList as default, } export type { ListStyle, ListVariant } interface ListItemWithExpandedHandlerProps = DropdownListExpandedChangeEvent> extends // bases: ListItemProps, // states: Required, 'onExpandedChange'>>, // components: Required> { // positions: listIndex : number } const ListItemWithExpandedHandler = = DropdownListExpandedChangeEvent>(props: ListItemWithExpandedHandlerProps): JSX.Element|null => { // rest props: const { // positions: listIndex, // states: onExpandedChange, // components: listItemComponent, ...restListItemProps} = props; // handlers: const handleExpandedChange = onExpandedChange; const handleClickInternal = useEvent>((event) => { // conditions: if (event.defaultPrevented) return; // the event was already handled by user => nothing to do // clicked => request to hide the with `actionType`: handleExpandedChange({ expanded: false, actionType: listIndex } as TDropdownListExpandedChangeEvent); event.preventDefault(); // mark as handled }); const handleClick = useMergeEvents( // preserves the original `onClick` from `listItemComponent`: listItemComponent.props.onClick, // preserves the original `onClick` from `props`: props.onClick, // handlers: handleClickInternal, ); // jsx: /* */ return React.cloneElement>(listItemComponent, // props: { // other props: ...restListItemProps, ...listItemComponent.props, // overwrites restListItemProps (if any conflics) // handlers: onClick : handleClick, }, ); };