UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

764 lines (731 loc) 31.1 kB
import { SearchIcon, XIcon, TriangleDownIcon } from '@primer/octicons-react'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { SelectPanelMessage } from './SelectPanelMessage.js'; import { IconButton } from '../Button/IconButton.js'; import { LinkButton } from '../Button/LinkButton.js'; import { ButtonComponent } from '../Button/Button.js'; import { useId } from '../hooks/useId.js'; import { useProvidedStateOrCreate } from '../hooks/useProvidedStateOrCreate.js'; import useSafeTimeout from '../hooks/useSafeTimeout.js'; import { FilteredActionListLoadingTypes } from '../FilteredActionList/FilteredActionListLoaders.js'; import { announceFromElement, announce } from '@primer/live-region-element'; import classes from './SelectPanel.module.css.js'; import { clsx } from 'clsx'; import { debounce } from '@github/mini-throttle'; import { useResponsiveValue } from '../hooks/useResponsiveValue.js'; import { Banner } from '../Banner/index.js'; import { isAlphabetKey } from '../hooks/useMnemonics.js'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { useFeatureFlag } from '../FeatureFlags/useFeatureFlag.js'; import { useProvidedRefOrCreate } from '../hooks/useProvidedRefOrCreate.js'; import { AnchoredOverlay } from '../AnchoredOverlay/AnchoredOverlay.js'; import Heading from '../Heading/Heading.js'; import { FilteredActionList } from '../FilteredActionList/FilteredActionList.js'; const SHORT_DELAY_MS = 500; const LONG_DELAY_MS = 1000; const EMPTY_MESSAGE = { title: 'No items available', description: '' }; const DefaultEmptyMessage = /*#__PURE__*/jsx(SelectPanelMessage, { variant: "empty", title: EMPTY_MESSAGE.title, children: EMPTY_MESSAGE.description }, "empty-message"); async function announceText(text, delayMs = SHORT_DELAY_MS) { const liveRegion = document.querySelector('live-region'); liveRegion === null || liveRegion === void 0 ? void 0 : liveRegion.clear(); // clear previous announcements await announce(text, { delayMs, from: liveRegion ? liveRegion : undefined // announce will create a liveRegion if it doesn't find one }); } async function announceLoading() { await announceText('Loading.'); } // onCancel is optional with variant=anchored, but required with variant=modal function isMultiSelectVariant(selected) { return Array.isArray(selected); } const focusZoneSettings = { // Let FilteredActionList handle focus zone disabled: true }; const areItemsEqual = (itemA, itemB) => { // prefer checking equivality by item.id if (typeof itemA.id !== 'undefined') return itemA.id === itemB.id;else return itemA === itemB; }; const doesItemsIncludeItem = (items, item) => { return items.some(i => areItemsEqual(i, item)); }; const defaultRenderAnchor = props => { const { children, ...rest } = props; return /*#__PURE__*/jsx(ButtonComponent, { trailingAction: TriangleDownIcon, ...rest, children: children }); }; defaultRenderAnchor.displayName = "defaultRenderAnchor"; function Panel({ open, onOpenChange, renderAnchor = defaultRenderAnchor, anchorRef: externalAnchorRef, placeholder, placeholderText = 'Filter items', inputLabel = placeholderText, selected, title = isMultiSelectVariant(selected) ? 'Select items' : 'Select an item', subtitle, onSelectedChange, filterValue: externalFilterValue, onFilterChange: externalOnFilterChange, items, footer, textInputProps, overlayProps, loading, initialLoadingType = 'spinner', className, height, width, id, message, notice, onCancel, variant = 'anchored', secondaryAction, showSelectedOptionsFirst = true, disableFullscreenOnNarrow, align, showSelectAll = false, ...listProps }) { var _listProps$groupMetad; const titleId = useId(); const subtitleId = useId(); const [dataLoadedOnce, setDataLoadedOnce] = useState(false); const [isLoading, setIsLoading] = useState(false); const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, ''); const { safeSetTimeout, safeClearTimeout } = useSafeTimeout(); const loadingDelayTimeoutId = useRef(null); const loadingManagedInternally = loading === undefined; const loadingManagedExternally = !loadingManagedInternally; const [inputRef, setInputRef] = React.useState(null); const [listContainerElement, setListContainerElement] = useState(null); const [needsNoItemsAnnouncement, setNeedsNoItemsAnnouncement] = useState(false); const isNarrowScreenSize = useResponsiveValue({ narrow: true, regular: false, wide: false }, false); const [selectedOnSort, setSelectedOnSort] = useState([]); const [prevItems, setPrevItems] = useState([]); const [prevOpen, setPrevOpen] = useState(open); const initialHeightRef = useRef(0); const initialScaleRef = useRef(1); const noticeRef = useRef(null); const [isKeyboardVisible, setIsKeyboardVisible] = useState(false); const [availablePanelHeight, setAvailablePanelHeight] = useState(undefined); const KEYBOARD_VISIBILITY_THRESHOLD = 10; const featureFlagFullScreenOnNarrow = useFeatureFlag('primer_react_select_panel_fullscreen_on_narrow'); const usingFullScreenOnNarrow = disableFullscreenOnNarrow ? false : featureFlagFullScreenOnNarrow; const shouldOrderSelectedFirst = useFeatureFlag('primer_react_select_panel_order_selected_at_top') && showSelectedOptionsFirst; // Single select modals work differently, they have an intermediate state where the user has selected an item but // has not yet confirmed the selection. This is the only time the user can cancel the selection. const isSingleSelectModal = variant === 'modal' && !isMultiSelectVariant(selected); const [intermediateSelected, setIntermediateSelected] = useState(isSingleSelectModal ? selected : undefined); // Reset the intermediate selected item when the panel is open/closed useEffect(() => { setIntermediateSelected(isSingleSelectModal ? selected : undefined); }, [isSingleSelectModal, open, selected]); const onListContainerRefChanged = useCallback(node => { setListContainerElement(node); if (!node && needsNoItemsAnnouncement) { setNeedsNoItemsAnnouncement(false); } }, [needsNoItemsAnnouncement]); const onInputRefChanged = useCallback(ref => { setInputRef(ref); }, [setInputRef]); const resetSort = useCallback(() => { if (isMultiSelectVariant(selected)) { setSelectedOnSort(selected); } else if (selected) { setSelectedOnSort([selected]); } else { setSelectedOnSort([]); } }, [selected]); const onFilterChange = useCallback((value, e) => { if (loadingManagedInternally) { if (loadingDelayTimeoutId.current) { safeClearTimeout(loadingDelayTimeoutId.current); } if (dataLoadedOnce) { // If data has already been loaded once, delay the spinner a bit. This also helps // not show and then immediately hide the spinner if items are loaded quickly, i.e. // not async. loadingDelayTimeoutId.current = safeSetTimeout(() => { setIsLoading(true); announceLoading(); }, LONG_DELAY_MS); } else { // If this is the first data load and there are no items, show the loading spinner // immediately if (items.length === 0) { setIsLoading(true); } // We still want to announce if loading is taking too long loadingDelayTimeoutId.current = safeSetTimeout(() => { announceLoading(); }, LONG_DELAY_MS); } } externalOnFilterChange(value, e); setInternalFilterValue(value); if (!value) { resetSort(); } }, [loadingManagedInternally, externalOnFilterChange, setInternalFilterValue, dataLoadedOnce, safeSetTimeout, safeClearTimeout, items.length, resetSort]); const handleSelectAllChange = useCallback(checked => { // Exit early if not in multi-select mode if (!isMultiSelectVariant(selected)) { return; } const multiSelectOnChange = onSelectedChange; const selectedArray = selected; const selectedItemsNotInFilteredView = selectedArray.filter(selectedItem => !items.some(item => areItemsEqual(item, selectedItem))); if (checked) { multiSelectOnChange([...selectedItemsNotInFilteredView, ...items]); } else { multiSelectOnChange(selectedItemsNotInFilteredView); } }, [items, onSelectedChange, selected]); // disable body scroll when the panel is open on narrow screens useEffect(() => { if (open && isNarrowScreenSize && usingFullScreenOnNarrow) { const bodyOverflowStyle = document.body.style.overflow || ''; // If the body is already set to overflow: hidden, it likely means // that there is already a modal open. In that case, we should bail // so we don't re-enable scroll after the second dialog is closed. if (bodyOverflowStyle === 'hidden') { return; } document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = bodyOverflowStyle; }; } }, [isNarrowScreenSize, open, usingFullScreenOnNarrow]); useEffect(() => { if (open) { if (items.length === 0 && !(isLoading || loading)) { // we need to wait for the listContainerElement to disappear before announcing no items, otherwise it will be interrupted setNeedsNoItemsAnnouncement(true); } } if (loadingManagedExternally) { if (items.length > 0) { setDataLoadedOnce(true); } return; } if (isLoading || items.length > 0) { setIsLoading(false); setDataLoadedOnce(true); } if (loadingDelayTimeoutId.current) { safeClearTimeout(loadingDelayTimeoutId.current); } // Only fire this effect if items have changed // eslint-disable-next-line react-hooks/exhaustive-deps }, [items]); useEffect(() => { if (inputRef !== null && inputRef !== void 0 && inputRef.current) { const ref = inputRef.current; // We would normally expect AnchoredOverlay's focus trap to automatically focus the input, // but for some reason the ref isn't populated until _after_ the panel is open, which is // too late. So, we focus manually here. if (open) { ref.focus(); } } }, [inputRef, open]); // Manage loading announcements when loadingManagedExternally useEffect(() => { if (loadingManagedExternally) { if (isLoading) { // Delay the announcement a bit, just in case the loading is quick loadingDelayTimeoutId.current = safeSetTimeout(() => { announceLoading(); }, LONG_DELAY_MS); } else { // If loading is done, we can clear the loading announcement if (loadingDelayTimeoutId.current) { safeClearTimeout(loadingDelayTimeoutId.current); } } } }, [isLoading, loadingManagedExternally, safeSetTimeout, safeClearTimeout]); // Populate panel with items on first open useEffect(() => { if (loadingManagedExternally) return; // If data was already loaded once, do nothing if (dataLoadedOnce) return; // Only load data when the panel is open if (open) { // Only trigger filter change event if there are no items if (items.length === 0) { // Trigger filter event to populate panel on first open onFilterChange(filterValue, null); } } }, [open, dataLoadedOnce, onFilterChange, filterValue, items, loadingManagedExternally, listContainerElement]); useEffect(() => { if (!window.visualViewport || !open || !isNarrowScreenSize) { return; } initialHeightRef.current = window.visualViewport.height; initialScaleRef.current = window.visualViewport.scale; const handleViewportChange = debounce(() => { if (window.visualViewport) { const currentScale = window.visualViewport.scale; const isZooming = currentScale !== initialScaleRef.current; if (!isZooming) { const currentHeight = window.visualViewport.height; const keyboardVisible = initialHeightRef.current - currentHeight > KEYBOARD_VISIBILITY_THRESHOLD; setIsKeyboardVisible(keyboardVisible); setAvailablePanelHeight(keyboardVisible ? currentHeight : undefined); } } }, 100); // keeping this check to satisfy typescript but need eslint to ignore redundancy rule // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (window.visualViewport) { // Using visualViewport to more reliably detect viewport changes across different browsers, which specifically requires these listeners // eslint-disable-next-line github/prefer-observers window.visualViewport.addEventListener('resize', handleViewportChange); // eslint-disable-next-line github/prefer-observers window.visualViewport.addEventListener('scroll', handleViewportChange); } return () => { if (window.visualViewport) { window.visualViewport.removeEventListener('resize', handleViewportChange); window.visualViewport.removeEventListener('scroll', handleViewportChange); } handleViewportChange.cancel(); }; }, [open, isNarrowScreenSize]); useEffect(() => { const announceNotice = async () => { if (!noticeRef.current) return; const liveRegion = document.querySelector('live-region'); liveRegion === null || liveRegion === void 0 ? void 0 : liveRegion.clear(); await announceFromElement(noticeRef.current, { from: liveRegion ? liveRegion : undefined }); }; if (open && notice) { announceNotice(); } }, [notice, open]); const anchorRef = useProvidedRefOrCreate(externalAnchorRef); const onOpen = useCallback(gesture => onOpenChange(true, gesture), [onOpenChange]); const onCancelRequested = useCallback(() => { onOpenChange(false, 'cancel'); }, [onOpenChange]); const onClose = useCallback(gesture => { // Clicking outside should cancel the selection only on modals if (variant === 'modal' && gesture === 'click-outside') { onCancel === null || onCancel === void 0 ? void 0 : onCancel(); } if (gesture === 'close') { onCancel === null || onCancel === void 0 ? void 0 : onCancel(); onCancelRequested(); } else { onOpenChange(false, gesture); } }, [onOpenChange, variant, onCancel, onCancelRequested]); const renderMenuAnchor = useMemo(() => { if (renderAnchor === null) { return null; } const selectedItems = Array.isArray(selected) ? selected : [...(selected ? [selected] : [])]; return props => { return renderAnchor({ ...props, children: selectedItems.length ? selectedItems.map(item => item.text).join(', ') : placeholder }); }; }, [placeholder, renderAnchor, selected]); const isItemCurrentlySelected = useCallback(item => { // For multi-select, we just need to check if the item is in the selected array if (isMultiSelectVariant(selected)) { return doesItemsIncludeItem(selected, item); } // For single-select modal, there is an intermediate state when the user has selected // an item but has not yet saved the selection. We need to check for this state. if (isSingleSelectModal) { return (intermediateSelected === null || intermediateSelected === void 0 ? void 0 : intermediateSelected.id) !== undefined ? intermediateSelected.id === item.id : intermediateSelected === item; } // For single-select anchored, we just need to check if the item is the selected item return (selected === null || selected === void 0 ? void 0 : selected.id) !== undefined ? selected.id === item.id : selected === item; }, [selected, intermediateSelected, isSingleSelectModal]); const itemsToRender = useMemo(() => { return items.map(item => { return { ...item, role: 'option', id: item.id, selected: 'selected' in item && item.selected === undefined ? undefined : isItemCurrentlySelected(item), onAction: (itemFromAction, event) => { var _item$onAction; (_item$onAction = item.onAction) === null || _item$onAction === void 0 ? void 0 : _item$onAction.call(item, itemFromAction, event); if (event.defaultPrevented) { return; } if (isMultiSelectVariant(selected)) { const otherSelectedItems = selected.filter(selectedItem => !areItemsEqual(selectedItem, item)); const newSelectedItems = doesItemsIncludeItem(selected, item) ? otherSelectedItems : [...otherSelectedItems, item]; const multiSelectOnChange = onSelectedChange; multiSelectOnChange(newSelectedItems); return; } if (isSingleSelectModal) { if ((intermediateSelected === null || intermediateSelected === void 0 ? void 0 : intermediateSelected.id) === item.id) { // if the item is already selected, we need to unselect it setIntermediateSelected(undefined); } else { setIntermediateSelected(item); } return; } // single select anchored, direct save on click const singleSelectOnChange = onSelectedChange; singleSelectOnChange(item === selected ? undefined : item); onClose('selection'); } }; }).sort((itemA, itemB) => { if (shouldOrderSelectedFirst) { // itemA is selected (for sorting purposes) if an object in selectedOnSort matches every property of itemA, except for the selected property const itemASelected = selectedOnSort.some(item => Object.entries(item).every(([key, value]) => { if (key === 'selected') { return true; } return itemA[key] === value; })); // itemB is selected (for sorting purposes) if an object in selectedOnSort matches every property of itemA, except for the selected property const itemBSelected = selectedOnSort.some(item => Object.entries(item).every(([key, value]) => { if (key === 'selected') { return true; } return itemB[key] === value; })); // order selected items first if (itemASelected > itemBSelected) { return -1; } else if (itemASelected < itemBSelected) { return 1; } } return 0; }); }, [onClose, onSelectedChange, items, selected, isItemCurrentlySelected, isSingleSelectModal, intermediateSelected, shouldOrderSelectedFirst, selectedOnSort]); if (prevItems !== items) { setPrevItems(items); if (prevItems.length === 0 && items.length > 0) { resetSort(); } } if (open !== prevOpen) { setPrevOpen(open); resetSort(); } const focusTrapSettings = { initialFocusRef: inputRef || undefined }; const extendedTextInputProps = useMemo(() => { return { className: classes.TextInput, contrast: true, leadingVisual: SearchIcon, 'aria-label': inputLabel, ...textInputProps }; }, [inputLabel, textInputProps]); const loadingType = () => { if (dataLoadedOnce) { return FilteredActionListLoadingTypes.input; } else { if (initialLoadingType === 'spinner') { return FilteredActionListLoadingTypes.bodySpinner; } else { return FilteredActionListLoadingTypes.bodySkeleton; } } }; function getMessage() { if (items.length === 0 && !message) { return DefaultEmptyMessage; } else if (message) { return /*#__PURE__*/jsx(SelectPanelMessage, { title: message.title, variant: message.variant, icon: message.icon, action: message.action, children: message.body }); } } // We add permanent save and cancel buttons on: // - modals const showPermanentCancelSaveButtons = variant === 'modal'; // The next two could be collapsed, left them separate for readability // We add a responsive save and cancel button on: // - anchored panels with multi select if there is onCancel const showResponsiveCancelSaveButtons = variant !== 'modal' && usingFullScreenOnNarrow && isMultiSelectVariant(selected) && onCancel !== undefined; // The responsive save and close button is only covering a very specific case: // - anchored panel with multi select if there is no onCancel. // This variant should disappear in the future, once onCancel is required, // but for now we need to support it so there is a user friendly way to close the panel. const showResponsiveSaveAndCloseButton = variant !== 'modal' && usingFullScreenOnNarrow && isMultiSelectVariant(selected) && onCancel === undefined; // If there is any element in the footer, we render it. const renderFooter = secondaryAction !== undefined || showPermanentCancelSaveButtons || showResponsiveSaveAndCloseButton || showResponsiveCancelSaveButtons; // If there's any permanent elements in the footer, we show it always. // The save button is only shown on small screens. const displayFooter = secondaryAction !== undefined || showPermanentCancelSaveButtons ? 'always' : showResponsiveSaveAndCloseButton || showResponsiveCancelSaveButtons ? 'only-small' : undefined; const stretchSecondaryAction = showResponsiveSaveAndCloseButton || showResponsiveCancelSaveButtons ? 'only-big' : showPermanentCancelSaveButtons ? 'never' : 'always'; const stretchSaveButton = showResponsiveSaveAndCloseButton && secondaryAction === undefined ? 'only-small' : 'never'; /* * SelectPanel uses two close button implementations for different use cases: * * 1. AnchoredOverlay close button - Enabled on narrow screens (showXCloseIcon logic) * * 2. SelectPanel modal close button - Used for modal variant on wider screens * (variant === 'modal' && !isNarrowScreenSize logic below) * * The dual approach handles different responsive behaviors: AnchoredOverlay manages * close functionality for narrow fullscreen, while SelectPanel handles modal close on desktop. */ const showXCloseIcon = (onCancel !== undefined || !isMultiSelectVariant(selected)) && usingFullScreenOnNarrow; const currentResponsiveVariant = useResponsiveValue(usingFullScreenOnNarrow ? { regular: 'anchored', narrow: 'fullscreen' } : undefined, 'anchored'); const preventBubbling = customOnKeyDown => event => { // skip if a TextInput has focus customOnKeyDown === null || customOnKeyDown === void 0 ? void 0 : customOnKeyDown(event); const activeElement = document.activeElement; if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') return; // skip if used with modifier to preserve shortcuts like ⌘ + F const hasModifier = event.ctrlKey || event.altKey || event.metaKey; if (hasModifier) return; // skip if it's not the forward slash or an alphabet key if (event.key !== '/' && !isAlphabetKey(event.nativeEvent)) { return; } // if this is a typeahead event, don't propagate outside of menu event.stopPropagation(); }; return /*#__PURE__*/jsxs(Fragment, { children: [/*#__PURE__*/jsx(AnchoredOverlay, { renderAnchor: renderMenuAnchor, anchorRef: anchorRef, align: align, open: open, onOpen: onOpen, onClose: onClose, overlayProps: { role: 'dialog', 'aria-labelledby': titleId, 'aria-describedby': subtitle ? subtitleId : undefined, ...overlayProps, ...(variant === 'modal' ? { /* override AnchoredOverlay position */ top: '50vh', left: '50vw', anchorSide: undefined } : {}), style: { /* override AnchoredOverlay position */ transform: variant === 'modal' ? 'translate(-50%, -50%)' : undefined, // set maxHeight based on calculated availablePanelHeight when keyboard is visible ...(isKeyboardVisible ? { maxHeight: availablePanelHeight !== undefined ? `${availablePanelHeight}px` : 'auto' } : {}) }, onKeyDown: preventBubbling(overlayProps === null || overlayProps === void 0 ? void 0 : overlayProps.onKeyDown) }, focusTrapSettings: focusTrapSettings, focusZoneSettings: focusZoneSettings, height: height, width: width, anchorId: id, variant: usingFullScreenOnNarrow ? { regular: 'anchored', narrow: 'fullscreen' } : undefined, pinPosition: !height, className: classes.Overlay, displayCloseButton: showXCloseIcon, closeButtonProps: { 'aria-label': 'Cancel and close' }, children: /*#__PURE__*/jsxs("div", { className: classes.Wrapper, "data-variant": variant, children: [/*#__PURE__*/jsxs("div", { className: classes.Header, "data-variant": currentResponsiveVariant, children: [/*#__PURE__*/jsxs("div", { children: [/*#__PURE__*/jsx(Heading, { as: "h1", id: titleId, className: classes.Title, children: title }), subtitle ? /*#__PURE__*/jsx("div", { id: subtitleId, className: classes.Subtitle, children: subtitle }) : null] }), variant === 'modal' && !isNarrowScreenSize ? /*#__PURE__*/jsx(IconButton, { type: "button", variant: "invisible", icon: XIcon, "aria-label": "Cancel and close", className: classes.ResponsiveCloseButton, onClick: () => { onCancel === null || onCancel === void 0 ? void 0 : onCancel(); onCancelRequested(); } }) : null] }), notice && /*#__PURE__*/jsx("div", { ref: noticeRef, children: /*#__PURE__*/jsx(Banner, { variant: notice.variant === 'error' ? 'critical' : notice.variant, description: notice.text, title: "Notice", hideTitle: true, className: classes.Notice, layout: "compact" }) }), /*#__PURE__*/jsx(FilteredActionList, { filterValue: filterValue, onFilterChange: onFilterChange, onListContainerRefChanged: onListContainerRefChanged // @ts-expect-error it needs a non nullable ref , onInputRefChanged: onInputRefChanged, placeholderText: placeholderText, ...listProps, variant: (_listProps$groupMetad = listProps.groupMetadata) !== null && _listProps$groupMetad !== void 0 && _listProps$groupMetad.length ? 'horizontal-inset' : 'inset', role: "listbox" // browsers give aria-labelledby precedence over aria-label so we need to make sure // we don't accidentally override props.aria-label , "aria-labelledby": listProps['aria-label'] ? undefined : titleId, "aria-multiselectable": isMultiSelectVariant(selected) ? 'true' : 'false', selectionVariant: isSingleSelectModal ? 'radio' : isMultiSelectVariant(selected) ? 'multiple' : 'single', items: itemsToRender, textInputProps: extendedTextInputProps, loading: loading || isLoading && !message, loadingType: loadingType(), onSelectAllChange: showSelectAll ? handleSelectAllChange : undefined // hack because the deprecated ActionList does not support this prop , message: getMessage(), messageText: { title: (message === null || message === void 0 ? void 0 : message.title) || EMPTY_MESSAGE.title, description: typeof (message === null || message === void 0 ? void 0 : message.body) === 'string' ? message.body : EMPTY_MESSAGE.description }, fullScreenOnNarrow: usingFullScreenOnNarrow, className: clsx(className, classes.FilteredActionList) }), footer ? /*#__PURE__*/jsx("div", { className: classes.Footer, children: footer }) : renderFooter ? /*#__PURE__*/jsxs("div", { "data-display-footer": displayFooter, "data-stretch-secondary-action": stretchSecondaryAction, "data-stretch-save-button": stretchSaveButton, className: clsx(classes.Footer, classes.ResponsiveFooter), children: [/*#__PURE__*/jsx("div", { "data-stretch-secondary-action": stretchSecondaryAction, className: classes.SecondaryAction, children: secondaryAction }), showPermanentCancelSaveButtons || showResponsiveCancelSaveButtons ? /*#__PURE__*/jsxs("div", { "data-stretch-save-button": stretchSaveButton, className: clsx(classes.CancelSaveButtons, { [classes.ResponsiveSaveButton]: showResponsiveCancelSaveButtons }), children: [/*#__PURE__*/jsx(ButtonComponent, { size: "medium", onClick: () => { onCancel === null || onCancel === void 0 ? void 0 : onCancel(); onCancelRequested(); }, children: "Cancel" }), /*#__PURE__*/jsx(ButtonComponent, { block: onCancel === undefined, variant: "primary", size: "medium", onClick: () => { if (isSingleSelectModal) { const singleSelectOnChange = onSelectedChange; singleSelectOnChange(intermediateSelected); } onClose(variant === 'modal' ? 'selection' : 'click-outside'); }, children: "Save" })] }) : null, showResponsiveSaveAndCloseButton ? /*#__PURE__*/jsx("div", { className: classes.ResponsiveSaveButton, "data-stretch-save-button": stretchSaveButton, children: /*#__PURE__*/jsx(ButtonComponent, { block: true, variant: "primary", size: "medium", onClick: () => { onClose('click-outside'); }, children: "Save and close" }) }) : null] }) : null] }) }), variant === 'modal' && open ? /*#__PURE__*/jsx("div", { className: classes.Backdrop }) : null] }); } const SecondaryButton = props => { return /*#__PURE__*/jsx(ButtonComponent, { block: true, ...props, children: props.children }); }; SecondaryButton.displayName = "SecondaryButton"; const SecondaryLink = props => { return /*#__PURE__*/jsx(LinkButton, { ...props, variant: "invisible", block: true, children: props.children }); }; SecondaryLink.displayName = "SecondaryLink"; const SelectPanel = Object.assign(Panel, { __SLOT__: Symbol('SelectPanel'), SecondaryActionButton: SecondaryButton, SecondaryActionLink: SecondaryLink }); export { SelectPanel };