diff --git a/.changeset/selectpanel-modal.md b/.changeset/selectpanel-modal.md new file mode 100644 index 00000000000..3fda37fdd3b --- /dev/null +++ b/.changeset/selectpanel-modal.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +SelectPanel: Add variant="modal" diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-responsive-width-light-modern-action-list--true-full-screen-on-narrow--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-responsive-width-light-modern-action-list--true-full-screen-on-narrow--true-linux.png index 95d51676103..600cb936710 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-responsive-width-light-modern-action-list--true-full-screen-on-narrow--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-responsive-width-light-modern-action-list--true-full-screen-on-narrow--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-colorblind-linux.png new file mode 100644 index 00000000000..5298efbf7b4 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-colorblind-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-colorblind-modern-action-list--true-linux.png new file mode 100644 index 00000000000..d4aca3d26ad Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-colorblind-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-dimmed-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-dimmed-linux.png new file mode 100644 index 00000000000..46a8d41f7d1 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-dimmed-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-dimmed-modern-action-list--true-linux.png new file mode 100644 index 00000000000..c304a6f9044 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-dimmed-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-high-contrast-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-high-contrast-linux.png new file mode 100644 index 00000000000..3ee13eaa9f6 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-high-contrast-modern-action-list--true-linux.png new file mode 100644 index 00000000000..c4dffa9d41f Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-linux.png new file mode 100644 index 00000000000..ca780ea1c39 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-modern-action-list--true-linux.png new file mode 100644 index 00000000000..0aaa83f9bc9 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-tritanopia-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-tritanopia-linux.png new file mode 100644 index 00000000000..5298efbf7b4 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-tritanopia-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-tritanopia-modern-action-list--true-linux.png new file mode 100644 index 00000000000..d4aca3d26ad Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-dark-tritanopia-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-colorblind-linux.png new file mode 100644 index 00000000000..9415507b834 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-colorblind-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-colorblind-modern-action-list--true-linux.png new file mode 100644 index 00000000000..4f21ee826e5 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-colorblind-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-high-contrast-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-high-contrast-linux.png new file mode 100644 index 00000000000..93b0c111136 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-high-contrast-modern-action-list--true-linux.png new file mode 100644 index 00000000000..ffcc3f5f4e2 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-linux.png new file mode 100644 index 00000000000..b4dd3b7a8b9 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-modern-action-list--true-linux.png new file mode 100644 index 00000000000..5e170a8d00f Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-tritanopia-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-tritanopia-linux.png new file mode 100644 index 00000000000..9415507b834 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-tritanopia-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-tritanopia-modern-action-list--true-linux.png new file mode 100644 index 00000000000..4f21ee826e5 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Multi-Select-Modal-light-tritanopia-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-colorblind-linux.png new file mode 100644 index 00000000000..b32485c40c2 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-colorblind-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-colorblind-modern-action-list--true-linux.png new file mode 100644 index 00000000000..cba82293888 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-colorblind-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-dimmed-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-dimmed-linux.png new file mode 100644 index 00000000000..3062b35e614 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-dimmed-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-dimmed-modern-action-list--true-linux.png new file mode 100644 index 00000000000..9ae9fb10f83 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-dimmed-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-high-contrast-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-high-contrast-linux.png new file mode 100644 index 00000000000..40997f086c9 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-high-contrast-modern-action-list--true-linux.png new file mode 100644 index 00000000000..7f99f01ff1d Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-linux.png new file mode 100644 index 00000000000..ecdc094333d Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-modern-action-list--true-linux.png new file mode 100644 index 00000000000..30951f413ba Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-tritanopia-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-tritanopia-linux.png new file mode 100644 index 00000000000..b32485c40c2 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-tritanopia-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-tritanopia-modern-action-list--true-linux.png new file mode 100644 index 00000000000..cba82293888 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-dark-tritanopia-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-colorblind-linux.png new file mode 100644 index 00000000000..78a2803ae0b Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-colorblind-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-colorblind-modern-action-list--true-linux.png new file mode 100644 index 00000000000..19200dc98be Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-colorblind-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-high-contrast-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-high-contrast-linux.png new file mode 100644 index 00000000000..a7c512b5844 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-high-contrast-modern-action-list--true-linux.png new file mode 100644 index 00000000000..75f45bd2ea4 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-linux.png new file mode 100644 index 00000000000..81708bd1ead Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-modern-action-list--true-linux.png new file mode 100644 index 00000000000..fd36f3557e1 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-tritanopia-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-tritanopia-linux.png new file mode 100644 index 00000000000..78a2803ae0b Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-tritanopia-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-tritanopia-modern-action-list--true-linux.png new file mode 100644 index 00000000000..19200dc98be Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-Modal-light-tritanopia-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-dark-dimmed-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-dark-dimmed-modern-action-list--true-linux.png index d672b63a446..7830a254f47 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-dark-dimmed-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-dark-dimmed-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-light-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-light-colorblind-linux.png index a57fc5e36cb..176f9cf1cd9 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-light-colorblind-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Single-Select-light-colorblind-linux.png differ diff --git a/e2e/components/SelectPanel.test.ts b/e2e/components/SelectPanel.test.ts index 2a2ff4ba1ee..6e77e8d69f7 100644 --- a/e2e/components/SelectPanel.test.ts +++ b/e2e/components/SelectPanel.test.ts @@ -15,6 +15,8 @@ const scenarios = matrix({ {id: 'components-selectpanel-features--with-item-dividers', name: 'With Item Dividers'}, {id: 'components-selectpanel-features--with-label-internally', name: 'With Label Internally'}, {id: 'components-selectpanel-features--with-label-visually-hidden', name: 'With Label Visually Hidden'}, + {id: 'components-selectpanel-features--multi-select-modal', name: 'Multi Select Modal'}, + {id: 'components-selectpanel-features--single-select-modal', name: 'Single Select Modal'}, { id: 'components-selectpanel-features--with-placeholder-for-search-input', name: 'With Placeholder for Search Input', diff --git a/packages/react/src/ActionList/Selection.tsx b/packages/react/src/ActionList/Selection.tsx index 1e4d11a2b37..70d88af1c30 100644 --- a/packages/react/src/ActionList/Selection.tsx +++ b/packages/react/src/ActionList/Selection.tsx @@ -8,6 +8,7 @@ import Box from '../Box' import {useFeatureFlag} from '../FeatureFlags' import classes from './ActionList.module.css' import {actionListCssModulesFlag} from './featureflag' +import Radio from '../Radio' type SelectionProps = Pick export const Selection: React.FC> = ({selected, className}) => { @@ -34,6 +35,15 @@ export const Selection: React.FC> = ({se } } + if (selectionVariant === 'radio') { + return ( + + {/* This is just a way to get the visuals from Radio, but it should be ignored in terms of accessibility */} + + + ) + } + if (selectionVariant === 'single' || listRole === 'menu') { if (enabled) { return ( diff --git a/packages/react/src/ActionList/shared.ts b/packages/react/src/ActionList/shared.ts index bae0b66c2a9..f3fd0cbb5a3 100644 --- a/packages/react/src/ActionList/shared.ts +++ b/packages/react/src/ActionList/shared.ts @@ -124,7 +124,7 @@ export type ActionListProps = React.PropsWithChildren<{ /** * Whether multiple Items or a single Item can be selected. */ - selectionVariant?: 'single' | 'multiple' + selectionVariant?: 'single' | 'radio' | 'multiple' /** * Display a divider above each `Item` in this `List` when it does not follow a `Header` or `Divider`. */ diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index cb4de3af3a3..3f5b87a501d 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -216,6 +216,7 @@ export const AnchoredOverlay: React.FC @@ -266,6 +269,7 @@ const Overlay = React.forwardRef( role = 'none', visibility = 'visible', width = 'auto', + responsiveVariant, ...props }, forwardedRef, @@ -323,6 +327,7 @@ const Overlay = React.forwardRef( right={right} height={height} visibility={visibility} + data-responsive={responsiveVariant} {...props} /> diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx index 7261a2362b4..d6bd8178021 100644 --- a/packages/react/src/Radio/Radio.tsx +++ b/packages/react/src/Radio/Radio.tsx @@ -51,6 +51,7 @@ const Radio = React.forwardRef( required, value, className, + 'aria-hidden': ariaHidden = false, ...rest }: RadioProps, ref, @@ -62,7 +63,7 @@ const Radio = React.forwardRef( } const name = nameProp || radioGroupContext?.name - if (!name) { + if (!name && !ariaHidden) { // eslint-disable-next-line no-console console.warn( 'A radio input must have a `name` attribute. Pass `name` as a prop directly to each Radio, or nest them in a `RadioGroup` component with a `name` prop', @@ -84,6 +85,7 @@ const Radio = React.forwardRef( required={required} onChange={handleOnChange} className={clsx(className, sharedClasses.Input, classes.Radio)} + aria-hidden={ariaHidden} {...rest} /> ) diff --git a/packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx b/packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx index 1c2ba96ba77..7876588048a 100644 --- a/packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx @@ -7,6 +7,8 @@ import {Button} from '../Button' import {SelectPanel} from '.' import type {ItemInput} from '../deprecated/ActionList/List' import FormControl from '../FormControl' +import Text from '../Text' +import {MultiSelectModal, SingleSelect, SingleSelectModal, WithOnCancel} from './SelectPanel.features.stories' const meta: Meta = { title: 'Components/SelectPanel/Dev', @@ -187,3 +189,45 @@ export const WithSxAndCSS = () => { ) } + +export const AllVariants = () => { + return ( + <> + + Showcase of all the SelectPanel variants + +
+ + Test the different interactions below to see how the SelectPanel behaves in different selection and anchoring + modes. The size of the screen also affects how the user interacts with the SelectPanel. + +
+
+ + Single Select Panel +
+ This panel allows selecting a single item from the list. + +
+ + Single Select Modal +
+ This modal allows selecting a single item with a modal interface. + +
+ + Multi Select Panel +
+ This panel allows selecting multiple items from the list. + +
+ + Multi Select Modal + +
+ This modal allows selecting multiple items with a modal interface. +
+ + + ) +} diff --git a/packages/react/src/SelectPanel/SelectPanel.docs.json b/packages/react/src/SelectPanel/SelectPanel.docs.json index 9bd5aea0acb..ec8bdcd7649 100644 --- a/packages/react/src/SelectPanel/SelectPanel.docs.json +++ b/packages/react/src/SelectPanel/SelectPanel.docs.json @@ -130,7 +130,7 @@ { "name": "onCancel", "type": "() => void", - "description": "(Narrow screens) Callback when the user hits cancel or close", + "description": "(Narrow screens and variant=modal) Callback when the user hits cancel or close", "defaultValue": "" }, { @@ -145,6 +145,12 @@ "defaultValue": "", "description": "See [TextInput props](/react/TextInput#props)." }, + { + "name": "variant", + "type": "'anchored' | 'modal'", + "description": "Anchored by default, SelectPanel can be opened as a modal", + "defaultValue": "'anchored'" + }, { "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 4429d6f8ccd..8bb4e4893ca 100644 --- a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx @@ -167,12 +167,6 @@ export const SingleSelect = () => { const [selected, setSelected] = useState(items[0]) const [filter, setFilter] = useState('') const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) - // design guidelines say to sort selected items first - const selectedItemsSortedFirst = filteredItems.sort((a, b) => { - if (a.text === selected?.text) return -1 - if (b.text === selected?.text) return 1 - return 0 - }) const [open, setOpen] = useState(false) return ( @@ -187,12 +181,12 @@ export const SingleSelect = () => { placeholder="Select labels" // button text when no items are selected open={open} onOpenChange={setOpen} - items={selectedItemsSortedFirst} + items={filteredItems} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} width="medium" - message={selectedItemsSortedFirst.length === 0 ? NoResultsMessage(filter) : undefined} + message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined} /> ) @@ -202,14 +196,6 @@ export const MultiSelect = () => { const [selected, setSelected] = useState(items.slice(1, 3)) const [filter, setFilter] = useState('') const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) - // design guidelines say to sort selected items first - const selectedItemsSortedFirst = filteredItems.sort((a, b) => { - const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) - const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) - if (aIsSelected && !bIsSelected) return -1 - if (!aIsSelected && bIsSelected) return 1 - return 0 - }) const [open, setOpen] = useState(false) return ( @@ -226,12 +212,12 @@ export const MultiSelect = () => { )} open={open} onOpenChange={setOpen} - items={selectedItemsSortedFirst} + items={filteredItems} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} width="medium" - message={selectedItemsSortedFirst.length === 0 ? NoResultsMessage(filter) : undefined} + message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined} /> ) @@ -756,14 +742,6 @@ export const WithOnCancel = () => { const [selected, setSelected] = React.useState(intialSelection) const [filter, setFilter] = React.useState('') const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) - // design guidelines say to sort selected items first - const selectedItemsSortedFirst = filteredItems.sort((a, b) => { - const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) - const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) - if (aIsSelected && !bIsSelected) return -1 - if (!aIsSelected && bIsSelected) return 1 - return 0 - }) const [open, setOpen] = useState(false) React.useEffect(() => { @@ -784,7 +762,7 @@ export const WithOnCancel = () => { )} open={open} onOpenChange={setOpen} - items={selectedItemsSortedFirst} + items={filteredItems} selected={selected} onSelectedChange={setSelected} onCancel={() => setSelected(intialSelection)} @@ -794,3 +772,68 @@ export const WithOnCancel = () => { ) } + +export const MultiSelectModal = () => { + const [intialSelection, setInitialSelection] = React.useState(items.slice(1, 3)) + + const [selected, setSelected] = React.useState(intialSelection) + const [filter, setFilter] = React.useState('') + const [open, setOpen] = useState(false) + + React.useEffect(() => { + if (!open) setInitialSelection(selected) // Save selection as initialSelection for next time + }, [open, selected]) + + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + + return ( + ( + + )} + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onCancel={() => setSelected(intialSelection)} + onFilterChange={setFilter} + width="medium" + /> + ) +} + +export const SingleSelectModal = () => { + const [selected, setSelected] = useState(undefined) + const [filter, setFilter] = useState('') + const [open, setOpen] = useState(false) + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + + return ( + ( + + )} + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onCancel={() => {}} + onFilterChange={setFilter} + width="medium" + /> + ) +} diff --git a/packages/react/src/SelectPanel/SelectPanel.module.css b/packages/react/src/SelectPanel/SelectPanel.module.css index 8028a96dd2d..d0f1d9e3651 100644 --- a/packages/react/src/SelectPanel/SelectPanel.module.css +++ b/packages/react/src/SelectPanel/SelectPanel.module.css @@ -13,7 +13,7 @@ .Header { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; padding-top: var(--base-size-8); padding-right: var(--base-size-8); padding-left: var(--base-size-8); @@ -126,15 +126,31 @@ @media screen and (--viewportRange-narrow) { display: inline-grid; } + + &:where([data-variant='modal'] &) { + display: inline-grid; + } } .ResponsiveFooter { display: none; padding: var(--base-size-16); + gap: var(--stack-gap-condensed); + justify-content: right; @media screen and (--viewportRange-narrow) { display: flex; gap: var(--stack-gap-condensed); justify-content: right; } + + &:where([data-variant='modal'] &) { + display: flex; + } +} + +.Backdrop { + position: absolute; + inset: 0; + background-color: var(--overlay-backdrop-bgColor); } diff --git a/packages/react/src/SelectPanel/SelectPanel.test.tsx b/packages/react/src/SelectPanel/SelectPanel.test.tsx index 58e2ff21afa..b36dcee132a 100644 --- a/packages/react/src/SelectPanel/SelectPanel.test.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.test.tsx @@ -968,6 +968,36 @@ for (const useModernActionList of [false, true]) { ).toHaveAttribute('aria-selected', 'true') }) }) + + describe('As Modal', () => { + it('selections render as radios when variant modal and single select', async () => { + const user = userEvent.setup() + + renderWithFlag( + {}} selected={undefined} />, + useModernActionList, + ) + + await user.click(screen.getByText('Select items')) + + if (useModernActionList) { + expect(screen.getAllByRole('radio').length).toBe(items.length) + } + + expect(screen.getByRole('button', {name: 'Save'})).toBeVisible() + expect(screen.getByRole('button', {name: 'Cancel'})).toBeVisible() + }) + it('save and oncancel buttons are present when variant modal', async () => { + const user = userEvent.setup() + + renderWithFlag( {}} />, useModernActionList) + + await user.click(screen.getByText('Select items')) + + expect(screen.getByRole('button', {name: 'Save'})).toBeVisible() + expect(screen.getByRole('button', {name: 'Cancel'})).toBeVisible() + }) + }) }) }) } diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 28557696e90..85946ea0456 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -92,14 +92,17 @@ interface SelectPanelBaseProps { body: string | React.ReactElement variant: 'empty' | 'error' | 'warning' } - onCancel?: () => void } +// onCancel is optional with variant=anchored, but required with variant=modal +type SelectPanelVariantProps = {variant?: 'anchored'; onCancel?: () => void} | {variant: 'modal'; onCancel: () => void} + export type SelectPanelProps = SelectPanelBaseProps & - Omit & + Omit & Pick & AnchoredOverlayWrapperAnchorProps & - (SelectPanelSingleSelection | SelectPanelMultiSelection) + (SelectPanelSingleSelection | SelectPanelMultiSelection) & + SelectPanelVariantProps function isMultiSelectVariant( selected: SelectPanelSingleSelection['selected'] | SelectPanelMultiSelection['selected'], @@ -157,6 +160,7 @@ export function SelectPanel({ message, notice, onCancel, + variant = 'anchored', ...listProps }: SelectPanelProps): JSX.Element { const titleId = useId() @@ -176,6 +180,18 @@ export function SelectPanel({ const usingModernActionList = useFeatureFlag('primer_react_select_panel_modern_action_list') const usingFullScreenOnNarrow = useFeatureFlag('primer_react_select_panel_fullscreen_on_narrow') + // 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: FilteredActionListProps['onListContainerRefChanged'] = useCallback( (node: HTMLElement | null) => { setListContainerElement(node) @@ -328,9 +344,13 @@ export function SelectPanel({ ) const onClose = useCallback( (gesture: Parameters>[0] | 'selection' | 'escape') => { + // Clicking outside should cancel the selection only on modals + if (variant === 'modal' && gesture === 'click-outside') { + onCancel?.() + } onOpenChange(false, gesture) }, - [onOpenChange], + [onOpenChange, variant, onCancel], ) const onCancelRequested = useCallback(() => { @@ -352,14 +372,31 @@ export function SelectPanel({ } }, [placeholder, renderAnchor, selected]) + const isItemCurrentlySelected = useCallback( + (item: ItemInput) => { + // 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?.id === item.id + } + + // For single-select anchored, we just need to check if the item is the selected item + return selected?.id === item.id + }, + [selected, intermediateSelected, isSingleSelectModal], + ) + const itemsToRender = useMemo(() => { return items.map(item => { - const isItemSelected = isMultiSelectVariant(selected) ? doesItemsIncludeItem(selected, item) : selected === item - return { ...item, role: 'option', - selected: 'selected' in item && item.selected === undefined ? undefined : isItemSelected, + selected: 'selected' in item && item.selected === undefined ? undefined : isItemCurrentlySelected(item), onAction: (itemFromAction, event) => { item.onAction?.(itemFromAction, event) @@ -378,14 +415,23 @@ export function SelectPanel({ return } - // single select + if (isSingleSelectModal) { + if (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 as SelectPanelSingleSelection['onSelectedChange'] singleSelectOnChange(item === selected ? undefined : item) onClose('selection') }, } as ItemProps }) - }, [onClose, onSelectedChange, items, selected]) + }, [onClose, onSelectedChange, items, selected, isItemCurrentlySelected, isSingleSelectModal, intermediateSelected]) const focusTrapSettings = { initialFocusRef: inputRef || undefined, @@ -431,122 +477,143 @@ export function SelectPanel({ } } + // We add a save and cancel button on narrow screens when SelectPanel is full-screen + const showCancelSaveButtons = variant === 'modal' || (isMultiSelectVariant(selected) && usingFullScreenOnNarrow) // because of instant selection, canceling on single select is the same as closing the panel, no onCancel needed - const shouldShowXButton = (onCancel || !isMultiSelectVariant(selected)) && usingFullScreenOnNarrow + const showXCloseIcon = + variant === 'modal' || ((onCancel || !isMultiSelectVariant(selected)) && usingFullScreenOnNarrow) return ( - -
-
-
- - {title} - - {subtitle ? ( -
- {subtitle} -
+ <> + +
+
+
+ + {title} + + {subtitle ? ( +
+ {subtitle} +
+ ) : null} +
+ {showXCloseIcon ? ( + { + onCancel?.() + onCancelRequested() + }} + /> ) : null}
- {shouldShowXButton ? ( - { - onCancel?.() - onCancelRequested() - }} - /> - ) : null} -
- {notice && ( -
- {iconForNoticeVariant[notice.variant]} -
{notice.text}
-
- )} - - {footer ? ( -
{footer}
- ) : isMultiSelectVariant(selected) && usingFullScreenOnNarrow ? ( - /* Save and Cancel buttons are only useful for multiple selection, single selection instantly closes the panel */ -
- {/* we add a save and cancel button on narrow screens when SelectPanel is full-screen */} - {onCancel && ( + {notice && ( +
+ {iconForNoticeVariant[notice.variant]} +
{notice.text}
+
+ )} + + {footer ? ( +
{footer}
+ ) : showCancelSaveButtons ? ( + /* Save and Cancel buttons are only useful for multiple selection, single selection instantly closes the panel */ +
+ {onCancel && ( + + )} - )} - -
- ) : null} -
-
+
+ ) : null} +
+ + {variant === 'modal' && open ?
: null} + ) } diff --git a/packages/react/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap index 99f22fc126d..7b751fcffcf 100644 --- a/packages/react/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap @@ -30,22 +30,24 @@ exports[`AnchoredOverlay should render consistently when open 1`] = ` max-width: calc(100vw - 2rem); } -.c1:where([data-variant='fullscreen']) { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - margin: 0; - border-radius: unset; -} - @media (forced-colors:active) { .c1 { outline: solid 1px transparent; } } +@media screen and (max-width:calc(768px - 0.02px)) { + .c1:where([data-responsive='fullscreen']) { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + margin: 0; + border-radius: unset; + } +} +
- selectionVariant?: ActionListProps['selectionVariant'] | 'instant' + selectionVariant?: 'single' | 'multiple' | 'instant' id?: string defaultOpen?: boolean