Skip to content
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
b4e4030
add and minimally style new SelectAll section with checkbox and label…
llastflowers Jul 14, 2025
c99f398
make new story have no items selected at start
llastflowers Jul 15, 2025
90a6e25
add select all functionality, deselect all with changing label, and i…
llastflowers Jul 16, 2025
a2edc86
fix Select all checkbox alignment issues
llastflowers Jul 16, 2025
337e676
Merge branch 'main' into llastflowers/5348/SelectPanel-select-all-fea…
llastflowers Jul 16, 2025
49711d5
Add new SelectPanel `Select all` feature
llastflowers Jul 16, 2025
301789a
add unit tests
llastflowers Jul 16, 2025
7b7546a
fix for lint error
llastflowers Jul 16, 2025
b0f1c92
add SelectPanel with Select all to e2e
llastflowers Jul 17, 2025
d5ddbef
test(vrt): update snapshots
llastflowers Jul 17, 2025
a6a0d27
test(vrt): update snapshots
llastflowers Jul 17, 2025
655ceac
refactoring and cleanup
llastflowers Jul 17, 2025
4b7068e
oops forgot to remove now-unnecessary dependency
llastflowers Jul 17, 2025
6b03f88
remove unused prop and add new feature documentation
llastflowers Jul 18, 2025
705c750
fix a selection state persistence issue
llastflowers Jul 18, 2025
46844d6
test(vrt): update snapshots
llastflowers Jul 18, 2025
24097b9
manually fix flaky vrt
llastflowers Jul 18, 2025
82ec32c
consolidate props for 'select all' behavior
francinelucca Jul 22, 2025
43c0577
Merge branch 'main' into llastflowers/5348/SelectPanel-select-all-fea…
francinelucca Jul 22, 2025
c5fb0a0
remove selectAll state
francinelucca Jul 22, 2025
6ab2d56
Merge branch 'llastflowers/5348/SelectPanel-select-all-feature' of gi…
francinelucca Jul 22, 2025
dcb068d
spacing tweaks
llastflowers Jul 22, 2025
e3b0a2e
test(vrt): update snapshots
llastflowers Jul 22, 2025
c404a19
fix flaky vrt
llastflowers Jul 22, 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/rude-cougars-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

Add new SelectPanel `Select all` feature
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.
1 change: 1 addition & 0 deletions e2e/components/SelectPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const scenarios = matrix({
id: 'components-selectpanel-features--with-placeholder-for-search-input',
name: 'With Placeholder for Search Input',
},
{id: 'components-selectpanel-features--with-select-all', name: 'With Select All'},
{id: 'components-selectpanel-examples--above-tall-body', name: 'Above Tall Body'},
{id: 'components-selectpanel-examples--height-variations-and-scroll', name: 'Height Variations and Scroll'},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,23 @@
}
}
}

.SelectAllContainer {
display: flex;
align-items: center;
padding-block: var(--base-size-4);
padding-inline: var(--base-size-16);
background: var(--bgColor-muted);
border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);
}

.SelectAllCheckbox {
/* -1px hack to offset 1px border-bottom causing uneven alignment */
/* stylelint-disable-next-line primer/spacing */
margin: var(--base-size-4) var(--base-size-8) calc(var(--base-size-4) - 1px) 0;
}

.SelectAllLabel {
font-size: var(--text-body-size-medium);
color: var(--fgColor-muted);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
} from './FilteredActionListLoaders'
import {announce} from '@primer/live-region-element'
import {debounce} from '@github/mini-throttle'
import classes from './FilteredActionList.module.css'
import Checkbox from '../Checkbox'

const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8}

Expand All @@ -42,6 +44,7 @@ export interface FilteredActionListProps
inputRef?: React.RefObject<HTMLInputElement>
className?: string
announcementsEnabled?: boolean
onSelectAllChange?: (checked: boolean) => void
}

