From dad67590cfbfd45d36f6b16265ea2fbd21d73542 Mon Sep 17 00:00:00 2001 From: Guadalupe Camacho Date: Wed, 22 Jul 2026 15:18:19 -0700 Subject: [PATCH 01/12] Add button a11y requirements, decouple error message from live region --- .../src/components/button/button.a11y.md | 8 +++++++ resources/js/common/layouts/AuthBase.vue | 2 +- .../auth/components/login/login-form.ts | 22 ++++++++++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 packages/craftcms-ui/src/components/button/button.a11y.md diff --git a/packages/craftcms-ui/src/components/button/button.a11y.md b/packages/craftcms-ui/src/components/button/button.a11y.md new file mode 100644 index 00000000000..edc4836fda8 --- /dev/null +++ b/packages/craftcms-ui/src/components/button/button.a11y.md @@ -0,0 +1,8 @@ +# `craft-button` accessibility + +## Requirements + +- [x] Visible focus indicator via `:focus-visible` pseudo-class. This includes: + - An offset outline to maintain visibility against the button background color. + - An outline color that uses the adaptive color token (`--c-color-focus-outline`) for visibility in both light and dark modes. +- [x] Named slots (are provided for (SC 1.3.2: Meaningful Sequence) \ No newline at end of file diff --git a/resources/js/common/layouts/AuthBase.vue b/resources/js/common/layouts/AuthBase.vue index c675589546f..97ab5cbb056 100644 --- a/resources/js/common/layouts/AuthBase.vue +++ b/resources/js/common/layouts/AuthBase.vue @@ -82,7 +82,7 @@ .cp-login__powered-by { display: block; margin-block-start: calc(70rem / 16); - opacity: 0.92;; + opacity: 0.92; text-align: center; &:hover, diff --git a/resources/js/modules/auth/components/login/login-form.ts b/resources/js/modules/auth/components/login/login-form.ts index 3713c24794e..13718b1a773 100644 --- a/resources/js/modules/auth/components/login/login-form.ts +++ b/resources/js/modules/auth/components/login/login-form.ts @@ -73,7 +73,8 @@ export default class CraftLoginForm extends LitElement { @query('craft-input-password.login-password') private _passwordInput?: HTMLElement & {value: string}; @query('.login-remember-me') private _rememberMeInput?: HTMLInputElement; - + @query('.cp-visually-hidden[role="status"]') + private _liveRegion!: HTMLDivElement; override async connectedCallback() { super.connectedCallback(); @@ -207,10 +208,21 @@ export default class CraftLoginForm extends LitElement { #setError(message: string) { this._error = message.trim(); - const live = this.shadowRoot?.querySelector( - '.cp-visually-hidden[role="status"]' - ); - if (live) live.textContent = message; + this.#setLiveRegion(message); + } + + #setLiveRegion(message: string) { + if (!this._liveRegion) return; + + this._liveRegion.textContent = message.trim(); + + setTimeout(() => { + const textContent = this._liveRegion.textContent; + + if (textContent === message) { + this._liveRegion.textContent = ''; + } + }, 5000); } #handleSuccess(returnUrl: string) { From cf224345eadc63da1f69ace4f2039d1b0f21d049 Mon Sep 17 00:00:00 2001 From: Guadalupe Camacho Date: Thu, 23 Jul 2026 10:49:25 -0700 Subject: [PATCH 02/12] Add new component/composable for live region, use in AuthBase and both login and reset forms --- .../js/common/components/GlobalLiveRegion.vue | 17 +++++++++++ .../composables/useAnnouncementStore.ts | 28 +++++++++++++++++++ resources/js/common/layouts/AuthBase.vue | 2 ++ .../auth/components/login/login-form.ts | 22 +++------------ .../components/login/login-reset-password.ts | 10 ++++++- 5 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 resources/js/common/components/GlobalLiveRegion.vue create mode 100644 resources/js/common/composables/useAnnouncementStore.ts diff --git a/resources/js/common/components/GlobalLiveRegion.vue b/resources/js/common/components/GlobalLiveRegion.vue new file mode 100644 index 00000000000..ad96e20080e --- /dev/null +++ b/resources/js/common/components/GlobalLiveRegion.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/resources/js/common/composables/useAnnouncementStore.ts b/resources/js/common/composables/useAnnouncementStore.ts new file mode 100644 index 00000000000..ce55377b387 --- /dev/null +++ b/resources/js/common/composables/useAnnouncementStore.ts @@ -0,0 +1,28 @@ +import { ref } from 'vue'; + +interface AnnouncerOptions { + timeout: number; +} + +const announcement = ref(null); +const announcerTimeout = ref | null>(null); + +export function useAnnouncementStore(options: Partial = {}) { + function announce(message: string | null) { + if (!message) { + return; + } + + if (announcerTimeout.value !== null) { + clearTimeout(announcerTimeout.value); + } + + announcement.value = message; + + announcerTimeout.value = setTimeout(() => { + announcement.value = null; + }, options.timeout || 5000); + } + + return {announcement, announce}; +} diff --git a/resources/js/common/layouts/AuthBase.vue b/resources/js/common/layouts/AuthBase.vue index 97ab5cbb056..a1bcb4f5e10 100644 --- a/resources/js/common/layouts/AuthBase.vue +++ b/resources/js/common/layouts/AuthBase.vue @@ -3,6 +3,7 @@ import useCraftData from '@/common/composables/useCraftData'; import craftCmsLogoUrl from '@public/images/craftcms.svg'; import {t} from '@craftcms/ui'; + import GlobalLiveRegion from '@/common/components/GlobalLiveRegion.vue'; const props = withDefaults( defineProps<{ @@ -19,6 +20,7 @@