diff --git a/.changepacks/changepack_log_radio_group_a11y.json b/.changepacks/changepack_log_radio_group_a11y.json new file mode 100644 index 000000000..b0c5dad88 --- /dev/null +++ b/.changepacks/changepack_log_radio_group_a11y.json @@ -0,0 +1,7 @@ +{ + "changes": { + "packages/components/package.json": "Patch" + }, + "note": "RadioGroup is a radiogroup with an optional label, its radios share a name (generated when not given) so arrow keys move between them, and it keeps the typed values of its options: false, 0 and '' stay selectable and onChange receives the option's own value rather than a string. Button-variant radios are visually hidden instead of display:none, so they stay focusable and show a focus ring, and the default radio's focus outline is valid CSS and shown on :focus-visible. Checkbox uses an id it is given for its labels as well as its input, where the labels used to point at a generated id", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_react_18_support.json b/.changepacks/changepack_log_react_18_support.json new file mode 100644 index 000000000..ad74861ef --- /dev/null +++ b/.changepacks/changepack_log_react_18_support.json @@ -0,0 +1,8 @@ +{ + "changes": { + "packages/react/package.json": "Patch", + "packages/components/package.json": "Patch" + }, + "note": "@devup-ui/react and @devup-ui/components take React as a peer of ^18 || ^19 instead of `*` and no longer depend on React 19 themselves, so a React 18 app no longer gets a second React. Select and Stepper read their context with useContext instead of React 19's use() and provide it with Context.Provider instead of React 19's (which crashes React 18), and Select repositions its container from an effect instead of a callback ref returning cleanup, which React 18 ignores", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_select_a11y.json b/.changepacks/changepack_log_select_a11y.json new file mode 100644 index 000000000..594b6596a --- /dev/null +++ b/.changepacks/changepack_log_select_a11y.json @@ -0,0 +1,7 @@ +{ + "changes": { + "packages/components/package.json": "Patch" + }, + "note": "Select follows the listbox pattern: the trigger has aria-haspopup, aria-controls and opens with ArrowDown/ArrowUp; the popup is a listbox (aria-multiselectable for checkbox selects) whose options carry role=option, aria-selected and aria-disabled; opening focuses the selected or first option, arrows/Home/End move between enabled options, Enter/Space choose one, and Escape or choosing closes the popup and returns focus to the trigger. An outside click on a controlled select now calls onOpenChange(false) instead of changing hidden internal state", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_toggle_switch.json b/.changepacks/changepack_log_toggle_switch.json new file mode 100644 index 000000000..744f10d0f --- /dev/null +++ b/.changepacks/changepack_log_toggle_switch.json @@ -0,0 +1,7 @@ +{ + "changes": { + "packages/components/package.json": "Patch" + }, + "note": "Toggle is an accessible switch: role=switch with aria-checked, focusable with a visible focus ring, toggled by Space and Enter, labelled through aria-label or aria-labelledby, and submitted under the name it is given; when disabled it leaves the tab order, ignores keys and its hidden input is disabled", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_ui_kit_behavior.json b/.changepacks/changepack_log_ui_kit_behavior.json new file mode 100644 index 000000000..128cff001 --- /dev/null +++ b/.changepacks/changepack_log_ui_kit_behavior.json @@ -0,0 +1,9 @@ +{ + "changes": { + "packages/components/package.json": "Patch", + "packages/react/package.json": "Patch", + "packages/reset-css/package.json": "Patch" + }, + "note": "Components behave for keyboard, screen readers and both React 18 and 19: Button, Input and Textarea no longer force generic aria-labels (the visible text or an associated label names them), Input and Textarea mark errors with aria-invalid and aria-describedby pointing at the message, a loading Button sets aria-busy, is disabled by default (disableWhileLoading) and announces loadingLabel, Input clear dispatches a real change event instead of a fabricated one, color customization variables sit on the container so every part sees them and every public color key matches its CSS custom property (new keys inputDisabledBackground, inputDisabledText, inputPlaceholder, primaryBackground, Textarea disabledBackground and disabledText), Select positions against the viewport and clears the opposite inset, SelectTrigger asChild composes the child onClick, onKeyDown and ref with its own, Stepper counts without a controlled value, keeps its bound and disabled safeguards and renders its text mode as an output element. Select no longer forces aria-labels over its trigger, options, confirm button and container: an option is named by its content, the listbox by its trigger (aria-labelledby) or by what the caller passes. ThemeScript takes nonce and other script props and never aborts on blocked storage or missing matchMedia, follows the system while no theme was chosen and themes saved in other tabs; getTheme, setTheme and initTheme are safe on the server and initTheme returns a function that stops following. reset-css clips accessible-hidden content as sanitize.css does. No new build errors.", + "date": "2026-10-01T00:00:00.000Z" +} \ No newline at end of file diff --git a/.changepacks/changepack_log_ui_kit_forward_ref.json b/.changepacks/changepack_log_ui_kit_forward_ref.json new file mode 100644 index 000000000..883e0e61b --- /dev/null +++ b/.changepacks/changepack_log_ui_kit_forward_ref.json @@ -0,0 +1,7 @@ +{ + "changes": { + "packages/components/package.json": "Patch" + }, + "note": "Button, Checkbox, Input, Radio and Textarea forward their ref to the button, input or textarea they render, so a ref reaches the form element on React 18 as well as React 19", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/bun.lock b/bun.lock index 906ce66d6..412668d4f 100644 --- a/bun.lock +++ b/bun.lock @@ -512,7 +512,6 @@ "dependencies": { "@devup-ui/react": "workspace:^", "clsx": "^2.1", - "react": "^19.3.0", }, "devDependencies": { "@devup-ui/vite-plugin": "workspace:^", @@ -522,6 +521,7 @@ "@types/react": "^19.3", "@typescript/typescript6": "^6.0", "eslint-plugin-storybook": "^10.6", + "react": "^19.3.0", "rollup-plugin-preserve-directives": "^0.4", "storybook": "^10.6", "typescript": "^7.0.2", @@ -530,7 +530,7 @@ }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "react": "*", + "react": "^18 || ^19", }, }, "packages/eslint-plugin": { @@ -580,12 +580,12 @@ "version": "1.0.44", "dependencies": { "csstype-extra": "latest", - "react": "^19.3", }, "devDependencies": { "@types/node": "^26.6", "@types/react": "^19.3", "@typescript/typescript6": "^6.0", + "react": "^19.3", "rollup-plugin-preserve-directives": "^0.4", "typescript": "^7.0.2", "vite": "^8.3", @@ -593,7 +593,7 @@ }, "peerDependencies": { "csstype-extra": "*", - "react": "*", + "react": "^18 || ^19", }, }, "packages/reset-css": { diff --git a/packages/components/package.json b/packages/components/package.json index 7572a3c1e..f45b583e0 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -44,7 +44,6 @@ "types": "./dist/index.d.ts", "dependencies": { "@devup-ui/react": "workspace:^", - "react": "^19.3.0", "clsx": "^2.1" }, "devDependencies": { @@ -59,10 +58,11 @@ "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "vite": "^8.3", - "vite-plugin-dts": "^5.1" + "vite-plugin-dts": "^5.1", + "react": "^19.3.0" }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "react": "*" + "react": "^18 || ^19" } } diff --git a/packages/components/src/__tests__/forward-ref.browser.test.tsx b/packages/components/src/__tests__/forward-ref.browser.test.tsx new file mode 100644 index 000000000..59d355290 --- /dev/null +++ b/packages/components/src/__tests__/forward-ref.browser.test.tsx @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'bun:test' +import { render } from 'bun-test-env-dom' +import { createRef } from 'react' + +import { Button, Input, Radio, Textarea } from '../index' + +describe('forwardRef', () => { + it('hands each component ref to its form element', () => { + const button = createRef() + const input = createRef() + const radio = createRef() + const textarea = createRef() + render( + <> + + + r + " @@ -12,7 +12,7 @@ exports[`Textarea should render with default props 1`] = ` exports[`Textarea should render with disabled prop 1`] = ` "
-
" @@ -21,7 +21,7 @@ exports[`Textarea should render with disabled prop 1`] = ` exports[`Textarea should render error style when error is true 1`] = ` "
-
" @@ -30,9 +30,9 @@ exports[`Textarea should render error style when error is true 1`] = ` exports[`Textarea should render with error message 1`] = ` "
- - + Error message
@@ -42,7 +42,7 @@ exports[`Textarea should render with error message 1`] = ` exports[`Textarea should have typography when typography is provided 1`] = ` "
-
" diff --git a/packages/components/src/components/Textarea/__tests__/index.browser.test.tsx b/packages/components/src/components/Textarea/__tests__/index.browser.test.tsx index 349769a79..c509ec00d 100644 --- a/packages/components/src/components/Textarea/__tests__/index.browser.test.tsx +++ b/packages/components/src/components/Textarea/__tests__/index.browser.test.tsx @@ -32,29 +32,25 @@ describe('Textarea', () => { it('should render error style when error is true', () => { const { container } = render(