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
39 changes: 39 additions & 0 deletions .changeset/accessibility-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
"@cloudflare/kumo": minor
---

Improve accessibility across Kumo components, charts, and docs.

### Components

- Improve CommandPalette screen reader context with a dialog title, named inputs, busy, loading, and empty-state announcements, localizable labels for status and action text, and decorative icons hidden from assistive technology.
- Hide Banner icons from assistive technology because they duplicate the surrounding banner text.
- Improve SensitiveInput semantics with localizable labels and status text, warnings for missing or generic primary names, clearer warning and error text, compact hit areas that do not change the visual layout where possible, and decorative icons that are hidden from assistive technology.
- Add accessible-name warnings for Switch, associate Switch groups with descriptions and errors, announce invalid and error states, and expand Switch hit areas without changing compact visuals where possible.
- Improve Button icon-only guidance with accessible-name warnings, decorative icon handling, and compact hit areas that preserve the existing visual size where possible.
- Associate Checkbox, Radio, and Switch groups with group descriptions and error messages so assistive technology receives the surrounding form context.
- Associate helper and error text for aria-label-only Select, Combobox, Autocomplete, SensitiveInput, and InputGroup patterns.
- Add Combobox and Autocomplete warnings for placeholder-only naming, and document localized clear, show, and remove labels.
- Make Breadcrumbs navigation and copy action labels localizable, hide decorative separators and icons from assistive technology, and keep clipboard actions focus-visible.
- Add localized Table selection and resize labels, slimmer resize hit areas, and more targeted metadata so row and column controls are announced clearly.
- Expand compact control hit areas without changing compact visuals where possible by using non-layout or pseudo-element targets.

### Charts

- Add chart wrapper label and description support so charts can expose concise names and longer context.
- Merge ECharts aria and reduced-motion options with Kumo defaults instead of replacing app-provided configuration.
- Announce Timeseries loading state and add non-drag range controls with localizable labels and value formatting.
- Improve Sankey labels and descriptions, enable aria support, add keyboard node and link actions, and make action labels localizable.
- Clarify ChartLegend static versus interactive semantics, use native buttons for interactive items, expose aria-pressed state, and expand compact hit areas without changing compact visuals where possible.

### Blocks

- Improve DeleteResource confirmation with alert-dialog semantics, a dialog description, and announced error messages.

### Docs

