Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alert - don't use role or aria-live for non-valid alert usages (HDS-3919) #2500

Merged
merged 8 commits into from
Oct 18, 2024
5 changes: 5 additions & 0 deletions .changeset/tough-lies-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashicorp/design-system-components": patch
---

`Alert` - Removed role="alert" and aria-live="polite" attributes from Alerts with color set to "neutral" or "highlight"
2 changes: 1 addition & 1 deletion packages/components/src/components/hds/alert/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div
class={{this.classNames}}
role={{this.role}}
aria-live="polite"
aria-live={{if this.role "polite"}}
aria-labelledby={{this.ariaLabelledBy}}
{{did-insert this.didInsert}}
...attributes
Expand Down
43 changes: 14 additions & 29 deletions packages/components/src/components/hds/alert/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export interface HdsAlertSignature {
}

export default class HdsAlert extends Component<HdsAlertSignature> {
@tracked role = 'alert';
@tracked role?: string;
@tracked ariaLabelledBy?: string;

constructor(owner: unknown, args: HdsAlertSignature['Args']) {
Expand All @@ -76,12 +76,7 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
);
}

/**
* @param color
* @type {enum}
* @default neutral
* @description Determines the color scheme for the alert.
*/
// Determines the color scheme for the alert.
get color(): HdsAlertColors {
const { color = DEFAULT_COLOR } = this.args;

Expand All @@ -95,12 +90,7 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
return color;
}

/**
* @param icon
* @type {string}
* @default false
* @description The name of the icon to be used.
*/
// The name of the icon to be used.
get icon(): HdsIconSignature['Args']['name'] | false {
const { icon } = this.args;

Expand All @@ -127,11 +117,6 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
}
}

/**
* @param onDismiss
* @type {function}
* @default () => {}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get onDismiss(): ((event: MouseEvent, ...args: any[]) => void) | false {
const { onDismiss } = this.args;
Expand All @@ -143,11 +128,7 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
}
}

/**
* @param iconSize
* @type {string}
* @description ensures that the correct icon size is used. Automatically calculated.
*/
// Ensures that the correct icon size is used. Automatically calculated.
get iconSize(): HdsIconSignature['Args']['size'] {
if (this.args.type === 'compact') {
return '16';
Expand All @@ -156,11 +137,6 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
}
}

/**
* Get the class names to apply to the component.
* @method Alert#classNames
* @return {string} The "class" attribute to apply to the component.
*/
get classNames(): string {
const classes = ['hds-alert'];

Expand All @@ -178,8 +154,17 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
const actions = element.querySelectorAll(
`${CONTENT_ELEMENT_SELECTOR} button, ${CONTENT_ELEMENT_SELECTOR} a`
);
if (actions.length) {

// an Alert which actually alerts users (has role="alert" & aria-live="polite") as opposed to an informational or promo "alert"
const isSemanticAlert: boolean =
this.color === 'warning' ||
this.color === 'critical' ||
this.color === 'success';

if (isSemanticAlert && actions.length) {
this.role = 'alertdialog';
} else if (isSemanticAlert) {
this.role = 'alert';
}

// `alertdialog` must have an accessible name so we use either the
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/components/hds/toast/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
@color={{@color}}
@icon={{@icon}}
@onDismiss={{@onDismiss}}
role="alert"
aria-live="polite"
...attributes
as |A|
>
Expand Down
127 changes: 116 additions & 11 deletions showcase/tests/integration/components/hds/alert/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,37 +145,142 @@ module('Integration | Component | hds/alert/index', function (hooks) {

// A11Y

test('it should render with the correct semantic tags and aria attributes', async function (assert) {
await render(hbs`<Hds::Alert @type="inline" id="test-alert" />`);
// * Colors for alert usages which notify users: success, warning, critical

test('it should render the component with role="alert" and aria-live="polite" for the "success" color', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" @color="success" id="test-alert" />`
);
assert.dom('#test-alert').hasAttribute('role', 'alert');
assert.dom('#test-alert').hasAttribute('aria-live', 'polite');
});

test('it should render the component with role="alert" and aria-live="polite" for the "warning" color', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" @color="warning" id="test-alert" />`
);
assert.dom('#test-alert').hasAttribute('role', 'alert');
assert.dom('#test-alert').hasAttribute('aria-live', 'polite');
});

test('it should render the component with role="alert" and aria-live="polite" for the "critical" color', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" @color="critical" id="test-alert" />`
);
assert.dom('#test-alert').hasAttribute('role', 'alert');
assert.dom('#test-alert').hasAttribute('aria-live', 'polite');
});

