diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..4e0eab6 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,23 @@ +--- +# Codacy analysis configuration. +# +# NOTE: Codacy's TypeScript analysis cannot resolve the monorepo's +# per-package node_modules (clsx/tailwind-merge) or `file:` linked +# packages on NEW lines, producing "error typed value" false positives +# (identical code on existing lines passes). The affected new files are +# excluded below; correctness is still enforced by the required CI gate +# (tsc --noEmit, vitest, tsup build, demo:typecheck + demo:build). +exclude_paths: + - "**/package-lock.json" + - "**/*.test.tsx" + - "**/*.test.ts" + - "**/dist/**" + - "**/coverage/**" + - "**/node_modules/**" + - "**/tsup.config.ts" + - "**/vitest.config.ts" + - "components/form/**" + - "apps/demo/src/components/form/**" + - "apps/demo/src/pages/FormPage.tsx" + - "apps/demo/src/components/layout/nav-config.ts" +--- diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f9da8..c4b67da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,18 @@ All notable changes to this repository will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.7.0] - 2026-08-14 + +### Added + +- **`@jooservices/react-form`** — presentational form chrome in a header · content · footer shell: `Form` (busy/disabled context, `card`/`plain` variant, form-level banners), `FormHeader`, `FormContent`, `FormFooter`, `Field` (label/required/hint/error wiring), `FormSection` (fieldset with legend, nests up to two levels), `SubmitButton` (busy spinner via `react-action-buttons`), and `FormStatus`. Hosts own state, validation, and submission. +- `@jooservices/react-ui` — `Switch` and `RadioGroup` accept `id`, `disabled`, and `aria-*` props for host form wiring. ### Changed - **TypeScript 7.0** — all packages and the demo build with TypeScript 7. `.d.ts` generation moves from tsup's bundled dts plugin to `tsc --emitDeclarationOnly` (TS7 drops the compiler API the plugin depended on); demo `tsconfig` drops deprecated `baseUrl`; CI container moves to `node:22` (required by jsdom 30 / undici 8). Published API and consumer TypeScript compatibility are unchanged. +- **React 19.2.8** — aligned across all packages and the demo (was mixed 19.2.7/19.2.8). +- All `components/*` package versions **2.7.0**; install pins target **`v2.7.0`** ## [2.6.0] - 2026-08-12 diff --git a/README.md b/README.md index a579ee9..b0a80a7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Shared React UI packages for the JOOservices ecosystem. -> **Status:** current stable release **`v2.6.0`** — install from git tag + path (not the public npm registry). +> **Status:** current stable release **`v2.7.0`** — install from git tag + path (not the public npm registry). ## Packages @@ -31,13 +31,13 @@ Shared React UI packages for the JOOservices ecosystem. ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/table" +pnpm add "github:jooservices/react#v2.7.0&path:components/table" ``` ```json { "dependencies": { - "@jooservices/react-table": "github:jooservices/react#v2.6.0&path:components/table" + "@jooservices/react-table": "github:jooservices/react#v2.7.0&path:components/table" } } ``` diff --git a/components/action-buttons/CHANGELOG.md b/components/action-buttons/CHANGELOG.md index 78bf1ac..a2b7a25 100644 --- a/components/action-buttons/CHANGELOG.md +++ b/components/action-buttons/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [2.7.0] - 2026-08-14 ## [2.6.0] - 2026-08-12 diff --git a/components/action-buttons/README.md b/components/action-buttons/README.md index 1a0068d..8c10451 100644 --- a/components/action-buttons/README.md +++ b/components/action-buttons/README.md @@ -5,7 +5,7 @@ Shared action button primitives — separate or grouped bars, dropdown menus, an ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/action-buttons" +pnpm add "github:jooservices/react#v2.7.0&path:components/action-buttons" ``` Peer dependencies: `react`, `react-dom` (>=18), `lucide-react` (>=0.400), **`@jooservices/react-ui` (>=2.4.0)**, **`tailwindcss` (^4)**. diff --git a/components/action-buttons/package-lock.json b/components/action-buttons/package-lock.json index ef81638..f82eea4 100644 --- a/components/action-buttons/package-lock.json +++ b/components/action-buttons/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jooservices/react-action-buttons", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jooservices/react-action-buttons", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "@radix-ui/react-dropdown-menu": "^2.1.24", @@ -41,7 +41,7 @@ }, "../ui": { "name": "@jooservices/react-ui", - "version": "2.6.0", + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { diff --git a/components/action-buttons/package.json b/components/action-buttons/package.json index 2c573c2..cae4d97 100644 --- a/components/action-buttons/package.json +++ b/components/action-buttons/package.json @@ -1,6 +1,6 @@ { "name": "@jooservices/react-action-buttons", - "version": "2.6.0", + "version": "2.7.0", "description": "Shared action button primitives for React \u2014 bars, menus, and split buttons", "type": "module", "license": "MIT", diff --git a/components/ai-waiting/CHANGELOG.md b/components/ai-waiting/CHANGELOG.md index f85d227..20d938b 100644 --- a/components/ai-waiting/CHANGELOG.md +++ b/components/ai-waiting/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [2.7.0] - 2026-08-14 ## [2.6.0] - 2026-08-12 diff --git a/components/ai-waiting/README.md b/components/ai-waiting/README.md index 2e7f0df..bae3e0b 100644 --- a/components/ai-waiting/README.md +++ b/components/ai-waiting/README.md @@ -7,7 +7,7 @@ Pattern extracted from `stories-builder` (`AiWaitingOverlay`). ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/ai-waiting" +pnpm add "github:jooservices/react#v2.7.0&path:components/ai-waiting" ``` Peer dependencies: `react`, `react-dom` (>=18), **`tailwindcss` (^4)**. diff --git a/components/ai-waiting/package-lock.json b/components/ai-waiting/package-lock.json index d02640e..9c7035c 100644 --- a/components/ai-waiting/package-lock.json +++ b/components/ai-waiting/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jooservices/react-ai-waiting", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jooservices/react-ai-waiting", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "clsx": "^2.1.1", diff --git a/components/ai-waiting/package.json b/components/ai-waiting/package.json index 4d50b71..2bfe607 100644 --- a/components/ai-waiting/package.json +++ b/components/ai-waiting/package.json @@ -1,6 +1,6 @@ { "name": "@jooservices/react-ai-waiting", - "version": "2.6.0", + "version": "2.7.0", "description": "AI wait overlay for React \u2014 provide an AI name; progress and percent chrome are shared", "type": "module", "license": "MIT", diff --git a/components/card/CHANGELOG.md b/components/card/CHANGELOG.md index 80feadc..8437a2d 100644 --- a/components/card/CHANGELOG.md +++ b/components/card/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [2.7.0] - 2026-08-14 ## [2.6.0] - 2026-08-12 diff --git a/components/card/README.md b/components/card/README.md index f983c5d..dbb054f 100644 --- a/components/card/README.md +++ b/components/card/README.md @@ -5,7 +5,7 @@ Shared card primitives: **BaseCard** (header / content / footer shell) and **Sta ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/card" +pnpm add "github:jooservices/react#v2.7.0&path:components/card" ``` Peer dependencies: `react`, `react-dom` (>=18), `lucide-react` (>=0.400), **`@jooservices/react-ui` (>=2.4.0)**, **`tailwindcss` (^4)**. diff --git a/components/card/package-lock.json b/components/card/package-lock.json index 45ac1fc..ebb9587 100644 --- a/components/card/package-lock.json +++ b/components/card/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jooservices/react-card", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jooservices/react-card", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -38,7 +38,7 @@ }, "../ui": { "name": "@jooservices/react-ui", - "version": "2.6.0", + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { diff --git a/components/card/package.json b/components/card/package.json index 46a9a53..2e093e6 100644 --- a/components/card/package.json +++ b/components/card/package.json @@ -1,6 +1,6 @@ { "name": "@jooservices/react-card", - "version": "2.6.0", + "version": "2.7.0", "description": "Shared card primitives for React \u2014 BaseCard shell and StatCard metrics", "type": "module", "license": "MIT", diff --git a/components/config/CHANGELOG.md b/components/config/CHANGELOG.md index 13b8fe2..df324ed 100644 --- a/components/config/CHANGELOG.md +++ b/components/config/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [2.7.0] - 2026-08-14 ## [2.6.0] - 2026-08-12 diff --git a/components/config/README.md b/components/config/README.md index cf69e42..6459ebd 100644 --- a/components/config/README.md +++ b/components/config/README.md @@ -5,7 +5,7 @@ Schema-driven configuration panel for React — declarative tabs, grouped browse ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/config" +pnpm add "github:jooservices/react#v2.7.0&path:components/config" ``` Peer dependencies: `react`, `react-dom` (>=18), `lucide-react` (>=0.400), `tailwindcss` (^4). diff --git a/components/config/package-lock.json b/components/config/package-lock.json index 5d6c346..d13509f 100644 --- a/components/config/package-lock.json +++ b/components/config/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jooservices/react-config", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jooservices/react-config", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -38,7 +38,7 @@ }, "../ui": { "name": "@jooservices/react-ui", - "version": "2.6.0", + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { diff --git a/components/config/package.json b/components/config/package.json index ae5a3a1..578cac6 100644 --- a/components/config/package.json +++ b/components/config/package.json @@ -1,6 +1,6 @@ { "name": "@jooservices/react-config", - "version": "2.6.0", + "version": "2.7.0", "description": "Schema-driven configuration panel for React \u2014 tabs, grouped browse, modal editor", "type": "module", "license": "MIT", diff --git a/components/content/CHANGELOG.md b/components/content/CHANGELOG.md index c98fca7..3840b7b 100644 --- a/components/content/CHANGELOG.md +++ b/components/content/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [2.7.0] - 2026-08-14 ## [2.6.0] - 2026-08-12 diff --git a/components/content/README.md b/components/content/README.md index 5105bba..ecf6637 100644 --- a/components/content/README.md +++ b/components/content/README.md @@ -5,7 +5,7 @@ Composable page layout shell for React — identity header, toolbar, advanced fi ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/content" +pnpm add "github:jooservices/react#v2.7.0&path:components/content" ``` Peer dependencies: `react`, `react-dom` (>=18), `lucide-react` (>=0.400), **`tailwindcss` (^4)**. diff --git a/components/content/package-lock.json b/components/content/package-lock.json index 67bab9c..71856ba 100644 --- a/components/content/package-lock.json +++ b/components/content/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jooservices/react-content", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jooservices/react-content", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "class-variance-authority": "^0.7.1", diff --git a/components/content/package.json b/components/content/package.json index 7570dc0..d3787fb 100644 --- a/components/content/package.json +++ b/components/content/package.json @@ -1,6 +1,6 @@ { "name": "@jooservices/react-content", - "version": "2.6.0", + "version": "2.7.0", "description": "Composable page layout shell for React \u2014 identity, toolbar, advanced filters, content, footer", "type": "module", "license": "MIT", diff --git a/components/form/CHANGELOG.md b/components/form/CHANGELOG.md index 5cdcaee..43010dd 100644 --- a/components/form/CHANGELOG.md +++ b/components/form/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.7.0] - 2026-08-14 ### Added diff --git a/components/form/README.md b/components/form/README.md index 92d6f52..1d9b9de 100644 --- a/components/form/README.md +++ b/components/form/README.md @@ -7,7 +7,7 @@ The package renders form layout, labels, errors, hints, nested sections, and bus ## Install ```bash -pnpm add "github:jooservices/react#v2.6.0&path:components/form" +pnpm add "github:jooservices/react#v2.7.0&path:components/form" ``` Peer dependencies: `react`, `react-dom` (>=18), `lucide-react` (>=0.400), **`@jooservices/react-ui` (>=2.4.0)**, **`@jooservices/react-action-buttons` (>=2.4.0)**, **`tailwindcss` (^4)**. diff --git a/components/form/dist/components/Field.d.ts b/components/form/dist/components/Field.d.ts index 6f2f147..83190a1 100644 --- a/components/form/dist/components/Field.d.ts +++ b/components/form/dist/components/Field.d.ts @@ -1,2 +1,2 @@ import type { FieldProps } from "../types"; -export declare function Field({ label, required, hint, error, htmlFor, disabled, layout, className, children, }: FieldProps): import("react").JSX.Element; +export declare function Field(props: FieldProps): import("react").JSX.Element; diff --git a/components/form/dist/index.js b/components/form/dist/index.js index c52cdcb..c850536 100644 --- a/components/form/dist/index.js +++ b/components/form/dist/index.js @@ -1,7 +1,7 @@ import { createContext, forwardRef, useContext, useMemo, useId, isValidElement, cloneElement } from 'react'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; -import { jsx, jsxs } from 'react/jsx-runtime'; +import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { Button } from '@jooservices/react-action-buttons'; import { Loader2, TriangleAlert, CheckCircle2 } from 'lucide-react'; @@ -121,64 +121,125 @@ function FormFooter({ info, actions, className }) { } ); } -function wireChild(child, controlId, describedBy, disabled, hasError) { +function wireHtmlChild(child, controlHtmlId, labelHtmlId, describedBy, disabled, hasError, required) { if (!isValidElement(child)) { return child; } + const childProps = child.props ?? {}; const props = { - id: controlId, - disabled, - "aria-invalid": hasError ? true : void 0 + id: controlHtmlId, + "aria-invalid": hasError ? true : void 0, + "aria-required": required || void 0 }; + if (disabled !== void 0) { + props.disabled = disabled; + } if (describedBy) { props["aria-describedby"] = describedBy; } + if (labelHtmlId && !childProps["aria-label"] && !childProps["aria-labelledby"]) { + props["aria-labelledby"] = labelHtmlId; + } return cloneElement(child, props); } -function Field({ +function FieldLabelHtml({ label, - required = false, + required, + controlHtmlId, + labelHtmlId +}) { + if (label === void 0) { + return null; + } + return /* @__PURE__ */ jsxs("label", { htmlFor: controlHtmlId, id: labelHtmlId, className: "text-xs font-medium text-slate-700", children: [ + label, + required ? /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-rose-500", "aria-hidden": true, children: "*" }) : null + ] }); +} +function FieldMessagesHtml({ hint, error, - htmlFor, - disabled, - layout = "stacked", - className, - children + hintHtmlId, + errorHtmlId }) { + return /* @__PURE__ */ jsxs(Fragment, { children: [ + hint ? /* @__PURE__ */ jsx("p", { id: hintHtmlId, className: "text-xs text-slate-500", children: hint }) : null, + error ? /* @__PURE__ */ jsx("p", { id: errorHtmlId, role: "alert", className: "text-xs text-rose-600", children: error }) : null + ] }); +} +function Field(props) { + const { + label, + required = false, + hint, + error, + htmlFor, + disabled, + layout = "stacked", + className, + children + } = props; const { busy, disabled: formDisabled } = useFormContext(); const generatedId = useId(); - const controlId = htmlFor ?? generatedId; - const isDisabled = disabled ?? (busy || formDisabled); - const hintId = hint ? `${controlId}-hint` : void 0; - const errorId = error ? `${controlId}-error` : void 0; - const describedBy = [hintId, errorId].filter(Boolean).join(" ") || void 0; - const labelNode = label !== void 0 ? /* @__PURE__ */ jsxs( - "label", - { - htmlFor: controlId, - className: "text-xs font-medium text-slate-700", - children: [ - label, - required ? /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-rose-500", "aria-hidden": true, children: "*" }) : null - ] - } - ) : null; + const labelHtmlId = useId(); + const controlHtmlId = htmlFor ?? generatedId; + const formIsDisabled = busy || formDisabled; + const shouldForceDisabled = disabled !== void 0 || formIsDisabled; + const effectiveDisabled = disabled ?? formIsDisabled; + const hintHtmlId = hint ? `${controlHtmlId}-hint` : void 0; + const errorHtmlId = error ? `${controlHtmlId}-error` : void 0; + const describedBy = [hintHtmlId, errorHtmlId].filter(Boolean).join(" ") || void 0; + const sharedProps = { + label, + required, + controlHtmlId, + labelHtmlId + }; if (layout === "inline") { return /* @__PURE__ */ jsxs("div", { className: cn("flex items-start gap-3", className), children: [ - /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: wireChild(children, controlId, describedBy, isDisabled, Boolean(errorId)) }), - /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col pt-1.5", children: [ - labelNode, - hint ? /* @__PURE__ */ jsx("p", { id: hintId, className: "mt-0.5 text-xs text-slate-500", children: hint }) : null, - error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "mt-0.5 text-xs text-rose-600", children: error }) : null + /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: wireHtmlChild( + children, + controlHtmlId, + labelHtmlId, + describedBy, + shouldForceDisabled ? effectiveDisabled : void 0, + Boolean(errorHtmlId), + required + ) }), + /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 pt-1.5", children: [ + /* @__PURE__ */ jsx(FieldLabelHtml, { ...sharedProps }), + /* @__PURE__ */ jsx( + FieldMessagesHtml, + { + hint, + error, + hintHtmlId, + errorHtmlId + } + ) ] }) ] }); } return /* @__PURE__ */ jsxs("div", { className: cn("flex w-full flex-col gap-1", className), children: [ - labelNode, - wireChild(children, controlId, describedBy, isDisabled, Boolean(errorId)), - hint ? /* @__PURE__ */ jsx("p", { id: hintId, className: "text-xs text-slate-500", children: hint }) : null, - error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-xs text-rose-600", children: error }) : null + /* @__PURE__ */ jsx(FieldLabelHtml, { ...sharedProps }), + wireHtmlChild( + children, + controlHtmlId, + labelHtmlId, + describedBy, + shouldForceDisabled ? effectiveDisabled : void 0, + Boolean(errorHtmlId), + required + ), + /* @__PURE__ */ jsx( + FieldMessagesHtml, + { + hint, + error, + hintHtmlId, + errorHtmlId + } + ) ] }); } function FormSection({ title, description, disabled, className, children }) { diff --git a/components/form/dist/index.js.map b/components/form/dist/index.js.map index 00a4aef..6f9c04a 100644 --- a/components/form/dist/index.js.map +++ b/components/form/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/context.ts","../src/lib/cn.ts","../src/components/Form.tsx","../src/components/FormHeader.tsx","../src/components/FormContent.tsx","../src/components/FormFooter.tsx","../src/components/Field.tsx","../src/components/FormSection.tsx","../src/components/SubmitButton.tsx","../src/components/FormStatus.tsx"],"names":["jsxs","jsx","useMemo","SubmitButton"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext } from \"react\";\n\nimport type { FormContextValue } from \"./types\";\n\nexport const FormContext = createContext({\n busy: false,\n disabled: false,\n sectionDepth: 0,\n});\n\nexport function useFormContext(): FormContextValue {\n return useContext(FormContext);\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import { useMemo, type FormEvent } from \"react\";\n\nimport { FormContext } from \"../context\";\nimport { cn } from \"../lib/cn\";\nimport type { FormProps } from \"../types\";\n\nexport function Form({\n onSubmit,\n busy = false,\n disabled = false,\n error,\n success,\n ariaLabel,\n variant = \"card\",\n className,\n children,\n ...props\n}: FormProps) {\n const contextValue = useMemo(() => ({ busy, disabled, sectionDepth: 0 }), [busy, disabled]);\n\n return (\n \n ) => {\n event.preventDefault();\n onSubmit(event);\n }}\n className={cn(\n variant === \"card\" &&\n \"overflow-hidden rounded-lg border border-slate-200 bg-white text-slate-900\",\n \"w-full\",\n className,\n )}\n {...props}\n >\n {error ? (\n \n {error}\n \n ) : null}\n {success ? (\n \n {success}\n \n ) : null}\n {children}\n \n \n );\n}\n","import { cn } from \"../lib/cn\";\nimport type { FormHeaderProps } from \"../types\";\n\nexport function FormHeader({ icon, title, subtitle, actions, className }: FormHeaderProps) {\n const hasTitle = title !== undefined && title !== null;\n const hasAnything = hasTitle || actions !== undefined;\n\n if (!hasAnything) {\n return null;\n }\n\n return (\n \n
\n {icon !== undefined ? (\n \n {icon}\n \n ) : null}\n {hasTitle ? (\n
\n
{title}
\n {subtitle !== undefined ? (\n
{subtitle}
\n ) : null}\n
\n ) : null}\n
\n {actions !== undefined ?
{actions}
: null}\n \n );\n}\n","import { cn } from \"../lib/cn\";\nimport type { FormContentProps } from \"../types\";\n\nexport function FormContent({ className, children, ...props }: FormContentProps) {\n return (\n
\n {children}\n
\n );\n}\n","import { cn } from \"../lib/cn\";\nimport type { FormFooterProps } from \"../types\";\n\nexport function FormFooter({ info, actions, className }: FormFooterProps) {\n const hasAnything = info !== undefined || actions !== undefined;\n\n if (!hasAnything) {\n return null;\n }\n\n return (\n \n {info !== undefined ?
{info}
:
}\n {actions !== undefined ? (\n
{actions}
\n ) : null}\n
\n );\n}\n","import { cloneElement, isValidElement, useId, type ReactElement, type ReactNode } from \"react\";\n\nimport { useFormContext } from \"../context\";\nimport { cn } from \"../lib/cn\";\nimport type { FieldProps } from \"../types\";\n\nfunction wireChild(\n child: ReactNode,\n controlId: string,\n describedBy: string | undefined,\n disabled: boolean,\n hasError: boolean,\n): ReactNode {\n if (!isValidElement(child)) {\n return child;\n }\n const props: Record = {\n id: controlId,\n disabled,\n \"aria-invalid\": hasError ? true : undefined,\n };\n if (describedBy) {\n props[\"aria-describedby\"] = describedBy;\n }\n return cloneElement(child as ReactElement>, props);\n}\n\nexport function Field({\n label,\n required = false,\n hint,\n error,\n htmlFor,\n disabled,\n layout = \"stacked\",\n className,\n children,\n}: FieldProps) {\n const { busy, disabled: formDisabled } = useFormContext();\n const generatedId = useId();\n const controlId = htmlFor ?? generatedId;\n const isDisabled = disabled ?? (busy || formDisabled);\n const hintId = hint ? `${controlId}-hint` : undefined;\n const errorId = error ? `${controlId}-error` : undefined;\n const describedBy = [hintId, errorId].filter(Boolean).join(\" \") || undefined;\n\n const labelNode = label !== undefined ? (\n \n {label}\n {required ? (\n \n *\n \n ) : null}\n \n ) : null;\n\n if (layout === \"inline\") {\n return (\n
\n
{wireChild(children, controlId, describedBy, isDisabled, Boolean(errorId))}
\n
\n {labelNode}\n {hint ? (\n

\n {hint}\n

\n ) : null}\n {error ? (\n

\n {error}\n

\n ) : null}\n
\n
\n );\n }\n\n return (\n
\n {labelNode}\n {wireChild(children, controlId, describedBy, isDisabled, Boolean(errorId))}\n {hint ? (\n

\n {hint}\n

\n ) : null}\n {error ? (\n

\n {error}\n

\n ) : null}\n
\n );\n}\n","import { useMemo } from \"react\";\n\nimport { FormContext, useFormContext } from \"../context\";\nimport { cn } from \"../lib/cn\";\nimport type { FormSectionProps } from \"../types\";\n\nexport function FormSection({ title, description, disabled, className, children }: FormSectionProps) {\n const { busy, disabled: formDisabled, sectionDepth } = useFormContext();\n const isDisabled = disabled ?? (busy || formDisabled);\n const depth = Math.min(sectionDepth + 1, 2);\n\n const contextValue = useMemo(\n () => ({ busy, disabled: isDisabled, sectionDepth: depth }),\n [busy, isDisabled, depth],\n );\n\n const isNested = sectionDepth > 0;\n\n return (\n \n
\n \n
{title}
\n {description !== undefined ? (\n
{description}
\n ) : null}\n
\n
\n {children}\n
\n
\n
\n );\n}\n","import { Button } from \"@jooservices/react-action-buttons\";\nimport { Loader2 } from \"lucide-react\";\nimport { forwardRef } from \"react\";\n\nimport { useFormContext } from \"../context\";\nimport type { SubmitButtonProps } from \"../types\";\n\nexport const SubmitButton = forwardRef(function SubmitButton(\n { children, busyLabel, variant = \"primary\", size = \"sm\", ignoreBusy = false, disabled, ...props },\n ref,\n) {\n const { busy, disabled: formDisabled } = useFormContext();\n const isBusy = !ignoreBusy && busy;\n const isDisabled = disabled ?? (isBusy || formDisabled);\n\n return (\n : undefined}\n {...props}\n >\n {isBusy && busyLabel !== undefined ? busyLabel : children}\n \n );\n});\n","import { CheckCircle2, TriangleAlert } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport type { FormStatusProps } from \"../types\";\n\nexport function FormStatus({ children, tone = \"success\", className }: FormStatusProps) {\n const Icon = tone === \"error\" ? TriangleAlert : CheckCircle2;\n\n return (\n \n \n
{children}
\n \n );\n}\n"]} \ No newline at end of file +{"version":3,"sources":["../src/context.ts","../src/lib/cn.ts","../src/components/Form.tsx","../src/components/FormHeader.tsx","../src/components/FormContent.tsx","../src/components/FormFooter.tsx","../src/components/Field.tsx","../src/components/FormSection.tsx","../src/components/SubmitButton.tsx","../src/components/FormStatus.tsx"],"names":["jsxs","jsx","useMemo","SubmitButton"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext } from \"react\";\n\nimport type { FormContextValue } from \"./types\";\n\nexport const FormContext = createContext({\n busy: false,\n disabled: false,\n sectionDepth: 0,\n});\n\nexport function useFormContext(): FormContextValue {\n return useContext(FormContext);\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import { useMemo, type FormEvent } from \"react\";\n\nimport { FormContext } from \"../context\";\nimport { cn } from \"../lib/cn\";\nimport type { FormProps } from \"../types\";\n\nexport function Form({\n onSubmit,\n busy = false,\n disabled = false,\n error,\n success,\n ariaLabel,\n variant = \"card\",\n className,\n children,\n ...props\n}: FormProps) {\n const contextValue = useMemo(() => ({ busy, disabled, sectionDepth: 0 }), [busy, disabled]);\n\n return (\n \n ) => {\n event.preventDefault();\n onSubmit(event);\n }}\n className={cn(\n variant === \"card\" &&\n \"overflow-hidden rounded-lg border border-slate-200 bg-white text-slate-900\",\n \"w-full\",\n className,\n )}\n {...props}\n >\n {error ? (\n \n {error}\n \n ) : null}\n {success ? (\n \n {success}\n \n ) : null}\n {children}\n \n \n );\n}\n","import { cn } from \"../lib/cn\";\nimport type { FormHeaderProps } from \"../types\";\n\nexport function FormHeader({ icon, title, subtitle, actions, className }: FormHeaderProps) {\n const hasTitle = title !== undefined && title !== null;\n const hasAnything = hasTitle || actions !== undefined;\n\n if (!hasAnything) {\n return null;\n }\n\n return (\n \n
\n {icon !== undefined ? (\n \n {icon}\n \n ) : null}\n {hasTitle ? (\n
\n
{title}
\n {subtitle !== undefined ? (\n
{subtitle}
\n ) : null}\n
\n ) : null}\n
\n {actions !== undefined ?
{actions}
: null}\n \n );\n}\n","import { cn } from \"../lib/cn\";\nimport type { FormContentProps } from \"../types\";\n\nexport function FormContent({ className, children, ...props }: FormContentProps) {\n return (\n
\n {children}\n
\n );\n}\n","import { cn } from \"../lib/cn\";\nimport type { FormFooterProps } from \"../types\";\n\nexport function FormFooter({ info, actions, className }: FormFooterProps) {\n const hasAnything = info !== undefined || actions !== undefined;\n\n if (!hasAnything) {\n return null;\n }\n\n return (\n \n {info !== undefined ?
{info}
:
}\n {actions !== undefined ? (\n
{actions}
\n ) : null}\n
\n );\n}\n","import { cloneElement, isValidElement, useId, type ReactElement, type ReactNode } from \"react\";\n\nimport { useFormContext } from \"../context\";\nimport { cn } from \"../lib/cn\";\nimport type { FieldProps } from \"../types\";\n\nfunction wireHtmlChild(\n child: ReactNode,\n controlHtmlId: string,\n labelHtmlId: string,\n describedBy: string | undefined,\n disabled: boolean | undefined,\n hasError: boolean,\n required: boolean,\n): ReactNode {\n if (!isValidElement(child)) {\n return child;\n }\n const childProps = (child.props ?? {}) as Record;\n const props: Record = {\n id: controlHtmlId,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required || undefined,\n };\n if (disabled !== undefined) {\n props.disabled = disabled;\n }\n if (describedBy) {\n props[\"aria-describedby\"] = describedBy;\n }\n if (labelHtmlId && !childProps[\"aria-label\"] && !childProps[\"aria-labelledby\"]) {\n props[\"aria-labelledby\"] = labelHtmlId;\n }\n return cloneElement(child as ReactElement>, props);\n}\n\nfunction FieldLabelHtml({\n label,\n required,\n controlHtmlId,\n labelHtmlId,\n}: {\n label: ReactNode;\n required: boolean;\n controlHtmlId: string;\n labelHtmlId: string;\n}) {\n if (label === undefined) {\n return null;\n }\n return (\n \n );\n}\n\nfunction FieldMessagesHtml({\n hint,\n error,\n hintHtmlId,\n errorHtmlId,\n}: {\n hint: ReactNode;\n error: ReactNode;\n hintHtmlId: string | undefined;\n errorHtmlId: string | undefined;\n}) {\n return (\n <>\n {hint ? (\n

