From 632a5873ab623968c8c16fa08bc540d806c0e8ed Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 28 Jan 2026 09:59:30 +0530 Subject: [PATCH 1/5] feat: migrate checkbox to base ui --- .../components/checkbox/checkbox.module.css | 125 +++++++------- .../raystack/components/checkbox/checkbox.tsx | 77 ++------- .../raystack/components/checkbox/index.tsx | 2 +- packages/raystack/package.json | 3 +- pnpm-lock.yaml | 161 ++++++++++++++---- 5 files changed, 214 insertions(+), 154 deletions(-) diff --git a/packages/raystack/components/checkbox/checkbox.module.css b/packages/raystack/components/checkbox/checkbox.module.css index 8a8e3c303..4dc65bc52 100644 --- a/packages/raystack/components/checkbox/checkbox.module.css +++ b/packages/raystack/components/checkbox/checkbox.module.css @@ -1,66 +1,65 @@ .checkbox { - all: unset; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--rs-space-5); - height: var(--rs-space-5); - min-width: var(--rs-space-5); - min-height: var(--rs-space-5); - border-radius: var(--rs-radius-1); - background: var(--rs-color-background-base-primary); - border: 1px solid var(--rs-color-border-base-secondary); - cursor: pointer; - flex-shrink: 0; - } - - .checkbox:hover { - background: var(--rs-color-background-base-primary-hover); - border-color: var(--rs-color-border-base-focus); - } - - .checkbox[data-state="checked"] { - background: var(--rs-color-background-accent-emphasis); - border: none; - } - - .checkbox[data-state="checked"]:hover { - background: var(--rs-color-background-accent-emphasis-hover); - } + all: unset; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--rs-space-5); + height: var(--rs-space-5); + min-width: var(--rs-space-5); + min-height: var(--rs-space-5); + border-radius: var(--rs-radius-1); + background: var(--rs-color-background-base-primary); + border: 1px solid var(--rs-color-border-base-secondary); + cursor: pointer; + flex-shrink: 0; +} - /* Indeterminate state */ - .checkbox-indeterminate[data-state="checked"] { - background: var(--rs-color-background-neutral-tertiary); - border: none; - } +.checkbox:hover { + background: var(--rs-color-background-base-primary-hover); + border-color: var(--rs-color-border-base-focus); +} - .checkbox-indeterminate[data-state="checked"]:hover { - background: var(--rs-color-background-neutral-tertiary); - border: none; - } - - .checkbox-disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .checkbox-disabled:hover { - background: var(--rs-color-background-base-primary); - border-color: var(--rs-color-border-base-primary); - } - - .indicator { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - color: var(--rs-color-foreground-accent-emphasis); - } - - .icon { - width: var(--rs-space-5); - height: var(--rs-space-5); - } - \ No newline at end of file +.checkbox[data-checked] { + background: var(--rs-color-background-accent-emphasis); + border: none; +} + +.checkbox[data-checked]:hover { + background: var(--rs-color-background-accent-emphasis-hover); +} + +/* Indeterminate state */ +.checkbox[data-indeterminate] { + background: var(--rs-color-background-neutral-tertiary); + border: none; +} + +.checkbox[data-indeterminate]:hover { + background: var(--rs-color-background-neutral-tertiary); + border: none; +} + +.checkbox[data-disabled] { + opacity: 0.5; + cursor: not-allowed; +} + +.checkbox[data-disabled]:hover { + background: var(--rs-color-background-base-primary); + border-color: var(--rs-color-border-base-primary); +} + +.indicator { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: var(--rs-color-foreground-accent-emphasis); +} + +.icon { + width: var(--rs-space-5); + height: var(--rs-space-5); +} diff --git a/packages/raystack/components/checkbox/checkbox.tsx b/packages/raystack/components/checkbox/checkbox.tsx index 2a1e086f6..d67664c78 100644 --- a/packages/raystack/components/checkbox/checkbox.tsx +++ b/packages/raystack/components/checkbox/checkbox.tsx @@ -1,8 +1,8 @@ 'use client'; -import { VariantProps, cva, cx } from 'class-variance-authority'; -import { Checkbox as CheckboxPrimitive } from 'radix-ui'; -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; +import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'; +import { cx } from 'class-variance-authority'; +import { ElementRef, forwardRef } from 'react'; import styles from './checkbox.module.css'; @@ -42,63 +42,22 @@ const IndeterminateIcon = () => ( ); -const checkbox = cva(styles.checkbox); - -type CheckboxVariants = VariantProps; -type CheckedState = boolean | 'indeterminate'; - -export interface CheckboxProps - extends Omit< - ComponentPropsWithoutRef, - keyof CheckboxVariants - >, - CheckboxVariants { - checked?: CheckedState; - defaultChecked?: CheckedState; - onCheckedChange?: (checked: CheckedState) => void; -} - export const Checkbox = forwardRef< ElementRef, - CheckboxProps ->( - ( - { className, disabled, checked, defaultChecked, onCheckedChange, ...props }, - forwardedRef - ) => { - const isIndeterminate = - checked === 'indeterminate' || defaultChecked === 'indeterminate'; - - return ( - { - if (onCheckedChange) { - // If it's currently indeterminate, next state will be unchecked - if (checked === 'indeterminate') { - onCheckedChange(false); - } else { - onCheckedChange(value); - } - } - }} - disabled={disabled} - ref={forwardedRef} - {...props} - > - - {isIndeterminate ? : } - - - ); - } -); + CheckboxPrimitive.Root.Props +>(({ className, indeterminate, ...props }, ref) => { + return ( + + + {indeterminate ? : } + + + ); +}); Checkbox.displayName = 'Checkbox'; diff --git a/packages/raystack/components/checkbox/index.tsx b/packages/raystack/components/checkbox/index.tsx index 2d4f967d9..83a0ba15d 100644 --- a/packages/raystack/components/checkbox/index.tsx +++ b/packages/raystack/components/checkbox/index.tsx @@ -1 +1 @@ -export { Checkbox } from "./checkbox"; +export { Checkbox } from './checkbox'; diff --git a/packages/raystack/package.json b/packages/raystack/package.json index d86e1689c..35527459d 100644 --- a/packages/raystack/package.json +++ b/packages/raystack/package.json @@ -80,6 +80,7 @@ "license": "ISC", "devDependencies": { "@figma/code-connect": "^1.3.5", + "@raystack/tools-config": "workspace:*", "@rollup/plugin-commonjs": "^25.0.2", "@rollup/plugin-image": "^3.0.2", "@rollup/plugin-node-resolve": "^15.1.0", @@ -107,13 +108,13 @@ "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-preserve-directives": "^0.4.0", "rollup-plugin-tsconfig-paths": "^1.5.2", - "@raystack/tools-config": "workspace:*", "semver": "^7.6.0", "typescript": "~5.4.3", "vitest": "^3.2.4" }, "dependencies": { "@ariakit/react": "^0.4.16", + "@base-ui/react": "^1.1.0", "@radix-ui/react-icons": "^1.3.2", "@tanstack/match-sorter-utils": "^8.8.4", "@tanstack/react-table": "^8.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50068f491..d10663bd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,9 @@ importers: '@ariakit/react': specifier: ^0.4.16 version: 0.4.16(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@base-ui/react': + specifier: ^1.1.0 + version: 1.1.0(@types/react@19.1.9)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@19.2.1) @@ -965,14 +968,14 @@ packages: resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.26.9': - resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.0': resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -997,6 +1000,27 @@ packages: resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.1.0': + resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@base-ui/utils@0.2.4': + resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@bconnorwhite/module@2.0.2': resolution: {integrity: sha512-ck1me5WMgZKp06gnJrVKEkytpehTTQbvsAMbF1nGPeHri/AZNhj87++PSE2LOxmZqM0EtGMaqeLdx7Lw7SUnTA==} @@ -1450,15 +1474,30 @@ packages: '@floating-ui/core@1.6.4': resolution: {integrity: sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + '@floating-ui/dom@1.6.7': resolution: {integrity: sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + '@floating-ui/react-dom@2.1.1': resolution: {integrity: sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.4': resolution: {integrity: sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==} @@ -7999,6 +8038,9 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -8499,6 +8541,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -8935,6 +8980,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + usehooks-ts@2.9.1: resolution: {integrity: sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==} engines: {node: '>=16.15.0', npm: '>=8'} @@ -10120,14 +10170,12 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.26.9': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.27.0': dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.6': {} + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -10174,6 +10222,31 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@base-ui/react@1.1.0(@types/react@19.1.9)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@base-ui/utils': 0.2.4(@types/react@19.1.9)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@floating-ui/utils': 0.2.10 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + reselect: 5.1.1 + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.2.1) + optionalDependencies: + '@types/react': 19.1.9 + + '@base-ui/utils@0.2.4(@types/react@19.1.9)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@floating-ui/utils': 0.2.10 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.1) + optionalDependencies: + '@types/react': 19.1.9 + '@bconnorwhite/module@2.0.2': dependencies: find-up: 5.0.0 @@ -10249,7 +10322,7 @@ snapshots: '@emotion/babel-plugin@11.11.0': dependencies: '@babel/helper-module-imports': 7.25.9 - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 '@emotion/serialize': 1.1.4 @@ -10276,7 +10349,7 @@ snapshots: '@emotion/react@11.11.4(@types/react@18.2.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 '@emotion/serialize': 1.1.4 @@ -10499,11 +10572,20 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.4 + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + '@floating-ui/dom@1.6.7': dependencies: '@floating-ui/core': 1.6.4 '@floating-ui/utils': 0.2.4 + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + '@floating-ui/react-dom@2.1.1(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/dom': 1.6.7 @@ -10516,6 +10598,14 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + '@floating-ui/react-dom@2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.4': {} '@formatjs/intl-localematcher@0.6.2': @@ -11248,6 +11338,7 @@ snapshots: '@parcel/types': 2.12.0(@parcel/core@2.12.0) transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' '@parcel/plugin@2.9.2(@parcel/core@2.12.0)': dependencies: @@ -11666,6 +11757,8 @@ snapshots: '@parcel/types': 2.12.0(@parcel/core@2.12.0) '@parcel/utils': 2.12.0 nullthrows: 1.1.1 + transitivePeerDependencies: + - '@swc/helpers' '@parcel/workers@2.9.2(@parcel/core@2.12.0)': dependencies: @@ -11708,7 +11801,7 @@ snapshots: '@radix-ui/primitive@1.0.0': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/primitive@1.1.2': {} @@ -11968,7 +12061,7 @@ snapshots: '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 react: 18.3.1 '@radix-ui/react-compose-refs@1.1.2(@types/react@18.2.12)(react@18.3.1)': @@ -12005,7 +12098,7 @@ snapshots: '@radix-ui/react-context@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 react: 18.3.1 '@radix-ui/react-context@1.1.2(@types/react@18.2.12)(react@18.3.1)': @@ -12028,7 +12121,7 @@ snapshots: '@radix-ui/react-dialog@1.0.0(@types/react@18.2.12)(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-context': 1.0.0(react@18.3.1) @@ -12134,7 +12227,7 @@ snapshots: '@radix-ui/react-dismissable-layer@1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-primitive': 1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1) @@ -12214,7 +12307,7 @@ snapshots: '@radix-ui/react-focus-guards@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 react: 18.3.1 '@radix-ui/react-focus-guards@1.1.2(@types/react@18.2.12)(react@18.3.1)': @@ -12237,7 +12330,7 @@ snapshots: '@radix-ui/react-focus-scope@1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-primitive': 1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) @@ -12318,7 +12411,7 @@ snapshots: '@radix-ui/react-id@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) react: 18.3.1 @@ -12627,7 +12720,7 @@ snapshots: '@radix-ui/react-portal@1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-primitive': 1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 19.2.1(react@18.3.1) @@ -12664,7 +12757,7 @@ snapshots: '@radix-ui/react-presence@1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) react: 18.3.1 @@ -12702,7 +12795,7 @@ snapshots: '@radix-ui/react-primitive@1.0.0(react-dom@19.2.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-slot': 1.0.0(react@18.3.1) react: 18.3.1 react-dom: 19.2.1(react@18.3.1) @@ -12998,7 +13091,7 @@ snapshots: '@radix-ui/react-slot@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) react: 18.3.1 @@ -13230,7 +13323,7 @@ snapshots: '@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 react: 18.3.1 '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.2.12)(react@18.3.1)': @@ -13253,7 +13346,7 @@ snapshots: '@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) react: 18.3.1 @@ -13304,7 +13397,7 @@ snapshots: '@radix-ui/react-use-escape-keydown@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) react: 18.3.1 @@ -13345,7 +13438,7 @@ snapshots: '@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 react: 18.3.1 '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.2.12)(react@18.3.1)': @@ -14538,7 +14631,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -15325,7 +15418,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 csstype: 3.1.3 dom-serializer@1.4.1: @@ -18752,7 +18845,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.2.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -18855,7 +18948,7 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.0 regex-recursion@6.0.2: dependencies: @@ -18983,6 +19076,8 @@ snapshots: requires-port@1.0.0: {} + reselect@5.1.1: {} + resolve-alpn@1.2.1: {} resolve-cwd@3.0.0: @@ -19550,6 +19645,8 @@ snapshots: symbol-tree@3.2.4: {} + tabbable@6.4.0: {} + table@6.9.0: dependencies: ajv: 8.17.1 @@ -20028,6 +20125,10 @@ snapshots: dependencies: react: 19.2.1 + use-sync-external-store@1.6.0(react@19.2.1): + dependencies: + react: 19.2.1 + usehooks-ts@2.9.1(react-dom@19.2.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 From ac925a7eacd258982cf834831e584bf94e2d821b Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 28 Jan 2026 14:28:44 +0530 Subject: [PATCH 2/5] feat: update checkbox tests and docs --- .../content/docs/components/checkbox/demo.ts | 15 ++++-- .../content/docs/components/checkbox/props.ts | 11 +++-- .../checkbox/__tests__/checkbox.test.tsx | 49 +++++++++---------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/apps/www/src/content/docs/components/checkbox/demo.ts b/apps/www/src/content/docs/components/checkbox/demo.ts index 3cc2a5765..505412262 100644 --- a/apps/www/src/content/docs/components/checkbox/demo.ts +++ b/apps/www/src/content/docs/components/checkbox/demo.ts @@ -13,9 +13,14 @@ export const playground = { type: 'playground', controls: { checked: { - type: 'select', - options: ['true', 'false', 'indeterminate'], - initialValue: 'true' + type: 'checkbox', + initialValue: false, + defaultValue: false + }, + indeterminate: { + type: 'checkbox', + initialValue: false, + defaultValue: false }, disabled: { type: 'checkbox', @@ -34,11 +39,11 @@ export const statesExamples = { }, { name: 'Checked', - code: `` + code: `` }, { name: 'Indeterminate', - code: `` + code: `` }, { name: 'Disabled', diff --git a/apps/www/src/content/docs/components/checkbox/props.ts b/apps/www/src/content/docs/components/checkbox/props.ts index 1d2a70bc7..220c452ac 100644 --- a/apps/www/src/content/docs/components/checkbox/props.ts +++ b/apps/www/src/content/docs/components/checkbox/props.ts @@ -2,17 +2,22 @@ export interface CheckboxProps { /** * The controlled state of the checkbox */ - checked?: boolean | 'indeterminate'; + checked?: boolean; /** * The default state when initially rendered */ - defaultChecked?: boolean | 'indeterminate'; + defaultChecked?: boolean; /** * Event handler called when the state changes */ - onCheckedChange?: (checked: boolean | 'indeterminate') => void; + onCheckedChange?: (checked: boolean) => void; + + /** + * When true, the checkbox is in an indeterminate state + */ + indeterminate?: boolean; /** * When true, prevents the user from interacting with the checkbox diff --git a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx index 0cac22f06..eec6d6147 100644 --- a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx +++ b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx @@ -73,28 +73,25 @@ describe('Checkbox', () => { }); describe('Indeterminate State', () => { - it('applies indeterminate class', () => { - render(); + it('has data-indeterminate attribute when indeterminate', () => { + render(); const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toHaveClass(styles['checkbox-indeterminate']); + expect(checkbox).toHaveAttribute('data-indeterminate'); }); - it('renders with defaultChecked as indeterminate', () => { - render(); + it('can be both checked and indeterminate', () => { + render(); const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toHaveClass(styles['checkbox-indeterminate']); + expect(checkbox).toHaveAttribute('aria-checked', 'mixed'); + expect(checkbox).toHaveAttribute('data-indeterminate'); }); - it('transitions from indeterminate to unchecked on click', () => { - const handleChange = vi.fn(); - render( - - ); - - const checkbox = screen.getByRole('checkbox'); - fireEvent.click(checkbox); - - expect(handleChange).toHaveBeenCalledWith(false); + it('shows indeterminate icon when indeterminate', () => { + const { container } = render(); + const indicator = container.querySelector(`.${styles.indicator}`); + expect(indicator).toBeInTheDocument(); + const svg = indicator?.querySelector('svg'); + expect(svg).toBeInTheDocument(); }); }); @@ -102,13 +99,13 @@ describe('Checkbox', () => { it('renders as disabled when disabled prop is true', () => { render(); const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toBeDisabled(); + expect(checkbox).toHaveAttribute('aria-disabled', 'true'); }); - it('applies disabled class', () => { + it('has data-disabled attribute when disabled', () => { render(); const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toHaveClass(styles['checkbox-disabled']); + expect(checkbox).toHaveAttribute('data-disabled'); }); it('does not trigger onCheckedChange when disabled', () => { @@ -124,15 +121,15 @@ describe('Checkbox', () => { it('can be disabled while checked', () => { render(); const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toBeDisabled(); + expect(checkbox).toHaveAttribute('aria-disabled', 'true'); expect(checkbox).toBeChecked(); }); it('can be disabled while indeterminate', () => { - render(); + render(); const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toBeDisabled(); - expect(checkbox).toHaveClass(styles['checkbox-indeterminate']); + expect(checkbox).toHaveAttribute('aria-disabled', 'true'); + expect(checkbox).toHaveAttribute('data-indeterminate'); }); }); @@ -145,7 +142,7 @@ describe('Checkbox', () => { fireEvent.click(checkbox); expect(handleChange).toHaveBeenCalledTimes(1); - expect(handleChange).toHaveBeenCalledWith(true); + expect(handleChange).toHaveBeenCalledWith(true, expect.anything()); }); it('toggles from unchecked to checked', () => { @@ -155,7 +152,7 @@ describe('Checkbox', () => { const checkbox = screen.getByRole('checkbox'); fireEvent.click(checkbox); - expect(handleChange).toHaveBeenCalledWith(true); + expect(handleChange).toHaveBeenCalledWith(true, expect.anything()); }); it('toggles from checked to unchecked', () => { @@ -165,7 +162,7 @@ describe('Checkbox', () => { const checkbox = screen.getByRole('checkbox'); fireEvent.click(checkbox); - expect(handleChange).toHaveBeenCalledWith(false); + expect(handleChange).toHaveBeenCalledWith(false, expect.anything()); }); it('supports focus events', () => { From 0232244553d561c7855408d09826c6970a168656 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 28 Jan 2026 15:08:07 +0530 Subject: [PATCH 3/5] feat: migrate avatar to base ui --- .../content/docs/components/avatar/index.mdx | 1 + .../content/docs/components/avatar/props.ts | 10 +++- .../raystack/components/avatar/avatar.tsx | 54 +++++++++---------- packages/raystack/components/avatar/index.tsx | 4 +- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/apps/www/src/content/docs/components/avatar/index.mdx b/apps/www/src/content/docs/components/avatar/index.mdx index 9ff19f09e..1b9b07d7f 100644 --- a/apps/www/src/content/docs/components/avatar/index.mdx +++ b/apps/www/src/content/docs/components/avatar/index.mdx @@ -2,6 +2,7 @@ title: Avatar description: An image element with a fallback for representing the user. source: packages/raystack/components/avatar +tag: new --- import { diff --git a/apps/www/src/content/docs/components/avatar/props.ts b/apps/www/src/content/docs/components/avatar/props.ts index 0180ca048..c8db83473 100644 --- a/apps/www/src/content/docs/components/avatar/props.ts +++ b/apps/www/src/content/docs/components/avatar/props.ts @@ -44,8 +44,14 @@ export interface AvatarProps { | 'crimson' | 'gold'; - /** Boolean to merge props onto child element */ - asChild?: boolean; + /** + * Allows you to replace the component's HTML element with a different tag, + * or compose it with another component. Accepts a ReactElement or a function + * that returns the element to render. + * + * @remarks `ReactElement | function` + */ + render?: React.ReactElement; /** Additional CSS class names */ className?: string; diff --git a/packages/raystack/components/avatar/avatar.tsx b/packages/raystack/components/avatar/avatar.tsx index 838c47619..ee0a7170c 100644 --- a/packages/raystack/components/avatar/avatar.tsx +++ b/packages/raystack/components/avatar/avatar.tsx @@ -1,14 +1,12 @@ -import { VariantProps, cva, cx } from 'class-variance-authority'; -import { Avatar as AvatarPrimitive } from 'radix-ui'; +import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar'; +import { cva, cx, VariantProps } from 'class-variance-authority'; import { ComponentPropsWithoutRef, - ElementRef, - ReactElement, - ReactNode, forwardRef, - isValidElement + isValidElement, + ReactElement, + ReactNode } from 'react'; -import { Box } from '../box'; import styles from './avatar.module.css'; import { AVATAR_COLORS } from './utils'; @@ -133,15 +131,17 @@ const image = cva(styles.image); * @desc Recursively get the avatar props even if it's * wrapped in another component like Tooltip, Flex, etc. */ -export const getAvatarProps = (element: ReactElement): AvatarProps => { - const { props } = element; +export const getAvatarProps = ( + element: ReactElement +): AvatarProps => { + const props = element.props as AvatarProps & { children?: ReactNode }; if (element.type === Avatar) { return props; } if (props.children) { - if (isValidElement(props.children)) { + if (isValidElement(props.children)) { return getAvatarProps(props.children); } } @@ -149,7 +149,7 @@ export const getAvatarProps = (element: ReactElement): AvatarProps => { }; export interface AvatarProps - extends ComponentPropsWithoutRef, + extends AvatarPrimitive.Root.Props, VariantProps { size?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13; src?: string; @@ -157,30 +157,28 @@ export interface AvatarProps fallback?: ReactNode; variant?: 'solid' | 'soft'; color?: AVATAR_COLORS; - asChild?: boolean; className?: string; } -const AvatarRoot = forwardRef< - ElementRef, - AvatarProps ->( +const AvatarRoot = forwardRef( ( { className, alt, src, fallback, size, radius, variant, color, ...props }, ref ) => ( - - - - - {fallback} - - - + + + + {fallback} + + ) ); diff --git a/packages/raystack/components/avatar/index.tsx b/packages/raystack/components/avatar/index.tsx index 70db771c1..80be713d5 100644 --- a/packages/raystack/components/avatar/index.tsx +++ b/packages/raystack/components/avatar/index.tsx @@ -1,2 +1,2 @@ -export { Avatar, AvatarGroup } from "./avatar"; -export { getAvatarColor } from "./utils"; +export { Avatar, AvatarGroup } from './avatar'; +export { getAvatarColor } from './utils'; From e46d9ccb3cf408e37fa9424de078da57b62a0ec5 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 28 Jan 2026 15:26:40 +0530 Subject: [PATCH 4/5] fix: avatar tests --- .../avatar/__tests__/avatar.test.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/raystack/components/avatar/__tests__/avatar.test.tsx b/packages/raystack/components/avatar/__tests__/avatar.test.tsx index d8ff32a3c..641060525 100644 --- a/packages/raystack/components/avatar/__tests__/avatar.test.tsx +++ b/packages/raystack/components/avatar/__tests__/avatar.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { Avatar, AvatarGroup } from '../avatar'; import styles from '../avatar.module.css'; import { getAvatarColor } from '../utils'; @@ -312,6 +312,8 @@ describe('Avatar', () => { class MockImage extends EventTarget { _src: string = ''; + _complete: boolean = false; + onload: (() => void) | null = null; constructor() { super(); @@ -327,20 +329,27 @@ class MockImage extends EventTarget { return; } this._src = src; - this.onSrcChange(); + // Simulate async image loading + setTimeout(() => { + this._complete = true; + // Call onload callback if set + if (this.onload) { + this.onload(); + } + // Also dispatch the event + this.dispatchEvent(new Event('load')); + }, 0); } get complete() { - return !this.src; + return this._complete; } get naturalWidth() { - return this.complete ? 300 : 0; + return this._complete ? 300 : 0; } - onSrcChange() { - setTimeout(() => { - this.dispatchEvent(new Event('load')); - }, 100); + get naturalHeight() { + return this._complete ? 300 : 0; } } From eccbe8fe73a9e492a14ffc8d2fbb26701b879f2b Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 28 Jan 2026 21:38:25 +0530 Subject: [PATCH 5/5] feat: migrate radio --- .../components/typetable/typetable.module.css | 5 + .../src/components/typetable/typetable.tsx | 12 +- .../src/content/docs/components/radio/demo.ts | 58 +++-- .../content/docs/components/radio/index.mdx | 9 +- .../content/docs/components/radio/props.ts | 54 +++-- .../components/radio/__tests__/radio.test.tsx | 201 +++++++----------- packages/raystack/components/radio/index.ts | 2 +- .../components/radio/radio.module.css | 12 +- packages/raystack/components/radio/radio.tsx | 81 +++---- 9 files changed, 198 insertions(+), 236 deletions(-) diff --git a/apps/www/src/components/typetable/typetable.module.css b/apps/www/src/components/typetable/typetable.module.css index 305aea381..9d422fb25 100644 --- a/apps/www/src/components/typetable/typetable.module.css +++ b/apps/www/src/components/typetable/typetable.module.css @@ -99,3 +99,8 @@ flex-wrap: wrap; gap: var(--rs-space-1); } +.required { + color: var(--rs-color-foreground-danger-primary); + position: relative; + top: -4px; +} diff --git a/apps/www/src/components/typetable/typetable.tsx b/apps/www/src/components/typetable/typetable.tsx index 90ec1d63d..2f15a29ff 100644 --- a/apps/www/src/components/typetable/typetable.tsx +++ b/apps/www/src/components/typetable/typetable.tsx @@ -45,7 +45,10 @@ export interface TypeNode { export function TypeTable({ type, className -}: { type: Record; className?: string }) { +}: { + type: Record; + className?: string; +}) { const entries = Object.entries(type); return ( @@ -87,7 +90,7 @@ function Item({ className={deprecated ? styles.propNameDeprecated : styles.propName} > {name} - {!required && '?'} + {required ? * : ''} {typeDescriptionLink ? ( @@ -130,7 +133,10 @@ function Item({ language='tsx' className={cx(styles.fieldCode, styles.fieldValue)} > - {String(typeDescription ?? type)} + {String(type) + + (!required && !String(type).includes('undefined') + ? ' | undefined' + : '')} diff --git a/apps/www/src/content/docs/components/radio/demo.ts b/apps/www/src/content/docs/components/radio/demo.ts index b0a3ca802..f52931f60 100644 --- a/apps/www/src/content/docs/components/radio/demo.ts +++ b/apps/www/src/content/docs/components/radio/demo.ts @@ -3,22 +3,22 @@ export const preview = { type: 'code', code: ` - - + + - - - - - - - - - - + + + + + + + + + + - ` + ` }; export const stateDemo = { @@ -27,22 +27,22 @@ export const stateDemo = { { name: 'Default', code: ` - + - + -` +` }, { name: 'Disabled', code: ` - + - + -` +` } ] }; @@ -50,22 +50,20 @@ export const stateDemo = { export const labelDemo = { type: 'code', code: ` - - + - + - + - + - - ` + ` }; export const formDemo = { @@ -77,18 +75,18 @@ export const formDemo = { alert(JSON.stringify(Object.fromEntries(formData))); }}> - + - + - + - + ` diff --git a/apps/www/src/content/docs/components/radio/index.mdx b/apps/www/src/content/docs/components/radio/index.mdx index 0123ac444..2433ca45c 100644 --- a/apps/www/src/content/docs/components/radio/index.mdx +++ b/apps/www/src/content/docs/components/radio/index.mdx @@ -16,13 +16,14 @@ import { Radio } from "@raystack/apsara"; ## Radio Props -### Radio Props +### Radio.Group Props + + - +### Radio Props -### Radio.Item Props + - ## Examples diff --git a/apps/www/src/content/docs/components/radio/props.ts b/apps/www/src/content/docs/components/radio/props.ts index b2a311d64..b490a44e9 100644 --- a/apps/www/src/content/docs/components/radio/props.ts +++ b/apps/www/src/content/docs/components/radio/props.ts @@ -1,12 +1,12 @@ -export interface RadioRootProps { +export interface RadioGroupProps { /** The value of the radio item that should be checked by default. */ - defaultValue?: string; + defaultValue?: any; /** The controlled value of the radio item that is checked. */ - value?: string; + value?: any; /** Event handler called when the value changes. */ - onValueChange?: (value: string) => void; + onValueChange?: (value: any, event: Event) => void; /** When true, prevents user interaction with the radio group. */ disabled?: boolean; @@ -14,29 +14,43 @@ export interface RadioRootProps { /** The name of the radio group when submitted as a form field. */ name?: string; - /** When true, indicates that a value must be selected before the form can be submitted. */ - required?: boolean; - - /** The orientation of the radio group. */ - orientation?: 'horizontal' | 'vertical'; - - /** The reading direction of the radio group. */ - dir?: 'ltr' | 'rtl'; - - /** A label for the radio group that is announced by screen readers. */ - ariaLabel?: string; + /** Additional CSS class name. */ + className?: string; + + /** + * Allows you to replace the component's HTML element with a different tag, or compose it with another component. + * Accepts a `ReactElement` or a function that returns the element to render. + * + * @remarks `ReactElement | function` + */ + render?: + | React.ReactElement + | ((props: React.HTMLAttributes) => React.ReactElement); } -export interface RadioItemProps { +export interface RadioProps { /** The unique value of the radio item. */ - value: string; + value: any; /** When true, prevents user interaction with this radio item. */ disabled?: boolean; - /** When true, indicates that this radio item must be checked. */ - required?: boolean; - /** The unique identifier for the radio item. */ id?: string; + + /** Additional CSS class name. */ + className?: string; + + /** + * Allows you to replace the component's HTML element with a different tag, or compose it with another component. + * Accepts a `ReactElement` or a function that returns the element to render. + * + * @remarks `ReactElement | function` + */ + render?: + | React.ReactElement + | (( + props: React.HTMLAttributes, + state: { checked: boolean } + ) => React.ReactElement); } diff --git a/packages/raystack/components/radio/__tests__/radio.test.tsx b/packages/raystack/components/radio/__tests__/radio.test.tsx index 00cd7ff89..8845b06ca 100644 --- a/packages/raystack/components/radio/__tests__/radio.test.tsx +++ b/packages/raystack/components/radio/__tests__/radio.test.tsx @@ -1,15 +1,15 @@ import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; -import { Radio, RadioItem } from '../radio'; +import { Radio } from '../radio'; describe('Radio', () => { describe('Basic Rendering', () => { it('renders radio group', () => { render( - - - + + + ); const radioGroup = screen.getByRole('radiogroup'); expect(radioGroup).toBeInTheDocument(); @@ -17,11 +17,11 @@ describe('Radio', () => { it('renders multiple radio items', () => { render( - - - - - + + + + + ); const radios = screen.getAllByRole('radio'); @@ -32,10 +32,10 @@ describe('Radio', () => { describe('Selection Behavior', () => { it('allows single selection', () => { render( - - - - + + + + ); const [radio1, radio2] = screen.getAllByRole('radio'); @@ -51,11 +51,11 @@ describe('Radio', () => { it('works with defaultValue', () => { render( - - - - - + + + + + ); const radios = screen.getAllByRole('radio'); @@ -66,10 +66,10 @@ describe('Radio', () => { it('works as controlled component', () => { const { rerender } = render( - - - - + + + + ); const [radio1, radio2] = screen.getAllByRole('radio'); @@ -77,10 +77,10 @@ describe('Radio', () => { expect(radio2).not.toBeChecked(); rerender( - - - - + + + + ); expect(radio1).not.toBeChecked(); @@ -90,56 +90,56 @@ describe('Radio', () => { it('calls onValueChange when selection changes', () => { const handleChange = vi.fn(); render( - - - - + + + + ); const radio2 = screen.getAllByRole('radio')[1]; fireEvent.click(radio2); - expect(handleChange).toHaveBeenCalledWith('option2'); + expect(handleChange).toHaveBeenCalledWith('option2', expect.anything()); }); }); describe('Disabled State', () => { it('disables entire radio group', () => { render( - - - - + + + + ); const radios = screen.getAllByRole('radio'); radios.forEach(radio => { - expect(radio).toBeDisabled(); + expect(radio).toHaveAttribute('data-disabled'); }); }); it('disables individual radio items', () => { render( - - - - - + + + + + ); const radios = screen.getAllByRole('radio'); - expect(radios[0]).not.toBeDisabled(); - expect(radios[1]).toBeDisabled(); - expect(radios[2]).not.toBeDisabled(); + expect(radios[0]).not.toHaveAttribute('data-disabled'); + expect(radios[1]).toHaveAttribute('data-disabled'); + expect(radios[2]).not.toHaveAttribute('data-disabled'); }); it('does not allow selection of disabled items', () => { const handleChange = vi.fn(); render( - - - - + + + + ); const disabledRadio = screen.getAllByRole('radio')[1]; @@ -154,17 +154,17 @@ describe('Radio', () => { it('supports arrow key navigation', async () => { const user = userEvent.setup(); render( - - - - - + + + + + ); const [radio1, radio2, radio3] = screen.getAllByRole('radio'); // Focus first radio - await radio1.focus(); + radio1.focus(); expect(document.activeElement).toBe(radio1); // Arrow down should move to next @@ -183,17 +183,17 @@ describe('Radio', () => { it('wraps around when navigating past boundaries', async () => { const user = userEvent.setup(); render( - - - - - + + + + + ); const [radio1, , radio3] = screen.getAllByRole('radio'); // Focus last radio - await radio3.focus(); + radio3.focus(); // Arrow down from last should wrap to first await user.keyboard('{ArrowDown}'); @@ -208,9 +208,9 @@ describe('Radio', () => { describe('Accessibility', () => { it('has correct ARIA attributes on group', () => { render( - - - + + + ); const radioGroup = screen.getByRole('radiogroup'); @@ -219,10 +219,10 @@ describe('Radio', () => { it('has correct ARIA attributes on items', () => { render( - - - - + + + + ); const radio1 = screen.getByLabelText('First option'); @@ -231,76 +231,31 @@ describe('Radio', () => { expect(radio1).toHaveAttribute('aria-checked', 'true'); expect(radio2).toHaveAttribute('aria-checked', 'false'); }); - - it('supports required attribute', () => { - render( - - - - ); - - const radioGroup = screen.getByRole('radiogroup'); - expect(radioGroup).toHaveAttribute('aria-required', 'true'); - }); - }); - - describe('Form Integration', () => { - it('works with form name attribute', () => { - const { container } = render( -
- - - - -
- ); - - const radios = container.querySelectorAll('input[type="radio"]'); - radios.forEach(radio => { - expect(radio).toHaveAttribute('name', 'preference'); - }); - }); - - it('respects form disabled state', () => { - render( -
- - - - -
- ); - - const radios = screen.getAllByRole('radio'); - radios.forEach(radio => { - expect(radio).toBeDisabled(); - }); - }); }); describe('Data Attributes', () => { - it('has data-state attribute on items', () => { + it('has data-checked attribute on selected items', () => { render( - - - - + + + + ); const [radio1, radio2] = screen.getAllByRole('radio'); - expect(radio1).toHaveAttribute('data-state', 'checked'); - expect(radio2).toHaveAttribute('data-state', 'unchecked'); + expect(radio1).toHaveAttribute('data-checked'); + expect(radio2).toHaveAttribute('data-unchecked'); }); it('has data-disabled attribute when disabled', () => { render( - - - + + + ); const radio = screen.getByRole('radio'); - expect(radio).toHaveAttribute('data-disabled', ''); + expect(radio).toHaveAttribute('data-disabled'); }); }); }); diff --git a/packages/raystack/components/radio/index.ts b/packages/raystack/components/radio/index.ts index c8a266a51..d9dae10e1 100644 --- a/packages/raystack/components/radio/index.ts +++ b/packages/raystack/components/radio/index.ts @@ -1 +1 @@ -export { Radio } from "./radio"; +export { Radio } from './radio'; diff --git a/packages/raystack/components/radio/radio.module.css b/packages/raystack/components/radio/radio.module.css index 74285b704..a30d81675 100644 --- a/packages/raystack/components/radio/radio.module.css +++ b/packages/raystack/components/radio/radio.module.css @@ -24,12 +24,12 @@ background: var(--rs-color-background-base-primary-hover); } -.radioitem[data-state="checked"] { +.radioitem[data-checked] { border: 1px solid var(--rs-color-background-accent-emphasis); background: var(--rs-color-background-accent-emphasis); } -.radioitem[data-state="checked"]:hover { +.radioitem[data-checked]:hover { border-color: var(--rs-color-background-accent-emphasis-hover); background: var(--rs-color-background-accent-emphasis-hover); } @@ -41,8 +41,8 @@ cursor: not-allowed; } -.radioitem[data-disabled][data-state="checked"], -.radioitem[data-disabled][data-state="checked"]:hover { +.radioitem[data-disabled][data-checked], +.radioitem[data-disabled][data-checked]:hover { background: var(--rs-color-background-accent-primary); border-color: var(--rs-color-background-accent-primary); } @@ -57,7 +57,7 @@ } .indicator::after { - content: ''; + content: ""; display: block; width: var(--rs-radius-3); height: var(--rs-radius-3); @@ -67,4 +67,4 @@ .radioitem[data-disabled] .indicator::after { background: var(--rs-color-foreground-base-emphasis); -} \ No newline at end of file +} diff --git a/packages/raystack/components/radio/radio.tsx b/packages/raystack/components/radio/radio.tsx index c79e2d1ea..ff1d0ac93 100644 --- a/packages/raystack/components/radio/radio.tsx +++ b/packages/raystack/components/radio/radio.tsx @@ -1,53 +1,36 @@ -import { VariantProps, cva } from 'class-variance-authority'; -import { RadioGroup as RadioGroupPrimitive } from 'radix-ui'; -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; +import { Radio as RadioPrimitive } from '@base-ui/react/radio'; +import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group'; +import { cx } from 'class-variance-authority'; +import { forwardRef } from 'react'; import styles from './radio.module.css'; -const RadioRoot = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -const radioItem = cva(styles.radioitem); - -export interface RadioItemProps - extends ComponentPropsWithoutRef {} - -export const RadioItem = forwardRef< - ElementRef, - RadioItemProps ->(({ className, ...props }, forwardedRef) => ( - - - -)); - -const indicator = cva(styles.indicator); -export interface thumbProps - extends ComponentPropsWithoutRef, - VariantProps {} - -const Indicator = forwardRef< - ElementRef, - thumbProps ->(({ className, ...props }, ref) => ( - -)); - -Indicator.displayName = RadioGroupPrimitive.Indicator.displayName; - -export const Radio = Object.assign(RadioRoot, { - Indicator: Indicator, - Item: RadioItem +const RadioGroup = forwardRef( + ({ className, ...props }, ref) => ( + + ) +); + +RadioGroup.displayName = 'Radio.Group'; + +const RadioItem = forwardRef( + ({ className, ...props }, forwardedRef) => ( + + + + ) +); + +RadioItem.displayName = 'Radio'; + +export const Radio = Object.assign(RadioItem, { + Group: RadioGroup });