Skip to content

Commit 534ca11

Browse files
mperrottijoshblack
andauthored
Replace aria-live and LiveRegion usage (#7230)
Co-authored-by: Josh Black <joshblack@github.com>
1 parent a193d30 commit 534ca11

File tree

7 files changed

+113
-161
lines changed

7 files changed

+113
-161
lines changed

.changeset/easy-suits-mate.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+
Replaces 'aria-live' usage and removes internal LiveRegion component

packages/react/src/DataTable/Pagination.tsx

Lines changed: 69 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {ChevronLeftIcon, ChevronRightIcon} from '@primer/octicons-react'
22
import type React from 'react'
33
import {useCallback, useMemo, useState} from 'react'
44
import {Button} from '../internal/components/ButtonReset'
5-
import {LiveRegion, LiveRegionOutlet, Message} from '../internal/components/LiveRegion'
5+
import {AriaStatus} from '../live-region'
66
import {VisuallyHidden} from '../VisuallyHidden'
77
import {warning} from '../utils/warning'
88
import type {ResponsiveValue} from '../hooks/useResponsiveValue'
@@ -99,72 +99,69 @@ export function Pagination({
9999
}, [pageCount, pageIndex, showPages])
100100

101101
return (
102-
<LiveRegion>
103-
<LiveRegionOutlet />
104-
<nav aria-label={label} className={clsx('TablePagination', classes.TablePagination)} id={id}>
105-
<Range pageStart={pageStart} pageEnd={pageEnd} totalCount={totalCount} />
106-
<ol
107-
className={clsx('TablePaginationSteps', classes.TablePaginationSteps)}
108-
data-hidden-viewport-ranges={getViewportRangesToHidePages().join(' ')}
109-
>
110-
<Step>
111-
<Button
112-
className={clsx('TablePaginationAction', classes.TablePaginationAction)}
113-
type="button"
114-
data-has-page={hasPreviousPage ? true : undefined}
115-
aria-disabled={!hasPreviousPage ? true : undefined}
116-
onClick={() => {
117-
if (!hasPreviousPage) {
118-
return
119-
}
120-
selectPreviousPage()
121-
}}
122-
>
123-
{hasPreviousPage ? <ChevronLeftIcon /> : null}
124-
<span>Previous</span>
125-
<VisuallyHidden>&nbsp;page</VisuallyHidden>
126-
</Button>
127-
</Step>
128-
{model.map((page, i) => {
129-
if (page.type === 'BREAK') {
130-
return <TruncationStep key={`truncation-${i}`} />
131-
} else if (page.type === 'NUM') {
132-
return (
133-
<Step key={i}>
134-
<Page
135-
active={!!page.selected}
136-
onClick={() => {
137-
selectPage(page.num - 1)
138-
}}
139-
>
140-
{page.num}
141-
{page.precedesBreak ? <VisuallyHidden></VisuallyHidden> : null}
142-
</Page>
143-
</Step>
144-
)
145-
}
146-
})}
147-
<Step>
148-
<Button
149-
className={clsx('TablePaginationAction', classes.TablePaginationAction)}
150-
type="button"
151-
data-has-page={hasNextPage ? true : undefined}
152-
aria-disabled={!hasNextPage ? true : undefined}
153-
onClick={() => {
154-
if (!hasNextPage) {
155-
return
156-
}
157-
selectNextPage()
158-
}}
159-
>
160-
<span>Next</span>
161-
<VisuallyHidden>&nbsp;page</VisuallyHidden>
162-
{hasNextPage ? <ChevronRightIcon /> : null}
163-
</Button>
164-
</Step>
165-
</ol>
166-
</nav>
167-
</LiveRegion>
102+
<nav aria-label={label} className={clsx('TablePagination', classes.TablePagination)} id={id}>
103+
<Range pageStart={pageStart} pageEnd={pageEnd} totalCount={totalCount} />
104+
<ol
105+
className={clsx('TablePaginationSteps', classes.TablePaginationSteps)}
106+
data-hidden-viewport-ranges={getViewportRangesToHidePages().join(' ')}
107+
>
108+
<Step>
109+
<Button
110+
className={clsx('TablePaginationAction', classes.TablePaginationAction)}
111+
type="button"
112+
data-has-page={hasPreviousPage ? true : undefined}
113+
aria-disabled={!hasPreviousPage ? true : undefined}
114+
onClick={() => {
115+
if (!hasPreviousPage) {
116+
return
117+
}
118+
selectPreviousPage()
119+
}}
120+
>
121+
{hasPreviousPage ? <ChevronLeftIcon /> : null}
122+
<span>Previous</span>
123+
<VisuallyHidden>&nbsp;page</VisuallyHidden>
124+
</Button>
125+
</Step>
126+
{model.map((page, i) => {
127+
if (page.type === 'BREAK') {
128+
return <TruncationStep key={`truncation-${i}`} />
129+
} else if (page.type === 'NUM') {
130+
return (
131+
<Step key={i}>
132+
<Page
133+
active={!!page.selected}
134+
onClick={() => {
135+
selectPage(page.num - 1)
136+
}}
137+
>
138+
{page.num}
139+
{page.precedesBreak ? <VisuallyHidden></VisuallyHidden> : null}
140+
</Page>
141+
</Step>
142+
)
143+
}
144+
})}
145+
<Step>
146+
<Button
147+
className={clsx('TablePaginationAction', classes.TablePaginationAction)}
148+
type="button"
149+
data-has-page={hasNextPage ? true : undefined}
150+
aria-disabled={!hasNextPage ? true : undefined}
151+
onClick={() => {
152+
if (!hasNextPage) {
153+
return
154+
}
155+
selectNextPage()
156+
}}
157+
>
158+
<span>Next</span>
159+
<VisuallyHidden>&nbsp;page</VisuallyHidden>
160+
{hasNextPage ? <ChevronRightIcon /> : null}
161+
</Button>
162+
</Step>
163+
</ol>
164+
</nav>
168165
)
169166
}
170167

