Skip to content

Commit

Permalink
fix(a11y): indicate required form controls using an asterisk (#1723)
Browse files Browse the repository at this point in the history
Co-authored-by: Dan Cormier <dcormier@stackoverflow.com>
  • Loading branch information
giamir and dancormier committed May 7, 2024
1 parent 00d8538 commit 457404b
Show file tree
Hide file tree
Showing 186 changed files with 514 additions and 104 deletions.
73 changes: 73 additions & 0 deletions docs/product/components/inputs.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,79 @@
<li>If you have a group of related inputs, use the <code class="stacks-code">fieldset</code> and <code class="stacks-code">legend</code> to group them together.</li>
</ul>
<p class="stacks-copy">For more information, please read Gov.UK's article, <a href="https://accessibility.blog.gov.uk/2016/07/22/using-the-fieldset-and-legend-elements/" target="_blank"><em>"Using the fieldset and legend elements"</em></a>.</p>

{% header "h3", "Required input fields" %}
<p class="stacks-copy">Labels or instructions must be provided when content requires user input. For any input field within a form that is required for successful data submission, provide the asterisk <code class="stacks-code">*</code> as a symbol and a legend advising the meaning of the symbol before the first use.</p>
<p class="stacks-copy">Stacks includes a special <code class="stacks-code">.s-required-symbol</code> class to ensure the symbol (asterisk) is clearly visible.</p>

<table class="wmn4 s-table s-table__bx-simple mb16">
<thead>
<tr>
<th class="s-table--cell2" scope="col">Class</th>
<th class="s-table--cell3" scope="col">Applies</th>
<th scope="col">Definition</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><code class="stacks-code">.s-required-symbol</code></th>
<td><code class="stacks-code">abbr</code> element enclosing the asterisk</td>
<td>Used to style the asterisk indicating that a specific field is required.</td>
</tr>
</tbody>
</table>
{% highlight html %}
<abbr class="s-required-symbol" title="required">*</abbr>
{% endhighlight %}
<p class="stacks-copy mt16">Required symbols are not necessary for areas where only a single input field is seen on the page (ex: sign up modals). For more information, see <a href="https://www.w3.org/WAI/WCAG22/Techniques/html/H90" target="_blank">WCAG Technique H90</a>.</p>

{% header "h3", "Required input fields example" %}
<div class="stacks-preview">
{% highlight html %}
<div class="d-flex w100 jc-space-between ai-center">
<h1 class="fs-headline1 fw-normal mb16">
Ask a question
</h1>
<p class="fs-caption fc-black-400">Required fields<abbr class="s-required-symbol" title="required">*</abbr></p>
</div>
<form class="d-flex fd-column gy16">
<div class="d-flex gy4 fd-column">
<label class="s-label" for="example-title-required">Title<abbr class="s-required-symbol" title="required">*</abbr></label>
<input class="s-input" id="example-title-required" type="text" placeholder="Type a title" />
</div>
<div class="d-flex gy4 fd-column">
<label class="s-label" for="example-body-required">Body<abbr class="s-required-symbol" title="required">*</abbr></label>
<textarea class="s-textarea hmn1" id="example-body-required" placeholder="Type a question"></textarea>
</div>
<div class="d-flex gy4 fd-column">
<label class="s-label" for="example-ask-members">Ask team members</label>
<input class="s-input" id="example-ask-members" type="text" placeholder="Type a name" />
</div>
</form>
{% endhighlight %}
<div class="stacks-preview--example">
<div class="d-flex w100 jc-space-between ai-center">
<h1 class="fs-headline1 fw-normal mb16">
Ask a question
</h1>
<p class="fs-caption fc-black-400">Required fields<abbr class="s-required-symbol" title="required">*</abbr></p>
</div>
<form class="d-flex fd-column gy16">
<div class="d-flex gy4 fd-column">
<label class="s-label" for="example-title-required">Title<abbr class="s-required-symbol" title="required">*</abbr></label>
<input class="s-input" id="example-title-required" type="text" placeholder="Type a title" />
</div>
<div class="d-flex gy4 fd-column">
<label class="s-label" for="example-body-required">Body<abbr class="s-required-symbol" title="required">*</abbr></label>
<textarea class="s-textarea hmn1" id="example-body-required" placeholder="Type a question"></textarea>
</div>
<div class="d-flex gy4 fd-column">
<label class="s-label" for="example-ask-members">Ask team members</label>
<input class="s-input" id="example-ask-members" type="text" placeholder="Type a name" />
</div>
</form>
</div>
</div>
</section>

<section class="stacks-section">
Expand Down
2 changes: 1 addition & 1 deletion docs/product/components/labels.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@

<section class="stacks-section">
{% header "h2", "Status" %}
<p class="stacks-copy">When you need to flag labels as required or optional, use the following flags. Use the full word “Required” or “Optional” for these flags instead of using asterisks. If a majority of a form’s inputs are required, it isn’t necessary to show that they’re required until after the form has been submitted with errors. It may be more appropriate to only mark the non-required fields as optional.</p>
<p class="stacks-copy">When you need to flag labels as required or optional, use the following flags. Use the full word “Required” or “Optional” for these flags instead of using asterisks. If a majority of a form’s inputs are required, use the asterisk symbol to indicate which fields are required and include a legend. See <a href="/product/components/inputs/#accessibility">Input Accessibility</a> for more details.</p>

{% header "h3", "Optional" %}
<div class="stacks-preview">
Expand Down
45 changes: 2 additions & 43 deletions lib/components/label/label.a11y.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,6 @@
import { html } from "@open-wc/testing";
import { runA11yTests } from "../../test/a11y-test-utils";
import "../../index";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const labelTemplate = ({ component, testid }: any) => {
return html`
<fieldset data-testid="${testid}" class="p8 ws3">${component}</fieldset>
`;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getChildren = (status?: any) => {
const typeClass =
status && status !== "base" ? `s-label--status__${status}` : "";
return `
Example label
${
status
? `
<span class="s-label--status ${typeClass}">${
status ?? "no type"
}</span>
`
: ""
}
`;
};
import getTestArgs from "./label.test.setup";

describe("label", () => {
runA11yTests({
baseClass: `s-label`,
modifiers: {
primary: ["sm", "md", "lg", "xl"],
},
children: {
"default": getChildren(),
"status": getChildren("base"),
"status-beta": getChildren("beta"),
"status-new": getChildren("new"),
"status-required": getChildren("required"),
},
tag: "label",
template: ({ component, testid }) =>
labelTemplate({ component, testid }),
});
runA11yTests(getTestArgs());
});
8 changes: 8 additions & 0 deletions lib/components/label/label.less
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@
padding: 0 var(--su2); // Helps the label visually line up with inputs
}

.s-required-symbol {
color: var(--red-400);
font-size: 125%;
font-weight: normal;
line-height: 0;
text-decoration: none !important;
}

// [1] In Core, we have *many* instances of `.s-label--status` used without the `.s-label` parent.
// While I'd prefer to enforce the requirement of the parent class, it's too much of a lift at this moment.
// We'll come back to it, hopefully when we have a pill component to replace the current usage of `.s-label--status`
Expand Down
67 changes: 67 additions & 0 deletions lib/components/label/label.test.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { html } from "@open-wc/testing";
import type { TestVariationArgs } from "../../test/test-utils";
import "../../index";

type StatusType = "base" | "beta" | "new" | "required";

const labelTemplate = ({
component,
testid,
disabled,
}: {
component: unknown;
testid: string;
disabled: boolean;
}) => {
return html`
<fieldset
data-testid="${testid}"
class="p8 ws3"
?disabled="${disabled}"
>
${component}
</fieldset>
`;
};

const getStatus = (status: StatusType | undefined) => {
if (!status) return "";
const statusTypeClass =
status !== "base" ? `s-label--status__${status}` : "";
return `<span class="s-label--status ${statusTypeClass}">${status}</span>`;
};

const getRequiredSymbol = (required: boolean | undefined) => {
return required
? `<abbr class="s-required-symbol" title="required">*</abbr>`
: "";
};

const getChildren = (text: string, status?: StatusType, required?: boolean) => {
return `${text}${getRequiredSymbol(required)} ${getStatus(status)}`;
};

const getTestArgs = (disabled = false): TestVariationArgs => {
const text = disabled ? "Disabled label" : "Example label";
const prefix = disabled ? "disabled-" : "";

return {
baseClass: `s-label`,
modifiers: {
primary: ["sm", "md", "lg", "xl"],
},
children: {
[`${prefix}default`]: getChildren(text),
[`${prefix}required`]: getChildren(text, undefined, true),
[`${prefix}status`]: getChildren(text, "base"),
[`${prefix}status-beta`]: getChildren(text, "beta"),
[`${prefix}status-new`]: getChildren(text, "new"),
[`${prefix}status-required`]: getChildren(text, "required"),
},
tag: "label",
template: ({ component, testid }) =>
labelTemplate({ component, testid, disabled }),
};
};

export default getTestArgs;
63 changes: 3 additions & 60 deletions lib/components/label/label.visual.test.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,8 @@
import { html } from "@open-wc/testing";
import { runVisualTests } from "../../test/visual-test-utils";
import "../../index";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const labelTemplate = ({ component, testid, isDisabled }: any) => {
return html`
<fieldset
data-testid="${testid}"
class="p8 ws3"
?disabled="${isDisabled}"
>
${component}
</fieldset>
`;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getChildren = (text: string, status?: any) => {
const typeClass =
status && status !== "base" ? `s-label--status__${status}` : "";
return `
${text}
${
status
? `
<span class="s-label--status ${typeClass}">${
status ?? "no type"
}</span>
`
: ""
}
`;
};
import getTestArgs from "./label.test.setup";

describe("label", () => {
[true, false].forEach((isDisabled) => {
const text = isDisabled ? "Disabled label" : "Example label";

runVisualTests({
baseClass: `s-label`,
modifiers: {
primary: ["sm", "md", "lg", "xl"],
},
children: isDisabled
? {
"disabled": getChildren(text),
"disabled-status": getChildren(text, "base"),
"disabled-status-beta": getChildren(text, "beta"),
"disabled-status-new": getChildren(text, "new"),
"disabled-status-required": getChildren(text, "required"),
}
: {
"default": getChildren(text),
"status": getChildren(text, "base"),
"status-beta": getChildren(text, "beta"),
"status-new": getChildren(text, "new"),
"status-required": getChildren(text, "required"),
},
tag: "label",
template: ({ component, testid }) =>
labelTemplate({ component, testid, isDisabled }),
});
[true, false].forEach((disabled) => {
runVisualTests(getTestArgs(disabled));
});
});
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions screenshots/Chromium/baseline/s-label-dark-lg-required.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions screenshots/Chromium/baseline/s-label-dark-md-required.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions screenshots/Chromium/baseline/s-label-dark-required.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions screenshots/Chromium/baseline/s-label-dark-sm-required.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions screenshots/Chromium/baseline/s-label-dark-xl-required.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 457404b

Please sign in to comment.