Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .claude/docs/framework_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ import { Show } from 'solid-js'
- More explicit conditional rendering semantics
- Consistent with Solid.js reactive patterns

#### Native Elements: Use `ark` Factory

In Solid components, **always use `ark.<element>` instead of native HTML elements** (e.g., `<div>`, `<span>`, `<button>`):

```tsx
import { ark } from '../factory'

// ✅ CORRECT - Use ark factory
<ark.div {...props} />
<ark.span data-kind="label">{text}</ark.span>
<ark.button onClick={handler}>Click</ark.button>

// ❌ WRONG - Native elements cause SSR issues
<div {...props} />
<span data-kind="label">{text}</span>
<button onClick={handler}>Click</button>
```

**Why this matters:**

- Native HTML elements compile to `template()` and `use()` calls from `solid-js/web`
- These functions only exist in the browser build, not the server build
- Deno and other SSR environments use the server build, causing crashes
- The `ark` factory uses `Dynamic` internally, which defers element creation to render time and is SSR-safe

### Vue Pattern

```vue
Expand Down
2 changes: 1 addition & 1 deletion .storybook/modules/floating-panel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
padding: 0;
border: 1px solid var(--demo-border);
border-radius: 0.25rem;
background-color: white;
background-color: var(--demo-bg-popover);
color: var(--demo-neutral-fg);

&:hover {
Expand Down
2 changes: 1 addition & 1 deletion .storybook/modules/image-cropper.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
& > * {
width: var(--cropper-handler-size);
height: var(--cropper-handler-size);
background: white;
background: var(--demo-bg-popover);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.3);
transition:
opacity 0.2s ease,
Expand Down
5 changes: 1 addition & 4 deletions .storybook/modules/switch.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@
outline: 2px solid var(--demo-coral-focus-ring);
outline-offset: 2px;
}

&[data-disabled] {
}
}

.Thumb {
Expand All @@ -52,7 +49,7 @@
justify-content: center;
width: 1.25rem;
height: 1.25rem;
background: white;
background: var(--demo-bg-thumb);
border-radius: 9999px;
box-shadow: var(--demo-shadow-sm);
transition: transform 0.15s ease;
Expand Down
42 changes: 16 additions & 26 deletions packages/react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,26 @@

### Added

- **Scroll Area**: Added overflow CSS variables to the viewport element for creating scroll fade effects:
- `--scroll-area-overflow-x-start`: Distance from horizontal start edge in pixels
- `--scroll-area-overflow-x-end`: Distance from horizontal end edge in pixels
- `--scroll-area-overflow-y-start`: Distance from vertical start edge in pixels
- `--scroll-area-overflow-y-end`: Distance from vertical end edge in pixels
- **Slider**: Added `thumbCollisionBehavior` prop to control how thumbs behave when they collide (`none`, `push`, `swap`)
- **Steps**: Added validation and skippable step support:
- `isStepValid(index)`: Block forward navigation when step is invalid (linear mode)
- `isStepSkippable(index)`: Mark steps as optional, bypassing validation
- `onStepInvalid({ step, action, targetStep })`: Callback when navigation is blocked
- **Tags Input**: Added `placeholder` prop that is applied to the input only when there are no tags
- **Tooltip**: Added `data-instant` attribute to content to indicate when animations should be instant
- **Scroll Area**: Added overflow CSS variables (`--scroll-area-overflow-{x,y}-{start,end}`) for scroll fade effects
- **Slider**: Added `thumbCollisionBehavior` prop (`none`, `push`, `swap`)
- **Steps**: Added `isStepValid`, `isStepSkippable`, and `onStepInvalid` for validation support
- **Tags Input**: Added `placeholder` prop (shown only when no tags exist)
- **Tooltip**: Added `data-instant` attribute for instant animations

### Fixed

- **Auto Resize**: Fixed issue where change event is not emitted after clearing a controlled textarea programmatically
- **Collection, Tree View**: Fixed initial focus issue when the first node or branch is disabled
- **Color Picker**: Fixed color not updating in controlled mode when selecting black shades
- **Floating Panel**: Fixed double-clicking title bar while minimized would incorrectly maximize instead of restore
- **Image Cropper**:
- Fixed issue where `reset()` destroys the cropper area
- Fixed issue where changing `aspectRatio` or `cropShape` props doesn't update the crop instantly
- Added symmetric resize support when holding `Alt` key during pointer drag
- Fixed panning bounds in fixed crop mode at various zoom levels
- **Number Input**: Fixed cursor positioning when clicking label or after scrubbing
- **Auto Resize**: Fixed change event not emitted after clearing controlled textarea
- **Checkbox**: Fixed individual checkbox props being overridden by `CheckboxGroup`
- **Collection, Tree View**: Fixed initial focus when first node/branch is disabled
- **Color Picker**: Fixed color not updating when selecting black shades in controlled mode
- **Floating Panel**: Fixed double-click on minimized title bar incorrectly maximizing
- **Image Cropper**: Fixed `reset()` destroying cropper, prop changes not updating instantly, and panning bounds
- **Number Input**: Fixed cursor positioning after clicking label or scrubbing
- **Pagination**: Fixed next trigger not disabled when `count` is `0`
- **Slider**: Fixed pointer movement when dragging slider thumb from its edge in `thumbAlignment="contain"` mode
- **Switch**: Fixed issue where `api.toggleChecked()` doesn't work as expected
- **Toast**: Fixed toasts created before the state machine connects not being shown
- **Tour**: Fixed janky scroll behavior when navigating between tour steps
- **Slider**: Fixed thumb drag from edge in `thumbAlignment="contain"` mode
- **Switch**: Fixed `api.toggleChecked()` not working
- **Toast**: Fixed toasts created before state machine connects not showing
- **Tour**: Fixed janky scroll between steps

## [5.30.0] - 2025-12-10

Expand Down
28 changes: 28 additions & 0 deletions packages/react/src/components/checkbox/tests/checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,31 @@ describe('Checkbox / Field', () => {
expect(screen.queryByText('Error Info')).not.toBeInTheDocument()
})
})

const WithGroup = () => (
<Checkbox.Group>
<Checkbox.Root value="one">
<Checkbox.Label>One</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
<Checkbox.Root value="two" disabled>
<Checkbox.Label>Two</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
<Checkbox.Root value="three">
<Checkbox.Label>Three</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
</Checkbox.Group>
)

describe('Checkbox / Group', () => {
it('should allow individual checkbox to be disabled', async () => {
render(<WithGroup />)

const checkboxes = screen.getAllByRole('checkbox')
expect(checkboxes[0]).not.toBeDisabled()
expect(checkboxes[1]).toBeDisabled()
expect(checkboxes[2]).not.toBeDisabled()
})
})
2 changes: 1 addition & 1 deletion packages/react/src/components/checkbox/use-checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useCheckbox = (ownProps: UseCheckboxProps = {}): UseCheckboxReturn
const field = useFieldContext()

const props = useMemo(() => {
return mergeProps(ownProps, checkboxGroup?.getItemProps({ value: ownProps.value }) ?? {})
return mergeProps(checkboxGroup?.getItemProps({ value: ownProps.value }) ?? {}, ownProps)
}, [ownProps, checkboxGroup])

const id = useId()
Expand Down
43 changes: 17 additions & 26 deletions packages/solid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,27 @@

### Added

- **Scroll Area**: Added overflow CSS variables to the viewport element for creating scroll fade effects:
- `--scroll-area-overflow-x-start`: Distance from horizontal start edge in pixels
- `--scroll-area-overflow-x-end`: Distance from horizontal end edge in pixels
- `--scroll-area-overflow-y-start`: Distance from vertical start edge in pixels
- `--scroll-area-overflow-y-end`: Distance from vertical end edge in pixels
- **Slider**: Added `thumbCollisionBehavior` prop to control how thumbs behave when they collide (`none`, `push`, `swap`)
- **Steps**: Added validation and skippable step support:
- `isStepValid(index)`: Block forward navigation when step is invalid (linear mode)
- `isStepSkippable(index)`: Mark steps as optional, bypassing validation
- `onStepInvalid({ step, action, targetStep })`: Callback when navigation is blocked
- **Tags Input**: Added `placeholder` prop that is applied to the input only when there are no tags
- **Tooltip**: Added `data-instant` attribute to content to indicate when animations should be instant
- **Scroll Area**: Added overflow CSS variables (`--scroll-area-overflow-{x,y}-{start,end}`) for scroll fade effects
- **Slider**: Added `thumbCollisionBehavior` prop (`none`, `push`, `swap`)
- **Steps**: Added `isStepValid`, `isStepSkippable`, and `onStepInvalid` for validation support
- **Tags Input**: Added `placeholder` prop (shown only when no tags exist)
- **Tooltip**: Added `data-instant` attribute for instant animations

### Fixed

- **Auto Resize**: Fixed issue where change event is not emitted after clearing a controlled textarea programmatically
- **Collection, Tree View**: Fixed initial focus issue when the first node or branch is disabled
- **Color Picker**: Fixed color not updating in controlled mode when selecting black shades
- **Floating Panel**: Fixed double-clicking title bar while minimized would incorrectly maximize instead of restore
- **Image Cropper**:
- Fixed issue where `reset()` destroys the cropper area
- Fixed issue where changing `aspectRatio` or `cropShape` props doesn't update the crop instantly
- Added symmetric resize support when holding `Alt` key during pointer drag
- Fixed panning bounds in fixed crop mode at various zoom levels
- **Number Input**: Fixed cursor positioning when clicking label or after scrubbing
- **SSR**: Fixed Deno SSR crashes by replacing native HTML elements with `ark` factory components
- **Auto Resize**: Fixed change event not emitted after clearing controlled textarea
- **Checkbox**: Fixed individual checkbox props being overridden by `CheckboxGroup`
- **Collection, Tree View**: Fixed initial focus when first node/branch is disabled
- **Color Picker**: Fixed color not updating when selecting black shades in controlled mode
- **Floating Panel**: Fixed double-click on minimized title bar incorrectly maximizing
- **Image Cropper**: Fixed `reset()` destroying cropper, prop changes not updating instantly, and panning bounds
- **Number Input**: Fixed cursor positioning after clicking label or scrubbing
- **Pagination**: Fixed next trigger not disabled when `count` is `0`
- **Slider**: Fixed pointer movement when dragging slider thumb from its edge in `thumbAlignment="contain"` mode
- **Switch**: Fixed issue where `api.toggleChecked()` doesn't work as expected
- **Toast**: Fixed toasts created before the state machine connects not being shown
- **Tour**: Fixed janky scroll behavior when navigating between tour steps
- **Slider**: Fixed thumb drag from edge in `thumbAlignment="contain"` mode
- **Switch**: Fixed `api.toggleChecked()` not working
- **Toast**: Fixed toasts created before state machine connects not showing
- **Tour**: Fixed janky scroll between steps

## [5.30.0] - 2025-12-10

Expand Down
29 changes: 29 additions & 0 deletions packages/solid/src/components/checkbox/tests/checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'
import user from '@testing-library/user-event'
import { Checkbox } from '../'
import { CheckboxWithField } from './field'
import { ComponentUnderTest } from './basic'
import { ControlledComponentUnderTest } from './controlled'
Expand Down Expand Up @@ -75,3 +76,31 @@ describe('Checkbox / Field', () => {
expect(screen.queryByText('Error Info')).not.toBeInTheDocument()
})
})

const WithGroup = () => (
<Checkbox.Group>
<Checkbox.Root value="one">
<Checkbox.Label>One</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
<Checkbox.Root value="two" disabled>
<Checkbox.Label>Two</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
<Checkbox.Root value="three">
<Checkbox.Label>Three</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
</Checkbox.Group>
)

describe('Checkbox / Group', () => {
it('should allow individual checkbox to be disabled', async () => {
render(() => <WithGroup />)

const checkboxes = screen.getAllByRole('checkbox')
expect(checkboxes[0]).not.toBeDisabled()
expect(checkboxes[1]).toBeDisabled()
expect(checkboxes[2]).not.toBeDisabled()
})
})
2 changes: 1 addition & 1 deletion packages/solid/src/components/checkbox/use-checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const useCheckbox = (ownProps: MaybeAccessor<UseCheckboxProps> = {}): Use

const props = createMemo(() => {
const resolvedProps = runIfFn(ownProps)
return mergeProps(resolvedProps, checkboxGroup?.().getItemProps({ value: resolvedProps.value }) ?? {})
return mergeProps(checkboxGroup?.().getItemProps({ value: resolvedProps.value }) ?? {}, resolvedProps)
}, [ownProps, checkboxGroup])

const id = createUniqueId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const DatePickerMonthSelect = (props: DatePickerMonthSelectProps) => {

return (
<ark.select {...mergedProps}>
<Index each={datePicker().getMonths()}>{(month) => <option value={month().value}>{month().label}</option>}</Index>
<Index each={datePicker().getMonths()}>
{(month) => <ark.option value={month().value}>{month().label}</ark.option>}
</Index>
</ark.select>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const DatePickerYearSelect = (props: DatePickerYearSelectProps) => {

return (
<ark.select {...mergedProps}>
<Index each={datePicker().getYears()}>{(year) => <option value={year().value}>{year().label}</option>}</Index>
<Index each={datePicker().getYears()}>
{(year) => <ark.option value={year().value}>{year().label}</ark.option>}
</Index>
</ark.select>
)
}
5 changes: 3 additions & 2 deletions packages/solid/src/components/frame/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Portal } from 'solid-js/web'
import { EnvironmentProvider } from '../../providers'
import type { Assign } from '../../types'
import { composeRefs } from '../../utils/compose-refs'
import { ark } from '../factory'
import { FrameContent } from './frame-content'

export interface FrameBaseProps {
Expand Down Expand Up @@ -82,7 +83,7 @@ export const Frame = (props: FrameProps) => {

return (
<EnvironmentProvider value={() => frameRef()?.contentDocument ?? document}>
<iframe {...localProps} ref={composeRefs(setFrameRef, localProps.ref)}>
<ark.iframe {...localProps} ref={composeRefs(setFrameRef, localProps.ref)}>
<Show when={mountNode()}>
{(node) => (
<Portal mount={node()}>
Expand All @@ -95,7 +96,7 @@ export const Frame = (props: FrameProps) => {
<Show when={mountNode()}>
<Portal mount={frameRef()!.contentDocument!.head}>{frameProps.head}</Portal>
</Show>
</iframe>
</ark.iframe>
</EnvironmentProvider>
)
}
3 changes: 2 additions & 1 deletion packages/solid/src/components/highlight/highlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ComponentProps } from 'solid-js'
import { For, Show } from 'solid-js'
import type { Assign } from '../../types'
import { createSplitProps } from '../../utils/create-split-props'
import { ark } from '../factory'
import { type UseHighlightProps, useHighlight } from './use-highlight'

export interface HighlightBaseProps extends UseHighlightProps {}
Expand All @@ -27,7 +28,7 @@ export const Highlight = (props: HighlightProps) => {
<For each={chunks()}>
{(chunk) => (
<Show when={chunk.match} fallback={chunk.text}>
<mark {...localProps}>{chunk.text}</mark>
<ark.mark {...localProps}>{chunk.text}</ark.mark>
</Show>
)}
</For>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type JsonNode, keyPathToKey } from '@zag-js/json-tree-utils'
import { type JSX, createMemo } from 'solid-js'
import { ark } from '../factory'

interface JsonTreeViewKeyNodeProps {
/**
Expand All @@ -16,10 +17,10 @@ export const JsonTreeViewKeyNode = (props: JsonTreeViewKeyNodeProps): JSX.Elemen
const key = createMemo(() => keyPathToKey(props.node.keyPath))
return (
<>
<span data-kind="key" data-non-enumerable={props.node.isNonEnumerable ? '' : undefined}>
<ark.span data-kind="key" data-non-enumerable={props.node.isNonEnumerable ? '' : undefined}>
{props.showQuotes ? `"${key()}"` : key()}
</span>
<span data-kind="colon">: </span>
</ark.span>
<ark.span data-kind="colon">: </ark.span>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export const NavigationMenuContent = (props: NavigationMenuContentProps) => {

return (
<Show when={isViewportRendered() && viewportNode()} fallback={content}>
<div {...api().getViewportProxyProps(contentProps)} />
<div {...api().getTriggerProxyProps(contentProps)} />
<ark.div {...api().getViewportProxyProps(contentProps)} />
<ark.div {...api().getTriggerProxyProps(contentProps)} />
<Portal mount={viewportNode()!}>{content}</Portal>
</Show>
)
Expand Down
6 changes: 3 additions & 3 deletions packages/solid/src/components/select/select-hidden-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ export const SelectHiddenSelect = (props: SelectHiddenSelectProps) => {
return (
<ark.select aria-describedby={field?.().ariaDescribedby} {...mergedProps}>
<Show when={isValueEmpty()}>
<option value="" />
<ark.option value="" />
</Show>
<Index each={select().collection.items}>
{(item) => (
<option
<ark.option
value={select().collection.getItemValue(item()) ?? ''}
disabled={select().collection.getItemDisabled(item())}
>
{select().collection.stringifyItem(item())}
</option>
</ark.option>
)}
</Index>
</ark.select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ export const SignaturePadSegment = (props: SignaturePadSegmentProps) => {
const mergedProps = mergeProps(() => signaturePad().getSegmentProps(), props)

return (
// biome-ignore lint/a11y/noSvgWithoutTitle: <ark.title> is used here
<ark.svg {...mergedProps}>
<title>Signature</title>
<For each={signaturePad().paths}>{(path) => <path {...signaturePad().getSegmentPathProps({ path })} />}</For>
<ark.title>Signature</ark.title>
<For each={signaturePad().paths}>{(path) => <ark.path {...signaturePad().getSegmentPathProps({ path })} />}</For>
<Show when={signaturePad().currentPath}>
{/* @ts-expect-error */}
<path {...signaturePad().getSegmentPathProps({ path: signaturePad().currentPath })} />
<ark.path {...signaturePad().getSegmentPathProps({ path: signaturePad().currentPath })} />
</Show>
</ark.svg>
)
Expand Down
Loading