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(a11y): indicate required form controls using an asterisk #1723

Merged
merged 5 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
dancormier marked this conversation as resolved.
Show resolved Hide resolved

{% 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.