UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

450 lines (449 loc) • 19.1 kB
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js"; import { useProvidedRefOrCreate } from "../hooks/useProvidedRefOrCreate.js"; import { useProvidedStateOrCreate } from "../hooks/useProvidedStateOrCreate.js"; import { useId as useId$1 } from "../hooks/useId.js"; import { useFocusZone } from "../hooks/useFocusZone.js"; import { ActionListContainerContext } from "../ActionList/ActionListContainerContext.js"; import { ActionList } from "../ActionList/index.js"; import Checkbox from "../Checkbox/Checkbox.js"; import useScrollFlash from "../hooks/useScrollFlash.js"; import { FilteredActionListLoadingTypes } from "./constants.js"; import { FilteredActionListBodyLoader } from "./FilteredActionListLoaders.js"; import FilteredActionList_module_css_default from "./FilteredActionList.module.css.js"; import { useAnnouncements } from "./useAnnouncements.js"; import { FilteredActionListInput } from "./FilteredActionListInput.js"; import { c } from "react-compiler-runtime"; import { clsx } from "clsx"; import { jsx, jsxs } from "react/jsx-runtime"; import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FocusKeys, scrollIntoView } from "@primer/behaviors"; import { isValidElementType } from "react-is"; import { useVirtualizer } from "@tanstack/react-virtual"; //#region src/FilteredActionList/FilteredActionList.tsx 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, focusPrependedElements, scrollBehavior, virtualized = false, ...listProps }) { if (virtualized && groupMetadata !== null && groupMetadata !== void 0 && groupMetadata.length) console.warn("FilteredActionList: `virtualized` has no effect when `groupMetadata` is provided. Grouped lists are rendered without virtualization."); const isVirtualized = virtualized && !(groupMetadata !== null && groupMetadata !== void 0 && groupMetadata.length); const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, void 0, ""); 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$1(actionListProps === null || actionListProps === void 0 ? void 0 : actionListProps.id); const inputDescriptionTextId = useId$1(); 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) 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 || firstSelectedItem.focus(); event.preventDefault(); } } else if (event.key === "Enter") { let firstItem; 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(); 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 || onListContainerRefChanged(node); }, [onListContainerRefChanged]); useEffect(() => { onInputRefChanged === null || onInputRefChanged === void 0 || onInputRefChanged(inputRef); }, [inputRef, onInputRefChanged]); const DEFAULT_VIRTUAL_ITEM_HEIGHT = 32; const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => scrollContainerRef.current, estimateSize: () => DEFAULT_VIRTUAL_ITEM_HEIGHT, overscan: 10, enabled: isVirtualized, getItemKey: (index) => { var _ref, _item_3$key, _item_3$id; const item_3 = items[index]; if (!item_3) return index.toString(); return (_ref = (_item_3$key = item_3.key) !== null && _item_3$key !== void 0 ? _item_3$key : (_item_3$id = item_3.id) === null || _item_3$id === void 0 ? void 0 : _item_3$id.toString()) !== null && _ref !== void 0 ? _ref : index.toString(); }, measureElement: (el) => el.scrollHeight }); const virtualItems = isVirtualized ? virtualizer.getVirtualItems() : void 0; const virtualizedItemEntries = useMemo(() => { if (!isVirtualized || !virtualItems) return void 0; return virtualItems.map((virtualItem) => { return { virtualItem, item: items[virtualItem.index], index: virtualItem.index }; }); }, [ isVirtualized, virtualItems, items ]); useFocusZone(!usingRovingTabindex ? { containerRef: { current: listContainerElement }, bindKeys: FocusKeys.ArrowVertical | FocusKeys.PageUpDown, focusOutBehavior: isVirtualized ? "stop" : focusOutBehavior, focusableElementFilter: (element) => { return !(element instanceof HTMLInputElement); }, activeDescendantFocus: inputRef, onActiveDescendantChanged: (current, previous, directlyActivated) => { activeDescendantRef.current = current; if (isVirtualized && current) { const index_0 = current.getAttribute("data-index"); const range = virtualizer.range; if (index_0 !== null && range && (Number(index_0) < range.startIndex || Number(index_0) >= range.endIndex)) virtualizer.scrollToIndex(Number(index_0), { align: "auto" }); } if (current && scrollContainerRef.current && (directlyActivated || focusPrependedElements)) scrollIntoView(current, scrollContainerRef.current, { ...menuScrollMargins, behavior: scrollBehavior }); onActiveDescendantChanged === null || onActiveDescendantChanged === void 0 || onActiveDescendantChanged(current, previous, directlyActivated); }, focusInStrategy: setInitialFocus ? "initial" : "previous", ignoreHoverEvents: disableSelectOnHover, focusPrependedElements } : void 0, [ listContainerElement, usingRovingTabindex, onActiveDescendantChanged, focusPrependedElements, isVirtualized ]); useEffect(() => { if (activeDescendantRef.current && scrollContainerRef.current) scrollIntoView(activeDescendantRef.current, scrollContainerRef.current, { ...menuScrollMargins, behavior: scrollBehavior }); }, [ items, inputRef, scrollContainerRef, scrollBehavior ]); useEffect(() => { if (usingRovingTabindex) { const inputAndListContainerElement = inputAndListContainerRef.current; if (!inputAndListContainerElement) return; const list = listRef.current; if (!list) return; 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 ]); 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, height: scrollContainerRef.current.clientHeight }); if (message) return message; let firstGroupIndex_0 = 0; const renderListItems = () => { if (groupMetadata !== null && groupMetadata !== void 0 && groupMetadata.length) return groupMetadata.map((group, index_1) => { var _group$header, _group$header2; if (index_1 === firstGroupIndex_0 && getItemListForEachGroup(group.groupId).length === 0) firstGroupIndex_0++; 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 : void 0, 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_5 }, itemIndex) => { var _ref2, _item_5$id; const key = (_ref2 = itemKey !== null && itemKey !== void 0 ? itemKey : (_item_5$id = item_5.id) === null || _item_5$id === void 0 ? void 0 : _item_5$id.toString()) !== null && _ref2 !== void 0 ? _ref2 : itemIndex.toString(); return /*#__PURE__*/ jsx(MappedActionListItem, { className: clsx(FilteredActionList_module_css_default.ActionListItem, "className" in item_5 ? item_5.className : void 0), "data-input-focused": isInputFocused ? "" : void 0, "data-first-child": index_1 === firstGroupIndex_0 && itemIndex === 0 ? "" : void 0, ...item_5, renderItem: listProps.renderItem }, key); })] }, index_1); }); if (isVirtualized && virtualizedItemEntries) return virtualizedItemEntries.map(({ virtualItem: virtualItem_0, item: { key: itemKey_0, ...item_6 }, index: index_2 }) => { var _ref3, _item_6$id; const key_0 = (_ref3 = itemKey_0 !== null && itemKey_0 !== void 0 ? itemKey_0 : (_item_6$id = item_6.id) === null || _item_6$id === void 0 ? void 0 : _item_6$id.toString()) !== null && _ref3 !== void 0 ? _ref3 : index_2.toString(); return /*#__PURE__*/ jsx(MappedActionListItem, { className: clsx(FilteredActionList_module_css_default.ActionListItem, "className" in item_6 ? item_6.className : void 0), "data-input-focused": isInputFocused ? "" : void 0, "data-first-child": index_2 === 0 ? "" : void 0, "data-index": virtualItem_0.index, ref: (node_0) => { if (node_0) virtualizer.measureElement(node_0); }, style: { position: "absolute", top: 0, left: 0, right: 0, transform: `translateY(${virtualItem_0.start}px)` }, ...item_6, renderItem: listProps.renderItem }, key_0); }); return items.map(({ key: itemKey_1, ...item_7 }, index_3) => { var _ref4, _item_7$id; const key_1 = (_ref4 = itemKey_1 !== null && itemKey_1 !== void 0 ? itemKey_1 : (_item_7$id = item_7.id) === null || _item_7$id === void 0 ? void 0 : _item_7$id.toString()) !== null && _ref4 !== void 0 ? _ref4 : index_3.toString(); return /*#__PURE__*/ jsx(MappedActionListItem, { className: clsx(FilteredActionList_module_css_default.ActionListItem, "className" in item_7 ? item_7.className : void 0), "data-input-focused": isInputFocused ? "" : void 0, "data-first-child": index_3 === 0 ? "" : void 0, ...item_7, renderItem: listProps.renderItem }, key_1); }); }; const actionListContent = /*#__PURE__*/ jsx(ActionList, { ref: usingRovingTabindex ? listRef : listContainerRefCallback, showDividers: showItemDividers, selectionVariant, ...listProps, ...actionListProps, role: "listbox", id: listId, className: clsx(FilteredActionList_module_css_default.ActionList, actionListProps === null || actionListProps === void 0 ? void 0 : actionListProps.className), style: isVirtualized ? { ...actionListProps === null || actionListProps === void 0 ? void 0 : actionListProps.style, height: virtualizer.getTotalSize(), position: "relative" } : actionListProps === null || actionListProps === void 0 ? void 0 : actionListProps.style, children: renderListItems() }); if (usingRovingTabindex) return /*#__PURE__*/ jsx(ActionListContainerContext.Provider, { value: { container: "FilteredActionList", listRole: "listbox", selectionAttribute: "aria-selected", selectionVariant, enableFocusZone: true }, children: actionListContent }); else return actionListContent; } return /*#__PURE__*/ jsxs("div", { ref: inputAndListContainerRef, className: clsx(className, FilteredActionList_module_css_default.Root), "data-testid": "filtered-action-list", "data-component": "FilteredActionList", children: [ /*#__PURE__*/ jsx(FilteredActionListInput, { inputRef, value: filterValue, onInputChange, onInputKeyPress, onInputKeyDown: usingRovingTabindex ? onInputKeyDown : void 0, placeholderText, listId, inputDescriptionTextId, loading: loading && !loadingType.appearsInBody, fullScreenOnNarrow, ...textInputProps }), /*#__PURE__*/ jsx(VisuallyHidden, { id: inputDescriptionTextId, children: "Items will be filtered as you type" }), onSelectAllChange !== void 0 && /*#__PURE__*/ jsxs("div", { className: FilteredActionList_module_css_default.SelectAllContainer, "data-component": "FilteredActionList.SelectAll", children: [/*#__PURE__*/ jsx(Checkbox, { id: "select-all-checkbox", className: FilteredActionList_module_css_default.SelectAllCheckbox, checked: selectAllChecked, indeterminate: selectAllIndeterminate, onChange: handleSelectAllChange, "data-component": "FilteredActionList.SelectAllCheckbox" }), /*#__PURE__*/ jsx("label", { className: FilteredActionList_module_css_default.SelectAllLabel, htmlFor: "select-all-checkbox", "data-component": "FilteredActionList.SelectAllLabel", children: selectAllLabelText })] }), /*#__PURE__*/ jsx("div", { ref: scrollContainerRef, className: FilteredActionList_module_css_default.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, ...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"; //#endregion export { FilteredActionList };