Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
435dde4
Add responsive variant to AnchoredOverlay
siddharthkp Mar 11, 2025
e6f46aa
add styles without css modules enabled
siddharthkp Mar 11, 2025
26e4a18
add vrt test for narrow screen
siddharthkp Mar 11, 2025
90141bb
mock MatchMedia for SelectPanel tests
siddharthkp Mar 11, 2025
d7ceb31
mock MatchMedia for AnchoredOverlay tests
siddharthkp Mar 11, 2025
09612ec
mock MatchMedia for components that use AnchoredOverlay
siddharthkp Mar 11, 2025
4b4b0b3
update tests that use anchoredOverlay
siddharthkp Mar 11, 2025
351d70d
test(vrt): update snapshots
siddharthkp Mar 11, 2025
64dcc35
Add a save button footer for full screen select panel
siddharthkp Mar 11, 2025
80e0ea1
don't use it in SelectPanel yet
siddharthkp Mar 11, 2025
83a600b
add changeset
siddharthkp Mar 11, 2025
5725f88
test(vrt): update snapshots
siddharthkp Mar 11, 2025
7ed663f
test(vrt): update snapshots
siddharthkp Mar 11, 2025
3ec96f0
Merge branch 'responsive-anchored-overlay' into selectpanel-responsiv…
siddharthkp Mar 11, 2025
4c7a4e3
test(vrt): update snapshots
siddharthkp Mar 11, 2025
d5efe8c
use AnchoredOverlay's variant
siddharthkp Mar 11, 2025
83df65c
test(vrt): update snapshots
siddharthkp Mar 11, 2025
ef3ce9e
add onCancel prop
siddharthkp Mar 13, 2025
7b1b0e1
test(vrt): update snapshots
siddharthkp Mar 13, 2025
c7b56e0
full-screen was renamed to fullscreen
siddharthkp Mar 13, 2025
4875568
Merge branch 'main' into selectpanel-responsive-save-button
siddharthkp Mar 13, 2025
95d0b5c
add to docs
siddharthkp Mar 14, 2025
a74eaf2
test(vrt): update snapshots
siddharthkp Mar 17, 2025
5f81715
don't show automatic footer actions for single selection
siddharthkp Mar 18, 2025
71eaed9
fix story for onCancel
siddharthkp Mar 18, 2025
f19bfb2
we don't need sx fallback anymore!
siddharthkp Mar 18, 2025
d9536e7
update dependencies
siddharthkp Mar 18, 2025
a717c83
fix padding
siddharthkp Mar 18, 2025
f91b71e
add onCancel to single selection story
siddharthkp Mar 19, 2025
12fb61b
Merge branch 'main' into selectpanel-responsive-save-button
siddharthkp Mar 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/responsive-selectpanel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

SelectPanel: Make SelectPanel full screen on narrow devices with a Save button
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@
"description": "Callback when the search input changes",
"defaultValue": ""
},
{
"name": "onCancel",
"type": "() => void",
"description": "(Narrow screens) Callback when the user hits cancel or close",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we expect this to be used in modal as well? If so maybe we need to correct this verbiage

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do, but we don't have that yet. Can update once we have modal.

It's going to be a strange description: Narrow screens or variant=modal 😅

"defaultValue": ""
},
{
"name": "overlayProps",
"type": "Partial<OverlayProps>",
Expand Down
52 changes: 52 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export const SingleSelect = () => {
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
onCancel={() => setOpen(false)}
width="medium"
/>
</FormControl>
Expand Down Expand Up @@ -560,3 +561,54 @@ export const AsyncFetch: StoryObj<SelectPanelProps> = {
},
},
}

export const WithOnCancel = () => {
const [intialSelection, setInitialSelection] = React.useState<ItemInput[]>(items.slice(1, 3))

const [selected, setSelected] = React.useState<ItemInput[]>(intialSelection)
const [filter, setFilter] = React.useState('')
const filteredItems = items.filter(
item =>
// design guidelines say to always show selected items in the list
selected.some(selectedItem => selectedItem.text === item.text) ||
// then filter the rest
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(() => {
if (!open) setInitialSelection(selected) // set initialSelection for next time
}, [open, selected])

return (
<FormControl>
<FormControl.Label>Labels</FormControl.Label>
<SelectPanel
title="Select labels"
placeholder="Select labels"
subtitle="Use labels to organize issues and pull requests"
renderAnchor={({children, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} {...anchorProps} aria-haspopup="dialog">
{children}
</Button>
)}
open={open}
onOpenChange={setOpen}
items={selectedItemsSortedFirst}
selected={selected}
onSelectedChange={setSelected}
onCancel={() => setSelected(intialSelection)}
onFilterChange={setFilter}
width="medium"
/>
</FormControl>
)
}
30 changes: 27 additions & 3 deletions packages/react/src/SelectPanel/SelectPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
flex-direction: column;
}

.Content {
.Header {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: var(--base-size-8);
padding-right: var(--base-size-16);
padding-left: var(--base-size-16);
padding-right: var(--base-size-8);
padding-left: var(--base-size-8);
}

.Title {
margin-left: var(--base-size-8);
font-size: var(--text-body-size-medium);
}

.Subtitle {
margin-left: var(--base-size-8);
font-size: var(--text-body-size-small);
color: var(--fgColor-muted);
}
Expand All @@ -31,3 +36,22 @@
height: inherit;
max-height: inherit;
}

.ResponsiveCloseButton {
display: none;

@media screen and (--viewportRange-narrow) {
display: inline-grid;
}
}

