From c9202de0d81ed35e59a04ce3f7a7e8ed44486dc4 Mon Sep 17 00:00:00 2001 From: Bruno Fantauzzi Date: Mon, 31 Aug 2026 02:29:59 -0300 Subject: [PATCH] fix(kumo): honor InputGroup label in name warning --- .changeset/quiet-input-groups.md | 5 +++ .../src/components/input-group/context.ts | 2 ++ .../input-group/input-group-input.tsx | 4 +++ .../input-group/input-group.test.tsx | 36 +++++++++++++++++++ .../components/input-group/input-group.tsx | 9 +++-- 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .changeset/quiet-input-groups.md diff --git a/.changeset/quiet-input-groups.md b/.changeset/quiet-input-groups.md new file mode 100644 index 0000000000..384bc06ebd --- /dev/null +++ b/.changeset/quiet-input-groups.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": patch +--- + +Fix the accessible-name warning for InputGroup inputs named by the parent label. diff --git a/packages/kumo/src/components/input-group/context.ts b/packages/kumo/src/components/input-group/context.ts index 8612854f71..653e9e5bb8 100644 --- a/packages/kumo/src/components/input-group/context.ts +++ b/packages/kumo/src/components/input-group/context.ts @@ -140,6 +140,8 @@ export interface InputGroupContextValue { focusMode: "container" | "individual" | "hybrid"; disabled: boolean; error?: FieldProps["error"]; + /** ID of visible label content supplied by InputGroup. */ + labelId?: string; /** Auto-generated id for the input element; used by the invisible label overlay. */ inputId: string; } diff --git a/packages/kumo/src/components/input-group/input-group-input.tsx b/packages/kumo/src/components/input-group/input-group-input.tsx index 6513caadd3..c6d680dd78 100644 --- a/packages/kumo/src/components/input-group/input-group-input.tsx +++ b/packages/kumo/src/components/input-group/input-group-input.tsx @@ -51,6 +51,9 @@ export const Input = forwardRef( // Use explicit id if provided, otherwise fall back to context id // (links the input to the invisible label overlay for click-to-focus). const inputId = props.id ?? context?.inputId; + const ariaLabelledBy = + props["aria-labelledby"] ?? + (props["aria-label"] ? undefined : context?.labelId); return ( ( disabled={context?.disabled || (props as any).disabled} aria-invalid={hasError || props["aria-invalid"]} {...props} + aria-labelledby={ariaLabelledBy} id={inputId} className={cn( // Base input layout: fill height, allow shrinking, strip native border/radius diff --git a/packages/kumo/src/components/input-group/input-group.test.tsx b/packages/kumo/src/components/input-group/input-group.test.tsx index c40aa4f16f..bf5951599b 100644 --- a/packages/kumo/src/components/input-group/input-group.test.tsx +++ b/packages/kumo/src/components/input-group/input-group.test.tsx @@ -384,6 +384,42 @@ describe("InputGroup", () => { }); describe("accessibility", () => { + it("does not warn when the parent InputGroup provides the input label", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { container } = render( + + + , + ); + + const input = screen.getByRole("textbox", { name: "Search" }); + const labelledBy = input.getAttribute("aria-labelledby"); + + expect(input.getAttribute("aria-label")).toBeNull(); + expect(labelledBy).toBeTruthy(); + expect(document.getElementById(labelledBy!)?.textContent).toBe("Search"); + expect(container.querySelectorAll("label label")).toHaveLength(0); + expect(warnSpy).not.toHaveBeenCalledWith( + expect.stringContaining("[Kumo Input]"), + ); + warnSpy.mockRestore(); + }); + + it("warns when InputGroup.Input has no accessible name", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + render( + + + , + ); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("[Kumo Input]"), + ); + warnSpy.mockRestore(); + }); + it("input has accessible name via aria-label", () => { render( diff --git a/packages/kumo/src/components/input-group/input-group.tsx b/packages/kumo/src/components/input-group/input-group.tsx index 8471897c2d..47bbe096e1 100644 --- a/packages/kumo/src/components/input-group/input-group.tsx +++ b/packages/kumo/src/components/input-group/input-group.tsx @@ -101,6 +101,8 @@ const Root = forwardRef< forwardedRef, ) => { const inputId = useId(); + const labelId = useId(); + const hasLabel = Boolean(label); const focusMode = detectFocusMode(children); const contextValue = useMemo( @@ -109,9 +111,10 @@ const Root = forwardRef< focusMode, disabled, error, + labelId: hasLabel ? labelId : undefined, inputId, }), - [size, focusMode, disabled, error, inputId], + [size, focusMode, disabled, error, hasLabel, inputId, labelId], ); // When label is provided, Field already renders a