Skip to content

Implement the secondaryAction prop and deprecate footer #5939

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

Merged
merged 24 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2a20f4c
Implement the secondaryAction prop and deprecate footer
hectahertz Apr 18, 2025
4ae49fc
Add changelog
hectahertz Apr 18, 2025
8c90cf1
Update docs
hectahertz Apr 18, 2025
4c06fff
Remove unnecessary secondaryActions
hectahertz Apr 18, 2025
1df4d44
Fix story id
hectahertz Apr 18, 2025
d7474ed
test(vrt): update snapshots
francinelucca Apr 18, 2025
28dcba5
Improve dev story
hectahertz Apr 19, 2025
b40b7eb
Simplify footer layout logic
hectahertz Apr 19, 2025
ea60bae
test(vrt): update snapshots
hectahertz Apr 19, 2025
114a556
test(vrt): update snapshots
francinelucca Apr 22, 2025
79a89eb
Fix footer cancel save logic
hectahertz Apr 22, 2025
a8ddbdd
test(vrt): update snapshots
hectahertz Apr 22, 2025
6c5cb9b
Merge branch 'main' into hectahertz/selectpanel-secondaryaction
francinelucca Apr 22, 2025
f3ff84e
Merge branch 'main' into hectahertz/selectpanel-secondaryaction
francinelucca Apr 22, 2025
7f87b2e
Add stretching to ResponsiveSaveButton
hectahertz Apr 23, 2025
6ba6180
Muted links
hectahertz Apr 23, 2025
f9ff79c
Responsive padding on the footer
hectahertz Apr 23, 2025
2b23ff1
Switch to LinkButton
hectahertz Apr 23, 2025
949b666
Style tweaks
hectahertz Apr 23, 2025
0fb543a
Move secondary actions to subcomponents
hectahertz Apr 23, 2025
40ffda6
test(vrt): update snapshots
francinelucca Apr 23, 2025
4dc8f8e
Fix css lint issues
hectahertz Apr 23, 2025
3f3646f
Merge branch 'main' into hectahertz/selectpanel-secondaryaction
francinelucca Apr 23, 2025
3aa8790
Remove results.json
hectahertz Apr 23, 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/fancy-webs-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Implement the secondaryAction prop and deprecate footer
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion e2e/components/SelectPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const scenarios = matrix({
{id: 'components-selectpanel--default', name: 'Default'},
{id: 'components-selectpanel-features--single-select', name: 'Single Select'},
{id: 'components-selectpanel-features--with-external-anchor', name: 'External Anchor'},
{id: 'components-selectpanel-features--with-footer', name: 'With Footer'},
{id: 'components-selectpanel-features--with-secondary-action', name: 'With Footer'},
{id: 'components-selectpanel-features--with-groups', name: 'With Groups'},
{id: 'components-selectpanel-features--with-item-dividers', name: 'With Item Dividers'},
{id: 'components-selectpanel-features--with-label-internally', name: 'With Label Internally'},
Expand Down
216 changes: 195 additions & 21 deletions packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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'
import Select from '../Select/Select'
import type {SelectPanelSecondaryAction} from './SelectPanel'

const meta: Meta<typeof SelectPanel> = {
title: 'Components/SelectPanel/Dev',
Expand Down Expand Up @@ -190,7 +191,144 @@ export const WithSxAndCSS = () => {
)
}

const simpleItems = [
{leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: 1},
{leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: 2},
{leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: 3},
{leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4},
{leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5},
{leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6},
{leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7},
]

// onCancel is optional with variant=anchored, but required with variant=modal
type ParamProps =
| {variant: 'anchored'; onCancel?: () => void; secondaryAction?: SelectPanelSecondaryAction}
| {variant: 'modal'; onCancel: () => void; secondaryAction?: SelectPanelSecondaryAction}

const SingleSelectParams = ({variant, onCancel, secondaryAction}: ParamProps) => {
const [selected, setSelected] = useState<ItemInput | undefined>(simpleItems[0])
const [filter, setFilter] = useState('')
const filteredItems = simpleItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
const [open, setOpen] = useState(false)

// Only the variant prop changes but Typescript doesn't easily understand that
return variant === 'anchored' ? (
<SelectPanel
renderAnchor={({children, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} {...anchorProps}>
{children ?? 'Select Labels'}
</Button>
)}
placeholder="Select labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
width="medium"
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
onCancel={onCancel}
secondaryAction={secondaryAction}
/>
) : (
<SelectPanel
renderAnchor={({children, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} {...anchorProps}>
{children ?? 'Select Labels'}
</Button>
)}
placeholder="Select labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
width="medium"
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
onCancel={onCancel}
secondaryAction={secondaryAction}
variant="modal"
/>
)
}

const MultiSelectParams = ({variant, onCancel, secondaryAction}: ParamProps) => {
const [selected, setSelected] = useState<ItemInput[]>(simpleItems.slice(1, 3))
const [filter, setFilter] = useState('')
const filteredItems = simpleItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
const [open, setOpen] = useState(false)

// Only the variant prop changes but Typescript doesn't easily understand that
return variant === 'anchored' ? (
<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={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
width="medium"
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
onCancel={onCancel}
secondaryAction={secondaryAction}
/>
) : (
<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={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
width="medium"
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
variant="modal"
onCancel={onCancel}
secondaryAction={secondaryAction}
/>
)
}

export const AllVariants = () => {
const modes: {
title: string
component: React.FunctionComponent<ParamProps>
variant: 'anchored' | 'modal'
}[] = [
{title: 'Single Select Panel', component: SingleSelectParams, variant: 'anchored'},
{title: 'Single Select Modal', component: SingleSelectParams, variant: 'modal'},
{title: 'Multi Select Panel', component: MultiSelectParams, variant: 'anchored'},
{title: 'Multi Select Modal', component: MultiSelectParams, variant: 'modal'},
]

const [secondaryAction, setSecondaryAction] = useState('button')

const secondaryActionElement =
secondaryAction === 'button' ? (
<SelectPanel.SecondaryActionButton>Edit labels</SelectPanel.SecondaryActionButton>
) : (
<SelectPanel.SecondaryActionLink href="#">Edit labels</SelectPanel.SecondaryActionLink>
)

return (
<>
<Text fontSize={3} fontWeight="bold">
Expand All @@ -199,35 +337,71 @@ export const AllVariants = () => {
<br />
<Text>
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.
modes.
</Text>
<br />
<Text>
The size of the screen also affects how the user interacts with the SelectPanel, so please do test on smaller
screens.
</Text>
<br />

<Text fontWeight="bold">Single Select Panel</Text>
<br />
<Text>This panel allows selecting a single item from the list.</Text>
<SingleSelect />
<br />

<Text fontWeight="bold">Single Select Modal</Text>
<Text>Also please consider any feature flags that might affect the component.</Text>
<br />
<Text>This modal allows selecting a single item with a modal interface.</Text>
<SingleSelectModal />
<br />

<Text fontWeight="bold">Multi Select Panel</Text>
<Text fontSize={2} fontWeight="bold">
Extra controls:
</Text>
<FormControl>
<FormControl.Label>secondaryAction</FormControl.Label>
<Select value={secondaryAction} onChange={e => setSecondaryAction(e.target.value)}>
<Select.Option value="button">Button</Select.Option>
<Select.Option value="link">Link</Select.Option>
</Select>
</FormControl>
<br />
<Text>This panel allows selecting multiple items from the list.</Text>
<WithOnCancel />
<br />

<Text fontWeight="bold">Multi Select Modal</Text>
<Text>
<br />
This modal allows selecting multiple items with a modal interface.
</Text>
<MultiSelectModal />
<table border={1} cellPadding="32">
<thead>
<tr>
<th>Variant</th>
<th>
With <code>onCancel</code>
</th>
<th>
With <code>onCancel</code> and <code>secondaryAction</code>
</th>
<th>
No <code>onCancel</code>
</th>
<th>
No <code>onCancel</code> and <code>secondaryAction</code>
</th>
</tr>
</thead>
<tbody>
{modes.map(({title, component: Component, variant}) => (
<tr key={title}>
<th>{title}</th>
<td>
<Component onCancel={() => {}} variant={variant} />
</td>
<td>
<Component onCancel={() => {}} secondaryAction={secondaryActionElement} variant={variant} />
</td>
<td>{variant === 'anchored' ? <Component variant={variant} /> : 'Not supported'}</td>
<td>
{variant === 'anchored' ? (
<Component secondaryAction={secondaryActionElement} variant={variant} />
) : (
'Not supported'

This comment was marked as resolved.

)}
</td>
</tr>
))}
</tbody>
</table>
</>
)
}
11 changes: 9 additions & 2 deletions packages/react/src/SelectPanel/SelectPanel.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"id": "components-selectpanel-features--with-external-anchor"
},
{
"id": "components-selectpanel-features--with-footer"
"id": "components-selectpanel-features--with-secondary-action"
},
{
"id": "components-selectpanel-features--with-groups"
Expand Down Expand Up @@ -155,7 +155,8 @@
"name": "footer",
"type": "string | React.ReactElement",
"defaultValue": "null",
"description": "Footer rendered at the end of the panel"
"description": "Please use `secondaryAction` instead.",
"deprecated": true
},
{
"name": "message",
Expand All @@ -167,6 +168,12 @@
"name": "notice",
"type": "{text: string | React.ReactElement; variant: 'empty' | 'error' | 'warning';}",
"description": "Optional notice to display on top of the panel"
},
{
"name": "secondaryAction",
"type": "React.ReactElement",
"defaultValue": "null",
"description": "Secondary action, it will be rendered in the footer of the panel"
}
],
"subcomponents": []
Expand Down
20 changes: 4 additions & 16 deletions packages/react/src/SelectPanel/SelectPanel.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,18 +260,10 @@ export const WithExternalAnchor = () => {
)
}

export const WithFooter = () => {
export const WithSecondaryAction = () => {
const [selected, setSelected] = useState<ItemInput[]>(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 (
Expand All @@ -286,18 +278,14 @@ export const WithFooter = () => {
placeholder="Select labels" // button text when no items are selected
open={open}
onOpenChange={setOpen}
items={selectedItemsSortedFirst}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
overlayProps={{width: 'small', height: 'medium'}}
footer={
<Button size="small" block>
Edit labels
</Button>
}
secondaryAction={<Button block>Edit labels</Button>}
width="medium"
message={selectedItemsSortedFirst.length === 0 ? NoResultsMessage(filter) : undefined}
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
/>
</FormControl>
)
Expand Down
Loading
Loading