Skip to content

Commit 5ca5c0a

Browse files
authored
experimental/SelectPanel: Cancel + close panel when user clicks outside (#4294)
* close on clicking outside * add link to decision log * Create nervous-dogs-change.md * add onCancel to all examples * oopsie!
1 parent 54ed0a8 commit 5ca5c0a

File tree

5 files changed

+69
-35
lines changed

5 files changed

+69
-35
lines changed

.changeset/nervous-dogs-change.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": patch
3+
---
4+
5+
experimental/SelectPanel: Cancel + close panel when user clicks outside

packages/react/src/drafts/SelectPanel2/SelectPanel.examples.stories.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export const Minimal = () => {
2929
// eslint-disable-next-line no-console
3030
console.log('form submitted')
3131
}
32+
const onCancel = () => {
33+
setSelectedLabelIds(initialSelectedLabels)
34+
}
3235

3336
const sortingFn = (itemA: {id: string}, itemB: {id: string}) => {
3437
const initialSelectedIds = data.issue.labelIds
@@ -44,7 +47,7 @@ export const Minimal = () => {
4447
<>
4548
<h1>Minimal SelectPanel</h1>
4649

47-
<SelectPanel title="Select labels" onSubmit={onSubmit}>
50+
<SelectPanel title="Select labels" onSubmit={onSubmit} onCancel={onCancel}>
4851
<SelectPanel.Button>Assign label</SelectPanel.Button>
4952

5053
<ActionList>
@@ -80,6 +83,9 @@ export const WithGroups = () => {
8083
const onSubmit = () => {
8184
data.issue.assigneeIds = selectedAssigneeIds // pretending to persist changes
8285
}
86+
const onCancel = () => {
87+
setSelectedAssigneeIds(initialAssigneeIds)
88+
}
8389

8490
/* Filtering */
8591
const [filteredUsers, setFilteredUsers] = React.useState(data.collaborators)
@@ -120,7 +126,12 @@ export const WithGroups = () => {
120126
<>
121127
<h1>SelectPanel with groups</h1>
122128

123-
<SelectPanel title="Request up to 100 reviewers" onSubmit={onSubmit} onClearSelection={onClearSelection}>
129+
<SelectPanel
130+
title="Request up to 100 reviewers"
131+
onSubmit={onSubmit}
132+
onCancel={onCancel}
133+
onClearSelection={onClearSelection}
134+
>
124135
<SelectPanel.Button
125136
variant="invisible"
126137
trailingAction={GearIcon}
@@ -194,7 +205,12 @@ export const AsyncWithSuspendedList = () => {
194205
return (
195206
<>
196207
<h1>Async: Suspended list</h1>
197-
<p>Fetching items once when the panel is opened (like repo labels)</p>
208+
<p>
209+
Fetching items once when the panel is opened (like repo labels)
210+
<br />
211+
Note: Save and Cancel is not implemented in this demo
212+
</p>
213+
198214
<SelectPanel title="Select labels">
199215
<SelectPanel.Button>Assign label</SelectPanel.Button>
200216

@@ -344,13 +360,16 @@ export const AsyncSearchWithUseTransition = () => {
344360
// eslint-disable-next-line no-console
345361
console.log('form submitted')
346362
}
363+
const onCancel = () => {
364+
setSelectedUserIds(initialAssigneeIds)
365+
}
347366

348367
return (
349368
<>
350369
<h1>Async: search with useTransition</h1>
351370
<p>Fetching items on every keystroke search (like github users)</p>
352371

353-
<SelectPanel title="Select collaborators" onSubmit={onSubmit}>
372+
<SelectPanel title="Select collaborators" onSubmit={onSubmit} onCancel={onCancel}>
354373
<SelectPanel.Button>Select assignees</SelectPanel.Button>
355374
<SelectPanel.Header>
356375
<SelectPanel.SearchInput loading={isPending} onChange={onSearchInputChange} />
@@ -481,6 +500,9 @@ export const WithFilterButtons = () => {
481500
// eslint-disable-next-line no-console
482501
console.log('form submitted')
483502
}
503+
const onCancel = () => {
504+
setSelectedRef(savedInitialRef)
505+
}
484506

485507
/* Filter */
486508
const [query, setQuery] = React.useState('')
@@ -522,9 +544,9 @@ export const WithFilterButtons = () => {
522544

523545
return (
524546
<>
525-
<h1>With Filter Buttons</h1>
547+
<h1>With Filter Buttons {savedInitialRef}</h1>
526548

527-
<SelectPanel title="Switch branches/tags" onSubmit={onSubmit}>
549+
<SelectPanel title="Switch branches/tags" onSubmit={onSubmit} onCancel={onCancel}>
528550
<SelectPanel.Button leadingVisual={GitBranchIcon} trailingVisual={TriangleDownIcon}>
529551
{savedInitialRef}
530552
</SelectPanel.Button>
@@ -582,13 +604,17 @@ export const WithFilterButtons = () => {
582604
}
583605

584606
export const ShortSelectPanel = () => {
585-
const [channels, setChannels] = React.useState({GitHub: false, Email: false})
607+
const initialChannels = {GitHub: false, Email: false}
608+
const [channels, setChannels] = React.useState(initialChannels)
586609
const [onlyFailures, setOnlyFailures] = React.useState(false)
587610

588611
const onSubmit = () => {
589612
// eslint-disable-next-line no-console
590613
console.log('form submitted')
591614
}
615+
const onCancel = () => {
616+
setChannels(initialChannels)
617+
}
592618

593619
const toggleChannel = (channel: keyof typeof channels) => {
594620
setChannels({...channels, [channel]: !channels[channel]})
@@ -602,7 +628,7 @@ export const ShortSelectPanel = () => {
602628
<p>
603629
Use <code>height=fit-content</code> to match height of contents
604630
</p>
605-
<SelectPanel title="Select notification channels" onSubmit={onSubmit}>
631+
<SelectPanel title="Select notification channels" onSubmit={onSubmit} onCancel={onCancel}>
606632
<SelectPanel.Button>
607633
<Text sx={{color: 'fg.muted'}}>Notify me:</Text>{' '}
608634
{Object.keys(channels)

packages/react/src/drafts/SelectPanel2/SelectPanel.features.stories.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export const WithWarning = () => {
6363
const onSubmit = () => {
6464
data.issue.assigneeIds = selectedAssigneeIds // pretending to persist changes
6565
}
66+
const onCancel = () => {
67+
setSelectedAssigneeIds(initialAssigneeIds)
68+
}
6669

6770
/* Filtering */
6871
const [filteredUsers, setFilteredUsers] = React.useState(data.collaborators)
@@ -107,6 +110,7 @@ export const WithWarning = () => {
107110
title="Set assignees"
108111
description={`Select up to ${MAX_LIMIT} people`}
109112
onSubmit={onSubmit}
113+
onCancel={onCancel}
110114
onClearSelection={onClearSelection}
111115
>
112116
<SelectPanel.Button
@@ -173,6 +177,9 @@ export const WithErrors = () => {
173177
const onSubmit = () => {
174178
data.issue.assigneeIds = selectedAssigneeIds // pretending to persist changes
175179
}
180+
const onCancel = () => {
181+
setSelectedAssigneeIds(initialAssigneeIds)
182+
}
176183

177184
/* Filtering */
178185
const [filteredUsers, setFilteredUsers] = React.useState(
@@ -254,7 +261,7 @@ export const WithErrors = () => {
254261
/>
255262
</Box>
256263

257-
<SelectPanel title="Set assignees" onSubmit={onSubmit} onClearSelection={onClearSelection}>
264+
<SelectPanel title="Set assignees" onSubmit={onSubmit} onCancel={onCancel} onClearSelection={onClearSelection}>
258265
<SelectPanel.Button
259266
variant="invisible"
260267
trailingAction={GearIcon}
@@ -325,6 +332,10 @@ export const ExternalAnchor = () => {
325332
console.log('form submitted')
326333
}
327334

335+
const onCancel = () => {
336+
setSelectedLabelIds(initialSelectedLabels)
337+
}
338+
328339
const sortingFn = (itemA: {id: string}, itemB: {id: string}) => {
329340
const initialSelectedIds = data.issue.labelIds
330341
if (initialSelectedIds.includes(itemA.id) && initialSelectedIds.includes(itemB.id)) return 1
@@ -364,7 +375,10 @@ export const ExternalAnchor = () => {
364375
setOpen(false) // close on submit
365376
onSubmit()
366377
}}
367-
onCancel={() => setOpen(false)} // close on cancel
378+
onCancel={() => {
379+
onCancel()
380+
setOpen(false) // close on cancel
381+
}}
368382
>
369383
<ActionList>
370384
{itemsToShow.map(label => (
@@ -402,6 +416,10 @@ export const AsModal = () => {
402416
console.log('form submitted')
403417
}
404418

419+
const onCancel = () => {
420+
setSelectedLabelIds(initialSelectedLabels)
421+
}
422+
405423
const sortingFn = (itemA: {id: string}, itemB: {id: string}) => {
406424
const initialSelectedIds = data.issue.labelIds
407425
if (initialSelectedIds.includes(itemA.id) && initialSelectedIds.includes(itemB.id)) return 1
@@ -416,7 +434,7 @@ export const AsModal = () => {
416434
<>
417435
<h1>SelectPanel as Modal</h1>
418436

419-
<SelectPanel variant="modal" title="Select labels" onSubmit={onSubmit}>
437+
<SelectPanel variant="modal" title="Select labels" onSubmit={onSubmit} onCancel={onCancel}>
420438
<SelectPanel.Button>Assign label</SelectPanel.Button>
421439

422440
<ActionList>

packages/react/src/drafts/SelectPanel2/SelectPanel.stories.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const Default = () => {
2525
data.issue.labelIds = selectedLabelIds // pretending to persist changes
2626
}
2727

28+
const onCancel = () => {
29+
setSelectedLabelIds(initialSelectedLabels)
30+
}
31+
2832
/* Filtering */
2933
const [filteredLabels, setFilteredLabels] = React.useState(data.labels)
3034
const [query, setQuery] = React.useState('')
@@ -61,16 +65,7 @@ export const Default = () => {
6165

6266
return (
6367
<>
64-
<SelectPanel
65-
title="Select labels"
66-
onSubmit={onSubmit}
67-
onCancel={() => {
68-
/* optional callback, for example: for multi-step overlay or to fire sync actions */
69-
// eslint-disable-next-line no-console
70-
console.log('panel was closed')
71-
}}
72-
onClearSelection={onClearSelection}
73-
>
68+
<SelectPanel title="Select labels" onSubmit={onSubmit} onCancel={onCancel} onClearSelection={onClearSelection}>
7469
<SelectPanel.Button>Assign label</SelectPanel.Button>
7570

7671
<SelectPanel.Header>

packages/react/src/drafts/SelectPanel2/SelectPanel.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,17 +207,10 @@ const Panel: React.FC<SelectPanelProps> = ({
207207
)
208208

209209
/*
210-
We don't close the panel when clicking outside.
211-
For many years, we used to save changes and closed the dialog (for label picker)
212-
which isn't accessible, clicking outside should discard changes and close the dialog
213-
Fixing this a11y bug would confuse users, so as a middle ground,
214-
we don't close the menu and nudge the user towards the footer actions
210+
We want to cancel and close the panel when user clicks outside.
211+
See decision log: https://github.com/github/primer/discussions/2614#discussioncomment-8544561
215212
*/
216-
const [footerAnimationEnabled, setFooterAnimationEnabled] = React.useState(false)
217-
const onClickOutside = () => {
218-
setFooterAnimationEnabled(true)
219-
window.setTimeout(() => setFooterAnimationEnabled(false), 350)
220-
}
213+
const onClickOutside = onInternalCancel
221214

222215
return (
223216
<>
@@ -241,9 +234,6 @@ const Panel: React.FC<SelectPanelProps> = ({
241234
...(variant === 'anchored' ? {margin: 0, top: position?.top, left: position?.left} : {}),
242235
'::backdrop': {backgroundColor: variant === 'anchored' ? 'transparent' : 'primer.canvas.backdrop'},
243236

244-
'& [data-selectpanel-primary-actions]': {
245-
animation: footerAnimationEnabled ? 'selectpanel-gelatine 350ms linear' : 'none',
246-
},
247237
'@keyframes selectpanel-gelatine': {
248238
'0%': {transform: 'scale(1, 1)'},
249239
'25%': {transform: 'scale(0.9, 1.1)'},
@@ -461,7 +451,7 @@ const SelectPanelFooter = ({...props}) => {
461451
<Box sx={{flexGrow: hidePrimaryActions ? 1 : 0}}>{props.children}</Box>
462452

463453
{hidePrimaryActions ? null : (
464-
<Box data-selectpanel-primary-actions sx={{display: 'flex', gap: 2}}>
454+
<Box sx={{display: 'flex', gap: 2}}>
465455
<Button size="small" type="button" onClick={() => onCancel()}>
466456
Cancel
467457
</Button>

0 commit comments

Comments
 (0)