fix(a11y): Toggle aria-invalid briefly when async onChange rejects
When an async onChange handler rejects, the toggle snapped back to its previous value silently — screen reader users had no signal that the action failed, only that the toggle suddenly returned to its original state. Now aria-invalid="true" exposes the failure on the role=switch button for 1.5s after rejection, so assistive tech announces the error. Self-clears via setTimeout; no API change for callers.
This commit is contained in:
@@ -17,6 +17,10 @@
|
|||||||
// Kept separate from the `loading` prop so callers can also force loading externally.
|
// Kept separate from the `loading` prop so callers can also force loading externally.
|
||||||
let pending = $state(false);
|
let pending = $state(false);
|
||||||
|
|
||||||
|
// Briefly true after an async onChange rejects, so screen readers hear that
|
||||||
|
// the action failed rather than silently inferring it from the snap-back.
|
||||||
|
let snapBack = $state(false);
|
||||||
|
|
||||||
// Effective busy state covers both the externally-controlled prop and our internal pending flag.
|
// Effective busy state covers both the externally-controlled prop and our internal pending flag.
|
||||||
let busy = $derived(loading || pending);
|
let busy = $derived(loading || pending);
|
||||||
|
|
||||||
@@ -41,7 +45,11 @@
|
|||||||
await result;
|
await result;
|
||||||
checked = next;
|
checked = next;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Swallow; the toggle simply stays at its previous value.
|
// Swallow; the toggle stays at its previous value. Flag the snap-back
|
||||||
|
// briefly so assistive tech announces aria-invalid="true" and screen
|
||||||
|
// reader users hear that the action failed.
|
||||||
|
snapBack = true;
|
||||||
|
setTimeout(() => { snapBack = false; }, 1500);
|
||||||
} finally {
|
} finally {
|
||||||
pending = false;
|
pending = false;
|
||||||
}
|
}
|
||||||
@@ -68,6 +76,7 @@
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-disabled={disabled ? "true" : undefined}
|
aria-disabled={disabled ? "true" : undefined}
|
||||||
aria-busy={busy ? "true" : undefined}
|
aria-busy={busy ? "true" : undefined}
|
||||||
|
aria-invalid={snapBack ? "true" : undefined}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full shadow-sm flex items-center justify-center
|
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full shadow-sm flex items-center justify-center
|
||||||
|
|||||||
Reference in New Issue
Block a user