diff --git a/packages/react/src/SelectPanel/SelectPanel.docs.json b/packages/react/src/SelectPanel/SelectPanel.docs.json index fbdeeecb387..817b1dd87da 100644 --- a/packages/react/src/SelectPanel/SelectPanel.docs.json +++ b/packages/react/src/SelectPanel/SelectPanel.docs.json @@ -68,6 +68,12 @@ "defaultValue": "", "description": "See [TextInput props](/react/TextInput#props)." }, + { + "name": "variant", + "type": "'anchored' | 'modal'", + "defaultValue": "anchored", + "description": "Anchored to the button or centered on the screen. Modal variant also adds Save and Cancel buttons." + }, { "name": "footer", "type": "string | React.ReactElement", diff --git a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx index da4bb2084be..2fd40b19d6d 100644 --- a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx @@ -369,3 +369,66 @@ export const WithGroups = () => { /> ) } + +export const ModalVariant = () => { + const [selected, setSelected] = React.useState([items[0], items[1]]) + const [filter, setFilter] = React.useState('') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [open, setOpen] = useState(false) + const buttonRef = useRef(null) + + return ( + <> +

Multi Select Panel as Modal

+ ( + + )} + anchorRef={buttonRef} + placeholderText="Filter Labels" + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onFilterChange={setFilter} + /> + + ) +} + +export const ModalVariantWithSecondaryAction = () => { + const [selected, setSelected] = React.useState([items[0], items[1]]) + const [filter, setFilter] = React.useState('') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [open, setOpen] = useState(false) + const buttonRef = useRef(null) + + return ( + <> +

Multi Select Panel as Modal with secondary action

+ Edit labels} + renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => ( + + )} + anchorRef={buttonRef} + placeholderText="Filter Labels" + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onFilterChange={setFilter} + /> + + ) +} diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index a94b461e169..d537a087060 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -29,23 +29,25 @@ interface SelectPanelMultiSelection { onSelectedChange: (selected: ItemInput[]) => void } +type OpenGestures = 'anchor-click' | 'anchor-key-press' +type CloseGestures = 'click-outside' | 'escape' | 'selection' | 'save-click' | 'cancel-click' + interface SelectPanelBaseProps { // TODO: Make `title` required in the next major version title?: string | React.ReactElement subtitle?: string | React.ReactElement - onOpenChange: ( - open: boolean, - gesture: 'anchor-click' | 'anchor-key-press' | 'click-outside' | 'escape' | 'selection', - ) => void + onOpenChange: (open: boolean, gesture: OpenGestures | CloseGestures) => void placeholder?: string // TODO: Make `inputLabel` required in next major version inputLabel?: string overlayProps?: Partial footer?: string | React.ReactElement + // TODO: do we need keep the old variants for backward-compat? there isn't any usage in dotcom + variant?: 'anchored' | 'modal' | FilteredActionListProps['variant'] } export type SelectPanelProps = SelectPanelBaseProps & - Omit & + Omit & Pick & AnchoredOverlayWrapperAnchorProps & (SelectPanelSingleSelection | SelectPanelMultiSelection) @@ -90,7 +92,8 @@ export function SelectPanel({ items, footer, textInputProps, - overlayProps, + variant = 'anchored', + overlayProps = variant === 'modal' ? {maxWidth: 'medium', height: 'fit-content', maxHeight: 'large'} : undefined, sx, ...listProps }: SelectPanelProps): JSX.Element { @@ -111,12 +114,16 @@ export function SelectPanel({ [onOpenChange], ) const onClose = useCallback( - (gesture: Parameters>[0] | 'selection') => { + (gesture: Parameters>[0] | CloseGestures) => { onOpenChange(false, gesture) }, [onOpenChange], ) + const onCancel = () => { + // TODO + } + const renderMenuAnchor = useMemo(() => { if (renderAnchor === null) { return null @@ -258,6 +265,22 @@ export function SelectPanel({ } {...position} {...overlayProps} + sx={{ + // TODO: check styles, do we need all of these? + display: 'flex', + padding: 0, + color: 'fg.default', + + '&[data-variant="anchored"], &[data-variant="full-screen"]': { + margin: 0, + top: position?.top, + left: position?.left, + }, + '&[data-variant="modal"]': { + inset: 0, + margin: 'auto', + }, + }} > {usingModernActionList ? null : ( @@ -300,20 +323,57 @@ export function SelectPanel({ // than the Overlay (which would break scrolling the items) sx={{...sx, height: 'inherit', maxHeight: 'inherit'}} /> - {footer && ( + {footer || variant === 'modal' ? ( button': { + // make button full width if there's just one + width: variant === 'modal' ? 'auto' : '100%', + }, }} > {footer} + {variant === 'modal' ? ( + + + {/* TODO: loading state for save? */} + + + ) : null} - )} + ) : null} + + {variant === 'modal' && ( + + )} ) }