@primer/react
Version:
An implementation of GitHub's Primer Design System using React
450 lines (449 loc) • 19.1 kB
JavaScript
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 };