Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SelectPanel2: Cancel + close panel when user clicks outside #4294

Merged
merged 6 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/nervous-dogs-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

experimental/SelectPanel: Cancel + close panel when user clicks outside
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export const Minimal = () => {
// eslint-disable-next-line no-console
console.log('form submitted')
}
const onCancel = () => {
setSelectedLabelIds(initialSelectedLabels)
}

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

<SelectPanel title="Select labels" onSubmit={onSubmit}>
<SelectPanel title="Select labels" onSubmit={onSubmit} onCancel={onCancel}>
<SelectPanel.Button>Assign label</SelectPanel.Button>

<ActionList>
Expand Down Expand Up @@ -80,6 +83,9 @@ export const WithGroups = () => {
const onSubmit = () => {
data.issue.assigneeIds = selectedAssigneeIds // pretending to persist changes
}
const onCancel = () => {
setSelectedAssigneeIds(initialAssigneeIds)
}

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

<SelectPanel title="Request up to 100 reviewers" onSubmit={onSubmit} onClearSelection={onClearSelection}>
<SelectPanel
title="Request up to 100 reviewers"
onSubmit={onSubmit}
onCancel={onCancel}
onClearSelection={onClearSelection}
>
<SelectPanel.Button
variant="invisible"
trailingAction={GearIcon}
Expand Down Expand Up @@ -194,7 +205,12 @@ export const AsyncWithSuspendedList = () => {
return (
<>
<h1>Async: Suspended list</h1>
<p>Fetching items once when the panel is opened (like repo labels)</p>
<p>
Fetching items once when the panel is opened (like repo labels)
<br />
Note: Save and Cancel is not implemented in this demo
</p>

<SelectPanel title="Select labels">
<SelectPanel.Button>Assign label</SelectPanel.Button>

Expand Down Expand Up @@ -344,13 +360,16 @@ export const AsyncSearchWithUseTransition = () => {
// eslint-disable-next-line no-console
console.log('form submitted')
}
const onCancel = () => {
setSelectedUserIds(initialAssigneeIds)
}

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

<SelectPanel title="Select collaborators" onSubmit={onSubmit}>
<SelectPanel title="Select collaborators" onSubmit={onSubmit} onCancel={onCancel}>
<SelectPanel.Button>Select assignees</SelectPanel.Button>
<SelectPanel.Header>
<SelectPanel.SearchInput loading={isPending} onChange={onSearchInputChange} />
Expand Down Expand Up @@ -481,6 +500,9 @@ export const WithFilterButtons = () => {
// eslint-disable-next-line no-console
console.log('form submitted')
}
const onCancel = () => {
setSelectedRef(savedInitialRef)
}

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

return (
<>
<h1>With Filter Buttons</h1>
<h1>With Filter Buttons {savedInitialRef}</h1>

<SelectPanel title="Switch branches/tags" onSubmit={onSubmit}>
<SelectPanel title="Switch branches/tags" onSubmit={onSubmit} onCancel={onCancel}>
<SelectPanel.Button leadingVisual={GitBranchIcon} trailingVisual={TriangleDownIcon}>
{savedInitialRef}
</SelectPanel.Button>
Expand Down Expand Up @@ -582,13 +604,17 @@ export const WithFilterButtons = () => {
}

export const ShortSelectPanel = () => {
const [channels, setChannels] = React.useState({GitHub: false, Email: false})
const initialChannels = {GitHub: false, Email: false}
const [channels, setChannels] = React.useState(initialChannels)
const [onlyFailures, setOnlyFailures] = React.useState(false)

const onSubmit = () => {
// eslint-disable-next-line no-console
console.log('form submitted')
}
const onCancel = () => {
setChannels(initialChannels)
}

const toggleChannel = (channel: keyof typeof channels) => {
setChannels({...channels, [channel]: !channels[channel]})
Expand All @@ -602,7 +628,7 @@ export const ShortSelectPanel = () => {
<p>
Use <code>height=fit-content</code> to match height of contents
</p>
<SelectPanel title="Select notification channels" onSubmit={onSubmit}>
<SelectPanel title="Select notification channels" onSubmit={onSubmit} onCancel={onCancel}>
<SelectPanel.Button>
<Text sx={{color: 'fg.muted'}}>Notify me:</Text>{' '}
{Object.keys(channels)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export const WithWarning = () => {
const onSubmit = () => {
data.issue.assigneeIds = selectedAssigneeIds // pretending to persist changes
}
const onCancel = () => {
setSelectedAssigneeIds(initialAssigneeIds)
}

/* Filtering */
const [filteredUsers, setFilteredUsers] = React.useState(data.collaborators)
Expand Down Expand Up @@ -107,6 +110,7 @@ export const WithWarning = () => {
title="Set assignees"
description={`Select up to ${MAX_LIMIT} people`}
onSubmit={onSubmit}
onCancel={onCancel}
onClearSelection={onClearSelection}
>
<SelectPanel.Button
Expand Down Expand Up @@ -173,6 +177,9 @@ export const WithErrors = () => {
const onSubmit = () => {
data.issue.assigneeIds = selectedAssigneeIds // pretending to persist changes
}
const onCancel = () => {
setSelectedAssigneeIds(initialAssigneeIds)
}

/* Filtering */
const [filteredUsers, setFilteredUsers] = React.useState(
Expand Down Expand Up @@ -254,7 +261,7 @@ export const WithErrors = () => {
/>
</Box>

<SelectPanel title="Set assignees" onSubmit={onSubmit} onClearSelection={onClearSelection}>
<SelectPanel title="Set assignees" onSubmit={onSubmit} onCancel={onCancel} onClearSelection={onClearSelection}>
<SelectPanel.Button
variant="invisible"
trailingAction={GearIcon}
Expand Down Expand Up @@ -325,6 +332,10 @@ export const ExternalAnchor = () => {
console.log('form submitted')
}

const onCancel = () => {
setSelectedLabelIds(initialSelectedLabels)
}

const sortingFn = (itemA: {id: string}, itemB: {id: string}) => {
const initialSelectedIds = data.issue.labelIds
if (initialSelectedIds.includes(itemA.id) && initialSelectedIds.includes(itemB.id)) return 1
Expand Down Expand Up @@ -364,7 +375,10 @@ export const ExternalAnchor = () => {
setOpen(false) // close on submit
onSubmit()
}}
onCancel={() => setOpen(false)} // close on cancel
onCancel={() => {
onCancel()
setOpen(false) // close on cancel
}}
>
<ActionList>
{itemsToShow.map(label => (
Expand Down Expand Up @@ -402,6 +416,10 @@ export const AsModal = () => {
console.log('form submitted')
}

const onCancel = () => {
setSelectedLabelIds(initialSelectedLabels)
}

const sortingFn = (itemA: {id: string}, itemB: {id: string}) => {
const initialSelectedIds = data.issue.labelIds
if (initialSelectedIds.includes(itemA.id) && initialSelectedIds.includes(itemB.id)) return 1
Expand All @@ -416,7 +434,7 @@ export const AsModal = () => {
<>
<h1>SelectPanel as Modal</h1>

<SelectPanel variant="modal" title="Select labels" onSubmit={onSubmit}>
<SelectPanel variant="modal" title="Select labels" onSubmit={onSubmit} onCancel={onCancel}>
<SelectPanel.Button>Assign label</SelectPanel.Button>

<ActionList>
Expand Down
15 changes: 5 additions & 10 deletions packages/react/src/drafts/SelectPanel2/SelectPanel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const Default = () => {
data.issue.labelIds = selectedLabelIds // pretending to persist changes
}

const onCancel = () => {
setSelectedLabelIds(initialSelectedLabels)
}

/* Filtering */
const [filteredLabels, setFilteredLabels] = React.useState(data.labels)
const [query, setQuery] = React.useState('')
Expand Down Expand Up @@ -61,16 +65,7 @@ export const Default = () => {

return (
<>
<SelectPanel
title="Select labels"
onSubmit={onSubmit}
onCancel={() => {
/* optional callback, for example: for multi-step overlay or to fire sync actions */
// eslint-disable-next-line no-console
console.log('panel was closed')
}}
onClearSelection={onClearSelection}
>
<SelectPanel title="Select labels" onSubmit={onSubmit} onCancel={onCancel} onClearSelection={onClearSelection}>
<SelectPanel.Button>Assign label</SelectPanel.Button>

<SelectPanel.Header>
Expand Down
18 changes: 4 additions & 14 deletions packages/react/src/drafts/SelectPanel2/SelectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,10 @@ const Panel: React.FC<SelectPanelProps> = ({
)

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

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

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

{hidePrimaryActions ? null : (
<Box data-selectpanel-primary-actions sx={{display: 'flex', gap: 2}}>
<Box sx={{display: 'flex', gap: 2}}>
<Button size="small" type="button" onClick={() => onCancel()}>
Cancel
</Button>
Expand Down
Loading