UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

218 lines (214 loc) • 9.62 kB
import { FocusKeys, scrollIntoView } from '@primer/behaviors'; import React, { useCallback, useRef, useState, useEffect } from 'react'; import styled from 'styled-components'; import Box from '../Box/Box.js'; import TextInput from '../TextInput/TextInput.js'; import { get } from '../constants.js'; 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 { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden.js'; import { FilteredActionListBodyLoader, FilteredActionListLoadingTypes } from './FilteredActionListLoaders.js'; import { isValidElementType } from 'react-is'; import { useAnnouncements } from './useAnnouncements.js'; function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } const menuScrollMargins = { startMargin: 0, endMargin: 8 }; const StyledHeader = styled.div.withConfig({ displayName: "FilteredActionListWithModernActionList__StyledHeader", componentId: "sc-1d2kw5v-0" })(["box-shadow:0 1px 0 ", ";z-index:1;"], get('colors.border.default')); function FilteredActionList({ loading = false, placeholderText, filterValue: externalFilterValue, loadingType = FilteredActionListLoadingTypes.bodySpinner, onFilterChange, onListContainerRefChanged, onInputRefChanged, items, textInputProps, inputRef: providedInputRef, sx, groupMetadata, showItemDividers, className, announcementsEnabled = true, ...listProps }) { const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, ''); const onInputChange = useCallback(e => { const value = e.target.value; onFilterChange(value, e); setInternalFilterValue(value); }, [onFilterChange, setInternalFilterValue]); const scrollContainerRef = useRef(null); const inputRef = useProvidedRefOrCreate(providedInputRef); const [listContainerElement, setListContainerElement] = useState(null); const activeDescendantRef = useRef(); const listId = useId(); const inputDescriptionTextId = useId(); const onInputKeyPress = useCallback(event => { if (event.key === 'Enter' && activeDescendantRef.current) { event.preventDefault(); event.nativeEvent.stopImmediatePropagation(); // Forward Enter key press to active descendant so that item gets activated const activeDescendantEvent = new KeyboardEvent(event.type, event.nativeEvent); activeDescendantRef.current.dispatchEvent(activeDescendantEvent); } }, [activeDescendantRef]); const listContainerRefCallback = useCallback(node => { setListContainerElement(node); onListContainerRefChanged === null || onListContainerRefChanged === undefined ? undefined : onListContainerRefChanged(node); }, [onListContainerRefChanged]); useEffect(() => { onInputRefChanged === null || onInputRefChanged === undefined ? undefined : onInputRefChanged(inputRef); }, [inputRef, onInputRefChanged]); useFocusZone({ containerRef: { current: listContainerElement }, bindKeys: FocusKeys.ArrowVertical | FocusKeys.PageUpDown, focusOutBehavior: 'wrap', 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); } } }, [ // List container isn't in the DOM while loading. Need to re-bind focus zone when it changes. listContainerElement]); useEffect(() => { // if items changed, we want to instantly move active descendant into view if (activeDescendantRef.current && scrollContainerRef.current) { scrollIntoView(activeDescendantRef.current, scrollContainerRef.current, { ...menuScrollMargins, behavior: 'auto' }); } }, [items]); useAnnouncements(items, { current: listContainerElement }, inputRef, announcementsEnabled); useScrollFlash(scrollContainerRef); function getItemListForEachGroup(groupId) { const itemsInGroup = []; for (const item of items) { // Look up the group associated with the current item. if (item.groupId === groupId) { itemsInGroup.push(item); } } return itemsInGroup; } return /*#__PURE__*/React.createElement(Box, { display: "flex", flexDirection: "column", overflow: "hidden", sx: sx, className: className, "data-testid": "filtered-action-list" }, /*#__PURE__*/React.createElement(StyledHeader, null, /*#__PURE__*/React.createElement(TextInput, _extends({ ref: inputRef, block: true, width: "auto", color: "fg.default", value: filterValue, onChange: onInputChange, onKeyPress: onInputKeyPress, 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 }, textInputProps))), /*#__PURE__*/React.createElement(VisuallyHidden, { id: inputDescriptionTextId }, "Items will be filtered as you type"), /*#__PURE__*/React.createElement(Box, { ref: scrollContainerRef, overflow: "auto", display: "flex", flexGrow: 1 }, loading && scrollContainerRef.current && loadingType.appearsInBody ? /*#__PURE__*/React.createElement(FilteredActionListBodyLoader, { loadingType: loadingType, height: scrollContainerRef.current.clientHeight }) : /*#__PURE__*/React.createElement(ActionList, _extends({ ref: listContainerRefCallback, showDividers: showItemDividers }, listProps, { role: "listbox", id: listId, sx: { flexGrow: 1 } }), groupMetadata !== null && groupMetadata !== undefined && groupMetadata.length ? groupMetadata.map((group, index) => { var _group$header, _group$header2; return /*#__PURE__*/React.createElement(ActionList.Group, { key: index }, /*#__PURE__*/React.createElement(ActionList.GroupHeading, { variant: (_group$header = group.header) !== null && _group$header !== undefined && _group$header.variant ? group.header.variant : undefined }, (_group$header2 = group.header) !== null && _group$header2 !== undefined && _group$header2.title ? group.header.title : `Group ${group.groupId}`), getItemListForEachGroup(group.groupId).map((item, index) => { var _ref, _item$key, _item$id; const key = (_ref = (_item$key = item.key) !== null && _item$key !== undefined ? _item$key : (_item$id = item.id) === null || _item$id === undefined ? undefined : _item$id.toString()) !== null && _ref !== undefined ? _ref : index.toString(); return /*#__PURE__*/React.createElement(MappedActionListItem, _extends({ key: key }, item, { renderItem: listProps.renderItem })); })); }) : items.map((item, index) => { var _ref2, _item$key2, _item$id2; const key = (_ref2 = (_item$key2 = item.key) !== null && _item$key2 !== undefined ? _item$key2 : (_item$id2 = item.id) === null || _item$id2 === undefined ? undefined : _item$id2.toString()) !== null && _ref2 !== undefined ? _ref2 : index.toString(); return /*#__PURE__*/React.createElement(MappedActionListItem, _extends({ key: key }, item, { renderItem: listProps.renderItem })); })))); } FilteredActionList.displayName = "FilteredActionList"; function MappedActionListItem(item) { // keep backward compatibility for renderItem // escape hatch for custom Item rendering if (typeof item.renderItem === 'function') return item.renderItem(item); const { id, description, descriptionVariant, text, trailingVisual: TrailingVisual, leadingVisual: LeadingVisual, trailingText, trailingIcon: TrailingIcon, onAction, children, ...rest } = item; return /*#__PURE__*/React.createElement(ActionList.Item, _extends({ role: "option" // @ts-ignore - for now , onSelect: e => { if (typeof onAction === 'function') onAction(item, e); }, "data-id": id }, rest), LeadingVisual ? /*#__PURE__*/React.createElement(ActionList.LeadingVisual, null, /*#__PURE__*/React.createElement(LeadingVisual, null)) : null, children, text, description ? /*#__PURE__*/React.createElement(ActionList.Description, { variant: descriptionVariant }, description) : null, TrailingVisual ? /*#__PURE__*/React.createElement(ActionList.TrailingVisual, null, typeof TrailingVisual !== 'string' && isValidElementType(TrailingVisual) ? /*#__PURE__*/React.createElement(TrailingVisual, null) : TrailingVisual) : TrailingIcon || trailingText ? /*#__PURE__*/React.createElement(ActionList.TrailingVisual, null, trailingText, TrailingIcon && /*#__PURE__*/React.createElement(TrailingIcon, null)) : null); } MappedActionListItem.displayName = "MappedActionListItem"; FilteredActionList.displayName = 'FilteredActionList'; export { FilteredActionList };