- Add skip link behavior and improve SidebarNav disclosure state, current-page announcements, mobile drawer focus management, and inert background behavior.
- Improve StickyDocHeader handling for hidden duplicate content and expand icon link hit areas without changing compact visuals where possible.
- Hide decorative icons from assistive technology and respect reduced-motion preferences in docs UI where motion is used.
- Clean up demos and snippets that used empty links, clickable divs, placeholder-only controls, unlabeled icons, and placeholder href values.
- Add accessibility and internationalization guidance to affected docs pages so examples call out labels, descriptions, clear, show, and remove actions, chart context, dialog errors, and compact target patterns.
40 changes: 23 additions & 17 deletions packages/kumo-docs-astro/src/components/KumoMenuIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ interface KumoMenuIconProps {

export function KumoMenuIcon({ className }: KumoMenuIconProps) {
const [isHovered, setIsHovered] = useState(false);
const animatedPathClassName =
"origin-center-top transition-all duration-800 motion-reduce:transition-none";
const animatedRectClassName =
"origin-left-center transition-all duration-800 motion-reduce:transition-none";

return (
<div
Expand All @@ -22,6 +26,8 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-full w-full"
aria-hidden="true"
focusable="false"
>
<defs>
<clipPath id="contentClip">
Expand All @@ -32,7 +38,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
{/* Vertical connector - NOT clipped */}
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "translate-y-[22px] scale-y-0 opacity-0"
: "translate-y-0 scale-y-100 opacity-100",
Expand All @@ -49,7 +55,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
<g clipPath="url(#contentClip)">
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[33px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -63,7 +69,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-12px scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -77,7 +83,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[51px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -91,7 +97,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[70px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -105,7 +111,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "translate-y-[25px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -119,7 +125,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[5px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -133,7 +139,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[19px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -147,7 +153,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[19px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -161,7 +167,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[5px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -175,7 +181,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "translate-y-[9px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -189,7 +195,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "translate-y-[9px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -203,7 +209,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<path
className={cn(
"origin-center-top transition-all duration-800",
animatedPathClassName,
isHovered
? "-translate-y-[60px] scale-y-[0.01]"
: "translate-y-0 scale-y-100",
Expand All @@ -220,7 +226,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
{/* Hamburger lines (on top, not clipped) */}
<rect
className={cn(
"origin-left-center transition-all duration-800",
animatedRectClassName,
isHovered
? "h-[6px] translate-x-[0.36px] translate-y-[24.88px]"
: "h-[7px]",
Expand All @@ -235,7 +241,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<rect
className={cn(
"origin-left-center transition-all duration-800",
animatedRectClassName,
isHovered
? "h-[6px] translate-x-[7.36px] translate-y-[30.88px] scale-x-[0.863]"
: "h-[9px]",
Expand All @@ -250,7 +256,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<rect
className={cn(
"origin-left-center transition-all duration-800",
animatedRectClassName,
isHovered
? "h-[6px] -translate-x-[3.64px] -translate-y-[11.12px] scale-x-[1.114] opacity-0"
: "h-[6px] opacity-100",
Expand All @@ -265,7 +271,7 @@ export function KumoMenuIcon({ className }: KumoMenuIconProps) {
/>
<rect
className={cn(
"origin-left-center transition-all duration-800",
animatedRectClassName,
isHovered
? "h-[6px] translate-x-[9.36px] -translate-y-[5.12px] scale-x-[0.83]"
: "h-[6px]",
Expand Down
66 changes: 55 additions & 11 deletions packages/kumo-docs-astro/src/components/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ interface SearchDialogProps {
onOpenChange: (open: boolean) => void;
}

const SEARCH_DIALOG_MESSAGES = {
dialogTitle: "Search Kumo documentation",
inputLabel: "Search documentation",
loadError: "Failed to load search index",
emptyIdle: "Type to search docs",
placeholder: "Search docs...",
} as const;

/** Build URL path from component type and name */
function getComponentUrl(type: string, name: string): string {
const slug =
Expand Down Expand Up @@ -295,13 +303,41 @@ function asSearchResults(items: SearchItem[]): SearchGroup[] {
function getTypeIcon(type: "component" | "block" | "layout" | "page") {
switch (type) {
case "block":
return <StackIcon size={16} weight="duotone" />;
return (
<StackIcon
aria-hidden="true"
focusable="false"
size={16}
weight="duotone"
/>
);
case "layout":
return <SquaresFourIcon size={16} weight="duotone" />;
return (
<SquaresFourIcon
aria-hidden="true"
focusable="false"
size={16}
weight="duotone"
/>
);
case "page":
return <BookOpenIcon size={16} weight="duotone" />;
return (
<BookOpenIcon
aria-hidden="true"
focusable="false"
size={16}
weight="duotone"
/>
);
default:
return <CubeIcon size={16} weight="duotone" />;
return (
<CubeIcon
aria-hidden="true"
focusable="false"
size={16}
weight="duotone"
/>
);
}
}

Expand Down Expand Up @@ -344,7 +380,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
setError(null);
} catch (err) {
console.error("Failed to load component registry:", err);
setError("Failed to load search index");
setError(SEARCH_DIALOG_MESSAGES.loadError);
} finally {
setLoading(false);
}
Expand Down Expand Up @@ -430,6 +466,10 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
0,
);
const isSearching = query.trim().length > 0;
const resultStatus =
!loading && !error && hasResults
? `${totalResults} result${totalResults === 1 ? "" : "s"}`
: "";

return (
<CommandPalette.Root<SearchGroup, SearchItem>
Expand All @@ -441,12 +481,16 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
itemToStringValue={(group: SearchGroup) => group.label}
onSelect={handleSelect}
getSelectableItems={getSelectableItems}
dialogTitle={SEARCH_DIALOG_MESSAGES.dialogTitle}
filter={() => true}
>
<CommandPalette.Input
placeholder="Search docs..."
aria-label={SEARCH_DIALOG_MESSAGES.inputLabel}
placeholder={SEARCH_DIALOG_MESSAGES.placeholder}
leading={
<MagnifyingGlassIcon
aria-hidden="true"
focusable="false"
className="h-4 w-4 text-kumo-subtle"
weight="bold"
/>
Expand All @@ -456,14 +500,14 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
{loading ? (
<CommandPalette.Loading />
) : error ? (
<div className="p-8 text-center">
<div role="alert" className="p-8 text-center">
<p className="text-kumo-subtle">{error}</p>
</div>
) : !hasResults ? (
<CommandPalette.Empty>
{query.trim()
? `No results found for "${query}"`
: "Type to search docs"}
: SEARCH_DIALOG_MESSAGES.emptyIdle}
</CommandPalette.Empty>
) : (
<CommandPalette.Results>
Expand Down Expand Up @@ -515,9 +559,9 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
</CommandPalette.List>
<CommandPalette.Footer>
<span className="text-kumo-subtle">
{hasResults
? `${totalResults} result${totalResults === 1 ? "" : "s"}`
: ""}
<span role="status" aria-live="polite" aria-atomic="true">
{resultStatus}
</span>
</span>
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
Expand Down
Loading
Loading