.ResponsiveFooter {
display: none;
padding: var(--base-size-16);

@media screen and (--viewportRange-narrow) {
display: flex;
gap: var(--stack-gap-condensed);
justify-content: right;
}
}
102 changes: 79 additions & 23 deletions packages/react/src/SelectPanel/SelectPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {SearchIcon, TriangleDownIcon} from '@primer/octicons-react'
import {SearchIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import type {AnchoredOverlayProps} from '../AnchoredOverlay'
import {AnchoredOverlay} from '../AnchoredOverlay'
Expand All @@ -11,7 +11,7 @@ import type {OverlayProps} from '../Overlay'
import type {TextInputProps} from '../TextInput'
import type {ItemProps, ItemInput} from './types'

import {Button} from '../Button'
import {Button, IconButton} from '../Button'
import {useProvidedRefOrCreate} from '../hooks'
import type {FocusZoneHookSettings} from '../hooks/useFocusZone'
import {useId} from '../hooks/useId'
Expand All @@ -22,7 +22,6 @@ import type {FilteredActionListLoadingType} from '../FilteredActionList/Filtered
import {FilteredActionListLoadingTypes} from '../FilteredActionList/FilteredActionListLoaders'
import {useFeatureFlag} from '../FeatureFlags'
import {announce} from '@primer/live-region-element'

import classes from './SelectPanel.module.css'
import {clsx} from 'clsx'

Expand Down Expand Up @@ -128,6 +127,7 @@ interface SelectPanelBaseProps {
footer?: string | React.ReactElement
initialLoadingType?: InitialLoadingType
className?: string
onCancel?: () => void
}

export type SelectPanelProps = SelectPanelBaseProps &
Expand Down Expand Up @@ -189,6 +189,7 @@ export function SelectPanel({
height,
width,
id,
onCancel,
...listProps
}: SelectPanelProps): JSX.Element {
const titleId = useId()
Expand Down Expand Up @@ -352,7 +353,7 @@ export function SelectPanel({
[onOpenChange],
)
const onClose = useCallback(
(gesture: Parameters<Exclude<AnchoredOverlayProps['onClose'], undefined>>[0] | 'selection') => {
(gesture: Parameters<Exclude<AnchoredOverlayProps['onClose'], undefined>>[0] | 'selection' | 'escape') => {
onOpenChange(false, gesture)
},
[onOpenChange],
Expand Down Expand Up @@ -454,6 +455,7 @@ export function SelectPanel({
height={height}
width={width}
anchorId={id}
variant={{regular: 'anchored', narrow: 'fullscreen'}}
pinPosition={!height}
>
<LiveRegionOutlet />
Expand All @@ -472,24 +474,54 @@ export function SelectPanel({
sx={enabled ? undefined : {display: 'flex', flexDirection: 'column', height: 'inherit', maxHeight: 'inherit'}}
className={enabled ? classes.Wrapper : undefined}
>
<Box sx={enabled ? undefined : {pt: 2, px: 3}} className={enabled ? classes.Content : undefined}>
<Heading
as="h1"
id={titleId}
sx={enabled ? undefined : {fontSize: 1}}
className={enabled ? classes.Title : undefined}
>
{title}
</Heading>
{subtitle ? (
<Box
id={subtitleId}
sx={enabled ? undefined : {fontSize: 0, color: 'fg.muted'}}
className={enabled ? classes.Subtitle : undefined}
<Box
sx={
enabled
? undefined
: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 2,
paddingRight: 2,
paddingLeft: 2,
}
}
className={enabled ? classes.Header : undefined}
>
<div>
<Heading
as="h1"
id={titleId}
sx={enabled ? undefined : {fontSize: 1, marginLeft: 2}}
className={enabled ? classes.Title : undefined}
>
{subtitle}
</Box>
) : null}
{title}
</Heading>
{subtitle ? (
<Box
id={subtitleId}
sx={enabled ? undefined : {marginLeft: 2, fontSize: 0, color: 'fg.muted'}}
className={enabled ? classes.Subtitle : undefined}
>
{subtitle}
</Box>
) : null}
</div>
{onCancel && (
<IconButton
type="button"
variant="invisible"
icon={XIcon}
aria-label="Cancel and close"
sx={enabled ? undefined : {display: ['inline-grid', 'inline-grid', 'none', 'none']}}
className={enabled ? classes.ResponsiveCloseButton : undefined}
onClick={() => {
onCancel()
onClose('escape')
}}
/>
)}
</Box>
<FilteredActionList
filterValue={filterValue}
Expand All @@ -514,7 +546,7 @@ export function SelectPanel({
className={enabled ? clsx(className, classes.FilteredActionList) : className}
announcementsEnabled={false}
/>
{footer && (
{footer ? (
<Box
sx={
enabled
Expand All @@ -530,7 +562,31 @@ export function SelectPanel({
>
{footer}
</Box>
)}
) : isMultiSelectVariant(selected) ? (
/* Save and Cancel buttons are only useful for multiple selection, single selection instantly closes the panel */
<div className={clsx(classes.Footer, classes.ResponsiveFooter)}>
{/* we add a save and cancel button on narrow screens when SelectPanel is full-screen */}
{onCancel && (
<Button
size="medium"
onClick={() => {
onCancel()
onClose('escape')
}}
>
Cancel
</Button>
)}
<Button
variant="primary"
size="medium"
block={onCancel ? false : true}
onClick={() => onClose('click-outside')}
>
Save
</Button>
</div>
) : null}
</Box>
</AnchoredOverlay>
</LiveRegion>
Expand Down
Loading