Skip to content
Merged
6 changes: 3 additions & 3 deletions packages/@adobe/react-spectrum/src/dialog/AlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import AlertMedium from '@spectrum-icons/ui/AlertMedium';
import {AriaLabelingProps, DOMProps, DOMRef, StyleProps} from '@react-types/shared';
import {Button, SpectrumButtonProps} from '../button/Button';
import {ButtonGroup} from '../buttongroup/ButtonGroup';
import {chain} from 'react-aria/chain';
Expand All @@ -19,7 +20,6 @@ import {Content} from '../view/Content';
import {Dialog} from './Dialog';
import {DialogContext, DialogContextValue} from './context';
import {Divider} from '../divider/Divider';
import {DOMProps, DOMRef, StyleProps} from '@react-types/shared';
import {filterDOMProps} from 'react-aria/filterDOMProps';
import {Heading} from '../text/Heading';
import intlMessages from '../../intl/dialog/*.json';
Expand All @@ -28,7 +28,7 @@ import styles from '@adobe/spectrum-css-temp/components/dialog/vars.css';
import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter';
import {useStyleProps} from '../utils/styleProps';

export interface SpectrumAlertDialogProps extends DOMProps, StyleProps {
export interface SpectrumAlertDialogProps extends AriaLabelingProps, DOMProps, StyleProps {
/** The [visual style](https://spectrum.adobe.com/page/alert-dialog/#Options) of the AlertDialog. */
variant?: 'confirmation' | 'information' | 'destructive' | 'error' | 'warning';
/** The title of the AlertDialog. */
Expand Down Expand Up @@ -105,7 +105,7 @@ export const AlertDialog = forwardRef(function AlertDialog(
size="M"
role="alertdialog"
ref={ref}
{...filterDOMProps(props)}>
{...filterDOMProps(props, {labelable: true})}>
<Heading>{title}</Heading>
{(variant === 'error' || variant === 'warning') && (
<AlertMedium slot="typeIcon" aria-label={stringFormatter.format('alert')} />
Expand Down
6 changes: 3 additions & 3 deletions packages/@adobe/react-spectrum/src/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const Dialog = React.forwardRef(function Dialog(props: SpectrumDialogProp
let domRef = useDOMRef(ref);
let gridRef = useRef(null);
let sizeVariant = sizeMap[type] || sizeMap[size];
let {dialogProps, titleProps} = useDialog(mergeProps(contextProps, props), domRef);
let {dialogProps, titleProps, contentProps} = useDialog(mergeProps(contextProps, props), domRef);

// oxlint-disable-next-line react/react-compiler
let hasHeader = useHasChild(`.${styles['spectrum-Dialog-header']}`, unwrapDOMRef(gridRef));
Expand Down Expand Up @@ -100,7 +100,7 @@ export const Dialog = React.forwardRef(function Dialog(props: SpectrumDialogProp
},
typeIcon: {UNSAFE_className: styles['spectrum-Dialog-typeIcon']},
divider: {UNSAFE_className: styles['spectrum-Dialog-divider'], size: 'M'},
content: {UNSAFE_className: styles['spectrum-Dialog-content']},
content: {UNSAFE_className: styles['spectrum-Dialog-content'], ...contentProps},
footer: {UNSAFE_className: styles['spectrum-Dialog-footer']},
buttonGroup: {
UNSAFE_className: classNames(styles, 'spectrum-Dialog-buttonGroup', {
Expand All @@ -110,7 +110,7 @@ export const Dialog = React.forwardRef(function Dialog(props: SpectrumDialogProp
}
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[hasFooter, hasHeader, titleProps]
[hasFooter, hasHeader, titleProps, contentProps]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ export const ProgressBarBase = React.forwardRef(function ProgressBarBase(

let barStyle: CSSProperties = {};
if (!isIndeterminate) {
let percentage = (value - minValue) / (maxValue - minValue);
let range = maxValue - minValue;
let percentage = range === 0 ? 0 : (value - minValue) / range;
barStyle.width = `${Math.round(percentage * 100)}%`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export const ProgressCircle = React.forwardRef(function ProgressCircle(
let subMask1Style: CSSProperties = {};
let subMask2Style: CSSProperties = {};
if (!isIndeterminate) {
let percentage = ((value - minValue) / (maxValue - minValue)) * 100;
let range = maxValue - minValue;
let percentage = range === 0 ? 0 : ((value - minValue) / range) * 100;
let angle;
if (percentage > 0 && percentage <= 50) {
angle = -180 + (percentage / 50) * 180;
Expand Down
33 changes: 33 additions & 0 deletions packages/@adobe/react-spectrum/test/dialog/AlertDialog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,37 @@ describe('AlertDialog', function () {
let primaryBtn = getByTestId('rsp-AlertDialog-confirmButton');
expect(primaryBtn).toBeDefined();
});

it('should have aria-describedby pointing to the content', function () {
let {getByRole} = render(
<Provider theme={theme}>
<AlertDialog variant="confirmation" title="the title" primaryActionLabel="confirm">
Content body
</AlertDialog>
</Provider>
);

let dialog = getByRole('alertdialog');
expect(dialog).toHaveAttribute('aria-describedby');
let contentId = dialog.getAttribute('aria-describedby');
let content = document.getElementById(contentId);
expect(content).not.toBeNull();
expect(content.textContent).toBe('Content body');
});

it('accepts custom aria-describedby', function () {
let {getByRole} = render(
<Provider theme={theme}>
<AlertDialog
aria-describedby="content-id"
variant="confirmation"
title="the title"
primaryActionLabel="confirm">
Content body
</AlertDialog>
</Provider>
);

expect(getByRole('alertdialog')).toHaveAttribute('aria-describedby', 'content-id');
});
});
10 changes: 10 additions & 0 deletions packages/@adobe/react-spectrum/test/list/ListView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,11 @@ describe('ListView', function () {
});

it("should support single tap to perform row selection with screen reader if onAction isn't provided", async function () {
// oxlint-disable-next-line no-unused-vars
using uaMock = jest
.spyOn(navigator, 'userAgent', 'get')
.mockImplementation(() => 'Android');

let tree = renderSelectionList({
onSelectionChange,
selectionMode: 'multiple',
Expand Down Expand Up @@ -1281,6 +1286,11 @@ describe('ListView', function () {
});

it('should support single tap to perform onAction with screen reader', async function () {
// oxlint-disable-next-line no-unused-vars
using uaMock = jest
.spyOn(navigator, 'userAgent', 'get')
.mockImplementation(() => 'Android');

let tree = renderSelectionList({
onSelectionChange,
selectionMode: 'multiple',
Expand Down
10 changes: 10 additions & 0 deletions packages/@adobe/react-spectrum/test/table/TableTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3414,6 +3414,11 @@ export let tableTests = () => {
describe('needs pointerEvents', function () {
installPointerEvent();
it("should support single tap to perform row selection with screen reader if onAction isn't provided", function () {
// oxlint-disable-next-line no-unused-vars
using uaMock = jest
.spyOn(navigator, 'userAgent', 'get')
.mockImplementation(() => 'Android');

let onSelectionChange = jest.fn();
let tree = renderTable({onSelectionChange, selectionStyle: 'highlight'});

Expand Down Expand Up @@ -3482,6 +3487,11 @@ export let tableTests = () => {
});

it('should support single tap to perform onAction with screen reader', function () {
// oxlint-disable-next-line no-unused-vars
using uaMock = jest
.spyOn(navigator, 'userAgent', 'get')
.mockImplementation(() => 'Android');

let onSelectionChange = jest.fn();
let onAction = jest.fn();
let tree = renderTable({onSelectionChange, selectionStyle: 'highlight', onAction});
Expand Down
8 changes: 6 additions & 2 deletions packages/@react-spectrum/s2/src/AlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
*/

import AlertTriangle from '../s2wf-icons/S2_Icon_AlertTriangle_20_N.svg';
import {AriaLabelingProps, DOMProps, DOMRef} from '@react-types/shared';
import {Button} from './Button';
import {ButtonGroup} from './ButtonGroup';
import {CenterBaseline} from './CenterBaseline';
import {chain} from 'react-aria/chain';
import {Content, Heading} from './Content';
import {Dialog} from './Dialog';
import {DOMProps, DOMRef} from '@react-types/shared';
import {filterDOMProps} from 'react-aria/filterDOMProps';
import {forwardRef, ReactNode} from 'react';
import {IconContext} from './Icon';
import intlMessages from '../intl/*.json';
Expand All @@ -27,7 +28,7 @@ import {style} from '../style' with {type: 'macro'};
import {UnsafeStyles} from './style-utils' with {type: 'macro'};
import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter';

export interface AlertDialogProps extends DOMProps, UnsafeStyles {
export interface AlertDialogProps extends AriaLabelingProps, DOMProps, UnsafeStyles {
/**
* The [visual style](https://spectrum.adobe.com/page/alert-dialog/#Options) of the AlertDialog.
*
Expand Down Expand Up @@ -105,8 +106,11 @@ export const AlertDialog = forwardRef(function AlertDialog(props: AlertDialogPro
buttonVariant = 'negative';
}

let domProps = filterDOMProps(props, {labelable: true});

return (
<Dialog
{...domProps}
role="alertdialog"
ref={ref}
size={props.size}
Expand Down
33 changes: 22 additions & 11 deletions packages/@react-spectrum/s2/src/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
DialogProps as RACDialogProps
} from 'react-aria-components/Dialog';
import {Provider} from 'react-aria-components/slots';
import {TextContext as RACTextContext} from 'react-aria-components/Text';
import {style} from '../style' with {type: 'macro'};
import {StyleProps} from './style-utils';
import {useDOMRef} from './useDOMRef';
Expand Down Expand Up @@ -197,17 +198,27 @@ export const Dialog = forwardRef(function Dialog(props: DialogProps, ref: DOMRef
{props.isDismissible && <CloseButton styles={style({marginBottom: 12})} />}
</div>
{/* Main content */}
<Provider
values={[
[ImageContext, {hidden: true}],
[HeadingContext, {isHidden: true}],
[HeaderContext, {isHidden: true}],
[ContentContext, {styles: content}],
[FooterContext, {isHidden: true}],
[ButtonGroupContext, {isHidden: true}]
]}>
{children}
</Provider>
<RACTextContext.Consumer>
{value => {
let contentValue = {};
if (value && 'slots' in value && value.slots?.description) {
contentValue = value.slots.description;
}
return (
<Provider
values={[
[ImageContext, {hidden: true}],
[HeadingContext, {isHidden: true}],
[HeaderContext, {isHidden: true}],
[ContentContext, {styles: content, ...contentValue}],
[FooterContext, {isHidden: true}],
[ButtonGroupContext, {isHidden: true}]
]}>
{children}
</Provider>
);
}}
</RACTextContext.Consumer>
{/* Footer and button group */}
<div
className={style({
Expand Down
85 changes: 85 additions & 0 deletions packages/@react-spectrum/s2/test/AlertDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {act, pointerMap, render} from '@react-spectrum/test-utils-internal';
import {ActionButton} from '../src/ActionButton';
import {AlertDialog} from '../src/AlertDialog';
import {Content} from '../src/Content';
import {DialogTrigger} from '../src/DialogTrigger';
import React from 'react';
import userEvent from '@testing-library/user-event';

describe('AlertDialog', () => {
let user;
beforeAll(() => {
jest.useFakeTimers();
user = userEvent.setup({delay: null, pointerMap});
});

afterEach(() => {
jest.clearAllMocks();
act(() => jest.runAllTimers());
});

afterAll(function () {
jest.restoreAllMocks();
});

it('automatically links to the content with aria-describedby', async () => {
let {getByRole} = render(
<DialogTrigger>
<ActionButton>Open dialog</ActionButton>
<AlertDialog title="Test" primaryActionLabel="Test">
Test content
</AlertDialog>
</DialogTrigger>
);

let trigger = getByRole('button');
await user.click(trigger);
act(() => {
jest.runAllTimers();
});
let dialog = getByRole('alertdialog');
expect(dialog).toBeVisible();
let description = dialog.getAttribute('aria-describedby');
expect(description).toBeDefined();
let content = document.getElementById(description!);
expect(content).toHaveTextContent('Test content');
});

it('accepts custom aria-describedby', async () => {
let {getByRole} = render(
<DialogTrigger>
<ActionButton>Open dialog</ActionButton>
<AlertDialog aria-describedby="content-id" title="Test" primaryActionLabel="Test">
<Content>
<p id="content-id">Test content</p>
<p>Extra content</p>
</Content>
</AlertDialog>
</DialogTrigger>
);

let trigger = getByRole('button');
await user.click(trigger);
act(() => {
jest.runAllTimers();
});
let dialog = getByRole('alertdialog');
expect(dialog).toBeVisible();
let description = dialog.getAttribute('aria-describedby');
expect(description).toBeDefined();
let content = document.getElementById(description!);
expect(content).toHaveTextContent('Test content');
});
});
Loading
Loading