@primer/react
Version:
An implementation of GitHub's Primer Design System using React
149 lines (143 loc) • 4.3 kB
JavaScript
import { useRef, useReducer, useCallback, useMemo, useDeferredValue } from 'react';
import { AutocompleteContext, AutocompleteInputContext, AutocompleteDeferredInputContext } from './AutocompleteContext.js';
import AutocompleteInput from './AutocompleteInput.js';
import AutocompleteMenu from './AutocompleteMenu.js';
import AutocompleteOverlay from './AutocompleteOverlay.js';
import { useId } from '../hooks/useId.js';
import { jsx } from 'react/jsx-runtime';
const initialState = {
inputValue: '',
showMenu: false,
isMenuDirectlyActivated: false,
autocompleteSuggestion: '',
selectedItemLength: 0
};
const reducer = (state, action) => {
const {
type,
payload
} = action;
switch (type) {
case 'inputValue':
return {
...state,
inputValue: payload
};
case 'showMenu':
return {
...state,
showMenu: payload
};
case 'isMenuDirectlyActivated':
return {
...state,
isMenuDirectlyActivated: payload
};
case 'autocompleteSuggestion':
return {
...state,
autocompleteSuggestion: payload
};
case 'selectedItemLength':
return {
...state,
selectedItemLength: payload
};
default:
return state;
}
};
const Autocomplete = ({
children,
id: idProp
}) => {
const activeDescendantRef = useRef(null);
const scrollContainerRef = useRef(null);
const inputRef = useRef(null);
const [state, dispatch] = useReducer(reducer, initialState);
const {
inputValue,
showMenu,
autocompleteSuggestion,
isMenuDirectlyActivated,
selectedItemLength
} = state;
const setInputValue = useCallback(value => {
dispatch({
type: 'inputValue',
payload: value
});
}, []);
const setShowMenu = useCallback(value => {
dispatch({
type: 'showMenu',
payload: value
});
}, []);
const setAutocompleteSuggestion = useCallback(value => {
dispatch({
type: 'autocompleteSuggestion',
payload: value
});
}, []);
const setIsMenuDirectlyActivated = useCallback(value => {
dispatch({
type: 'isMenuDirectlyActivated',
payload: value
});
}, []);
const setSelectedItemLength = useCallback(value => {
dispatch({
type: 'selectedItemLength',
payload: value
});
}, []);
const id = useId(idProp);
// Base context: refs, IDs, menu visibility, and callbacks
// Changes when menu opens/closes or selection changes, but NOT on every keystroke
const autocompleteContextValue = useMemo(() => ({
activeDescendantRef,
id,
inputRef,
scrollContainerRef,
selectedItemLength,
setAutocompleteSuggestion,
setInputValue,
setIsMenuDirectlyActivated,
setShowMenu,
setSelectedItemLength,
showMenu
}), [id, selectedItemLength, setAutocompleteSuggestion, setInputValue, setIsMenuDirectlyActivated, setShowMenu, setSelectedItemLength, showMenu]);
// Input state context: values that change on every keystroke
// Split to prevent Overlay from re-rendering during typing
const autocompleteInputContextValue = useMemo(() => ({
autocompleteSuggestion,
inputValue,
isMenuDirectlyActivated
}), [autocompleteSuggestion, inputValue, isMenuDirectlyActivated]);
// Deferred input value for expensive operations like filtering
// Menu subscribes to this instead of inputValue to avoid re-rendering on every keystroke
const deferredInputValue = useDeferredValue(inputValue);
const autocompleteDeferredInputContextValue = useMemo(() => ({
deferredInputValue
}), [deferredInputValue]);
return /*#__PURE__*/jsx(AutocompleteContext.Provider, {
value: autocompleteContextValue,
children: /*#__PURE__*/jsx(AutocompleteInputContext.Provider, {
value: autocompleteInputContextValue,
children: /*#__PURE__*/jsx(AutocompleteDeferredInputContext.Provider, {
value: autocompleteDeferredInputContextValue,
children: children
})
})
});
};
Autocomplete.displayName = "Autocomplete";
var Autocomplete$1 = Object.assign(Autocomplete, {
__SLOT__: Symbol('Autocomplete'),
Context: AutocompleteContext,
Input: AutocompleteInput,
Menu: AutocompleteMenu,
Overlay: AutocompleteOverlay
});
export { Autocomplete$1 as default };