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