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

fix(action): Add screen reader support for active and indicator props #5875

Merged
merged 12 commits into from
Dec 6, 2022
64 changes: 63 additions & 1 deletion src/components/action/action.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
import { newE2EPage } from "@stencil/core/testing";
import { accessible, disabled, hidden, renders, slots } from "../../tests/commonTests";
import { accessible, disabled, hidden, renders, slots, defaults } from "../../tests/commonTests";
import { CSS, SLOTS } from "./resources";

describe("calcite-action", () => {
it("has property defaults", async () =>
defaults("calcite-action", [
{
propertyName: "active",
defaultValue: false
},
{
propertyName: "appearance",
defaultValue: "solid"
},
{
propertyName: "compact",
defaultValue: false
},
{
propertyName: "disabled",
defaultValue: false
},
{
propertyName: "indicator",
defaultValue: false
},
{
propertyName: "loading",
defaultValue: false
},
{
propertyName: "scale",
defaultValue: "m"
},
{
propertyName: "textEnabled",
defaultValue: false
}
]));

it("renders", async () => renders("calcite-action", { display: "flex" }));

it("honors hidden attribute", async () => hidden("calcite-action"));
Expand Down Expand Up @@ -114,6 +150,10 @@ describe("calcite-action", () => {
it("should be accessible", async () => {
await accessible(`<calcite-action text="hello world"></calcite-action>`);
await accessible(`<calcite-action text="hello world" disabled text-enabled></calcite-action>`);
await accessible(`<calcite-action indicator text="hello world"></calcite-action>`);
await accessible(
`<calcite-action indicator indicator-message="Unsaved changes" text="hello world"></calcite-action>`
);
});

it("should have a tooltip", async () => {
Expand All @@ -127,4 +167,26 @@ describe("calcite-action", () => {
const referenceElement: HTMLElement = await tooltip.getProperty("referenceElement");
expect(referenceElement).toBeDefined();
});

it("should have a indicator live region", async () => {
const indicatorText = "Unsaved stuff";
const page = await newE2EPage();
await page.setContent(`<calcite-action></calcite-action>`);
await page.waitForChanges();

const action = await page.find("calcite-action");
const liveRegion = await page.find(`calcite-action >>> .${CSS.indicatorText}`);

expect(liveRegion.getAttribute("aria-live")).toBe("polite");
expect(liveRegion.getAttribute("role")).toBe("region");
expect(liveRegion.textContent).toBe("");

action.setProperty("indicator", true);
action.setProperty("indicatorMessage", indicatorText);
await page.waitForChanges();

expect(liveRegion.getAttribute("aria-live")).toBe("polite");
expect(liveRegion.getAttribute("role")).toBe("region");
expect(liveRegion.textContent).toBe(indicatorText);
});
});
4 changes: 4 additions & 0 deletions src/components/action/action.scss
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,7 @@
:host([indicator][active]) .button::after {
border-color: theme("colors.background.foreground.3");
}

.indicator-text {
@apply sr-only;
}
52 changes: 50 additions & 2 deletions src/components/action/action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Component, Element, Host, Method, Prop, h, forceUpdate, VNode } from "@
import { Alignment, Appearance, Scale } from "../interfaces";

import { CSS, TEXT, SLOTS } from "./resources";

import { guid } from "../../utils/guid";
import { createObserver } from "../../utils/observers";
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";
import { toAriaBoolean } from "../../utils/dom";
Expand Down Expand Up @@ -60,6 +60,18 @@ export class Action implements InteractiveComponent, LoadableComponent {
*/
@Prop({ reflect: true }) indicator = false;

/**
* Specifies the text label to display `indicator` is `true`.
*/
@Prop() indicatorMessage = "";

/**
* Specifies the text label to display `indicator` is `true`.
*
* @default "Unread changes"
*/
@Prop() intlIndicator?: string = TEXT.indicator;

/**
* Specifies the text label to display while loading.
*
Expand Down Expand Up @@ -104,6 +116,12 @@ export class Action implements InteractiveComponent, LoadableComponent {

mutationObserver = createObserver("mutation", () => forceUpdate(this));

guid = `calcite-action-${guid()}`;

indicatorId = `${this.guid}-indicator`;

buttonId = `${this.guid}-button`;

// --------------------------------------------------------------------------
//
// Lifecycle
Expand Down Expand Up @@ -165,6 +183,21 @@ export class Action implements InteractiveComponent, LoadableComponent {
) : null;
}

renderIndicatorText(): VNode {
const { indicator, intlIndicator, indicatorMessage, indicatorId, buttonId } = this;
return (
<div
aria-labelledby={buttonId}
aria-live="polite"
class={CSS.indicatorText}
id={indicatorId}
role="region"
>
{indicator ? indicatorMessage ?? intlIndicator : null}
</div>
);
}

renderIconContainer(): VNode {
const { loading, icon, scale, el, intlLoading } = this;
const iconScale = scale === "l" ? "m" : "s";
Expand Down Expand Up @@ -196,7 +229,18 @@ export class Action implements InteractiveComponent, LoadableComponent {
}

render(): VNode {
const { compact, disabled, loading, textEnabled, label, text } = this;
const {
active,
compact,
disabled,
loading,
textEnabled,
label,
text,
indicator,
indicatorId,
buttonId
} = this;

const ariaLabel = label || text;

Expand All @@ -210,16 +254,20 @@ export class Action implements InteractiveComponent, LoadableComponent {
<Host>
<button
aria-busy={toAriaBoolean(loading)}
aria-controls={indicator ? indicatorId : null}
aria-disabled={toAriaBoolean(disabled)}
aria-label={ariaLabel}
aria-pressed={toAriaBoolean(active)}
class={buttonClasses}
disabled={disabled}
id={buttonId}
ref={(buttonEl): HTMLButtonElement => (this.buttonEl = buttonEl)}
>
{this.renderIconContainer()}
{this.renderTextContainer()}
</button>
<slot name={SLOTS.tooltip} onSlotchange={this.handleTooltipSlotChange} />
{this.renderIndicatorText()}
</Host>
);
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/action/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const CSS = {
button: "button",
buttonTextVisible: "button--text-visible",
buttonCompact: "button--compact",
indicatorText: "indicator-text",
iconContainer: "icon-container",
slotContainer: "slot-container",
slotContainerHidden: "slot-container--hidden",
Expand All @@ -10,7 +11,8 @@ export const CSS = {
};

export const TEXT = {
loading: "Loading"
loading: "Loading",
indicator: "Unread changes"
};

export const SLOTS = {
Expand Down