Skip to content

Commit 271630f

Browse files
TylerJDevfrancineluccallastflowers
authored
SelectPanel: Add default empty message to announcement (#6346)
Co-authored-by: Marie Lucca <[email protected]> Co-authored-by: llastflowers <[email protected]>
1 parent bf0e492 commit 271630f

File tree

7 files changed

+122
-11
lines changed

7 files changed

+122
-11
lines changed

.changeset/fresh-lines-guess.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+
SelectPanel: Ensure empty message live region reads from provided or default message

packages/react/src/FilteredActionList/FilteredActionListWithDeprecatedActionList.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export interface FilteredActionListProps
4444
inputRef?: React.RefObject<HTMLInputElement>
4545
className?: string
4646
announcementsEnabled?: boolean
47+
messageText?: {
48+
title: string
49+
description: string
50+
}
4751
onSelectAllChange?: (checked: boolean) => void
4852
}
4953

packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export interface FilteredActionListProps
4343
textInputProps?: Partial<Omit<TextInputProps, 'onChange'>>
4444
inputRef?: React.RefObject<HTMLInputElement>
4545
message?: React.ReactNode
46+
messageText?: {
47+
title: string
48+
description: string
49+
}
4650
className?: string
4751
announcementsEnabled?: boolean
4852
fullScreenOnNarrow?: boolean
@@ -69,6 +73,7 @@ export function FilteredActionList({
6973
groupMetadata,
7074
showItemDividers,
7175
message,
76+
messageText,
7277
className,
7378
announcementsEnabled = true,
7479
fullScreenOnNarrow,
@@ -155,7 +160,7 @@ export function FilteredActionList({
155160
}
156161
}, [items])
157162

158-
useAnnouncements(items, {current: listContainerElement}, inputRef, announcementsEnabled, loading)
163+
useAnnouncements(items, {current: listContainerElement}, inputRef, announcementsEnabled, loading, messageText)
159164
useScrollFlash(scrollContainerRef)
160165

161166
const handleSelectAllChange = useCallback(

packages/react/src/FilteredActionList/useAnnouncements.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const useAnnouncements = (
4343
inputRef: React.RefObject<HTMLInputElement>,
4444
enabled: boolean = true,
4545
loading: boolean = false,
46+
message?: {title: string; description: string},
4647
) => {
4748
const liveRegion = document.querySelector('live-region')
4849

@@ -92,7 +93,7 @@ export const useAnnouncements = (
9293
liveRegion?.clear() // clear previous announcements
9394

9495
if (items.length === 0 && !loading) {
95-
announce('No matching items.', {delayMs})
96+
announce(`${message?.title}. ${message?.description}`, {delayMs})
9697
return
9798
}
9899

@@ -115,6 +116,6 @@ export const useAnnouncements = (
115116
})
116117
})
117118
},
118-
[announce, isFirstRender, items, listContainerRef, liveRegion, loading],
119+
[announce, isFirstRender, items, listContainerRef, liveRegion, loading, message],
119120
)
120121
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,34 @@ export const SelectPanelRepositionInsideDialog = () => {
465465
</Dialog>
466466
)
467467
}
468+
469+
export const WithDefaultMessage = () => {
470+
const [selected, setSelected] = useState<ItemInput[]>(items.slice(1, 3))
471+
const [filter, setFilter] = useState('')
472+
const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
473+
474+
const [open, setOpen] = useState(false)
475+
476+
return (
477+
<FormControl>
478+
<FormControl.Label>Labels</FormControl.Label>
479+
<SelectPanel
480+
title="Select labels"
481+
placeholder="Select labels" // button text when no items are selected
482+
subtitle="Use labels to organize issues and pull requests"
483+
renderAnchor={({children, ...anchorProps}) => (
484+
<Button trailingAction={TriangleDownIcon} {...anchorProps} aria-haspopup="dialog">
485+
{children}
486+
</Button>
487+
)}
488+
open={open}
489+
onOpenChange={setOpen}
490+
items={filteredItems}
491+
selected={selected}
492+
onSelectedChange={setSelected}
493+
onFilterChange={setFilter}
494+
width="medium"
495+
/>
496+
</FormControl>
497+
)
498+
}

packages/react/src/SelectPanel/SelectPanel.test.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ for (const useModernActionList of [false, true]) {
751751
jest.useRealTimers()
752752
})
753753

