From 3ff7058bda2346f30625e289f29adf1f840a4517 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 05:42:51 +0000 Subject: [PATCH] fix(web,h): type data-* attributes in props objects (#4082) JSX exempts hyphenated names from attribute checks, but an object typed with an element's props (ComponentProps<"div">, JSX.HTMLAttributes) rejected them. A template-literal index signature on ElementAttributes types them as the runtime treats them: true writes "", false and undefined remove. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018fTNh1FgQx2WTF5xoxwML1 --- .changeset/data-attribute-types.md | 6 ++++++ packages/h/jsx-runtime/src/jsx.d.ts | 7 +++++++ packages/web/jsx/jsx-h.d.ts | 7 +++++++ packages/web/jsx/jsx.d.ts | 7 +++++++ .../web/test/data-attributes.type-tests.ts | 20 +++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 .changeset/data-attribute-types.md create mode 100644 packages/web/test/data-attributes.type-tests.ts diff --git a/.changeset/data-attribute-types.md b/.changeset/data-attribute-types.md new file mode 100644 index 0000000000..f9f723ffb2 --- /dev/null +++ b/.changeset/data-attribute-types.md @@ -0,0 +1,6 @@ +--- +"@solidjs/web": patch +"@solidjs/h": patch +--- + +`data-*` attributes type-check in props objects (`ComponentProps<"div">`, `JSX.HTMLAttributes`), not only in JSX: a template-literal index signature on `ElementAttributes` (#4082). diff --git a/packages/h/jsx-runtime/src/jsx.d.ts b/packages/h/jsx-runtime/src/jsx.d.ts index e942fc55e8..0a7a1d8deb 100644 --- a/packages/h/jsx-runtime/src/jsx.d.ts +++ b/packages/h/jsx-runtime/src/jsx.d.ts @@ -892,6 +892,13 @@ export namespace JSX { extends CustomAttributes, PropAttributes, EventHandlersElement, AriaAttributes { // [key: ClassKeys]: boolean; + /** + * `data-*` attributes. JSX already accepts hyphenated names; this types + * them in props objects too (`ComponentProps<"div">`, a spread's source). + * `true` writes `""`; `false` and `undefined` remove the attribute. + */ + [key: `data-${string}`]: FunctionMaybe; + // properties innerHTML?: FunctionMaybe; textContent?: FunctionMaybe; diff --git a/packages/web/jsx/jsx-h.d.ts b/packages/web/jsx/jsx-h.d.ts index 61e6961203..a624bbd1b8 100644 --- a/packages/web/jsx/jsx-h.d.ts +++ b/packages/web/jsx/jsx-h.d.ts @@ -893,6 +893,13 @@ export namespace JSX { extends CustomAttributes, PropAttributes, EventHandlersElement, AriaAttributes { // [key: ClassKeys]: boolean; + /** + * `data-*` attributes. JSX already accepts hyphenated names; this types + * them in props objects too (`ComponentProps<"div">`, a spread's source). + * `true` writes `""`; `false` and `undefined` remove the attribute. + */ + [key: `data-${string}`]: FunctionMaybe; + // properties innerHTML?: FunctionMaybe; textContent?: FunctionMaybe; diff --git a/packages/web/jsx/jsx.d.ts b/packages/web/jsx/jsx.d.ts index dd07d748b9..fcb69e32a2 100644 --- a/packages/web/jsx/jsx.d.ts +++ b/packages/web/jsx/jsx.d.ts @@ -896,6 +896,13 @@ export namespace JSX { extends CustomAttributes, PropAttributes, EventHandlersElement, AriaAttributes { // [key: ClassKeys]: boolean; + /** + * `data-*` attributes. JSX already accepts hyphenated names; this types + * them in props objects too (`ComponentProps<"div">`, a spread's source). + * `true` writes `""`; `false` and `undefined` remove the attribute. + */ + [key: `data-${string}`]: string | number | boolean | undefined; + // properties innerHTML?: string; textContent?: string | number; diff --git a/packages/web/test/data-attributes.type-tests.ts b/packages/web/test/data-attributes.type-tests.ts new file mode 100644 index 0000000000..694125429e --- /dev/null +++ b/packages/web/test/data-attributes.type-tests.ts @@ -0,0 +1,20 @@ +// `data-*` attributes in props objects (#4082): JSX already accepts +// hyphenated names on intrinsic elements, and an object typed with the same +// element's props now accepts them too, so a function returning props to +// spread type-checks without a cast. +import type { ComponentProps, JSX } from "@solidjs/web"; + +export const attrs: JSX.HTMLAttributes = { "data-state": "open" }; +export const props: ComponentProps<"div"> = { role: "button", "data-state": "open" }; + +function disclosure(): ComponentProps<"button"> { + return { "aria-expanded": "false", "data-state": "closed", "data-count": 2, "data-open": false }; +} +export const forwarded: ComponentProps<"button"> = { ...disclosure(), "data-extra": true }; + +type P = Omit, "ref">; +export const withClass: P = { "data-x": "1", class: "a" }; +// @ts-expect-error unknown non-data keys are still rejected +export const unknownKey: P = { foo: 1 }; +// @ts-expect-error data values stay primitive +export const objectValue: P = { "data-x": {} };