Skip to content

Commit 988b4d4

Browse files
authored
RAC and S2 Pending Button (#7002)
RAC and S2 Pending Button
1 parent 25c9d16 commit 988b4d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+638
-76
lines changed

packages/@react-aria/dnd/src/DragManager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,10 @@ class DragSession {
492492
// Announce first drop target after drag start announcement finishes.
493493
// Otherwise, it will never get announced because drag start announcement is assertive.
494494
if (!this.initialFocused) {
495-
announce(item?.element.getAttribute('aria-label'), 'polite');
495+
let label = item?.element.getAttribute('aria-label');
496+
if (label) {
497+
announce(label, 'polite');
498+
}
496499
this.initialFocused = true;
497500
}
498501
}

packages/@react-aria/live-announcer/src/LiveAnnouncer.tsx

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,38 @@ const LIVEREGION_TIMEOUT_DELAY = 7000;
1717

1818
let liveAnnouncer: LiveAnnouncer | null = null;
1919

20+
type Message = string | {'aria-labelledby': string};
21+
2022
/**
2123
* Announces the message using screen reader technology.
2224
*/
2325
export function announce(
24-
message: string,
26+
message: Message,
2527
assertiveness: Assertiveness = 'assertive',
2628
timeout = LIVEREGION_TIMEOUT_DELAY
2729
) {
2830
if (!liveAnnouncer) {
2931
liveAnnouncer = new LiveAnnouncer();
32+
// wait for the live announcer regions to be added to the dom, then announce
33+
// otherwise Safari won't announce the message if it's added too quickly
34+
// found most times less than 100ms were not consistent when announcing with Safari
35+
36+
// IS_REACT_ACT_ENVIRONMENT is used by React 18. Previous versions checked for the `jest` global.
37+
// https://github.com/reactwg/react-18/discussions/102
38+
// if we're in a test environment, announce without waiting
39+
// @ts-ignore
40+
if (!(typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined')) {
41+
setTimeout(() => {
42+
if (liveAnnouncer?.isAttached()) {
43+
liveAnnouncer?.announce(message, assertiveness, timeout);
44+
}
45+
}, 100);
46+
} else {
47+
liveAnnouncer.announce(message, assertiveness, timeout);
48+
}
49+
} else {
50+
liveAnnouncer.announce(message, assertiveness, timeout);
3051
}
31-
32-
liveAnnouncer.announce(message, assertiveness, timeout);
3352
}
3453

3554
/**
@@ -58,34 +77,40 @@ export function destroyAnnouncer() {
5877
// is simple enough to implement without React, so that's what we do here.
5978
// See this discussion for more details: https://github.com/reactwg/react-18/discussions/125#discussioncomment-2382638
6079
class LiveAnnouncer {
61-
node: HTMLElement | null;
62-
assertiveLog: HTMLElement;
63-
politeLog: HTMLElement;
80+
node: HTMLElement | null = null;
81+
assertiveLog: HTMLElement | null = null;
82+
politeLog: HTMLElement | null = null;
6483

6584
constructor() {
66-
this.node = document.createElement('div');
67-
this.node.dataset.liveAnnouncer = 'true';
68-
// copied from VisuallyHidden
69-
Object.assign(this.node.style, {
70-
border: 0,
71-
clip: 'rect(0 0 0 0)',
72-
clipPath: 'inset(50%)',
73-
height: '1px',
74-
margin: '-1px',
75-
overflow: 'hidden',
76-
padding: 0,
77-
position: 'absolute',
78-
width: '1px',
79-
whiteSpace: 'nowrap'
80-
});
81-
82-
this.assertiveLog = this.createLog('assertive');
83-
this.node.appendChild(this.assertiveLog);
84-
85-
this.politeLog = this.createLog('polite');
86-
this.node.appendChild(this.politeLog);
87-
88-
document.body.prepend(this.node);
85+
if (typeof document !== 'undefined') {
86+
this.node = document.createElement('div');
87+
this.node.dataset.liveAnnouncer = 'true';
88+
// copied from VisuallyHidden
89+
Object.assign(this.node.style, {
90+
border: 0,
91+
clip: 'rect(0 0 0 0)',
92+
clipPath: 'inset(50%)',
93+
height: '1px',
94+
margin: '-1px',
95+
overflow: 'hidden',
96+
padding: 0,
97+
position: 'absolute',
98+
width: '1px',
99+
whiteSpace: 'nowrap'
100+
});
101+
102+
this.assertiveLog = this.createLog('assertive');
103+
this.node.appendChild(this.assertiveLog);
104+
105+
this.politeLog = this.createLog('polite');
106+
this.node.appendChild(this.politeLog);
107+
108+
document.body.prepend(this.node);
109+
}
110+
}
111+
112+
isAttached() {
113+
return this.node?.isConnected;
89114
}
90115

91116
createLog(ariaLive: string) {
@@ -105,18 +130,24 @@ class LiveAnnouncer {
105130
this.node = null;
106131
}
107132

108-
announce(message: string, assertiveness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY) {
133+
announce(message: Message, assertiveness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY) {
109134
if (!this.node) {
110135
return;
111136
}
112137

113138
let node = document.createElement('div');
114-
node.textContent = message;
139+
if (typeof message === 'object') {
140+
// To read an aria-labelledby, the element must have an appropriate role, such as img.
141+
node.setAttribute('role', 'img');
142+
node.setAttribute('aria-labelledby', message['aria-labelledby']);
143+
} else {
144+
node.textContent = message;
145+
}
115146

116147
if (assertiveness === 'assertive') {
117-
this.assertiveLog.appendChild(node);
148+
this.assertiveLog?.appendChild(node);
118149
} else {
119-
this.politeLog.appendChild(node);
150+
this.politeLog?.appendChild(node);
120151
}
121152

122153
if (message !== '') {
@@ -131,11 +162,11 @@ class LiveAnnouncer {
131162
return;
132163
}
133164

134-
if (!assertiveness || assertiveness === 'assertive') {
165+
if ((!assertiveness || assertiveness === 'assertive') && this.assertiveLog) {
135166
this.assertiveLog.innerHTML = '';
136167
}
137168

138-
if (!assertiveness || assertiveness === 'polite') {
169+
if ((!assertiveness || assertiveness === 'polite') && this.politeLog) {
139170
this.politeLog.innerHTML = '';
140171
}
141172
}

packages/@react-spectrum/autocomplete/test/SearchAutocomplete.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
jest.mock('@react-aria/live-announcer');
14-
import {act, fireEvent, pointerMap, render, screen, simulateDesktop, simulateMobile, waitFor, within} from '@react-spectrum/test-utils-internal';
14+
import {act, fireEvent, pointerMap, render, simulateDesktop, simulateMobile, waitFor, within} from '@react-spectrum/test-utils-internal';
1515
import {announce} from '@react-aria/live-announcer';
1616
import {Button} from '@react-spectrum/button';
1717
import Filter from '@spectrum-icons/workflow/Filter';
@@ -3228,7 +3228,9 @@ describe('SearchAutocomplete', function () {
32283228

32293229
let listbox = getByRole('listbox');
32303230
expect(listbox).toBeVisible();
3231-
expect(screen.getAllByRole('log')).toHaveLength(2);
3231+
expect(announce).toHaveBeenCalledTimes(2);
3232+
expect(announce).toHaveBeenNthCalledWith(1, '3 options available.');
3233+
expect(announce).toHaveBeenNthCalledWith(2, 'One');
32323234
platformMock.mockRestore();
32333235
});
32343236

packages/@react-spectrum/color/test/ColorPicker.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('ColorPicker', function () {
4040

4141
let button = getByRole('button');
4242
expect(button).toHaveTextContent('Fill');
43-
expect(within(button).getByRole('img')).toHaveAttribute('aria-label', 'vibrant red');
43+
expect(within(button).getByLabelText('vibrant red')).toBeInTheDocument();
4444

4545
await user.click(button);
4646

@@ -67,7 +67,7 @@ describe('ColorPicker', function () {
6767
act(() => dialog.focus());
6868
await user.keyboard('{Escape}');
6969
act(() => {jest.runAllTimers();});
70-
expect(within(button).getByRole('img')).toHaveAttribute('aria-label', 'dark vibrant blue');
70+
expect(within(button).getByLabelText('dark vibrant blue')).toBeInTheDocument();
7171
});
7272

7373
it('should have default value of black', async function () {
@@ -81,7 +81,7 @@ describe('ColorPicker', function () {
8181

8282
let button = getByRole('button');
8383
expect(button).toHaveTextContent('Fill');
84-
expect(within(button).getByRole('img')).toHaveAttribute('aria-label', 'black');
84+
expect(within(button).getByLabelText('black')).toBeInTheDocument();
8585

8686
await user.click(button);
8787

@@ -132,6 +132,6 @@ describe('ColorPicker', function () {
132132
act(() => getByRole('dialog').focus());
133133
await user.keyboard('{Escape}');
134134
act(() => {jest.runAllTimers();});
135-
expect(within(button).getByRole('img')).toHaveAttribute('aria-label', 'vibrant orange');
135+
expect(within(button).getByLabelText('vibrant orange')).toBeInTheDocument();
136136
});
137137
});

packages/@react-spectrum/combobox/test/ComboBox.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
jest.mock('@react-aria/live-announcer');
14-
import {act, fireEvent, pointerMap, render, screen, simulateDesktop, simulateMobile, waitFor, within} from '@react-spectrum/test-utils-internal';
14+
import {act, fireEvent, pointerMap, render, simulateDesktop, simulateMobile, waitFor, within} from '@react-spectrum/test-utils-internal';
1515
import {announce} from '@react-aria/live-announcer';
1616
import {Button} from '@react-spectrum/button';
1717
import {chain} from '@react-aria/utils';
@@ -5149,7 +5149,9 @@ describe('ComboBox', function () {
51495149

51505150
let listbox = getByRole('listbox');
51515151
expect(listbox).toBeVisible();
5152-
expect(screen.getAllByRole('log')).toHaveLength(2);
5152+
expect(announce).toHaveBeenCalledTimes(2);
5153+
expect(announce).toHaveBeenNthCalledWith(1, '3 options available.');
5154+
expect(announce).toHaveBeenNthCalledWith(2, 'One');
51535155
platformMock.mockRestore();
51545156
});
51555157

packages/@react-spectrum/provider/test/Provider.test.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
// needs to be imported first
14-
// eslint-disable-next-line
15-
import MatchMediaMock from 'jest-matchmedia-mock';
16-
// eslint-disable-next-line rsp-rules/sort-imports
1713
import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal';
1814
import {ActionButton, Button} from '@react-spectrum/button';
1915
import {Checkbox} from '@react-spectrum/checkbox';
16+
import MatchMediaMock from 'jest-matchmedia-mock';
2017
import {Provider} from '../';
2118
// eslint-disable-next-line rulesdir/useLayoutEffectRule
2219
import React, {useLayoutEffect, useRef} from 'react';

packages/@react-spectrum/s2/intl/ar-AE.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"button.pending": "قيد الانتظار",
23
"contextualhelp.help": "مساعدة",
34
"contextualhelp.info": "معلومات",
45
"dialog.alert": "تنبيه",

packages/@react-spectrum/s2/intl/bg-BG.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"button.pending": "недовършено",
23
"contextualhelp.help": "Помощ",
34
"contextualhelp.info": "Информация",
45
"dialog.alert": "Сигнал",

packages/@react-spectrum/s2/intl/cs-CZ.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"button.pending": "čeká na vyřízení",
23
"contextualhelp.help": "Nápověda",
34
"contextualhelp.info": "Informace",
45
"dialog.alert": "Výstraha",

packages/@react-spectrum/s2/intl/da-DK.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"button.pending": "afventende",
23
"contextualhelp.help": "Hjælp",
34
"contextualhelp.info": "Oplysninger",
45
"dialog.alert": "Advarsel",

0 commit comments

Comments
 (0)