Skip to content

Commit

Permalink
Revert RAC Pending Button (adobe#6986)
Browse files Browse the repository at this point in the history
Revert RAC Pending Button
  • Loading branch information
snowystinger authored Sep 5, 2024
1 parent 68403fe commit 10c31a7
Show file tree
Hide file tree
Showing 13 changed files with 61 additions and 535 deletions.
83 changes: 36 additions & 47 deletions packages/@react-aria/live-announcer/src/LiveAnnouncer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@ type Assertiveness = 'assertive' | 'polite';
/* Inspired by https://github.com/AlmeroSteyn/react-aria-live */
const LIVEREGION_TIMEOUT_DELAY = 7000;

let liveAnnouncer: LiveAnnouncer | null = null;

/**
* Announces the message using screen reader technology.
*/
export function announce(
message: string,
assertiveness: Assertiveness = 'assertive',
timeout = LIVEREGION_TIMEOUT_DELAY,
mode: 'message' | 'ids' = 'message'
timeout = LIVEREGION_TIMEOUT_DELAY
) {
if (!liveAnnouncer) {
liveAnnouncer = new LiveAnnouncer();
}

liveAnnouncer.announce(message, assertiveness, timeout, mode);
liveAnnouncer.announce(message, assertiveness, timeout);
}

/**
Expand Down Expand Up @@ -57,36 +58,34 @@ export function destroyAnnouncer() {
// is simple enough to implement without React, so that's what we do here.
// See this discussion for more details: https://github.com/reactwg/react-18/discussions/125#discussioncomment-2382638
class LiveAnnouncer {
node: HTMLElement | null = null;
assertiveLog: HTMLElement | null = null;
politeLog: HTMLElement | null = null;
node: HTMLElement | null;
assertiveLog: HTMLElement;
politeLog: HTMLElement;

constructor() {
if (typeof document !== 'undefined') {
this.node = document.createElement('div');
this.node.dataset.liveAnnouncer = 'true';
// copied from VisuallyHidden
Object.assign(this.node.style, {
border: 0,
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap'
});

this.assertiveLog = this.createLog('assertive');
this.node.appendChild(this.assertiveLog);

this.politeLog = this.createLog('polite');
this.node.appendChild(this.politeLog);

document.body.prepend(this.node);
}
this.node = document.createElement('div');
this.node.dataset.liveAnnouncer = 'true';
// copied from VisuallyHidden
Object.assign(this.node.style, {
border: 0,
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap'
});

this.assertiveLog = this.createLog('assertive');
this.node.appendChild(this.assertiveLog);

this.politeLog = this.createLog('polite');
this.node.appendChild(this.politeLog);

document.body.prepend(this.node);
}

createLog(ariaLive: string) {
Expand All @@ -106,24 +105,18 @@ class LiveAnnouncer {
this.node = null;
}

announce(message: string, assertiveness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY, mode: 'message' | 'ids' = 'message') {
announce(message: string, assertiveness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY) {
if (!this.node) {
return;
}

let node = document.createElement('div');
if (mode === 'message') {
node.textContent = message;
} else {
// To read an aria-labelledby, the element must have an appropriate role, such as img.
node.setAttribute('role', 'img');
node.setAttribute('aria-labelledby', message);
}
node.textContent = message;

if (assertiveness === 'assertive') {
this.assertiveLog?.appendChild(node);
this.assertiveLog.appendChild(node);
} else {
this.politeLog?.appendChild(node);
this.politeLog.appendChild(node);
}

if (message !== '') {
Expand All @@ -138,16 +131,12 @@ class LiveAnnouncer {
return;
}

if ((!assertiveness || assertiveness === 'assertive') && this.assertiveLog) {
if (!assertiveness || assertiveness === 'assertive') {
this.assertiveLog.innerHTML = '';
}

if ((!assertiveness || assertiveness === 'polite') && this.politeLog) {
if (!assertiveness || assertiveness === 'polite') {
this.politeLog.innerHTML = '';
}
}
}

// singleton, setup immediately so that the DOM is primed for the first announcement as soon as possible
// Safari has a race condition where the first announcement is not read if we wait until the first announce call
let liveAnnouncer: LiveAnnouncer | null = new LiveAnnouncer();
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

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

let listbox = getByRole('listbox');
expect(listbox).toBeVisible();
expect(announce).toHaveBeenCalledTimes(2);
expect(announce).toHaveBeenNthCalledWith(1, '3 options available.');
expect(announce).toHaveBeenNthCalledWith(2, 'One');
expect(screen.getAllByRole('log')).toHaveLength(2);
platformMock.mockRestore();
});

Expand Down
8 changes: 4 additions & 4 deletions packages/@react-spectrum/color/test/ColorPicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('ColorPicker', function () {

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

await user.click(button);

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

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

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

await user.click(button);

Expand Down Expand Up @@ -132,6 +132,6 @@ describe('ColorPicker', function () {
act(() => getByRole('dialog').focus());
await user.keyboard('{Escape}');
act(() => {jest.runAllTimers();});
expect(within(button).getByLabelText('vibrant orange')).toBeInTheDocument();
expect(within(button).getByRole('img')).toHaveAttribute('aria-label', 'vibrant orange');
});
});
6 changes: 2 additions & 4 deletions packages/@react-spectrum/combobox/test/ComboBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

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

let listbox = getByRole('listbox');
expect(listbox).toBeVisible();
expect(announce).toHaveBeenCalledTimes(2);
expect(announce).toHaveBeenNthCalledWith(1, '3 options available.');
expect(announce).toHaveBeenNthCalledWith(2, 'One');
expect(screen.getAllByRole('log')).toHaveLength(2);
platformMock.mockRestore();
});

Expand Down
6 changes: 4 additions & 2 deletions packages/@react-spectrum/provider/test/Provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
* governing permissions and limitations under the License.
*/

import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal';
// needs to be imported first
// eslint-disable-next-line
import MatchMediaMock from 'jest-matchmedia-mock';
// eslint-disable-next-line rsp-rules/sort-imports
import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal';
import {ActionButton, Button} from '@react-spectrum/button';
import {Checkbox} from '@react-spectrum/checkbox';
import MatchMediaMock from 'jest-matchmedia-mock';
import {Provider} from '../';
// eslint-disable-next-line rulesdir/useLayoutEffectRule
import React, {useLayoutEffect, useRef} from 'react';
Expand Down
132 changes: 0 additions & 132 deletions packages/react-aria-components/docs/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,138 +159,6 @@ A `Button` can be disabled using the `isDisabled` prop.

</details>

## Pending

A `Button` can be put into the pending state using the `isPending` prop.
Both a `Text` and [ProgressBar](ProgressBar.html) component are required to show the pending state correctly.
Make sure to internationalize the label you pass to the [ProgressBar](ProgressBar.html) component.

```tsx example
import {useEffect, useRef, useState} from 'react';
import {ProgressBar, Text} from 'react-aria-components';

function PendingButton(props) {
let [isPending, setPending] = useState(false);

let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
let handlePress = (e) => {
setPending(true);
timeout.current = setTimeout(() => {
setPending(false);
timeout.current = undefined;
}, 5000);
};

useEffect(() => {
return () => {
clearTimeout(timeout.current);
};
}, []);

return (
<Button
{...props}
isPending={isPending}
onPress={handlePress}>
{({isPending}) => (
<>
<Text className={isPending ? 'pending' : undefined}>Click me</Text>
<ProgressBar
aria-label="loading"
isIndeterminate
className={['spinner', (isPending ? 'spinner-pending' : '')].join(' ')}>
<span className={'loader'} />
</ProgressBar>
</>
)}
</Button>
);
}
<PendingButton />
```

<details>
<summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>

```css
@keyframes load {
99% {
visibility: hidden;
}

100% {
visibility: visible;
}
}

@keyframes hidden {
99% {
visibility: visible;
}

100% {
visibility: hidden;
}
}

.react-aria-Button {
position: relative;
}

.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
visibility: hidden;
}
.spinner-pending {
animation: 1s load;
animation-fill-mode: forwards;
}

.pending {
animation: 1s hidden;
animation-fill-mode: forwards;
visibility: visible;
}

.loader {
width: 20px;
height: 20px;
border: 3px solid var(--background-color);
border-radius: 50%;
display: inline-block;
position: relative;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.loader::after {
content: '';
box-sizing: border-box;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid;
border-color: var(--purple-400) transparent;
}

@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
```

</details>

## Link buttons

The `Button` component always represents a button semantically. To create a link that visually looks like a button, use the [Link](Link.html) component instead. You can reuse the same styles you apply to the `Button` component on the `Link`.
Expand Down
1 change: 0 additions & 1 deletion packages/react-aria-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"@react-aria/dnd": "^3.7.2",
"@react-aria/focus": "^3.18.2",
"@react-aria/interactions": "^3.22.2",
"@react-aria/live-announcer": "^3.3.4",
"@react-aria/menu": "^3.15.3",
"@react-aria/toolbar": "3.0.0-beta.8",
"@react-aria/tree": "3.0.0-alpha.5",
Expand Down
Loading

0 comments on commit 10c31a7

Please sign in to comment.