Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions custom-elements-manifest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ export default {
cssclass: {
mappedName: 'cssClasses',
isArray: true
},
role: {},
aria: {
isArray: true
},
focusable: {},
formassociated: {
mappedName: 'formAssociated'
},
keycontrol: {
mappedName: 'keyControls',
isArray: true
}
}
})
Expand Down
10 changes: 10 additions & 0 deletions src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ declare global {
* @cssclass forge-checkbox - Apply to the root element _(required)_.
* @cssclass forge-checkbox--dense - Makes the checkbox dense.
* @cssclass forge-checkbox__icon - Apply to a child of the root element to render the check and indeterminate icons _(required)_.
*
* @role checkbox
* @focusable
* @formassociated
*
* @aria aria-checked - Indicates whether the checkbox is checked.
* @aria aria-disabled - Indicates whether the checkbox is disabled.
* @aria aria-required - Indicates whether the checkbox is required.
*
* @keycontrol space - Toggles the checkbox checked or unchecked.
*/
@customElement({
name: CHECKBOX_CONSTANTS.elementName,
Expand Down
156 changes: 156 additions & 0 deletions src/stories/blocks/A11yInformation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { useOf } from '@storybook/blocks';
import { Code } from '@storybook/components';
import { getCustomElementsTagDeclaration, TagItem } from '../utils';
import { Checkmark, Section, UsageLink, Xmark } from './Shared';

import styles from './CustomArgTypes.module.scss';

function RoleLink({ role }: { role: string }) {
const href = `https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/${role.toLowerCase()}_role`;
return (
<a className={(styles as any).cardLink} href={href} target="_blank" rel="noreferrer noopener">
Learn about this role
</a>
);
}

function AriaAttributeLink({ attribute }: { attribute: string }) {
const href = `https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/${attribute.toLowerCase()}`;
const anchor = `<a href=${href} target="_blank" rel="noreferrer noopener">${attribute}</a>`;
return (
<Code>
<span dangerouslySetInnerHTML={{ __html: anchor }} />
</Code>
);
}

function AriaAttrsTable({ items }: { items: TagItem[] }) {
return (
<Section title="ARIA Attributes" name="aria-attributes">
<p>Unless stated otherwise, all ARIA attributes are managed by the component. In the majority of cases there's no need to set these manually.</p>
<table className={(styles as any).table}>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{items?.map((item: TagItem, index) => (
<tr key={`${item.name}-${index}`}>
<td>
<AriaAttributeLink attribute={item.name} />
</td>
<td>{item.description}</td>
</tr>
))}
</tbody>
</table>
</Section>
);
}

function KeyboardShortcutsTable({ items }: { items: TagItem[] }) {
const keyboardShortcuts: { name: string; keys: string[]; description: string }[] = items.map(item => {
const splitKeys = item.name.split('+');
splitKeys.map(key => key.trim().toLowerCase());
return {
name: item.name,
keys: splitKeys,
description: item.description
};
});
return (
<Section title="Keyboard Shortcuts" name="keyboard-shortcuts">
<table className={(styles as any).table}>
<thead>
<tr>
<th>Key</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
{keyboardShortcuts.map((keyboardShortcut, index) => (
<tr key={`${keyboardShortcut.name}-${index}`}>
<td>
{keyboardShortcut.keys.map((key, index) => (
<>
<span className={(styles as any).key}>{key}</span>
{index < keyboardShortcut.keys.length - 1 ? ' + ' : null}
</>
))}
</td>
<td>{keyboardShortcut.description}</td>
</tr>
))}
</tbody>
</table>
</Section>
);
}

function A11yCard({ children, title, icon, theme }: { children: React.ReactNode; title?: string; icon: React.ReactNode; theme?: string }) {
const backgroundColor = theme ? `var(--forge-theme-${theme}-container-low)` : 'var(--forge-theme-surface-container-low)';
const borderColor = theme ? `var(--forge-theme-${theme}-container)` : 'var(--forge-theme-outline)';
const textColor = theme ? `var(--forge-theme-on-${theme}-container-low)` : 'var(--forge-theme-text-medium)';
const cardStyles = {
background: backgroundColor,
borderColor: borderColor,
color: textColor,
'--_icon-color': textColor
} as React.CSSProperties;

return (
<>
<div className={(styles as any).card} style={cardStyles}>
<span className={(styles as any).cardIcon}>{icon}</span>
<div>
{title ? <div className={(styles as any).cardTitle}>{title}</div> : null}
<div className={(styles as any).cardValue}>{children}</div>
</div>
</div>
</>
);
}

export default function A11yInformation({ children }: { children: React.ReactNode }) {
const resolvedOf = useOf('story', ['story']);
const tagName = resolvedOf.story.component as string;

if (!tagName || typeof tagName !== 'string') {
return null;
}

const declaration = getCustomElementsTagDeclaration(tagName);
const role = declaration.role?.name ?? 'presentation';
const focusable = !!declaration.focusable;
const formAssociated = !!declaration.formAssociated;
const ariaAttributes = declaration.aria ?? [];
const keyboardShortcuts = declaration.keyControls ?? [];
const roleSvg = (
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
<path d="M480-720q-33 0-56.5-23.5T400-800q0-33 23.5-56.5T480-880q33 0 56.5 23.5T560-800q0 33-23.5 56.5T480-720ZM360-120v-480q-49-4-99-11t-98-18q-17-4-27.5-19t-5.5-32q5-17 21-25t34-4q70 15 145.5 22t149.5 7q74 0 149.5-7T775-709q18-4 34 4t21 25q5 17-5.5 32T797-629q-48 11-98 18t-99 11v480q0 17-11.5 28.5T560-80q-17 0-28.5-11.5T520-120v-200h-80v200q0 17-11.5 28.5T400-80q-17 0-28.5-11.5T360-120Z" />
</svg>
);

return (
<>
<div className={(styles as any).cardContainer}>
<A11yCard title="Role" icon={roleSvg} theme="info">
<div>{role}</div>
<RoleLink role={role} />
</A11yCard>
<A11yCard icon={focusable ? <Checkmark /> : <Xmark />} theme={focusable ? 'success' : undefined}>
{(focusable ? 'F' : 'Not f') + 'ocusable'}
</A11yCard>
<A11yCard icon={formAssociated ? <Checkmark /> : <Xmark />} theme={formAssociated ? 'success' : undefined}>
{(formAssociated ? 'F' : 'Not f') + 'orm associated'}
</A11yCard>
</div>
{children}
{ariaAttributes.length ? <AriaAttrsTable items={ariaAttributes} /> : null}
{keyboardShortcuts.length ? <KeyboardShortcutsTable items={keyboardShortcuts} /> : null}
<UsageLink text="Accessibility" href="?path=/docs/getting-started-accessibility--docs" />
</>
);
}
1 change: 0 additions & 1 deletion src/stories/blocks/CssOnlyInformation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Source, useOf } from '@storybook/blocks';
import { getCustomElementsTagDeclaration, TagItem } from '../utils';
import { NameDescriptionTable, Section, UsageLink } from './Shared';
import { css } from 'lit';

function friendlyNameFromTagName(tagName: string) {
return tagName
Expand Down
54 changes: 54 additions & 0 deletions src/stories/blocks/CustomArgTypes.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,57 @@
text-align: start;
}
}

.cardContainer {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-block-start: 24px !important;
padding-block-end: 8px;
}

.card {
display: grid;
grid-template-columns: auto 1fr;
column-gap: 16px;
align-items: center;
padding-block: 8px;
padding-inline: 20px;
border-radius: 4px;
border-style: solid;
border-width: 1px;
}

.cardIcon {
grid-column: 1;
}

.cardTitle {
grid-column: 2;
font-size: 14px !important;
}

.cardValue {
grid-column: 2;
font-weight: bold;
}

.cardLink {
display: block;
color: var(--forge-theme-primary) !important;
font-size: 13px !important;
font-style: italic;
font-weight: normal;
}

.key {
display: inline-block;
padding-inline: 8px;
border-radius: 4px;
border: 1px solid var(--forge-theme-outline);
border-bottom-width: 2px;
background-color: var(--forge-theme-surface-bright);
color: var(--forge-theme-on-surface-bright);
font-family: ui-monospace, Menlo, Monaco, 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Droid Sans Mono', 'Courier New', monospace !important;
font-size: 13px !important;
}
8 changes: 6 additions & 2 deletions src/stories/blocks/CustomArgTypes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HeaderMdx, Markdown, useOf } from '@storybook/blocks';
import { Code } from '@storybook/components';
import { TagItem, getBranchName, getCustomElementType, getCustomElementsTagDeclaration, getCustomElementsTagModule, htmlEncode } from '../utils';
import { NameDescriptionTable, Section, UsageLink } from './Shared';
import { Checkmark, NameDescriptionTable, Section, UsageLink } from './Shared';

