@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
131 lines (117 loc) • 4.64 kB
text/typescript
import React from 'react';
import {createElemPropsHook} from '@workday/canvas-kit-react/common';
import {getCursor} from '@workday/canvas-kit-react/collection';
import {useComboboxModel} from './useComboboxModel';
/**
* `useComboboxKeyboardTypeAhead` handles type-ahead. It will try and find a matching item in the list. It also returns the keys types so far.
* This hook is best used when composing hooks and in conjunction with `useComboboxInput`.
*
* ```tsx
* // Example for a `Select` input
* export const useSelectInput = composeHooks(
* createElemPropsHook(useComboboxModel)((model, ref, elemProps: {keySoFar?: string} = {}) => {
* return {
* onKeyDown(event: React.KeyboardEvent) {
* // onKeyDown logic ...
* },
* } as const;
* }),
* useComboboxKeyboardTypeAhead,
* useComboboxInput
* );
* ```
*/
export const useComboboxKeyboardTypeAhead = createElemPropsHook(useComboboxModel)(model => {
const keySoFar = React.useRef('');
const timer = React.useRef<ReturnType<typeof setTimeout>>();
const [keyTypedSofar, setKeyTypedSofar] = React.useState(keySoFar.current);
const startClearKeysSoFarTimer = () => {
if (timer.current) {
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
keySoFar.current = '';
setKeyTypedSofar(keySoFar.current);
}, 500);
};
// Based on the the key typed, this will try and find an index in the array where it matches the string
// so if a user types `de` it will try and find an index in the array whose id matches `de` like denver
const getIndexByStartString = (
startIndex: number,
startString: string,
endIndex: number = model.state.items.length,
ignoreDisabled: boolean = true
): number => {
for (let i = startIndex; i < endIndex; i++) {
const label = model.state.items[i].textValue.toLowerCase();
if (
label.startsWith(startString.toLowerCase()) &&
(!ignoreDisabled ||
(ignoreDisabled && !model.state.nonInteractiveIds.includes(model.state.items[i].id)))
) {
return i;
}
}
return -1;
};
const currentItemIndex =
model.state.items.length > 0
? model.navigation.getItem(getCursor(model.state), model)?.index || 0
: 0;
const handleKeyboardTypeAhead = (key: string, numOptions: number) => {
// If the starting point is beyond the list of options, reset it
// to the beginning of the list
const startNumber = keySoFar.current.length === 0 ? currentItemIndex + 1 : currentItemIndex;
const start = startNumber === numOptions ? 0 : startNumber;
// Keeps track of the current key types and adds to it
// if you type `de` vs `d` for denver
keySoFar.current += key;
setKeyTypedSofar(keySoFar.current);
startClearKeysSoFarTimer();
// First, look for a match from start to end
let matchIndex = getIndexByStartString(start, keySoFar.current);
getIndexByStartString(start, keySoFar.current);
// If a match isn't found between start and end, wrap the search
// around and search again from the beginning (0) to start
if (matchIndex === -1) {
matchIndex = getIndexByStartString(0, keySoFar.current, start);
}
// A match was found...
if (matchIndex > -1) {
model.events.goTo({id: model.state.items[matchIndex].id});
if (model.state.visibility === 'hidden') {
// If the menu is closed, fire the change event
// go to that item and select based on its id
model.events.select({id: model.state.items[matchIndex].id});
}
}
};
return {
onKeyDown(event: React.KeyboardEvent) {
// Call type ahead excluding backspace
if (event.key.length === 1 && event.key.match(/\S/)) {
handleKeyboardTypeAhead(event.key, model.state.items.length);
}
// If spacebar is typed
if (
(event.key === 'Spacebar' || event.key === ' ') &&
model.state.visibility === 'visible' &&
keySoFar.current !== ''
) {
// If the user is in the middle of typing a string, treat
// space key as type-ahead rather than option selection
handleKeyboardTypeAhead(' ', model.state.items.length);
}
if (
(event.key === 'Spacebar' || event.key === ' ') &&
model.state.visibility === 'hidden' &&
keySoFar.current !== ''
) {
// If the user is in the middle of typing a string, treat
// space key as type-ahead rather than option selection
handleKeyboardTypeAhead(' ', model.state.items.length);
}
},
keySoFar: keyTypedSofar,
};
});