96 lines
3.7 KiB
JavaScript
96 lines
3.7 KiB
JavaScript
'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;
|
|
}
|