UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

468 lines (461 loc) • 17.5 kB
import { c } from 'react-compiler-runtime'; import { scrollIntoView, FocusKeys } from '@primer/behaviors'; import { useCallback, useRef, useState, useEffect, forwardRef } from 'react'; import { ActionList } from '../ActionList/index.js'; import { useFocusZone } from '../hooks/useFocusZone.js'; import { useId } from '../hooks/useId.js'; import { useProvidedRefOrCreate } from '../hooks/useProvidedRefOrCreate.js'; import { useProvidedStateOrCreate } from '../hooks/useProvidedStateOrCreate.js'; import useScrollFlash from '../hooks/useScrollFlash.js'; import { FilteredActionListLoadingTypes, FilteredActionListBodyLoader } from './FilteredActionListLoaders.js'; import classes from './FilteredActionList.module.css.js'; import { ActionListContainerContext } from '../ActionList/ActionListContainerContext.js'; import { isValidElementType } from 'react-is'; import { useAnnouncements } from './useAnnouncements.js'; import { clsx } from 'clsx'; import { jsxs, jsx } from 'react/jsx-runtime'; import TextInput from '../TextInput/TextInput.js'; import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden.js'; import Checkbox from '../Checkbox/Checkbox.js'; const menuScrollMargins = { startMargin: 0, endMargin: 8 }; function FilteredActionList({ loading = false, placeholderText, filterValue: externalFilterValue, loadingType = FilteredActionListLoadingTypes.bodySpinner, onFilterChange, onListContainerRefChanged, onInputRefChanged, items, textInputProps, inputRef: providedInputRef, scrollContainerRef: providedScrollContainerRef, groupMetadata, showItemDividers, message, messageText, className, selectionVariant, announcementsEnabled = true, fullScreenOnNarrow, onSelectAllChange, actionListProps, focusOutBehavior = 'wrap', _PrivateFocusManagement = 'active-descendant', onActiveDescendantChanged, disableSelectOnHover = false, setInitialFocus = false, ...listProps }) { const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, ''); const onInputChange = useCallback(e => { const value = e.target.value; onFilterChange(value, e); setInternalFilterValue(value); }, [onFilterChange, setInternalFilterValue]); const inputAndListContainerRef = useRef(null); const listRef = useRef(null); const scrollContainerRef = useProvidedRefOrCreate(providedScrollContainerRef); const inputRef = useProvidedRefOrCreate(providedInputRef); const usingRovingTabindex = _PrivateFocusManagement === 'roving-tabindex'; const [listContainerElement, setListContainerElement] = useState(null); const activeDescendantRef = useRef(); const listId = useId(actionListProps === null || actionListProps === void 0 ? void 0 : actionListProps.id); const inputDescriptionTextId = useId(); const [isInputFocused, setIsInputFocused] = useState(false); const selectAllChecked = items.length > 0 && items.every(item => item.selected); const selectAllIndeterminate = !selectAllChecked && items.some(item_0 => item_0.selected); const selectAllLabelText = selectAllChecked ? 'Deselect all' : 'Select all'; const getItemListForEachGroup = useCallback(groupId => { const itemsInGroup = []; for (const item_1 of items) { // Look up the group associated with the current item. if (item_1.groupId === groupId) { itemsInGroup.push(item_1); } } return itemsInGroup; }, [items]); const onInputKeyDown = useCallback(event => { if (event.key === 'ArrowDown') { if (listRef.current) { const firstSelectedItem = listRef.current.querySelector('[role="option"]'); firstSelectedItem === null || firstSelectedItem === void 0 ? void 0 : firstSelectedItem.focus(); event.preventDefault(); } } else if (event.key === 'Enter') { let firstItem; // If there are groups, it's not guaranteed that the first item is the actual first item in the first - // as groups are rendered in the order of the groupId provided if (groupMetadata) { let firstGroupIndex = 0; for (let i = 0; i < groupMetadata.length; i++) { if (getItemListForEachGroup(groupMetadata[i].groupId).length > 0) { break; } else { firstGroupIndex++; } } const firstGroup = groupMetadata[firstGroupIndex].groupId; firstItem = items.filter(item_2 => item_2.groupId === firstGroup)[0]; } else { firstItem = items[0]; } if (firstItem.onAction) { firstItem.onAction(firstItem, event); event.preventDefault(); } } }, [items, groupMetadata, getItemListForEachGroup]); const onInputKeyPress = useCallback(event_0 => { if (event_0.key === 'Enter' && activeDescendantRef.current) { event_0.preventDefault(); event_0.nativeEvent.stopImmediatePropagation(); // Forward Enter key press to active descendant so that item gets activated const activeDescendantEvent = new KeyboardEvent(event_0.type, event_0.nativeEvent); activeDescendantRef.current.dispatchEvent(activeDescendantEvent); } }, [activeDescendantRef]); const listContainerRefCallback = useCallback(node => { setListContainerElement(node); onListContainerRefChanged === null || onListContainerRefChanged === void 0 ? void 0 : onListContainerRefChanged(node); }, [onListContainerRefChanged]); useEffect(() => { onInputRefChanged === null || onInputRefChanged === void 0 ? void 0 : onInputRefChanged(inputRef); }, [inputRef, onInputRefChanged]); useFocusZone(!usingRovingTabindex ? { containerRef: { current: listContainerElement }, bindKeys: FocusKeys.ArrowVertical | FocusKeys.PageUpDown, focusOutBehavior, focusableElementFilter: element => { return !(element instanceof HTMLInputElement); }, activeDescendantFocus: inputRef, onActiveDescendantChanged: (current, previous, directlyActivated) => { activeDescendantRef.current = current; if (current && scrollContainerRef.current && directlyActivated) { scrollIntoView(current, scrollContainerRef.current, menuScrollMargins); } onActiveDescendantChanged === null || onActiveDescendantChanged === void 0 ? void 0 : onActiveDescendantChanged(current, previous, directlyActivated); }, focusInStrategy: setInitialFocus ? 'initial' : 'previous', ignoreHoverEvents: disableSelectOnHover } : undefined, [listContainerElement, usingRovingTabindex, onActiveDescendantChanged]); useEffect(() => { if (activeDescendantRef.current && scrollContainerRef.current) { scrollIntoView(activeDescendantRef.current, scrollContainerRef.current, { ...menuScrollMargins, behavior: 'auto' }); } }, [items, inputRef, scrollContainerRef]); useEffect(() => { if (usingRovingTabindex) { const inputAndListContainerElement = inputAndListContainerRef.current; if (!inputAndListContainerElement) return; const list = listRef.current; if (!list) return; // Listen for focus changes within the container const handleFocusIn = event_1 => { if (event_1.target === inputRef.current || list.contains(event_1.target)) { setIsInputFocused(inputRef.current && inputRef.current === document.activeElement ? true : false); } }; inputAndListContainerElement.addEventListener('focusin', handleFocusIn); return () => { inputAndListContainerElement.removeEventListener('focusin', handleFocusIn); }; } }, [items, inputRef, listContainerElement, usingRovingTabindex]); // Re-run when items change to update active indicators useEffect(() => { if (usingRovingTabindex && !loading) { setIsInputFocused(inputRef.current && inputRef.current === document.activeElement ? true : false); } }, [loading, inputRef, usingRovingTabindex]); useAnnouncements(items, usingRovingTabindex ? listRef : { current: listContainerElement }, inputRef, announcementsEnabled, loading, messageText, _PrivateFocusManagement); useScrollFlash(scrollContainerRef); const handleSelectAllChange = useCallback(e_0 => { if (onSelectAllChange) { onSelectAllChange(e_0.target.checked); } }, [onSelectAllChange]); function getBodyContent() { if (loading && scrollContainerRef.current && loadingType.appearsInBody) { return /*#__PURE__*/jsx(FilteredActionListBodyLoader, { loadingType: loadingType, height: scrollContainerRef.current.clientHeight }); } if (message) { return message; } let firstGroupIndex_0 = 0; const actionListContent = /*#__PURE__*/jsx(ActionList, { ref: usingRovingTabindex ? listRef : listContainerRefCallback, showDividers: showItemDividers, selectionVariant: selectionVariant, ...listProps, ...actionListProps, role: "listbox", id: listId, className: clsx(classes.ActionList, actionListProps === null || actionListProps === void 0 ? void 0 : actionListProps.className), children: groupMetadata !== null && groupMetadata !== void 0 && groupMetadata.length ? groupMetadata.map((group, index) => { var _group$header, _group$header2; if (index === firstGroupIndex_0 && getItemListForEachGroup(group.groupId).length === 0) { firstGroupIndex_0++; // Increment firstGroupIndex if the first group has no items } return /*#__PURE__*/jsxs(ActionList.Group, { children: [/*#__PURE__*/jsx(ActionList.GroupHeading, { variant: (_group$header = group.header) !== null && _group$header !== void 0 && _group$header.variant ? group.header.variant : undefined, children: (_group$header2 = group.header) !== null && _group$header2 !== void 0 && _group$header2.title ? group.header.title : `Group ${group.groupId}` }), getItemListForEachGroup(group.groupId).map(({ key: itemKey, ...item_3 }, itemIndex) => { var _ref, _item_3$id; const key = (_ref = itemKey !== null && itemKey !== void 0 ? itemKey : (_item_3$id = item_3.id) === null || _item_3$id === void 0 ? void 0 : _item_3$id.toString()) !== null && _ref !== void 0 ? _ref : itemIndex.toString(); return /*#__PURE__*/jsx(MappedActionListItem, { className: clsx(classes.ActionListItem, 'className' in item_3 ? item_3.className : undefined), "data-input-focused": isInputFocused ? '' : undefined, "data-first-child": index === firstGroupIndex_0 && itemIndex === 0 ? '' : undefined, ...item_3, renderItem: listProps.renderItem }, key); })] }, index); }) : items.map(({ key: itemKey_0, ...item_4 }, index_0) => { var _ref2, _item_4$id; const key_0 = (_ref2 = itemKey_0 !== null && itemKey_0 !== void 0 ? itemKey_0 : (_item_4$id = item_4.id) === null || _item_4$id === void 0 ? void 0 : _item_4$id.toString()) !== null && _ref2 !== void 0 ? _ref2 : index_0.toString(); return /*#__PURE__*/jsx(MappedActionListItem, { className: clsx(classes.ActionListItem, 'className' in item_4 ? item_4.className : undefined), "data-input-focused": isInputFocused ? '' : undefined, "data-first-child": index_0 === 0 ? '' : undefined, ...item_4, renderItem: listProps.renderItem }, key_0); }) }); // Use ActionListContainerContext.Provider only for the old behavior (when feature flag is disabled) if (usingRovingTabindex) { return /*#__PURE__*/jsx(ActionListContainerContext.Provider, { value: { container: 'FilteredActionList', listRole: 'listbox', selectionAttribute: 'aria-selected', selectionVariant, enableFocusZone: true }, children: actionListContent }); } else { return actionListContent; } } const { className: textInputClassName, ...restTextInputProps } = textInputProps || {}; return /*#__PURE__*/jsxs("div", { ref: inputAndListContainerRef, className: clsx(className, classes.Root), "data-testid": "filtered-action-list", children: [/*#__PURE__*/jsx("div", { className: classes.Header, children: /*#__PURE__*/jsx(TextInput // @ts-expect-error it needs a non nullable ref , { ref: inputRef, block: true, width: "auto", color: "fg.default", value: filterValue, onChange: onInputChange, onKeyPress: onInputKeyPress, onKeyDown: usingRovingTabindex ? onInputKeyDown : () => {}, placeholder: placeholderText, role: "combobox", "aria-expanded": "true", "aria-autocomplete": "list", "aria-controls": listId, "aria-label": placeholderText, "aria-describedby": inputDescriptionTextId, loaderPosition: 'leading', loading: loading && !loadingType.appearsInBody, className: clsx(textInputClassName, { [classes.FullScreenTextInput]: fullScreenOnNarrow }), ...restTextInputProps }) }), /*#__PURE__*/jsx(VisuallyHidden, { id: inputDescriptionTextId, children: "Items will be filtered as you type" }), onSelectAllChange !== undefined && /*#__PURE__*/jsxs("div", { className: classes.SelectAllContainer, children: [/*#__PURE__*/jsx(Checkbox, { id: "select-all-checkbox", className: classes.SelectAllCheckbox, checked: selectAllChecked, indeterminate: selectAllIndeterminate, onChange: handleSelectAllChange }), /*#__PURE__*/jsx("label", { className: classes.SelectAllLabel, htmlFor: "select-all-checkbox", children: selectAllLabelText })] }), /*#__PURE__*/jsx("div", { ref: scrollContainerRef, className: classes.Container, children: getBodyContent() })] }); } FilteredActionList.displayName = "FilteredActionList"; const MappedActionListItem = /*#__PURE__*/forwardRef((item, ref) => { const $ = c(36); if (typeof item.renderItem === "function") { let t0; if ($[0] !== item) { t0 = item.renderItem(item); $[0] = item; $[1] = t0; } else { t0 = $[1]; } return t0; } let LeadingVisual; let TrailingIcon; let TrailingVisual; let children; let description; let descriptionVariant; let id; let onAction; let rest; let text; let trailingText; if ($[2] !== item) { ({ id, description, descriptionVariant, text, trailingVisual: TrailingVisual, leadingVisual: LeadingVisual, trailingText, trailingIcon: TrailingIcon, onAction, children, ...rest } = item); $[2] = item; $[3] = LeadingVisual; $[4] = TrailingIcon; $[5] = TrailingVisual; $[6] = children; $[7] = description; $[8] = descriptionVariant; $[9] = id; $[10] = onAction; $[11] = rest; $[12] = text; $[13] = trailingText; } else { LeadingVisual = $[3]; TrailingIcon = $[4]; TrailingVisual = $[5]; children = $[6]; description = $[7]; descriptionVariant = $[8]; id = $[9]; onAction = $[10]; rest = $[11]; text = $[12]; trailingText = $[13]; } let t0; if ($[14] !== item || $[15] !== onAction) { t0 = e => { if (typeof onAction === "function") { onAction(item, e); } }; $[14] = item; $[15] = onAction; $[16] = t0; } else { t0 = $[16]; } let t1; if ($[17] !== LeadingVisual) { t1 = LeadingVisual ? /*#__PURE__*/jsx(ActionList.LeadingVisual, { children: /*#__PURE__*/jsx(LeadingVisual, {}) }) : null; $[17] = LeadingVisual; $[18] = t1; } else { t1 = $[18]; } let t2; if ($[19] !== description || $[20] !== descriptionVariant) { t2 = description ? /*#__PURE__*/jsx(ActionList.Description, { variant: descriptionVariant, children: description }) : null; $[19] = description; $[20] = descriptionVariant; $[21] = t2; } else { t2 = $[21]; } let t3; if ($[22] !== TrailingIcon || $[23] !== TrailingVisual || $[24] !== trailingText) { t3 = TrailingVisual ? /*#__PURE__*/jsx(ActionList.TrailingVisual, { children: typeof TrailingVisual !== "string" && isValidElementType(TrailingVisual) ? /*#__PURE__*/jsx(TrailingVisual, {}) : TrailingVisual }) : TrailingIcon || trailingText ? /*#__PURE__*/jsxs(ActionList.TrailingVisual, { children: [trailingText, TrailingIcon && /*#__PURE__*/jsx(TrailingIcon, {})] }) : null; $[22] = TrailingIcon; $[23] = TrailingVisual; $[24] = trailingText; $[25] = t3; } else { t3 = $[25]; } let t4; if ($[26] !== children || $[27] !== id || $[28] !== ref || $[29] !== rest || $[30] !== t0 || $[31] !== t1 || $[32] !== t2 || $[33] !== t3 || $[34] !== text) { t4 = /*#__PURE__*/jsxs(ActionList.Item, { role: "option", onSelect: t0, "data-id": id, ref: ref, ...rest, children: [t1, children, text, t2, t3] }); $[26] = children; $[27] = id; $[28] = ref; $[29] = rest; $[30] = t0; $[31] = t1; $[32] = t2; $[33] = t3; $[34] = text; $[35] = t4; } else { t4 = $[35]; } return t4; }); FilteredActionList.displayName = 'FilteredActionList'; export { FilteredActionList };