const StyledHeader = styled.div`
Expand Down Expand Up @@ -125,6 +128,7 @@ export function FilteredActionList({
sx,
className,
announcementsEnabled = false,
onSelectAllChange,
...listProps
}: FilteredActionListProps): JSX.Element {
const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '')
Expand All @@ -137,13 +141,17 @@ export function FilteredActionList({
[onFilterChange, setInternalFilterValue],
)

const selectAllChecked = items.length > 0 && items.every(item => item.selected)
const selectAllIndeterminate = !selectAllChecked && items.some(item => item.selected)

const scrollContainerRef = useRef<HTMLDivElement>(null)
const [listContainerElement, setListContainerElement] = useState<HTMLDivElement | null>(null)
const inputRef = useProvidedRefOrCreate<HTMLInputElement>(providedInputRef)
const [needItemsChangedAnnouncement, setNeedItemsChangedAnnouncement] = useState<boolean>(false)
const activeDescendantRef = useRef<HTMLElement>()
const listId = useId()
const inputDescriptionTextId = useId()
const selectAllLabelText = selectAllChecked ? 'Deselect all' : 'Select all'
const onInputKeyPress: KeyboardEventHandler = useCallback(
event => {
if (event.key === 'Enter' && activeDescendantRef.current) {
Expand Down Expand Up @@ -219,6 +227,15 @@ export function FilteredActionList({

useScrollFlash(scrollContainerRef)

const handleSelectAllChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (onSelectAllChange) {
onSelectAllChange(e.target.checked)
}
},
[onSelectAllChange],
)

return (
<Box
display="flex"
Expand Down Expand Up @@ -249,6 +266,20 @@ export function FilteredActionList({
/>
</StyledHeader>
<VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden>
{onSelectAllChange !== undefined && (
<div className={classes.SelectAllContainer}>
<Checkbox
id="select-all-checkbox"
className={classes.SelectAllCheckbox}
checked={selectAllChecked}
indeterminate={selectAllIndeterminate}
onChange={handleSelectAllChange}
/>
<label className={classes.SelectAllLabel} htmlFor="select-all-checkbox">
{selectAllLabelText}
</label>
</div>
)}
<Box ref={scrollContainerRef} overflow="auto" flexGrow={1}>
{loading && scrollContainerRef.current && loadingType.appearsInBody ? (
<FilteredActionListBodyLoader loadingType={loadingType} height={scrollContainerRef.current.clientHeight} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {SxProp} from '../sx'
import type {FilteredActionListLoadingType} from './FilteredActionListLoaders'
import {FilteredActionListLoadingTypes, FilteredActionListBodyLoader} from './FilteredActionListLoaders'
import classes from './FilteredActionList.module.css'
import Checkbox from '../Checkbox'

import {isValidElementType} from 'react-is'
import type {RenderItemFn} from '../deprecated/ActionList/List'
Expand All @@ -45,6 +46,7 @@ export interface FilteredActionListProps
className?: string
announcementsEnabled?: boolean
fullScreenOnNarrow?: boolean
onSelectAllChange?: (checked: boolean) => void
}

const StyledHeader = styled.div`
Expand All @@ -70,6 +72,7 @@ export function FilteredActionList({
className,
announcementsEnabled = true,
fullScreenOnNarrow,
onSelectAllChange,
...listProps
}: FilteredActionListProps): JSX.Element {
const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '')
Expand All @@ -88,6 +91,11 @@ export function FilteredActionList({
const activeDescendantRef = useRef<HTMLElement>()
const listId = useId()
const inputDescriptionTextId = useId()

const selectAllChecked = items.length > 0 && items.every(item => item.selected)
const selectAllIndeterminate = !selectAllChecked && items.some(item => item.selected)

const selectAllLabelText = selectAllChecked ? 'Deselect all' : 'Select all'
const onInputKeyPress: KeyboardEventHandler = useCallback(
event => {
if (event.key === 'Enter' && activeDescendantRef.current) {
Expand Down Expand Up @@ -150,6 +158,15 @@ export function FilteredActionList({
useAnnouncements(items, {current: listContainerElement}, inputRef, announcementsEnabled, loading)
useScrollFlash(scrollContainerRef)

const handleSelectAllChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (onSelectAllChange) {
onSelectAllChange(e.target.checked)
}
},
[onSelectAllChange],
)

function getItemListForEachGroup(groupId: string) {
const itemsInGroup = []
for (const item of items) {
Expand Down Expand Up @@ -232,6 +249,20 @@ export function FilteredActionList({
/>
</StyledHeader>
<VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden>
{onSelectAllChange !== undefined && (
<div className={classes.SelectAllContainer}>
<Checkbox
id="select-all-checkbox"
className={classes.SelectAllCheckbox}
checked={selectAllChecked}
indeterminate={selectAllIndeterminate}
onChange={handleSelectAllChange}
/>
<label className={classes.SelectAllLabel} htmlFor="select-all-checkbox">
{selectAllLabelText}
</label>
</div>
)}
<div ref={scrollContainerRef} className={classes.Container}>
{getBodyContent()}
</div>
Expand Down
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 @@ -184,6 +184,12 @@
"description": "Whether to display the selected items at the top of the list",
"default": "true"
},
{
"name": "showSelectAll",
"type": "boolean",
"defaultValue": "false",
"description": "When `true` and on a multi-select SelectPanel, displays a 'Select all' checkbox that allows users to select or deselect all visible items at once. The checkbox label automatically toggles between 'Select all' and 'Deselect all' based on the current selection state, and shows an indeterminate state when some items are selected."
},
{
"name": "disableFullscreenOnNarrow",
"type": "boolean",
Expand Down
41 changes: 41 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -888,3 +888,44 @@ export const WithInactiveItems = () => {
</FormControl>
)
}

export const WithSelectAll = () => {
const [selected, setSelected] = useState<ItemInput[]>([])
const [filter, setFilter] = useState('')
const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))

const [open, setOpen] = useState(false)

return (
<FormControl>
<FormControl.Label>Labels</FormControl.Label>
<SelectPanel
title="Select labels"
placeholder="Select labels" // button text when no items are selected
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"
showSelectAll={true}
message={
filteredItems.length === 0
? {
variant: 'empty',
title: `No language found for \`${filter}\``,
body: 'Adjust your search term to find other languages',
}
: undefined
}
/>
</FormControl>
)
}
Loading
Loading