Files
mtdb_movie/common/resources/client/ui/navigation/menu/menu-trigger.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

130 lines
4.2 KiB
TypeScript
Executable File

import React, {cloneElement, forwardRef, ReactElement, useId} from 'react';
import {useListbox} from '@common/ui/forms/listbox/use-listbox';
import {Item} from '@common/ui/forms/listbox/item';
import {Section} from '@common/ui/forms/listbox/section';
import {Listbox} from '@common/ui/forms/listbox/listbox';
import {useListboxKeyboardNavigation} from '@common/ui/forms/listbox/use-listbox-keyboard-navigation';
import {createEventHandler} from '@common/utils/dom/create-event-handler';
import {useTypeSelect} from '@common/ui/forms/listbox/use-type-select';
import {ListBoxChildren, ListboxProps} from '@common/ui/forms/listbox/types';
import {useIsMobileMediaQuery} from '@common/utils/hooks/is-mobile-media-query';
import {SearchIcon} from '@common/icons/material/Search';
import {TextField} from '@common/ui/forms/input-field/text-field/text-field';
type Props = ListboxProps & {
searchPlaceholder?: string;
showSearchField?: boolean;
children: [ReactElement, ReactElement<ListBoxChildren<string | number>>];
};
export const MenuTrigger = forwardRef<HTMLButtonElement, Props>(
(props, ref) => {
const {
searchPlaceholder,
showSearchField,
children: [menuTrigger, menu],
floatingWidth = 'auto',
isLoading,
} = props;
const id = useId();
const isMobile = useIsMobileMediaQuery();
const listbox = useListbox(
{
...props,
clearInputOnItemSelection: true,
showEmptyMessage: showSearchField,
// on mobile menu will be shown as bottom drawer, so make it fullscreen width always
floatingWidth: isMobile ? 'auto' : floatingWidth,
virtualFocus: showSearchField,
role: showSearchField ? 'listbox' : 'menu',
loopFocus: !showSearchField,
children: menu.props.children,
},
ref,
);
const {
state: {isOpen, setIsOpen, activeIndex, inputValue, setInputValue},
listboxId,
focusItem,
listContent,
reference,
onInputChange,
} = listbox;
const {
handleTriggerKeyDown,
handleListboxKeyboardNavigation,
handleListboxSearchFieldKeydown,
} = useListboxKeyboardNavigation(listbox);
const {findMatchingItem} = useTypeSelect();
// focus matching item when user types, if dropdown is open
const handleListboxTypeSelect = (e: React.KeyboardEvent) => {
if (!isOpen) return;
const i = findMatchingItem(e, listContent, activeIndex);
if (i != null) {
focusItem('increment', i);
}
};
return (
<Listbox
onClick={e => e.stopPropagation()}
listbox={listbox}
onKeyDownCapture={
!showSearchField ? handleListboxTypeSelect : undefined
}
onKeyDown={handleListboxKeyboardNavigation}
onClose={showSearchField ? () => setInputValue('') : undefined}
aria-labelledby={id}
isLoading={isLoading}
searchField={
showSearchField ? (
<TextField
size="sm"
placeholder={searchPlaceholder}
startAdornment={<SearchIcon />}
className="flex-shrink-0 px-8 pb-8 pt-4"
autoFocus
aria-expanded={isOpen ? 'true' : 'false'}
aria-haspopup="listbox"
aria-controls={isOpen ? listboxId : undefined}
aria-autocomplete="list"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
value={inputValue}
onChange={onInputChange}
onKeyDown={e => {
handleListboxSearchFieldKeydown(e);
}}
/>
) : null
}
>
{cloneElement(menuTrigger, {
id,
'aria-expanded': isOpen ? 'true' : 'false',
'aria-haspopup': 'menu',
'aria-controls': isOpen ? listboxId : undefined,
ref: reference,
onKeyDown: handleTriggerKeyDown,
onClick: createEventHandler(e => {
menuTrigger.props?.onClick?.(e);
setIsOpen(!isOpen);
}),
})}
</Listbox>
);
},
);
export function Menu({children}: ListBoxChildren<string | number>) {
return children as unknown as ReactElement;
}
export {Item as MenuItem};
export {Section as MenuSection};