'use client'; import * as React from 'react'; import { useTimeout, mergeCallbacks } from '@fluentui/react-utilities'; import { useTriggerSlot } from '../../utils/useTriggerSlot'; import { getDropdownActionFromKey } from '../../utils/dropdownKeyActions'; /** * useButtonTriggerSlot returns a tuple of trigger/listbox shorthand, * with the semantics and event handlers needed for the Combobox and Dropdown components. * The element type of the ref should always match the element type used in the trigger shorthand. * * @internal */ export function useButtonTriggerSlot(triggerFromProps, ref, options) { 'use no memo'; const { state: { open, setOpen, getOptionById }, defaultProps, activeDescendantController } = options; // jump to matching option based on typing const searchString = React.useRef(''); const [setKeyTimeout, clearKeyTimeout] = useTimeout(); const moveToNextMatchingOption = (matcher, opt = { startFromNext: false })=>{ const { startFromNext } = opt; const activeOptionId = activeDescendantController.active(); const nextInOrder = activeDescendantController.find((id)=>{ const option = getOptionById(id); return !!option && matcher(option.text); }, { startFrom: startFromNext ? activeDescendantController.next({ passive: true }) : activeOptionId }); if (nextInOrder) { return nextInOrder; } // Cycle back to first match return activeDescendantController.find((id)=>{ const option = getOptionById(id); return !!option && matcher(option.text); }); }; const moveToNextMatchingOptionWithSameCharacterHandling = ()=>{ if (moveToNextMatchingOption((optionText)=>{ return optionText.toLocaleLowerCase().indexOf(searchString.current) === 0; }, { // Slowly pressing the same key will cycle through options startFromNext: searchString.current.length === 1 })) { return; } // if there are no direct matches, check if the search is all the same letter, e.g. "aaa" if (allCharactersSame(searchString.current) && moveToNextMatchingOption((optionText)=>{ return optionText.toLocaleLowerCase().indexOf(searchString.current[0]) === 0; }, { // if the search is all the same letter, cycle through options starting with that letter startFromNext: true })) { return; } activeDescendantController.blur(); }; const onTriggerKeyDown = (ev)=>{ // clear timeout, if it exists clearKeyTimeout(); // if the key was a char key, update search string if (getDropdownActionFromKey(ev) === 'Type') { // update search string searchString.current += ev.key.toLowerCase(); setKeyTimeout(()=>{ searchString.current = ''; }, 500); if (open) { moveToNextMatchingOptionWithSameCharacterHandling(); } // update state !open && setOpen(ev, true); } }; const trigger = useTriggerSlot(triggerFromProps, ref, { state: options.state, defaultProps, elementType: 'button', activeDescendantController }); trigger.onKeyDown = mergeCallbacks(onTriggerKeyDown, trigger.onKeyDown); return trigger; } /** * @returns - whether every character in the string is the same */ function allCharactersSame(str) { for(let i = 1; i < str.length; i++){ if (str[i] !== str[i - 1]) { return false; } } return true; }