Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6526,6 +6526,9 @@ Map {
"danger": {
"type": "bool",
},
"dangerDescription": {
"type": "string",
},
"decorator": {
"type": "node",
},
Expand Down Expand Up @@ -6696,6 +6699,9 @@ Map {
"danger": {
"type": "bool",
},
"dangerDescription": {
"type": "string",
},
"inputref": {
"args": [
[
Expand Down Expand Up @@ -12181,6 +12187,9 @@ Map {
"danger": {
"type": "bool",
},
"dangerDescription": {
"type": "string",
},
"loadingDescription": {
"type": "string",
},
Expand Down
8 changes: 5 additions & 3 deletions packages/react/src/components/Button/ButtonBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const ButtonBase = React.forwardRef(function ButtonBase<
as,
children,
className,
dangerDescription = 'danger',
dangerDescription = '',
disabled = false,
hasIconOnly = false,
href,
Expand Down Expand Up @@ -72,6 +72,8 @@ const ButtonBase = React.forwardRef(function ButtonBase<
);

const dangerButtonVariants = ['danger', 'danger--tertiary', 'danger--ghost'];
const hasDangerDescription =
dangerButtonVariants.includes(kind) && Boolean(dangerDescription);

let component: React.ElementType = 'button';
const assistiveId = useId('danger-description');
Expand All @@ -80,7 +82,7 @@ const ButtonBase = React.forwardRef(function ButtonBase<
let otherProps: Partial<ButtonBaseProps> = {
disabled,
type,
'aria-describedby': dangerButtonVariants.includes(kind)
'aria-describedby': hasDangerDescription
? assistiveId
: ariaDescribedBy || undefined,
'aria-pressed':
Expand All @@ -91,7 +93,7 @@ const ButtonBase = React.forwardRef(function ButtonBase<
};

let assistiveText: JSX.Element | null = null;
if (dangerButtonVariants.includes(kind)) {
if (hasDangerDescription) {
assistiveText = (
<span id={assistiveId} className={`${prefix}--visually-hidden`}>
{dangerDescription}
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/Button/__tests__/Button-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ describe('Button', () => {
}
);

it('does not render danger assistive text when dangerDescription is empty', () => {
render(
<Button kind="danger" dangerDescription="">
Delete
</Button>
);

expect(screen.getByRole('button', { name: 'Delete' })).not.toHaveAttribute(
'aria-describedby'
);
});

it.each([
['xs', 'cds--btn--xs'],
['sm', 'cds--btn--sm'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,21 @@ describe('ModalFooter', () => {
);

expect(screen.getByText('Submit')).toHaveClass('cds--btn--danger');
expect(screen.getByText('danger', { hidden: true })).toBeInTheDocument();
expect(screen.getByText('Submit')).not.toHaveAttribute('aria-describedby');
});

it('should allow a localized danger description for the primary button', () => {
render(
<ModalFooter
secondaryButtonText="Cancel"
primaryButtonText="Submit"
danger
dangerDescription="gefahr"
/>
);

expect(screen.getByText('Submit')).toHaveAttribute('aria-describedby');
expect(screen.getByText('gefahr', { hidden: true })).toBeInTheDocument();
});

it('should call onRequestClose when close requested', async () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/components/ComposedModal/ModalFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ export interface ModalFooterProps {
*/
danger?: boolean;

/**
* Specify the message read by screen readers for the danger primary button.
* Defaults to an empty string; provide localized text to opt in.
*/
dangerDescription?: string;

/**
* The `ref` callback for the primary button.
*/
Expand Down Expand Up @@ -204,6 +210,7 @@ export const ModalFooter = React.forwardRef<HTMLElement, ModalFooterProps>(
className: customClassName,
closeModal = noopFn,
danger,
dangerDescription = '',
inputref,
onRequestClose = noopFn,
onRequestSubmit = noopFn,
Expand Down Expand Up @@ -259,6 +266,7 @@ export const ModalFooter = React.forwardRef<HTMLElement, ModalFooterProps>(
onClick={onRequestSubmit}
className={primaryButtonClass}
disabled={loadingActive || primaryButtonDisabled}
dangerDescription={dangerDescription}
kind={danger ? 'danger' : 'primary'}
ref={inputref}>
{loadingStatus === 'inactive' ? (
Expand Down Expand Up @@ -302,6 +310,12 @@ ModalFooter.propTypes = {
*/
danger: PropTypes.bool,

/**
* Specify the message read by screen readers for the danger primary button.
* Defaults to an empty string; provide localized text to opt in.
*/
dangerDescription: PropTypes.string,

/**
* The `ref` callback for the primary button.
*/
Expand Down
29 changes: 29 additions & 0 deletions packages/react/src/components/Dialog/Dialog-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,35 @@ describe('Dialog', () => {
caf.mockRestore();
});

it('does not add a default danger description to `DialogFooter` primary actions', () => {
render(
<Dialog open>
<DialogFooter danger primaryButtonText="Delete" />
</Dialog>
);

expect(
screen.getByRole('button', { name: 'Delete' })
).not.toHaveAttribute('aria-describedby');
});

it('allows a localized danger description for `DialogFooter` primary actions', () => {
render(
<Dialog open>
<DialogFooter
danger
primaryButtonText="Delete"
dangerDescription="gefahr"
/>
</Dialog>
);

expect(
screen.getByRole('button', { name: 'gefahr Delete' })
).toHaveAttribute('aria-describedby');
expect(screen.getByText('gefahr', { hidden: true })).toBeInTheDocument();
});

it('prefers aria-label prop over deprecated ariaLabel prop', () => {
render(<Dialog open aria-label="label" ariaLabel="deprecated label" />);

Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,12 @@ interface DialogFooterProps extends HTMLAttributes<HTMLDivElement> {
*/
danger?: boolean;

/**
* Specify the message read by screen readers for the danger primary button.
* Defaults to an empty string; provide localized text to opt in.
*/
dangerDescription?: string;

/**
* Specify loading status
*/
Expand Down Expand Up @@ -832,6 +838,7 @@ const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(
loadingIconDescription,
onLoadingSuccess = noopFn,
danger = false,
dangerDescription = '',
...rest
},
ref
Expand Down Expand Up @@ -906,6 +913,7 @@ const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(
<Button
className={primaryButtonClass}
kind={danger ? 'danger' : 'primary'}
dangerDescription={dangerDescription}
disabled={loadingActive || primaryButtonDisabled}
onClick={onRequestSubmit}
ref={button}>
Expand Down Expand Up @@ -1006,6 +1014,12 @@ DialogFooter.propTypes = {
*/
danger: PropTypes.bool,

/**
* Specify the message read by screen readers for the danger primary button.
* Defaults to an empty string; provide localized text to opt in.
*/
dangerDescription: PropTypes.string,

/**
* Specify loading status
*/
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
{
children,
className,
dangerDescription = 'danger',
dangerDescription = '',
disabled,
kind = 'default',
label,
Expand Down Expand Up @@ -143,6 +143,7 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(

const isDisabled = disabled && !hasChildren;
const isDanger = kind === 'danger' && !hasChildren;
const hasDangerDescription = isDanger && Boolean(dangerDescription);

function registerItem() {
context.dispatch({
Expand Down Expand Up @@ -282,7 +283,7 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
<Text as="div" className={`${prefix}--menu-item__label`}>
{label}
</Text>
{isDanger && (
{hasDangerDescription && (
<span id={assistiveId} className={`${prefix}--visually-hidden`}>
{dangerDescription}
</span>
Expand Down
32 changes: 32 additions & 0 deletions packages/react/src/components/Modal/Modal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,38 @@ describe.each([
);
});

it('does not add a default danger description to the primary action', () => {
render(
<Component
danger
primaryButtonText="Delete"
data-testid="modal-danger-default"
/>
);

expect(screen.getByRole('button', { name: 'Delete' })).not.toHaveAttribute(
'aria-describedby'
);
});

it('allows a localized danger description to be provided', () => {
render(
<Component
danger
dangerDescription="gefahr"
primaryButtonText="Delete"
data-testid="modal-danger-localized"
/>
);

const button = screen.getByRole('button', { name: 'gefahr Delete' });

expect(button).toHaveAttribute('aria-describedby');
expect(screen.getByText('gefahr')).toHaveClass(
`${prefix}--visually-hidden`
);
});

it('disables buttons when inline loading status is active', () => {
render(
<Component
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/components/Modal/Modal.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default {
modalLabel: {
control: 'text',
},
dangerDescription: {
control: 'text',
},
numberOfButtons: {
description: 'Count of Footer Buttons',
options: Object.keys(buttons),
Expand Down
15 changes: 15 additions & 0 deletions packages/react/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ export interface ModalProps extends HTMLAttributes<HTMLDivElement> {
*/
danger?: boolean;

/**
* Specify the message read by screen readers for the danger primary button.
* Defaults to an empty string; provide localized text to opt in.
*/
dangerDescription?: string;

/**
* **Experimental**: Provide a decorator component to be rendered inside the `Modal` component
*/
Expand Down Expand Up @@ -293,6 +299,7 @@ const ModalDialog = React.forwardRef(function ModalDialog(
onSecondarySubmit,
primaryButtonDisabled = false,
danger,
dangerDescription = '',
alert,
secondaryButtons,
selectorPrimaryFocus = '[data-modal-primary-focus]',
Expand Down Expand Up @@ -746,6 +753,7 @@ const ModalDialog = React.forwardRef(function ModalDialog(
<Button
className={primaryButtonClass}
kind={danger ? 'danger' : 'primary'}
dangerDescription={dangerDescription}
disabled={loadingActive || primaryButtonDisabled}
onClick={onRequestSubmit}
ref={button}>
Expand Down Expand Up @@ -843,6 +851,7 @@ const ModalDialog = React.forwardRef(function ModalDialog(
<Button
className={primaryButtonClass}
kind={danger ? 'danger' : 'primary'}
dangerDescription={dangerDescription}
disabled={loadingActive || primaryButtonDisabled}
onClick={onRequestSubmit}
ref={button}>
Expand Down Expand Up @@ -925,6 +934,12 @@ Modal.propTypes = {
*/
danger: PropTypes.bool,

/**
* Specify the message read by screen readers for the danger primary button.
* Defaults to an empty string; provide localized text to opt in.
*/
dangerDescription: PropTypes.string,

/**
* **Experimental**: Provide a decorator component to be rendered inside the `Modal` component
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const OverflowMenuItem = frFn((props, ref) => {
const {
className,
closeMenu,
dangerDescription = 'danger',
dangerDescription = '',
disabled = false,
handleOverflowMenuItemFocus,
hasDivider = false,
Expand Down Expand Up @@ -151,6 +151,7 @@ const OverflowMenuItem = frFn((props, ref) => {
);

const TagToUse = href ? 'a' : 'button';
const hasDangerDescription = isDelete && Boolean(dangerDescription);

const assistiveId = useId('danger-description');

Expand All @@ -163,7 +164,7 @@ const OverflowMenuItem = frFn((props, ref) => {
<div className={`${prefix}--overflow-menu-options__option-content`}>
{itemText}
</div>
{isDelete && (
{hasDangerDescription && (
<span id={assistiveId} className={`${prefix}--visually-hidden`}>
{dangerDescription}
</span>
Expand Down
Loading
Loading