@primer/react
Version:
An implementation of GitHub's Primer Design System using React
141 lines (137 loc) • 5.84 kB
JavaScript
import { 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 '../deprecated/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';
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: "FilteredActionListWithDeprecatedActionList__StyledHeader",
componentId: "sc-o1spcd-0"
})(["box-shadow:0 1px 0 ", ";z-index:1;"], get('colors.border.default'));
function FilteredActionList({
loading = false,
loadingType = FilteredActionListLoadingTypes.bodySpinner,
placeholderText,
filterValue: externalFilterValue,
onFilterChange,
onListContainerRefChanged,
onInputRefChanged,
items,
textInputProps,
inputRef: providedInputRef,
sx,
className,
announcementsEnabled: _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 [listContainerElement, setListContainerElement] = useState(null);
const inputRef = useProvidedRefOrCreate(providedInputRef);
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
},
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]);
useScrollFlash(scrollContainerRef);
return /*#__PURE__*/React.createElement(Box, {
display: "flex",
flexDirection: "column",
overflow: "hidden",
flexGrow: 1,
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,
"aria-label": placeholderText,
"aria-controls": listId,
"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",
flexGrow: 1
}, loading && scrollContainerRef.current && loadingType.appearsInBody ? /*#__PURE__*/React.createElement(FilteredActionListBodyLoader, {
loadingType: loadingType,
height: scrollContainerRef.current.clientHeight
}) : /*#__PURE__*/React.createElement(ActionList, _extends({
ref: listContainerRefCallback,
items: items
}, listProps, {
role: "listbox",
id: listId
}))));
}
FilteredActionList.displayName = "FilteredActionList";
FilteredActionList.displayName = 'FilteredActionList';
export { FilteredActionList };