@primer/react
Version:
An implementation of GitHub's Primer Design System using React
471 lines (459 loc) • 18.1 kB
JavaScript
'use strict';
var reactCompilerRuntime = require('react-compiler-runtime');
var behaviors = require('@primer/behaviors');
var React = require('react');
var styled = require('styled-components');
var constants = require('../constants.js');
var index = require('../ActionList/index.js');
var useFocusZone = require('../hooks/useFocusZone.js');
var useId = require('../hooks/useId.js');
var useProvidedRefOrCreate = require('../hooks/useProvidedRefOrCreate.js');
var useProvidedStateOrCreate = require('../hooks/useProvidedStateOrCreate.js');
var useScrollFlash = require('../hooks/useScrollFlash.js');
var FilteredActionListLoaders = require('./FilteredActionListLoaders.js');
var FilteredActionList_module = require('./FilteredActionList.module.css.js');
var ActionListContainerContext = require('../ActionList/ActionListContainerContext.js');
var reactIs = require('react-is');
var useAnnouncements = require('./useAnnouncements.js');
var clsx = require('clsx');
var jsxRuntime = require('react/jsx-runtime');
var useFeatureFlag = require('../FeatureFlags/useFeatureFlag.js');
var Box = require('../Box/Box.js');
var TextInput = require('../TextInput/TextInput.js');
var VisuallyHidden = require('../VisuallyHidden/VisuallyHidden.js');
var Checkbox = require('../Checkbox/Checkbox.js');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var styled__default = /*#__PURE__*/_interopDefault(styled);
const menuScrollMargins = {
startMargin: 0,
endMargin: 8
};
const StyledHeader = styled__default.default.div.withConfig({
displayName: "FilteredActionList__StyledHeader",
componentId: "sc-1oqgb0s-0"
})(["box-shadow:0 1px 0 ", ";z-index:1;"], constants.get('colors.border.default'));
function FilteredActionList({
loading = false,
placeholderText,
filterValue: externalFilterValue,
loadingType = FilteredActionListLoaders.FilteredActionListLoadingTypes.bodySpinner,
onFilterChange,
onListContainerRefChanged,
onInputRefChanged,
items,
textInputProps,
inputRef: providedInputRef,
sx,
groupMetadata,
showItemDividers,
message,
messageText,
className,
selectionVariant,
announcementsEnabled = true,
fullScreenOnNarrow,
onSelectAllChange,
...listProps
}) {
const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate.useProvidedStateOrCreate(externalFilterValue, undefined, '');
const onInputChange = React.useCallback(e => {
const value = e.target.value;
onFilterChange(value, e);
setInternalFilterValue(value);
}, [onFilterChange, setInternalFilterValue]);
const inputAndListContainerRef = React.useRef(null);
const listRef = React.useRef(null);
const scrollContainerRef = React.useRef(null);
const inputRef = useProvidedRefOrCreate.useProvidedRefOrCreate(providedInputRef);
const usingRemoveActiveDescendant = useFeatureFlag.useFeatureFlag('primer_react_select_panel_remove_active_descendant');
const [listContainerElement, setListContainerElement] = React.useState(null);
const activeDescendantRef = React.useRef();
const listId = useId.useId();
const inputDescriptionTextId = useId.useId();
const [isInputFocused, setIsInputFocused] = React.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 = React.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 = React.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 = React.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]);
// BEGIN: Todo remove when we remove usingRemoveActiveDescendant
const listContainerRefCallback = React.useCallback(node => {
setListContainerElement(node);
onListContainerRefChanged === null || onListContainerRefChanged === void 0 ? void 0 : onListContainerRefChanged(node);
}, [onListContainerRefChanged]);
React.useEffect(() => {
onInputRefChanged === null || onInputRefChanged === void 0 ? void 0 : onInputRefChanged(inputRef);
}, [inputRef, onInputRefChanged]);
//END: Todo remove when we remove usingRemoveActiveDescendant
useFocusZone.useFocusZone(!usingRemoveActiveDescendant ? {
containerRef: {
current: listContainerElement
},
bindKeys: behaviors.FocusKeys.ArrowVertical | behaviors.FocusKeys.PageUpDown,
focusOutBehavior: 'wrap',
focusableElementFilter: element => {
return !(element instanceof HTMLInputElement);
},
activeDescendantFocus: inputRef,
onActiveDescendantChanged: (current, previous, directlyActivated) => {
activeDescendantRef.current = current;
if (current && scrollContainerRef.current && directlyActivated) {
behaviors.scrollIntoView(current, scrollContainerRef.current, menuScrollMargins);
}
}
} : undefined, [listContainerElement, usingRemoveActiveDescendant]);
React.useEffect(() => {
if (activeDescendantRef.current && scrollContainerRef.current) {
behaviors.scrollIntoView(activeDescendantRef.current, scrollContainerRef.current, {
...menuScrollMargins,
behavior: 'auto'
});
}
}, [items, inputRef]);
React.useEffect(() => {
if (usingRemoveActiveDescendant) {
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, usingRemoveActiveDescendant]); // Re-run when items change to update active indicators
React.useEffect(() => {
if (usingRemoveActiveDescendant && !loading) {
setIsInputFocused(inputRef.current && inputRef.current === document.activeElement ? true : false);
}
}, [loading, inputRef, usingRemoveActiveDescendant]);
useAnnouncements.useAnnouncements(items, usingRemoveActiveDescendant ? listRef : {
current: listContainerElement
}, inputRef, announcementsEnabled, loading, messageText);
useScrollFlash(scrollContainerRef);
const handleSelectAllChange = React.useCallback(e_0 => {
if (onSelectAllChange) {
onSelectAllChange(e_0.target.checked);
}
}, [onSelectAllChange]);
function getBodyContent() {
if (loading && scrollContainerRef.current && loadingType.appearsInBody) {
return /*#__PURE__*/jsxRuntime.jsx(FilteredActionListLoaders.FilteredActionListBodyLoader, {
loadingType: loadingType,
height: scrollContainerRef.current.clientHeight
});
}
if (message) {
return message;
}
let firstGroupIndex_0 = 0;
const actionListContent = /*#__PURE__*/jsxRuntime.jsx(index.ActionList, {
ref: usingRemoveActiveDescendant ? listRef : listContainerRefCallback,
showDividers: showItemDividers,
selectionVariant: selectionVariant,
...listProps,
role: "listbox",
id: listId,
sx: {
flexGrow: 1
},
children: groupMetadata !== null && groupMetadata !== void 0 && groupMetadata.length ? groupMetadata.map((group, index$1) => {
var _group$header, _group$header2;
if (index$1 === firstGroupIndex_0 && getItemListForEachGroup(group.groupId).length === 0) {
firstGroupIndex_0++; // Increment firstGroupIndex if the first group has no items
}
return /*#__PURE__*/jsxRuntime.jsxs(index.ActionList.Group, {
children: [/*#__PURE__*/jsxRuntime.jsx(index.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__*/jsxRuntime.jsx(MappedActionListItem, {
className: clsx.clsx(FilteredActionList_module.ActionListItem, 'className' in item_3 ? item_3.className : undefined),
"data-input-focused": isInputFocused ? '' : undefined,
"data-first-child": index$1 === firstGroupIndex_0 && itemIndex === 0 ? '' : undefined,
...item_3,
renderItem: listProps.renderItem
}, key);
})]
}, index$1);
}) : 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__*/jsxRuntime.jsx(MappedActionListItem, {
className: clsx.clsx(FilteredActionList_module.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 (usingRemoveActiveDescendant) {
return /*#__PURE__*/jsxRuntime.jsx(ActionListContainerContext.ActionListContainerContext.Provider, {
value: {
container: 'FilteredActionList',
listRole: 'listbox',
selectionAttribute: 'aria-selected',
selectionVariant,
enableFocusZone: true
},
children: actionListContent
});
} else {
return actionListContent;
}
}
return /*#__PURE__*/jsxRuntime.jsxs(Box, {
ref: inputAndListContainerRef,
display: "flex",
flexDirection: "column",
overflow: "hidden",
sx: sx,
className: className,
"data-testid": "filtered-action-list",
children: [/*#__PURE__*/jsxRuntime.jsx(StyledHeader, {
children: /*#__PURE__*/jsxRuntime.jsx(TextInput, {
ref: inputRef,
block: true,
width: "auto",
color: "fg.default",
value: filterValue,
onChange: onInputChange,
onKeyPress: onInputKeyPress,
onKeyDown: usingRemoveActiveDescendant ? 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.clsx(textInputProps === null || textInputProps === void 0 ? void 0 : textInputProps.className, fullScreenOnNarrow && FilteredActionList_module.FullScreenTextInput),
...textInputProps
})
}), /*#__PURE__*/jsxRuntime.jsx(VisuallyHidden.VisuallyHidden, {
id: inputDescriptionTextId,
children: "Items will be filtered as you type"
}), onSelectAllChange !== undefined && /*#__PURE__*/jsxRuntime.jsxs("div", {
className: FilteredActionList_module.SelectAllContainer,
children: [/*#__PURE__*/jsxRuntime.jsx(Checkbox, {
id: "select-all-checkbox",
className: FilteredActionList_module.SelectAllCheckbox,
checked: selectAllChecked,
indeterminate: selectAllIndeterminate,
onChange: handleSelectAllChange
}), /*#__PURE__*/jsxRuntime.jsx("label", {
className: FilteredActionList_module.SelectAllLabel,
htmlFor: "select-all-checkbox",
children: selectAllLabelText
})]
}), /*#__PURE__*/jsxRuntime.jsx("div", {
ref: scrollContainerRef,
className: FilteredActionList_module.Container,
children: getBodyContent()
})]
});
}
FilteredActionList.displayName = "FilteredActionList";
function MappedActionListItem(item) {
const $ = reactCompilerRuntime.c(35);
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__*/jsxRuntime.jsx(index.ActionList.LeadingVisual, {
children: /*#__PURE__*/jsxRuntime.jsx(LeadingVisual, {})
}) : null;
$[17] = LeadingVisual;
$[18] = t1;
} else {
t1 = $[18];
}
let t2;
if ($[19] !== description || $[20] !== descriptionVariant) {
t2 = description ? /*#__PURE__*/jsxRuntime.jsx(index.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__*/jsxRuntime.jsx(index.ActionList.TrailingVisual, {
children: typeof TrailingVisual !== "string" && reactIs.isValidElementType(TrailingVisual) ? /*#__PURE__*/jsxRuntime.jsx(TrailingVisual, {}) : TrailingVisual
}) : TrailingIcon || trailingText ? /*#__PURE__*/jsxRuntime.jsxs(index.ActionList.TrailingVisual, {
children: [trailingText, TrailingIcon && /*#__PURE__*/jsxRuntime.jsx(TrailingIcon, {})]
}) : null;
$[22] = TrailingIcon;
$[23] = TrailingVisual;
$[24] = trailingText;
$[25] = t3;
} else {
t3 = $[25];
}
let t4;
if ($[26] !== children || $[27] !== id || $[28] !== rest || $[29] !== t0 || $[30] !== t1 || $[31] !== t2 || $[32] !== t3 || $[33] !== text) {
t4 = /*#__PURE__*/jsxRuntime.jsxs(index.ActionList.Item, {
role: "option",
onSelect: t0,
"data-id": id,
...rest,
children: [t1, children, text, t2, t3]
});
$[26] = children;
$[27] = id;
$[28] = rest;
$[29] = t0;
$[30] = t1;
$[31] = t2;
$[32] = t3;
$[33] = text;
$[34] = t4;
} else {
t4 = $[34];
}
return t4;
}
FilteredActionList.displayName = 'FilteredActionList';
exports.FilteredActionList = FilteredActionList;