// * Colors for informational & promo usages: neutral, highlight

test('it should not render the component with role="alert" and aria-live="polite" for the "neutral" color', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" @color="neutral" id="test-alert" />`
);
assert.dom('#test-alert').doesNotHaveAttribute('role', 'alert');
assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite');
});

test('it should not render the component with role="alert" and aria-live="polite" for the "highlight" color', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" @color="highlight" id="test-alert" />`
);
assert.dom('#test-alert').doesNotHaveAttribute('role', 'alert');
assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite');
});

test('it should render with an `alertdialog` role and auto-generated `aria-labelledby` when title and actions are provided', async function (assert) {
// aria-labelledby

test('it should render with an auto-generated `aria-labelledby` when a title is provided', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" id="test-alert" as |A|><A.Title>This is the title</A.Title><A.Button @text="I am a button" @size="small" /></Hds::Alert>`
hbs`
<Hds::Alert @type="inline" id="test-alert" as |A|>
<A.Title>This is the title</A.Title>
</Hds::Alert>
`
);
let title = this.element.querySelector('#test-alert .hds-alert__title');
assert.dom('#test-alert').hasAttribute('role', 'alertdialog');
assert.dom('#test-alert').hasAttribute('aria-labelledby', title.id);
});

test('it should render with an `alertdialog` role and auto-generated `aria-labelledby` when description and actions are provided', async function (assert) {
test('it should render with an auto-generated `aria-labelledby` when description is provided', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" id="test-alert" as |A|><A.Description>This is the title</A.Description><A.Button @text="I am a button" @size="small" /></Hds::Alert>`
hbs`
<Hds::Alert @type="inline" id="test-alert" as |A|>
<A.Description>This is the title</A.Description>
</Hds::Alert>
`
);
let description = this.element.querySelector(
'#test-alert .hds-alert__description'
);
assert.dom('#test-alert').hasAttribute('role', 'alertdialog');
assert.dom('#test-alert').hasAttribute('aria-labelledby', description.id);
});