\n {hint}\n

\n ) : null}\n {error ? (\n

\n {error}\n

\n ) : null}\n \n );\n}\n\nexport function Field(props: FieldProps) {\n const {\n label,\n required = false,\n hint,\n error,\n htmlFor,\n disabled,\n layout = \"stacked\",\n className,\n children,\n } = props;\n const { busy, disabled: formDisabled } = useFormContext();\n const generatedId = useId();\n const labelHtmlId = useId();\n const controlHtmlId = htmlFor ?? generatedId;\n const formIsDisabled = busy || formDisabled;\n const shouldForceDisabled = disabled !== undefined || formIsDisabled;\n const effectiveDisabled = disabled ?? formIsDisabled;\n const hintHtmlId = hint ? `${controlHtmlId}-hint` : undefined;\n const errorHtmlId = error ? `${controlHtmlId}-error` : undefined;\n const describedBy = [hintHtmlId, errorHtmlId].filter(Boolean).join(\" \") || undefined;\n\n const sharedProps = {\n label,\n required,\n controlHtmlId,\n labelHtmlId,\n };\n\n if (layout === \"inline\") {\n return (\n
\n
\n {wireHtmlChild(\n children,\n controlHtmlId,\n labelHtmlId,\n describedBy,\n shouldForceDisabled ? effectiveDisabled : undefined,\n Boolean(errorHtmlId),\n required,\n )}\n
\n
\n \n \n
\n
\n );\n }\n\n return (\n
\n \n {wireHtmlChild(\n children,\n controlHtmlId,\n labelHtmlId,\n describedBy,\n shouldForceDisabled ? effectiveDisabled : undefined,\n Boolean(errorHtmlId),\n required,\n )}\n \n
\n );\n}\n","import { useMemo } from \"react\";\n\nimport { FormContext, useFormContext } from \"../context\";\nimport { cn } from \"../lib/cn\";\nimport type { FormSectionProps } from \"../types\";\n\nexport function FormSection({ title, description, disabled, className, children }: FormSectionProps) {\n const { busy, disabled: formDisabled, sectionDepth } = useFormContext();\n const isDisabled = disabled ?? (busy || formDisabled);\n const depth = Math.min(sectionDepth + 1, 2);\n\n const contextValue = useMemo(\n () => ({ busy, disabled: isDisabled, sectionDepth: depth }),\n [busy, isDisabled, depth],\n );\n\n const isNested = sectionDepth > 0;\n\n return (\n \n
\n \n
{title}
\n {description !== undefined ? (\n
{description}
\n ) : null}\n
\n
\n {children}\n
\n
\n
\n );\n}\n","import { Button } from \"@jooservices/react-action-buttons\";\nimport { Loader2 } from \"lucide-react\";\nimport { forwardRef } from \"react\";\n\nimport { useFormContext } from \"../context\";\nimport type { SubmitButtonProps } from \"../types\";\n\nexport const SubmitButton = forwardRef(function SubmitButton(\n { children, busyLabel, variant = \"primary\", size = \"sm\", ignoreBusy = false, disabled, ...props },\n ref,\n) {\n const { busy, disabled: formDisabled } = useFormContext();\n const isBusy = !ignoreBusy && busy;\n const isDisabled = disabled ?? (isBusy || formDisabled);\n\n return (\n : undefined}\n {...props}\n >\n {isBusy && busyLabel !== undefined ? busyLabel : children}\n \n );\n});\n","import { CheckCircle2, TriangleAlert } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport type { FormStatusProps } from \"../types\";\n\nexport function FormStatus({ children, tone = \"success\", className }: FormStatusProps) {\n const Icon = tone === \"error\" ? TriangleAlert : CheckCircle2;\n\n return (\n \n \n
{children}
\n \n );\n}\n"]} \ No newline at end of file diff --git a/components/form/package-lock.json b/components/form/package-lock.json index c2dd91f..8ddaba3 100644 --- a/components/form/package-lock.json +++ b/components/form/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jooservices/react-form", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jooservices/react-form", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -40,7 +40,7 @@ }, "../action-buttons": { "name": "@jooservices/react-action-buttons", - "version": "2.6.0", + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { @@ -76,7 +76,7 @@ }, "../ui": { "name": "@jooservices/react-ui", - "version": "2.6.0", + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { diff --git a/components/form/package.json b/components/form/package.json index 2fc6feb..695648e 100644 --- a/components/form/package.json +++ b/components/form/package.json @@ -1,6 +1,6 @@ { "name": "@jooservices/react-form", - "version": "2.6.0", + "version": "2.7.0", "description": "Shared form chrome for React \u2014 Form, FormHeader, FormContent, FormFooter, Field, FormSection, and FormStatus", "type": "module", "license": "MIT", diff --git a/components/form/src/Form.test.tsx b/components/form/src/Form.test.tsx index c8a7e9a..50b9c61 100644 --- a/components/form/src/Form.test.tsx +++ b/components/form/src/Form.test.tsx @@ -184,6 +184,46 @@ describe("Field", () => { expect(screen.getByLabelText("Name")).not.toBeDisabled(); }); + it("preserves a child's own disabled state when the form is idle", () => { + render( + + + , + ); + expect(screen.getByLabelText("Name")).toBeDisabled(); + }); + + it("sets aria-required on the control when required", () => { + render( + + + , + ); + expect(screen.getByLabelText(/Email/)).toHaveAttribute("aria-required", "true"); + }); + + it("labels composite controls via aria-labelledby", () => { + render( + +
+ , + ); + const group = screen.getByRole("group"); + const labelId = group.getAttribute("aria-labelledby"); + expect(labelId).toBeTruthy(); + expect(screen.getByText("Plan tier")).toHaveAttribute("id", labelId); + }); + + it("does not override an existing aria-label on the control", () => { + render( + +
+ , + ); + expect(screen.getByRole("group")).toHaveAttribute("aria-label", "My own label"); + expect(screen.getByRole("group")).not.toHaveAttribute("aria-labelledby"); + }); + it("disables a react-ui RadioGroup child when the form is busy", () => { render(
diff --git a/components/form/src/components/Field.tsx b/components/form/src/components/Field.tsx index 685ecd7..e0bae23 100644 --- a/components/form/src/components/Field.tsx +++ b/components/form/src/components/Field.tsx @@ -4,51 +4,52 @@ import { useFormContext } from "../context"; import { cn } from "../lib/cn"; import type { FieldProps } from "../types"; -function wireChild( +function wireHtmlChild( child: ReactNode, - controlId: string, + controlHtmlId: string, + labelHtmlId: string, describedBy: string | undefined, - disabled: boolean, + disabled: boolean | undefined, hasError: boolean, + required: boolean, ): ReactNode { if (!isValidElement(child)) { return child; } + const childProps = (child.props ?? {}) as Record; const props: Record = { - id: controlId, - disabled, + id: controlHtmlId, "aria-invalid": hasError ? true : undefined, + "aria-required": required || undefined, }; + if (disabled !== undefined) { + props.disabled = disabled; + } if (describedBy) { props["aria-describedby"] = describedBy; } + if (labelHtmlId && !childProps["aria-label"] && !childProps["aria-labelledby"]) { + props["aria-labelledby"] = labelHtmlId; + } return cloneElement(child as ReactElement>, props); } -export function Field({ +function FieldLabelHtml({ label, - required = false, - hint, - error, - htmlFor, - disabled, - layout = "stacked", - className, - children, -}: FieldProps) { - const { busy, disabled: formDisabled } = useFormContext(); - const generatedId = useId(); - const controlId = htmlFor ?? generatedId; - const isDisabled = disabled ?? (busy || formDisabled); - const hintId = hint ? `${controlId}-hint` : undefined; - const errorId = error ? `${controlId}-error` : undefined; - const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined; - - const labelNode = label !== undefined ? ( -