754-
it('should announce when no results are available', async () => {
754+
it('should announce default empty message when no results are available (no custom message is provided)', async () => {
755755
jest.useFakeTimers()
756756
const user = userEvent.setup({
757757
advanceTimers: jest.advanceTimersByTime,
@@ -765,7 +765,61 @@ for (const useModernActionList of [false, true]) {
765765

766766
jest.runAllTimers()
767767
await waitFor(async () => {
768-
expect(getLiveRegion().getMessage('polite')).toBe('No matching items.')
768+
expect(getLiveRegion().getMessage('polite')).toBe(
769+
"You haven't created any items yet. Please add or create new items to populate the list.",
770+
)
771+
})
772+
jest.useRealTimers()
773+
})
774+
775+
it('should announce custom empty message when no results are available', async () => {
776+
jest.useFakeTimers()
777+
const user = userEvent.setup({
778+
advanceTimers: jest.advanceTimersByTime,
779+
})
780+
781+
function SelectPanelWithCustomEmptyMessage() {
782+
const [filter, setFilter] = React.useState('')
783+
const [open, setOpen] = React.useState(false)
784+
785+
return (
786+
<ThemeProvider>
787+
<SelectPanel
788+
title="test title"
789+
subtitle="test subtitle"
790+
placeholder="Select items"
791+
placeholderText="Filter items"
792+
open={open}
793+
items={[]}
794+
onFilterChange={value => {
795+
setFilter(value)
796+
}}
797+
filterValue={filter}
798+
selected={[]}
799+
onSelectedChange={() => {}}
800+
onOpenChange={isOpen => {
801+
setOpen(isOpen)
802+
}}
803+
message={{
804+
title: 'Nothing found',
805+
body: `There's nothing here.`,
806+
variant: 'empty',
807+
}}
808+
/>
809+
</ThemeProvider>
810+
)
811+
}
812+
813+
renderWithFlag(<SelectPanelWithCustomEmptyMessage />, useModernActionList)
814+
815+
await user.click(screen.getByText('Select items'))
816+
817+
await user.type(document.activeElement!, 'zero')
818+
expect(screen.queryByRole('option')).toBeNull()
819+
820+
jest.runAllTimers()
821+
await waitFor(async () => {
822+
expect(getLiveRegion().getMessage('polite')).toBe(`Nothing found. There's nothing here.`)
769823
})
770824
jest.useRealTimers()
771825
})

packages/react/src/SelectPanel/SelectPanel.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ import type {ButtonProps, LinkButtonProps} from '../Button/types'
3030
// we add a delay so that it does not interrupt default screen reader announcement and queues after it
3131
const SHORT_DELAY_MS = 500
3232
const LONG_DELAY_MS = 1000
33+
const EMPTY_MESSAGE = {
34+
title: "You haven't created any items yet",
35+
description: 'Please add or create new items to populate the list.',
36+
}
3337

3438
const DefaultEmptyMessage = (
35-
<SelectPanelMessage variant="empty" title="You haven't created any items yet" key="empty-message">
36-
Please add or create new items to populate the list.
39+
<SelectPanelMessage variant="empty" title={EMPTY_MESSAGE.title} key="empty-message">
40+
{EMPTY_MESSAGE.description}
3741
</SelectPanelMessage>
3842
)
3943

@@ -53,7 +57,7 @@ async function announceLoading() {
5357
}
5458

5559
const announceNoItems = debounce((message?: string) => {
56-
announceText(message ?? 'No matching items.', LONG_DELAY_MS)
60+
announceText(message ?? `${EMPTY_MESSAGE.title}. ${EMPTY_MESSAGE.description}`, LONG_DELAY_MS)
5761
}, 250)
5862

5963
interface SelectPanelSingleSelection {
@@ -231,11 +235,11 @@ function Panel({
231235
(node: HTMLElement | null) => {
232236
setListContainerElement(node)
233237
if (!node && needsNoItemsAnnouncement) {
234-
announceNoItems()
238+
if (!usingModernActionList) announceNoItems()
235239
setNeedsNoItemsAnnouncement(false)
236240
}
237241
},
238-
[needsNoItemsAnnouncement],
242+
[needsNoItemsAnnouncement, usingModernActionList],
239243
)
240244

241245
const onInputRefChanged = useCallback(
@@ -350,7 +354,7 @@ function Panel({
350354
if (open) {
351355
if (items.length === 0 && !(isLoading || loading)) {
352356
// we need to wait for the listContainerElement to disappear before announcing no items, otherwise it will be interrupted
353-
if (!listContainerElement || !usingModernActionList) {
357+
if (!listContainerElement && !usingModernActionList) {
354358
announceNoItems(message?.title)
355359
} else {
356360
setNeedsNoItemsAnnouncement(true)
@@ -821,6 +825,13 @@ function Panel({
821825
// hack because the deprecated ActionList does not support this prop
822826
{...{
823827
message: getMessage(),
828+
messageText: {
829+
title: message?.title || EMPTY_MESSAGE.title,
830+
description:
831+
typeof message?.body === 'string'
832+
? message.body
833+
: EMPTY_MESSAGE.description || EMPTY_MESSAGE.description,
834+
},
824835
fullScreenOnNarrow: usingFullScreenOnNarrow,
825836
}}
826837
// inheriting height and maxHeight ensures that the FilteredActionList is never taller

0 commit comments

Comments
 (0)