import styles from './CustomArgTypes.module.scss';
import GitHubLogo from './GitHubLogo';
Expand Down Expand Up @@ -70,7 +70,11 @@ function PropsAttrsTable({ items, globalConfigProperties }: { items: TagItem[];
<td>
<Markdown>{item.description}</Markdown>
</td>
{!!globalConfigProperties?.length ? <td style={{ textAlign: 'center' }}>{globalConfigProperties.includes(item.name) ? '✅' : ''}</td> : null}
{!!globalConfigProperties?.length ? (
<td style={{ textAlign: 'center' }}>
{globalConfigProperties.includes(item.name) ? <Checkmark alt="Yes" color="var(--forge-theme-success)" /> : ''}
</td>
) : null}
</tr>
))}
</tbody>
Expand Down
14 changes: 14 additions & 0 deletions src/stories/blocks/Shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import { TagItem } from '../utils';

import styles from './CustomArgTypes.module.scss';

export function Checkmark({ alt, color }: { alt?: string; color?: string }) {
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m423.23-394.15-92.92-92.93q-8.31-8.3-20.89-8.5-12.57-.19-21.27 8.5-8.69 8.7-8.69 21.08 0 12.38 8.69 21.08l109.77 109.77q10.85 10.84 25.31 10.84 14.46 0 25.31-10.84l222.54-222.54q8.3-8.31 8.5-20.89.19-12.57-8.5-21.27-8.7-8.69-21.08-8.69-12.38 0-21.08 8.69l-205.69 205.7ZM480.07-100q-78.84 0-148.21-29.92t-120.68-81.21q-51.31-51.29-81.25-120.63Q100-401.1 100-479.93q0-78.84 29.92-148.21t81.21-120.68q51.29-51.31 120.63-81.25Q401.1-860 479.93-860q78.84 0 148.21 29.92t120.68 81.21q51.31 51.29 81.25 120.63Q860-558.9 860-480.07q0 78.84-29.92 148.21t-81.21 120.68q-51.29 51.31-120.63 81.25Q558.9-100 480.07-100Z"/></svg>';
const svgStyle = color ? ({ '--_icon-color': color } as React.CSSProperties) : {};
return <span style={svgStyle} role={alt ? 'img' : undefined} aria-label={alt ?? undefined} dangerouslySetInnerHTML={{ __html: svg }} />;
}

