Skip to content

Commit

Permalink
feat: associate all inputs with labels via unique IDs (#113)
Browse files Browse the repository at this point in the history
* feat: surround all inputs by labels for better a11y

* reafactor: use unique IDs for html label association

* feat: add support for non-react setups

* feat: add documentation for useIdWithFallback

* fix: run formatter

* refactor: import useId directly
  • Loading branch information
LBBO authored Jul 11, 2023
1 parent e81d958 commit 518080e
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 60 deletions.
18 changes: 18 additions & 0 deletions src/common/useIdWithFallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { useId } from "react"

/**
* A function to obtain a unique ID. If react is available, this
* is just a wrapper for React.useId(), meaning the ID will be
* consistent across SSR and CSR. Otherwise, it will be a random and
* unique ID on every call.
*/
export const useIdWithFallback = () => {
try {
return useId()
} catch (e) {
return Math.random().toString(36).substring(2)
}
}
3 changes: 2 additions & 1 deletion src/react-components/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
typographyStyle,
} from "../theme"
import { Message, MessageStyleProps } from "./message"
import { useIdWithFallback } from "../common/useIdWithFallback"

export interface CheckboxProps
extends React.InputHTMLAttributes<HTMLInputElement>,
Expand All @@ -26,7 +27,7 @@ export const Checkbox = ({
dataTestid,
...props
}: CheckboxProps): JSX.Element => {
const id = props.id ?? Math.random().toString(36).substring(2)
const id = props.id ?? useIdWithFallback()
return (
<div
data-testid={dataTestid}
Expand Down
3 changes: 2 additions & 1 deletion src/react-components/codebox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
gridStyle,
typographyStyle,
} from "../theme"
import { useIdWithFallback } from "../common/useIdWithFallback"

export interface CodeBoxProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
Expand All @@ -19,7 +20,7 @@ export const CodeBox = ({
className,
...props
}: CodeBoxProps): JSX.Element => {
const id = Math.random().toString(36).substring(2)
const id = useIdWithFallback()
return (
<div
className={cn(className, gridStyle({ gap: 16 }), codeboxStyle)}
Expand Down
12 changes: 10 additions & 2 deletions src/react-components/input-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
typographyStyle,
} from "../theme"
import { Message, MessageStyleProps } from "./message"
import { useIdWithFallback } from "../common/useIdWithFallback"

export interface InputFieldProps
extends React.InputHTMLAttributes<HTMLInputElement>,
Expand All @@ -26,18 +27,24 @@ export const InputField = ({
fullWidth,
className,
dataTestid,
id,
...props
}: InputFieldProps): JSX.Element => {
const inputId = id ?? useIdWithFallback()

return (
<div
data-testid={dataTestid}
className={cn(className, gridStyle({ gap: 4 }))}
>
{title && (
<div className={typographyStyle({ size: "small", type: "regular" })}>
<label
htmlFor={inputId}
className={typographyStyle({ size: "small", type: "regular" })}
>
{title}{" "}
{props.required && <span className={inputFieldTitleStyle}>*</span>}
</div>
</label>
)}
<input
className={cn(
Expand All @@ -46,6 +53,7 @@ export const InputField = ({
)}
style={{ width: fullWidth ? "100%" : "auto" }}
placeholder={" "} // we need this so the input css field border is not green by default
id={inputId}
{...props}
/>
{typeof helperMessage === "string" ? (
Expand Down
119 changes: 63 additions & 56 deletions src/react-components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
typographyStyle,
} from "../theme"
import { MenuLink, MenuLinkProps } from "./menu-link"
import { useIdWithFallback } from "../common/useIdWithFallback"

export type NavSectionLinks = {
name: string
Expand All @@ -37,60 +38,66 @@ export const Nav = ({
navSections,
className,
...props
}: NavProps) => (
<nav role="navigation" className={cn(navStyle, className)} {...props}>
<input id="collapse-nav" type="checkbox" />
<div
className={cn(
navSectionTitleStyle,
typographyStyle({ size: "caption" }),
colorSprinkle({ color: "accentDefault" }),
)}
>
{navTitle}
<label htmlFor="collapse-nav">
<i className={cn("fa", "fa-bars")}></i>
<i className={cn("fa", "fa-xmark")}></i>
</label>
</div>
<ul
className={cn(
navMainSectionStyle,
navMenuSectionStyle,
gridStyle({ gap: 24 }),
)}
>
{navSections.map((section, key) => (
<li
key={key}
{...(section.floatBottom && { className: navSectionBottom })}
>
{section.title && (
<div
className={cn(
typographyStyle({ size: "xsmall", type: "bold" }),
colorSprinkle({ color: "foregroundDefault" }),
navSectionTitleStyle,
)}
>
{section.title}
<i className={`fa fa-${section.titleIcon}`}></i>
</div>
)}
<ul className={navMenuSectionStyle}>
{section.links.map(({ testId, ...link }, key) => (
<li
key={key}
{...(link.selected && { className: navMenuLinkSelectedStyle })}
}: NavProps) => {
const collapseNavId = `collapse-ory-elements-nav-${useIdWithFallback()}`

return (
<nav role="navigation" className={cn(navStyle, className)} {...props}>
<input id={collapseNavId} type="checkbox" />
<div
className={cn(
navSectionTitleStyle,
typographyStyle({ size: "caption" }),
colorSprinkle({ color: "accentDefault" }),
)}
>
{navTitle}
<label htmlFor={collapseNavId}>
<i className={cn("fa", "fa-bars")}></i>
<i className={cn("fa", "fa-xmark")}></i>
</label>
</div>
<ul
className={cn(
navMainSectionStyle,
navMenuSectionStyle,
gridStyle({ gap: 24 }),
)}
>
{navSections.map((section, key) => (
<li
key={key}
{...(section.floatBottom && { className: navSectionBottom })}
>
{section.title && (
<div
className={cn(
typographyStyle({ size: "xsmall", type: "bold" }),
colorSprinkle({ color: "foregroundDefault" }),
navSectionTitleStyle,
)}
>
<MenuLink data-testid={testId} {...link}>
{link.name}
</MenuLink>
</li>
))}
</ul>
</li>
))}
</ul>
</nav>
)
{section.title}
<i className={`fa fa-${section.titleIcon}`}></i>
</div>
)}
<ul className={navMenuSectionStyle}>
{section.links.map(({ testId, ...link }, key) => (
<li
key={key}
{...(link.selected && {
className: navMenuLinkSelectedStyle,
})}
>
<MenuLink data-testid={testId} {...link}>
{link.name}
</MenuLink>
</li>
))}
</ul>
</li>
))}
</ul>
</nav>
)
}

0 comments on commit 518080e

Please sign in to comment.