0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(ui): add a11y support (#2076)

This commit is contained in:
simeng-li 2022-10-10 10:57:07 +08:00 committed by GitHub
parent afa2ac47ee
commit 2249d717a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 126 additions and 63 deletions

View file

@ -24,7 +24,7 @@
"@parcel/transformer-svg-react": "2.7.0",
"@peculiar/webcrypto": "^1.3.3",
"@silverhand/eslint-config": "1.0.0",
"@silverhand/eslint-config-react": "1.0.0",
"@silverhand/eslint-config-react": "1.1.0",
"@silverhand/essentials": "^1.2.1",
"@silverhand/jest-config": "1.0.0",
"@silverhand/ts-config": "1.0.0",

View file

@ -35,6 +35,8 @@ const IframeConfirmModal = ({
<iframe
sandbox={undefined}
className={isLoading ? styles.hidden : undefined}
// For styling use
// eslint-disable-next-line jsx-a11y/aria-role
role="iframe"
src={url}
title="terms"

View file

@ -19,6 +19,8 @@ const Drawer = ({ className, isOpen = false, children, onClose }: Props) => {
return (
<ReactModal
shouldCloseOnOverlayClick
// For styling use
// eslint-disable-next-line jsx-a11y/aria-role
role="popup"
isOpen={isOpen}
className={classNames(styles.drawer, className)}

View file

@ -1,5 +1,7 @@
import classNames from 'classnames';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './DropdownItem.module.scss';
type Props = {
@ -9,7 +11,13 @@ type Props = {
};
const DropdownItem = ({ onClick, className, children }: Props) => (
<li className={classNames(styles.item, className)} onClick={onClick}>
<li
role="menuitem"
tabIndex={0}
className={classNames(styles.item, className)}
onKeyDown={onKeyDownHandler(onClick)}
onClick={onClick}
>
{children}
</li>
);

View file

@ -1,6 +1,8 @@
import classNames from 'classnames';
import ReactModal, { Props as ModalProps } from 'react-modal';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';
export { default as DropdownItem } from './DropdownItem';
@ -19,7 +21,17 @@ const Dropdown = ({ onClose, children, className, ...rest }: Props) => {
onRequestClose={onClose}
{...rest}
>
<ul className={styles.list} onClick={onClose}>
<ul
role="menu"
tabIndex={0}
className={styles.list}
onKeyDown={onKeyDownHandler({
Esc: onClose,
Enter: onClose,
' ': onClose,
})}
onClick={onClose}
>
{children}
</ul>
</ReactModal>

View file

@ -79,6 +79,7 @@ const PhoneInput = ({
type="tel"
inputMode="tel"
autoComplete="tel-national"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
name={name}
placeholder={placeholder}

View file

@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import ArrowPrev from '@/assets/icons/arrow-prev.svg';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';
@ -16,7 +17,12 @@ const NavBar = ({ title }: Props) => {
return (
<div className={styles.navBar}>
<div
role="button"
tabIndex={0}
className={styles.backButton}
onKeyDown={onKeyDownHandler(() => {
navigate(-1);
})}
onClick={() => {
navigate(-1);
}}

View file

@ -2,6 +2,7 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import InfoIcon from '@/assets/icons/info-icon.svg';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';
@ -18,7 +19,18 @@ const Notification = ({ className, message, onClose }: Props) => {
<div className={classNames(styles.notification, className)}>
<InfoIcon className={styles.icon} />
<div className={styles.message}>{message}</div>
<a className={styles.link} onClick={onClose}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
role="button"
tabIndex={0}
className={styles.link}
onClick={onClose}
onKeyDown={onKeyDownHandler({
Esc: onClose,
Enter: onClose,
' ': onClose,
})}
>
{t('action.got_it')}
</a>
</div>

View file

@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import Checkbox from '@/components/Checkbox';
import TextLink from '@/components/TextLink';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';
@ -20,12 +21,24 @@ const TermsOfUse = ({ name, className, termsUrl, isChecked, onChange, onTermsCli
const prefix = t('description.agree_with_terms');
const toggle = () => {
onChange(!isChecked);
};
return (
<div
role="radio"
aria-checked={isChecked}
tabIndex={0}
className={classNames(styles.terms, className)}
onClick={() => {
onChange(!isChecked);
}}
onClick={toggle}
onKeyDown={onKeyDownHandler({
Esc: () => {
onChange(false);
},
Enter: toggle,
' ': toggle,
})}
>
<Checkbox name={name} checked={isChecked} className={styles.checkBox} />
<div className={styles.content}>

View file

@ -30,6 +30,8 @@ const Toast = ({ message, duration = 3000, callback }: Props) => {
return (
<ReactModal
// For styling use
// eslint-disable-next-line jsx-a11y/aria-role
role="toast"
isOpen={Boolean(message)}
overlayClassName={styles.toastContainer}

View file

@ -87,6 +87,7 @@ const CreateAccount = ({ className, autoFocus }: Props) => {
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<div className={styles.formFields}>
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
className={styles.inputField}
name="new-username"

View file

@ -121,6 +121,7 @@ const EmailPasswordless = ({
autoComplete="email"
inputMode="email"
placeholder={t('input.email')}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
className={styles.inputField}
{...register('email', emailValidation)}

View file

@ -136,6 +136,7 @@ const PhonePasswordless = ({
className={styles.inputField}
countryCallingCode={phoneNumber.countryCallingCode}
nationalNumber={phoneNumber.nationalNumber}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
countryList={countryList}
{...register('phone', phoneNumberValidation)}

View file

@ -94,6 +94,7 @@ const ResetPassword = ({ className, autoFocus }: Props) => {
type="password"
autoComplete="new-password"
placeholder={t('input.password')}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
{...register('password', passwordValidation)}
onClear={() => {

View file

@ -20,7 +20,10 @@ const SocialLanding = ({ className, connectorId, isLoading = false }: Props) =>
<div className={classNames(styles.container, className)}>
<div className={styles.connector}>
{connector ? (
<img src={theme === 'dark' ? connector.logoDark ?? connector.logo : connector.logo} />
<img
src={theme === 'dark' ? connector.logoDark ?? connector.logo : connector.logo}
alt="logo"
/>
) : (
connectorId
)}

View file

@ -93,6 +93,7 @@ const UsernameSignIn = ({ className, autoFocus }: Props) => {
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<div className={styles.formFields}>
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
className={styles.inputField}
name="username"

View file

@ -24,6 +24,7 @@ const Consent = () => {
return (
<div className={styles.wrapper}>
<img
alt="logo"
src={
(theme === 'dark' && experienceSettings?.branding.darkLogoUrl) ||
experienceSettings?.branding.logoUrl

View file

@ -18,10 +18,12 @@ const ForgotPassword = () => {
const forgotPasswordForm = useMemo(() => {
if (method === 'sms') {
// eslint-disable-next-line jsx-a11y/no-autofocus
return <PhonePasswordless autoFocus hasSwitch type="forgot-password" hasTerms={false} />;
}
if (method === 'email') {
// eslint-disable-next-line jsx-a11y/no-autofocus
return <EmailPasswordless autoFocus hasSwitch type="forgot-password" hasTerms={false} />;
}
}, [method]);

View file

@ -19,13 +19,16 @@ const Register = () => {
const registerForm = useMemo(() => {
if (method === 'sms') {
// eslint-disable-next-line jsx-a11y/no-autofocus
return <PhonePasswordless autoFocus type="register" />;
}
if (method === 'email') {
// eslint-disable-next-line jsx-a11y/no-autofocus
return <EmailPasswordless autoFocus type="register" />;
}
// eslint-disable-next-line jsx-a11y/no-autofocus
return <CreateAccount autoFocus />;
}, [method]);

View file

@ -13,6 +13,7 @@ const ResetPassword = () => {
<NavBar />
<div className={styles.container}>
<div className={styles.title}>{t('description.new_password')}</div>
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
<ResetPasswordForm autoFocus />
</div>
</div>

View file

@ -19,13 +19,16 @@ const SecondarySignIn = () => {
const signInForm = useMemo(() => {
if (method === 'sms') {
// eslint-disable-next-line jsx-a11y/no-autofocus
return <PhonePasswordless autoFocus type="sign-in" />;
}
if (method === 'email') {
// eslint-disable-next-line jsx-a11y/no-autofocus
return <EmailPasswordless autoFocus type="sign-in" />;
}
// eslint-disable-next-line jsx-a11y/no-autofocus
return <UsernameSignIn autoFocus />;
}, [method]);

View file

@ -0,0 +1,24 @@
/* istanbul ignore file */
// FIXME: @simeng-li
import { KeyboardEventHandler, KeyboardEvent } from 'react';
type callbackHandler<T> = ((event: KeyboardEvent<T>) => void) | undefined;
type callbackHandlerMap<T> = Record<string, callbackHandler<T>>;
export const onKeyDownHandler =
<T = Element>(callback?: callbackHandler<T> | callbackHandlerMap<T>): KeyboardEventHandler<T> =>
(event) => {
const { key } = event;
if (typeof callback === 'function' && [' ', 'Enter'].includes(key)) {
callback(event);
event.preventDefault();
}
if (typeof callback === 'object') {
callback[key]?.(event);
event.preventDefault();
}
};

73
pnpm-lock.yaml generated
View file

@ -600,7 +600,7 @@ importers:
'@parcel/transformer-svg-react': 2.7.0
'@peculiar/webcrypto': ^1.3.3
'@silverhand/eslint-config': 1.0.0
'@silverhand/eslint-config-react': 1.0.0
'@silverhand/eslint-config-react': 1.1.0
'@silverhand/essentials': ^1.2.1
'@silverhand/jest-config': 1.0.0
'@silverhand/ts-config': 1.0.0
@ -654,7 +654,7 @@ importers:
'@parcel/transformer-svg-react': 2.7.0_@parcel+core@2.7.0
'@peculiar/webcrypto': 1.3.3
'@silverhand/eslint-config': 1.0.0_swk2g7ygmfleszo5c33j4vooni
'@silverhand/eslint-config-react': 1.0.0_3jdvf2aalbcoibv3m53iflhmym
'@silverhand/eslint-config-react': 1.1.0_3jdvf2aalbcoibv3m53iflhmym
'@silverhand/essentials': 1.2.1
'@silverhand/jest-config': 1.0.0_bi2kohzqnxavgozw3csgny5hju
'@silverhand/ts-config': 1.0.0_typescript@4.7.4
@ -3853,27 +3853,6 @@ packages:
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
dev: true
/@silverhand/eslint-config-react/1.0.0_3jdvf2aalbcoibv3m53iflhmym:
resolution: {integrity: sha512-t+DOdP51k0iP84nkOhnUOTqgXFhy3k6MKckZIzX8h7Poz4o0Xr6Nmeslksce49Mticxw4KE/gLk4X6cidpjNaA==}
engines: {node: ^16.0.0 || ^18.0.0}
peerDependencies:
stylelint: ^14.9.1
dependencies:
'@silverhand/eslint-config': 1.0.0_swk2g7ygmfleszo5c33j4vooni
eslint-config-xo-react: 0.27.0_5dhhqh7zhie7fohiouyxntxzve
eslint-plugin-react: 7.29.3_eslint@8.21.0
eslint-plugin-react-hooks: 4.3.0_eslint@8.21.0
stylelint: 14.9.1
stylelint-config-xo-scss: 0.15.0_eqpuutlgonckfyjzwkrpusdvaa
transitivePeerDependencies:
- eslint
- eslint-import-resolver-webpack
- postcss
- prettier
- supports-color
- typescript
dev: true
/@silverhand/eslint-config-react/1.0.0_qoomm4vc6ijs52fnjlal4yoenm:
resolution: {integrity: sha512-t+DOdP51k0iP84nkOhnUOTqgXFhy3k6MKckZIzX8h7Poz4o0Xr6Nmeslksce49Mticxw4KE/gLk4X6cidpjNaA==}
engines: {node: ^16.0.0 || ^18.0.0}
@ -5174,7 +5153,7 @@ packages:
call-bind: 1.0.2
define-properties: 1.1.4
es-abstract: 1.20.4
get-intrinsic: 1.1.1
get-intrinsic: 1.1.3
is-string: 1.0.7
dev: true
@ -5197,8 +5176,8 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.1.3
es-abstract: 1.19.1
define-properties: 1.1.4
es-abstract: 1.20.4
dev: true
/arrify/1.0.1:
@ -7198,12 +7177,12 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
dependencies:
array-includes: 3.1.4
array-includes: 3.1.5
array.prototype.flatmap: 1.2.5
doctrine: 2.1.0
eslint: 8.21.0
estraverse: 5.3.0
jsx-ast-utils: 3.2.1
jsx-ast-utils: 3.3.3
minimatch: 3.1.2
object.entries: 1.1.5
object.fromentries: 2.0.5
@ -8192,7 +8171,7 @@ packages:
/has-property-descriptors/1.0.0:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
dependencies:
get-intrinsic: 1.1.1
get-intrinsic: 1.1.3
dev: true
/has-symbols/1.0.3:
@ -10139,14 +10118,6 @@ packages:
engines: {'0': node >= 0.2.0}
dev: true
/jsx-ast-utils/3.2.1:
resolution: {integrity: sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==}
engines: {node: '>=4.0'}
dependencies:
array-includes: 3.1.4
object.assign: 4.1.2
dev: true
/jsx-ast-utils/3.3.3:
resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
engines: {node: '>=4.0'}
@ -11978,8 +11949,8 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.1.3
es-abstract: 1.19.1
define-properties: 1.1.4
es-abstract: 1.20.4
dev: true
/object.fromentries/2.0.5:
@ -11987,15 +11958,15 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.1.3
es-abstract: 1.19.1
define-properties: 1.1.4
es-abstract: 1.20.4
dev: true
/object.hasown/1.1.0:
resolution: {integrity: sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==}
dependencies:
define-properties: 1.1.3
es-abstract: 1.19.1
define-properties: 1.1.4
es-abstract: 1.20.4
dev: true
/object.values/1.1.5:
@ -13688,14 +13659,6 @@ packages:
hasBin: true
dev: true
/regexp.prototype.flags/1.4.1:
resolution: {integrity: sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.1.3
dev: true
/regexp.prototype.flags/1.4.3:
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
engines: {node: '>= 0.4'}
@ -14545,12 +14508,12 @@ packages:
resolution: {integrity: sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==}
dependencies:
call-bind: 1.0.2
define-properties: 1.1.3
es-abstract: 1.19.1
get-intrinsic: 1.1.1
define-properties: 1.1.4
es-abstract: 1.20.4
get-intrinsic: 1.1.3
has-symbols: 1.0.3
internal-slot: 1.0.3
regexp.prototype.flags: 1.4.1
regexp.prototype.flags: 1.4.3
side-channel: 1.0.4
dev: true