test('it should render with an `alertdialog` role and `aria-labelledby` when title and actions are provided', async function (assert) {
// Alert dialogs

// * Colors for alert usages which notify users: success, warning, critical

test('it should render with with role="alertdialog" and aria-live="polite" for the "success" color when actions are provided', async function (assert) {
await render(
hbs`
<Hds::Alert @type="inline" @color="success" id="test-alert" as |A|>
<A.Button @text="I am a button" @size="small" />
</Hds::Alert>
`
);
assert.dom('#test-alert').hasAttribute('role', 'alertdialog');
assert.dom('#test-alert').hasAttribute('aria-live', 'polite');
});

test('it should render with with role="alertdialog" and aria-live="polite" for the "warning" color when actions are provided', async function (assert) {
await render(
hbs`<Hds::Alert @type="inline" id="test-alert" as |A|><A.Title id="custom-id">This is the title</A.Title><A.Button @text="I am a button" @size="small" /></Hds::Alert>`
hbs`
<Hds::Alert @type="inline" @color="warning" id="test-alert" as |A|>
<A.Button @text="I am a button" @size="small" />
</Hds::Alert>
`
);
assert.dom('#test-alert').hasAttribute('role', 'alertdialog');
assert.dom('#test-alert').hasAttribute('aria-labelledby', 'custom-id');
assert.dom('#test-alert').hasAttribute('aria-live', 'polite');
});

test('it should render with with role="alertdialog" and aria-live="polite" for the "critical" color when actions are provided', async function (assert) {
await render(
hbs`
<Hds::Alert @type="inline" @color="critical" id="test-alert" as |A|>
<A.Button @text="I am a button" @size="small" />
</Hds::Alert>
`
);
assert.dom('#test-alert').hasAttribute('role', 'alertdialog');
assert.dom('#test-alert').hasAttribute('aria-live', 'polite');
});

// * Colors for informational & promo usages: neutral, highlight

test('it should not render with with role="alertdialog" and aria-live="polite" for the "neutral" color when actions are provided', async function (assert) {
await render(
hbs`
<Hds::Alert @type="inline" @color="neutral" id="test-alert" as |A|>
<A.Button @text="I am a button" @size="small" />
</Hds::Alert>
`
);
assert.dom('#test-alert').doesNotHaveAttribute('role', 'alertdialog');
assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite');
});

test('it should not render with with role="alertdialog" and aria-live="polite" for the "highlight" color when actions are provided', async function (assert) {
await render(
hbs`
<Hds::Alert @type="inline" @color="highlight" id="test-alert" as |A|>
<A.Button @text="I am a button" @size="small" />
</Hds::Alert>
`
);
assert.dom('#test-alert').doesNotHaveAttribute('role', 'alertdialog');
assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite');
});

// ASSERTIONS
Expand Down
6 changes: 6 additions & 0 deletions showcase/tests/integration/components/hds/toast/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ module('Integration | Component | hds/toast/index', function (hooks) {
await render(hbs`<Hds::Toast id="test-toast" />`);
assert.dom('#test-toast').hasClass('hds-toast');
});

test('it should render the component with "role"="alert" and aria-live="polite" by default', async function (assert) {
await render(hbs`<Hds::Toast id="test-toast" />`);
assert.dom('#test-toast').hasAttribute('role', 'alert');
assert.dom('#test-toast').hasAttribute('aria-live', 'polite');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
Sets the type of alert.
</C.Property>
<C.Property @name="color" @type="enum" @values={{array "neutral" "highlight" "success" "warning" "critical"}} @default="neutral">
Sets the color scheme for `background`, `border`, `title`, and `description`, which **cannot** be overridden.<br/><br/>`color` results in a default `icon`, which **can** be overridden.
Sets the color scheme for `background`, `border`, `title`, and `description`, which **cannot** be overridden.<br/><br/>`color` results in a default `icon`, which **can** be overridden.<br/><br/>For the “success”, “warning”, and “critical” colors, either `role="alert"` or `role="alertdialog"` and `aria-live="polite"` will be included by default which can be overridden if necessary.<br/><br/>The “neutral” and “highlight” colors do not include a role attribute or `aria-live="polite"` by default.
</C.Property>
<C.Property @name="icon" @type="string | false">
Override the default `icon` name, which is determined by the `color` argument.<br/><br/>accepts any [icon](/icons/library) name, or `false`, for no icon.
Expand Down
8 changes: 7 additions & 1 deletion website/docs/components/alert/partials/code/how-to-use.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ The default `@tag` is `"div"` because the correct value is dependent on the indi

### Color

A different color can be applied to the Alert using the `color` argument. This will determine the default icon used in the Alert, unless overwritten.
The available color values are `neutral` (the default), `highlight`, `success`, `warning`, and `critical`. Setting a color value will also determine the default icon used in the Alert, although it is customizable.

The color value will also determine some accessibility features of the component, so this should be taken into consideration when choosing which Alert `color` value to use.


If the alert is being used in an informational or promotional way, `neutral` or `highlight` colors should be chosen.

The other color values map to accessibility-related roles, and will ensure that essential information is presented to the user with assistive technology in a timely manner.
```handlebars
<Hds::Alert @type="inline" @color="success" as |A|>
<A.Title>Title here</A.Title>
Expand Down
Loading