export function Xmark({ alt, color }: { alt?: string; color?: string }) {
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m480-437.85 122.92 122.93q8.31 8.3 20.89 8.5 12.57.19 21.27-8.5 8.69-8.7 8.69-21.08 0-12.38-8.69-21.08L522.15-480l122.93-122.92q8.3-8.31 8.5-20.89.19-12.57-8.5-21.27-8.7-8.69-21.08-8.69-12.38 0-21.08 8.69L480-522.15 357.08-645.08q-8.31-8.3-20.89-8.5-12.57-.19-21.27 8.5-8.69 8.7-8.69 21.08 0 12.38 8.69 21.08L437.85-480 314.92-357.08q-8.3 8.31-8.5 20.89-.19 12.57 8.5 21.27 8.7 8.69 21.08 8.69 12.38 0 21.08-8.69L480-437.85Zm.07 337.85q-78.84 0-148.21-29.92t-120.68-81.21q-51.31-51.29-81.25-120.63Q100-401.1 100-479.93q0-78.84 29.92-148.21t81.21-120.68q51.29-51.31 120.63-81.25Q401.1-860 479.93-860q78.84 0 148.21 29.92t120.68 81.21q51.31 51.29 81.25 120.63Q860-558.9 860-480.07q0 78.84-29.92 148.21t-81.21 120.68q-51.29 51.31-120.63 81.25Q558.9-100 480.07-100Z"/></svg>';
const svgStyle = color ? ({ '--_icon-color': color } as React.CSSProperties) : {};
return <span style={svgStyle} role={alt ? 'img' : undefined} aria-label={alt ?? undefined} dangerouslySetInnerHTML={{ __html: svg }} />;
}

export function UsageLink({ text, href }: { text: string; href: string }) {
return (
<p>
Expand Down
18 changes: 13 additions & 5 deletions src/stories/components/checkbox/Checkbox.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Meta, Title, Canvas } from '@storybook/blocks';
import CustomArgTypes from '../../blocks/CustomArgTypes';
import A11yInformation from '../../blocks/A11yInformation';
import CssOnlyInformation from '../../blocks/CssOnlyInformation';
import * as CheckboxStories from './Checkbox.stories';

Expand All @@ -20,11 +21,18 @@ Checkboxes can also have an indeterminate state, which is useful when the state

## Accessibility

- Verify that you can tab to each checkbox.
- Ensure there is a distinct visual cue that a checkbox is checked or unchecked.
- Ensure that there is a distinct visual cue that a checkbox is focused.
- Ensure that there is a distinct visual cue when a checkbox is disabled.
- Verify that pressing the space bar key, while focused on a checkbox, will toggle it checked and unchecked in the same manner as it would be with a mouse click.
<A11yInformation>
Always provide a label for your checkbox to ensure that it is accessible to all users. Any text
provided in the default slot functions as a label, but an HTML `<label>` element, `<forge-label>`
component, `aria-label`, or `aria-labelledby` attribute can also be used for this purpose. Of these
alternatives, `<label>` and `<forge-label>` are preferred because they also extend the clickable
area of your checkbox. Use visible labels instead of the ARIA attributes whenever possible.

If your checkbox is part of a group, you can use an HTML `<fieldset>` element component to group the
checkboxes together and provide a legend for the group. This will help users understand the
relationship between the checkboxes and the group they belong to.

</A11yInformation>

## CSS-Only

Expand Down
5 changes: 5 additions & 0 deletions src/stories/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ export interface Declaration {
cssFilePath?: TagItem;
cssClasses?: TagItem[];
globalConfigProperties?: GlobalConfigPropertyItem[];
role?: TagItem;
aria?: TagItem[];
focusable?: TagItem;
formAssociated?: TagItem;
keyControls?: TagItem[];
}

export interface DependencyItem {
Expand Down