UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

445 lines (444 loc) • 18.1 kB
import { useProvidedRefOrCreate } from "../../hooks/useProvidedRefOrCreate.js"; import { useAnchoredPosition } from "../../hooks/useAnchoredPosition.js"; import { useId as useId$1 } from "../../hooks/useId.js"; import Spinner from "../../Spinner/Spinner.js"; import { AriaStatus } from "../../live-region/AriaStatus.js"; import { invariant } from "../../utils/invariant.js"; import { IconButton } from "../../Button/IconButton.js"; import { ButtonComponent } from "../../Button/Button.js"; import { useResponsiveValue } from "../../hooks/useResponsiveValue.js"; import { isSlot } from "../../utils/is-slot.js"; import { useSlots } from "../../hooks/useSlots.js"; import { ActionListContainerContext } from "../../ActionList/ActionListContainerContext.js"; import Heading from "../../Heading/Heading.js"; import Link from "../../Link/Link.js"; import { BaseOverlay } from "../../Overlay/Overlay.js"; import { heightMap } from "../../Overlay/constants.js"; import TextInput_default from "../../TextInput/TextInput.js"; import { useFormControlForwardedProps } from "../../FormControl/_FormControlContext.js"; import Checkbox from "../../Checkbox/Checkbox.js"; import Octicon from "../../Octicon/Octicon.js"; import { InputLabel } from "../../internal/components/InputLabel.js"; import SelectPanel_module_css_default from "./SelectPanel.module.css.js"; import { clsx } from "clsx"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import React, { useEffect, useState } from "react"; import { AlertIcon, ArrowLeftIcon, FilterRemoveIcon, SearchIcon, XCircleFillIcon, XIcon } from "@primer/octicons-react"; //#region src/experimental/SelectPanel2/SelectPanel.tsx const SelectPanelContext = /*#__PURE__*/ React.createContext({ title: "", description: void 0, panelId: "", onCancel: () => {}, onClearSelection: void 0, searchQuery: "", setSearchQuery: () => {}, selectionVariant: "multiple", moveFocusToList: () => {} }); const responsiveButtonSizes = { narrow: "medium", regular: "small" }; const Panel = ({ title, description, variant: propsVariant, selectionVariant = "multiple", id, defaultOpen = false, open: propsOpen, anchorRef: providedAnchorRef, anchoredPositionSettings, onCancel: propsOnCancel, onClearSelection: propsOnClearSelection, onSubmit: propsOnSubmit, width = "medium", maxHeight = "large", className, ...props }) => { var _position$top, _position$left, _slots$header; const [internalOpen, setInternalOpen] = React.useState(defaultOpen); const currentVariant = useResponsiveValue(Object.assign({ regular: "anchored", narrow: "full-screen" }, typeof propsVariant === "string" ? { regular: propsVariant } : propsVariant), "anchored"); if (propsOpen !== void 0 && internalOpen !== propsOpen) setInternalOpen(propsOpen); let Anchor; const anchorRef = useProvidedRefOrCreate(providedAnchorRef); const onAnchorClick = () => { if (!internalOpen) setInternalOpen(true); else onInternalCancel(); }; const contents = React.Children.map(props.children, (child) => { if (/*#__PURE__*/ React.isValidElement(child) && (child.type === SelectPanelButton || isSlot(child, SelectPanelButton))) { Anchor = /*#__PURE__*/ React.cloneElement(child, { ref: anchorRef, onClick: child.props.onClick || onAnchorClick, "aria-haspopup": true, "aria-expanded": internalOpen }); return null; } return child; }); const onInternalClose = React.useCallback(() => { if (internalOpen === false) return; if (propsOpen === void 0) setInternalOpen(false); }, [internalOpen, propsOpen]); const onInternalCancel = React.useCallback(() => { onInternalClose(); if (typeof propsOnCancel === "function") propsOnCancel(); }, [onInternalClose, propsOnCancel]); const onInternalSubmit = (event) => { event === null || event === void 0 || event.preventDefault(); onInternalClose(); if (typeof propsOnSubmit === "function") propsOnSubmit(event); }; const onInternalClearSelection = () => { if (typeof propsOnClearSelection === "function") propsOnClearSelection(); }; const internalAfterSelect = (event) => { if (selectionVariant === "instant") onInternalSubmit(); if (event.type === "keypress") { if (event.key === "Enter") onInternalSubmit(); } }; const [searchQuery, setSearchQuery] = React.useState(""); const panelId = useId$1(id); const [slots, childrenInBody] = useSlots(contents, { header: SelectPanelHeader, footer: SelectPanelFooter }); const moveFocusToList = () => { var _dialogRef$current; const firstListElement = (_dialogRef$current = dialogRef.current) === null || _dialogRef$current === void 0 ? void 0 : _dialogRef$current.querySelector("ul[role=listbox] li:not([role=none])"); firstListElement === null || firstListElement === void 0 || firstListElement.focus(); }; const dialogRef = React.useRef(null); React.useEffect(() => { var _dialogRef$current2, _dialogRef$current3; if (internalOpen) (_dialogRef$current2 = dialogRef.current) === null || _dialogRef$current2 === void 0 || _dialogRef$current2.showModal(); else if ((_dialogRef$current3 = dialogRef.current) !== null && _dialogRef$current3 !== void 0 && _dialogRef$current3.open) dialogRef.current.close(); }, [internalOpen]); React.useEffect(() => { const dialogEl = dialogRef.current; dialogEl === null || dialogEl === void 0 || dialogEl.addEventListener("close", onInternalClose); return () => dialogEl === null || dialogEl === void 0 ? void 0 : dialogEl.removeEventListener("close", onInternalClose); }, [onInternalClose]); React.useEffect(() => { const dialogEl = dialogRef.current; const handler = (event) => { if (event.key === "Escape") onInternalCancel(); }; dialogEl === null || dialogEl === void 0 || dialogEl.addEventListener("keydown", handler); return () => dialogEl === null || dialogEl === void 0 ? void 0 : dialogEl.removeEventListener("keydown", handler); }, [onInternalCancel]); React.useEffect(function initialFocus() { if (internalOpen) { const searchInput = document.querySelector("dialog[open] input"); if (searchInput) searchInput.focus(); else moveFocusToList(); } }, [internalOpen]); const { position } = useAnchoredPosition({ anchorElementRef: anchorRef, floatingElementRef: dialogRef, side: "outside-bottom", align: "start", ...anchoredPositionSettings }, [ internalOpen, anchorRef.current, dialogRef.current ]); const onClickOutside = onInternalCancel; let maxHeightValue = heightMap[maxHeight]; if (currentVariant === "bottom-sheet") maxHeightValue = "calc(100vh - 64px)"; else if (currentVariant === "full-screen") maxHeightValue = "100vh"; return /*#__PURE__*/ jsxs(Fragment, { children: [Anchor, /*#__PURE__*/ jsx(BaseOverlay, { as: "dialog", ref: dialogRef, "aria-labelledby": `${panelId}--title`, "aria-describedby": description ? `${panelId}--description` : void 0, width, height: "fit-content", maxHeight, "data-variant": currentVariant, style: { "--max-height": maxHeightValue, "--position-top": `${(_position$top = position === null || position === void 0 ? void 0 : position.top) !== null && _position$top !== void 0 ? _position$top : 0}px`, "--position-left": `${(_position$left = position === null || position === void 0 ? void 0 : position.left) !== null && _position$left !== void 0 ? _position$left : 0}px`, visibility: internalOpen ? "visible" : "hidden", display: "flex" }, className: clsx(SelectPanel_module_css_default.Overlay, className), ...props, onClick: (event) => { if (event.target === event.currentTarget) onClickOutside(); }, children: internalOpen && /*#__PURE__*/ jsx(Fragment, { children: /*#__PURE__*/ jsx(SelectPanelContext.Provider, { value: { panelId, title, description, onCancel: onInternalCancel, onClearSelection: propsOnClearSelection ? onInternalClearSelection : void 0, searchQuery, setSearchQuery, selectionVariant, moveFocusToList }, children: /*#__PURE__*/ jsxs("form", { method: "dialog", onSubmit: onInternalSubmit, className: SelectPanel_module_css_default.Form, children: [ (_slots$header = slots.header) !== null && _slots$header !== void 0 ? _slots$header : /*#__PURE__*/ jsx(SelectPanelHeader, {}), /*#__PURE__*/ jsx("div", { className: SelectPanel_module_css_default.Container, children: /*#__PURE__*/ jsx(ActionListContainerContext.Provider, { value: { container: "SelectPanel", listRole: "listbox", selectionAttribute: "aria-selected", selectionVariant: selectionVariant === "instant" ? "single" : selectionVariant, afterSelect: internalAfterSelect, listLabelledBy: `${panelId}--title`, enableFocusZone: true }, children: childrenInBody }) }), slots.footer ] }) }) }) })] }); }; const SelectPanelButton = /*#__PURE__*/ React.forwardRef((props, anchorRef) => { const inputProps = useFormControlForwardedProps(props); const [labelText, setLabelText] = useState(""); useEffect(() => { const label = document.querySelector(`[for='${inputProps.id}']`); if (label !== null && label !== void 0 && label.textContent) setLabelText(label.textContent); }, [inputProps.id]); if (labelText) return /*#__PURE__*/ jsx(ButtonComponent, { ref: anchorRef, "aria-label": `${anchorRef.current.textContent}, ${labelText}`, ...inputProps }); else return /*#__PURE__*/ jsx(ButtonComponent, { ref: anchorRef, ...props }); }); SelectPanelButton.__SLOT__ = Symbol("SelectPanel.Button"); const SelectPanelHeader = ({ children, onBack, className, ...props }) => { const [slots, childrenWithoutSlots] = useSlots(children, { searchInput: SelectPanelSearchInput }); const { title, description, panelId, onCancel, onClearSelection } = React.useContext(SelectPanelContext); return /*#__PURE__*/ jsxs("div", { className: clsx(SelectPanel_module_css_default.Header, className), ...props, children: [ /*#__PURE__*/ jsxs("div", { className: SelectPanel_module_css_default.HeaderContent, "data-description": description ? true : void 0, "data-search-input": slots.searchInput ? true : void 0, children: [/*#__PURE__*/ jsxs("div", { className: SelectPanel_module_css_default.FlexBox, children: [onBack ? /*#__PURE__*/ jsx(IconButton, { type: "button", variant: "invisible", icon: ArrowLeftIcon, "aria-label": "Back", onClick: () => onBack() }) : null, /*#__PURE__*/ jsxs("div", { className: SelectPanel_module_css_default.TitleWrapper, "data-description": description ? true : void 0, "data-on-back": onBack ? true : void 0, children: [/*#__PURE__*/ jsx(Heading, { as: "h1", id: `${panelId}--title`, className: SelectPanel_module_css_default.Title, children: title }), description ? /*#__PURE__*/ jsx("span", { id: `${panelId}--description`, className: SelectPanel_module_css_default.Description, children: description }) : null] })] }), /*#__PURE__*/ jsxs("div", { children: [onClearSelection ? /*#__PURE__*/ jsx(IconButton, { type: "button", variant: "invisible", icon: FilterRemoveIcon, "aria-label": "Clear selection", onClick: onClearSelection }) : null, /*#__PURE__*/ jsx(IconButton, { type: "button", variant: "invisible", icon: XIcon, "aria-label": "Close", onClick: () => onCancel() })] })] }), slots.searchInput, childrenWithoutSlots ] }); }; SelectPanelHeader.displayName = "SelectPanelHeader"; SelectPanelHeader.__SLOT__ = Symbol("SelectPanel.Header"); const SelectPanelSearchInput = ({ onChange: propsOnChange, onKeyDown: propsOnKeyDown, className, ...props }) => { const inputRef = /*#__PURE__*/ React.createRef(); const { setSearchQuery, moveFocusToList } = React.useContext(SelectPanelContext); const internalOnChange = (event) => { if (typeof propsOnChange === "function") propsOnChange(event); else setSearchQuery(event.target.value); }; const internalKeyDown = (event) => { if (event.key === "ArrowDown") { event.preventDefault(); moveFocusToList(); } if (typeof propsOnKeyDown === "function") propsOnKeyDown(event); }; return /*#__PURE__*/ jsx(TextInput_default, { ref: inputRef, block: true, leadingVisual: SearchIcon, placeholder: "Search", trailingAction: /*#__PURE__*/ jsx(TextInput_default.Action, { icon: XCircleFillIcon, "aria-label": "Clear", tooltipDirection: "w", className: SelectPanel_module_css_default.ClearAction, onClick: () => { if (inputRef.current) inputRef.current.value = ""; if (typeof propsOnChange === "function") propsOnChange({ target: inputRef.current, currentTarget: inputRef.current }); } }), className: clsx(SelectPanel_module_css_default.TextInput, className), onChange: internalOnChange, onKeyDown: internalKeyDown, ...props }); }; SelectPanelSearchInput.displayName = "SelectPanelSearchInput"; SelectPanelSearchInput.__SLOT__ = Symbol("SelectPanel.SearchInput"); const FooterContext = /*#__PURE__*/ React.createContext(false); const SelectPanelFooter = ({ ...props }) => { const { onCancel, selectionVariant } = React.useContext(SelectPanelContext); const hidePrimaryActions = selectionVariant === "instant"; const buttonSize = useResponsiveValue(responsiveButtonSizes, "small"); if (hidePrimaryActions && !props.children) return null; return /*#__PURE__*/ jsx(FooterContext.Provider, { value: true, children: /*#__PURE__*/ jsxs("div", { className: SelectPanel_module_css_default.Footer, "data-hide-primary-actions": hidePrimaryActions || void 0, children: [/*#__PURE__*/ jsx("div", { className: SelectPanel_module_css_default.FooterContent, "data-hide-primary-actions": hidePrimaryActions || void 0, children: props.children }), hidePrimaryActions ? null : /*#__PURE__*/ jsxs("div", { className: SelectPanel_module_css_default.FooterActions, children: [/*#__PURE__*/ jsx(ButtonComponent, { type: "button", size: buttonSize, onClick: () => onCancel(), children: "Cancel" }), /*#__PURE__*/ jsx(ButtonComponent, { type: "submit", size: buttonSize, variant: "primary", children: "Save" })] })] }) }); }; SelectPanelFooter.displayName = "SelectPanelFooter"; SelectPanelFooter.__SLOT__ = Symbol("SelectPanel.Footer"); const SecondaryButton = (props) => { return /*#__PURE__*/ jsx(ButtonComponent, { type: "button", size: useResponsiveValue(responsiveButtonSizes, "small"), block: true, ...props }); }; SecondaryButton.displayName = "SecondaryButton"; const SecondaryLink = ({ className, ...props }) => { return /*#__PURE__*/ jsx(ButtonComponent, { as: Link, size: useResponsiveValue(responsiveButtonSizes, "small"), variant: "invisible", block: true, ...props, className: clsx(SelectPanel_module_css_default.SmallText, className), children: props.children }); }; SecondaryLink.displayName = "SecondaryLink"; const SecondaryCheckbox = ({ id, children, className, ...props }) => { const checkboxId = useId$1(id); const { selectionVariant } = React.useContext(SelectPanelContext); !(selectionVariant !== "instant") && invariant(false, "Sorry! SelectPanel.SecondaryAction with variant=\"checkbox\" is not allowed inside selectionVariant=\"instant\""); return /*#__PURE__*/ jsxs("div", { className: SelectPanel_module_css_default.SecondaryCheckbox, children: [/*#__PURE__*/ jsx(Checkbox, { id: checkboxId, className: clsx(SelectPanel_module_css_default.Checkbox, className), ...props }), /*#__PURE__*/ jsx(InputLabel, { htmlFor: checkboxId, className: SelectPanel_module_css_default.SmallText, children })] }); }; SecondaryCheckbox.displayName = "SecondaryCheckbox"; const SelectPanelSecondaryAction = ({ variant, ...props }) => { !React.useContext(FooterContext) && invariant(false, "SelectPanel.SecondaryAction is only allowed inside SelectPanel.Footer"); if (variant === "button") return /*#__PURE__*/ jsx(SecondaryButton, { ...props }); else if (variant === "link") return /*#__PURE__*/ jsx(SecondaryLink, { ...props }); else if (variant === "checkbox") return /*#__PURE__*/ jsx(SecondaryCheckbox, { ...props }); }; const SelectPanelLoading = ({ children = "Fetching items..." }) => { return /*#__PURE__*/ jsxs(AriaStatus, { announceOnShow: true, className: SelectPanel_module_css_default.SelectPanelLoading, children: [/*#__PURE__*/ jsx(Spinner, { size: "medium", srText: null }), /*#__PURE__*/ jsx("span", { className: SelectPanel_module_css_default.LoadingText, children })] }); }; SelectPanelLoading.displayName = "SelectPanelLoading"; const SelectPanelMessage = ({ variant = "warning", size = variant === "empty" ? "full" : "inline", title, children }) => { const MessageWrapper = variant === "empty" ? "div" : AriaStatus; if (size === "full") return /*#__PURE__*/ jsxs(MessageWrapper, { className: SelectPanel_module_css_default.MessageFull, children: [ variant !== "empty" ? /*#__PURE__*/ jsx(Octicon, { icon: AlertIcon, className: clsx(SelectPanel_module_css_default.Octicon, variant === "error" ? SelectPanel_module_css_default.Error : void 0, variant === "warning" ? SelectPanel_module_css_default.Warning : void 0) }) : null, /*#__PURE__*/ jsx("span", { className: SelectPanel_module_css_default.MessageTitle, children: title }), /*#__PURE__*/ jsx("span", { className: SelectPanel_module_css_default.MessageContent, children }) ] }); else return /*#__PURE__*/ jsxs(MessageWrapper, { className: SelectPanel_module_css_default.MessageInline, "data-variant": variant, children: [/*#__PURE__*/ jsx(AlertIcon, { size: 16 }), /*#__PURE__*/ jsx("div", { children })] }); }; const SelectPanel = Object.assign(Panel, { Button: SelectPanelButton, Header: SelectPanelHeader, SearchInput: SelectPanelSearchInput, Footer: SelectPanelFooter, Loading: SelectPanelLoading, Message: SelectPanelMessage, SecondaryAction: SelectPanelSecondaryAction }); //#endregion export { SelectPanel };