@@ -179,7 +176,11 @@ function Range({pageStart, pageEnd, totalCount}: RangeProps) {
179176
const end = pageEnd
180177
return (
181178
<>
182-
<Message value={`Showing ${start} through ${end} of ${totalCount}`} />
179+
<VisuallyHidden>
180+
<AriaStatus>
181+
Showing {start} through {end} of {totalCount}
182+
</AriaStatus>
183+
</VisuallyHidden>
183184
<p className={clsx('TablePaginationRange', classes.TablePaginationRange)}>
184185
{start}
185186
<VisuallyHidden>&nbsp;through&nbsp;</VisuallyHidden>

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {SkeletonAvatar} from '../SkeletonAvatar'
77
import {VisuallyHidden} from '../VisuallyHidden'
88
import {KebabHorizontalIcon} from '@primer/octicons-react'
99
import classes from './Skeleton.examples.stories.module.css'
10+
import {AriaStatus} from '../experimental'
1011

1112
export default {
1213
title: 'Components/Skeleton/Examples',
@@ -42,7 +43,9 @@ export const CommentsLoading = () => {
4243
{/** read by screen readers in place of the comments in a skeleton loading state */}
4344
{loading ? <VisuallyHidden>Comments are loading</VisuallyHidden> : null}
4445
{/** when loading is completed, it should be announced by the screen-reader */}
45-
<VisuallyHidden aria-live="polite">{loadingFinished ? 'Comments are loaded' : null}</VisuallyHidden>
46+
<VisuallyHidden>
47+
<AriaStatus>{loadingFinished ? 'Comments are loaded' : null}</AriaStatus>
48+
</VisuallyHidden>
4649
<div className={classes.CommentsSpacing}>
4750
<Button onClick={toggleLoadingState}>{loading ? 'Stop loading' : 'Start loading'}</Button>
4851
{Array.from({length: COMMENT_LIST_LENGTH}, (_, index) => (
@@ -101,7 +104,9 @@ export const CommentsLoadingWithSuspense = () => {
101104
{/** read by screen readers in place of the comments in a skeleton loading state */}
102105
{loadingStatus === 'pending' ? <VisuallyHidden>Comments are loading</VisuallyHidden> : null}
103106
{/** when loading is completed, it should be announced by the screen-reader */}
104-
<VisuallyHidden aria-live="polite">{loadingStatus === 'fulfilled' ? 'Comments are loaded' : null}</VisuallyHidden>
107+
<VisuallyHidden>
108+
<AriaStatus>{loadingStatus === 'fulfilled' ? 'Comments are loaded' : null}</AriaStatus>
109+
</VisuallyHidden>
105110

106111
{/* aria-busy is passed so the screenreader doesn't announce the skeleton state */}
107112
<div className={classes.CommentsSpacing} aria-busy={loadingStatus === 'pending'}>

packages/react/src/TreeView/TreeView.test.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React from 'react'
55
import type {SubTreeState} from './TreeView'
66
import {TreeView} from './TreeView'
77
import {GearIcon} from '@primer/octicons-react'
8+
import {getLiveRegion} from '../live-region/__tests__/test-helpers'
89

910
// TODO: Move this function into a shared location
1011
function renderWithTheme(
@@ -1391,7 +1392,14 @@ describe('State', () => {
13911392
})
13921393

13931394
describe('Asynchronous loading', () => {
1394-
it('updates aria live region when loading is done', () => {
1395+
afterEach(() => {
1396+
const liveRegion = document.querySelector('live-region')
1397+
if (liveRegion) {
1398+
document.body.removeChild(liveRegion)
1399+
}
1400+
})
1401+
1402+
it('updates aria live region when loading is done', async () => {
13951403
function TestTree() {
13961404
const [state, setState] = React.useState<SubTreeState>('initial')
13971405

@@ -1423,29 +1431,33 @@ describe('Asynchronous loading', () => {
14231431
</div>
14241432
)
14251433
}
1434+
const user = userEvent.setup()
14261435
const {getByRole} = renderWithTheme(<TestTree />)
14271436

14281437
const doneButton = getByRole('button', {name: 'Load'})
1429-
const liveRegion = getByRole('status')
1438+
const liveRegion = getLiveRegion()
14301439

14311440
// Live region should be empty
1432-
expect(liveRegion).toHaveTextContent('')
1441+
expect(liveRegion.getMessage('polite')).toBe('')
14331442

14341443
// Click load button to mimic async loading
1435-
fireEvent.click(doneButton)
1444+
await act(async () => {
1445+
await user.click(doneButton)
1446+
})
14361447

1437-
expect(liveRegion).toHaveTextContent('Parent content loading')
1448+
expect(liveRegion.getMessage('polite')).toBe('Parent content loading')
14381449

14391450
// Click done button to mimic the completion of async loading
1440-
fireEvent.click(doneButton)
1451+
await act(async () => {
1452+
await user.click(doneButton)
1453+
})
14411454

14421455
act(() => {
14431456
vi.runAllTimers()
14441457
})
14451458

14461459
// Live region should be updated
1447-
expect(liveRegion).not.toHaveTextContent('Child 2 is empty')
1448-
expect(liveRegion).toHaveTextContent('Parent content loaded')
1460+
expect(liveRegion.getMessage('polite')).toBe('Parent content loaded')
14491461
})
14501462

14511463
it('moves focus from loading item to first child', async () => {
@@ -1810,7 +1822,8 @@ describe('CSS Module Migration', () => {
18101822
</TreeView>
18111823
)
18121824

1813-
// Testing on the second child element because the first child element is visually hidden
1814-
expect(render(<TreeViewTestComponent />).container.children[1]).toHaveClass('test-class-name')
1825+
// Find the TreeView ul element (which should have the className)
1826+
const treeElement = render(<TreeViewTestComponent />).getByRole('tree')
1827+
expect(treeElement).toHaveClass('test-class-name')
18151828
})
18161829
})

packages/react/src/TreeView/TreeView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {useIsMacOS} from '../hooks'
2929
import {Tooltip} from '../TooltipV2'
3030
import {isSlot} from '../utils/is-slot'
3131
import type {FCWithSlotMarker} from '../utils/types'
32+
import {AriaStatus} from '../live-region'
3233

3334
// ----------------------------------------------------------------------------
3435
// Context
@@ -144,8 +145,8 @@ const Root: React.FC<TreeViewProps> = ({
144145
}}
145146
>
146147
<>
147-
<VisuallyHidden role="status" aria-live="polite" aria-atomic="true">
148-
{ariaLiveMessage}
148+
<VisuallyHidden>
149+
<AriaStatus announceOnShow>{ariaLiveMessage}</AriaStatus>
149150
</VisuallyHidden>
150151
<ul
151152
ref={containerRef}

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -595,9 +595,11 @@ const SelectPanelMessage: React.FC<SelectPanelMessageProps> = ({
595595
title,
596596
children,
597597
}) => {
598+
const MessageWrapper = variant === 'empty' ? 'div' : AriaStatus
599+
598600
if (size === 'full') {
599601
return (
600-
<div aria-live={variant === 'empty' ? undefined : 'polite'} className={classes.MessageFull}>
602+
<MessageWrapper className={classes.MessageFull}>
601603
{variant !== 'empty' ? (
602604
<Octicon
603605
icon={AlertIcon}
@@ -610,18 +612,14 @@ const SelectPanelMessage: React.FC<SelectPanelMessageProps> = ({
610612
) : null}
611613
<span className={classes.MessageTitle}>{title}</span>
612614
<span className={classes.MessageContent}>{children}</span>
613-
</div>
615+
</MessageWrapper>
614616
)
615617
} else {
616618
return (
617-
<div
618-
aria-live={variant === 'empty' ? undefined : 'polite'}
619-
className={classes.MessageInline}
620-
data-variant={variant}
621-
>
619+
<MessageWrapper className={classes.MessageInline} data-variant={variant}>
622620
<AlertIcon size={16} />
623621
<div>{children}</div>
624-
</div>
622+
</MessageWrapper>
625623
)
626624
}
627625
}

packages/react/src/internal/components/LiveRegion.tsx

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)