Skip to content
Open
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
2 changes: 2 additions & 0 deletions .agents/skills/apsara/references/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const [open, setOpen] = useState(false);
</Dialog>
```

Most change callbacks (`onValueChange`, `onOpenChange`, `onInputValueChange`, and the like) take the new value first and a details object second. Components that wrap Base UI pass Base UI's `eventDetails`. Other components pass their own `details`, for example `details.mode` on `ColorPicker` and `details.operation` on `FilterChip`. A few callbacks take only the value, for example CodeBlock `onValueChange` and ColorPicker `onModeChange`. Check the props table for the exact signature.

Several overlay roots (`Dialog`, `Menu`, `Popover`, `Tooltip`) also expose `createHandle` for advanced imperative open/close from outside the subtree — use only when a trigger can't live in the same tree.

## Select
Expand Down
45 changes: 45 additions & 0 deletions apps/www/src/content/docs/(overview)/upgrading.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,51 @@ One section per release, newest first, with only the changes that need action
from you. The full record of every release, features and fixes included, is
on [GitHub releases](https://github.com/raystack/apsara/releases).

## Unreleased: change callbacks get a details object

The change callbacks below pass a second argument after the value. A
component that wraps Base UI passes Base UI's `eventDetails`. A component that
keeps its own state passes a `details` object with the same `reason` and
`event` fields, plus any field of its own.

### 1. Update the three callbacks whose second argument changes

| `onValueChange` | Before | After |
|---|---|---|
| `TextArea` | `(value, event)` | `(value, eventDetails)` |
| `ColorPicker` | `(value, mode)` | `(value, details)` |
| `FilterChip` | `(value, operation)` | `(value, details)` |

TextArea's `event` was the React `ChangeEvent`. `eventDetails.event` is the
native event. ColorPicker's mode is `details.mode`, and FilterChip's operation
is `details.operation`.

```tsx
// Before
<ColorPicker onValueChange={(value, mode) => save(value, mode)} />
<FilterChip onValueChange={(value, operation) => apply(value, operation)} />
<TextArea onValueChange={(value, event) => track(event.nativeEvent)} />

// After
<ColorPicker onValueChange={(value, details) => save(value, details.mode)} />
<FilterChip onValueChange={(value, details) => apply(value, details.operation)} />
<TextArea onValueChange={(value, eventDetails) => track(eventDetails.event)} />
```

### 2. Check functions that take a second parameter

These callbacks gain the second argument. Code that reads only the value keeps
working. If you pass a function that takes a second parameter, check that it
expects the details object.

- Base UI `eventDetails`: Accordion `onValueChange`, Select `onValueChange`
and `onOpenChange`, Combobox `onValueChange` and `onInputValueChange`, Menu
and ContextMenu `onInputValueChange`, Sidebar.Group and Reasoning
`onOpenChange`.
- `details`: Sidebar `onOpenChange`, CodeBlock `onCollapseChange`, ChatPanel
`onModeChange`, DataView `onViewChange`, and `onOpenChange` on
Editor.SlashMenu, Editor.Mentions and PromptInput.Mentions.

## Unreleased: date filters compare whole days

`DataView`, `DataTable` and `FilterChip` read and compare dates as calendar
Expand Down
5 changes: 4 additions & 1 deletion apps/www/src/content/docs/ai-elements/chat-panel/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export interface ChatPanelProps {
defaultMode?: 'docked' | 'floating' | 'minimized';

/** Called when the mode changes. */
onModeChange?: (mode: 'docked' | 'floating' | 'minimized') => void;
onModeChange?: (
mode: 'docked' | 'floating' | 'minimized',
details: { reason: 'trigger-press'; event: Event }
) => void;

/**
* Which edge the panel docks to; also picks the corner used by the
Expand Down
5 changes: 4 additions & 1 deletion apps/www/src/content/docs/ai-elements/prompt-input/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ export interface PromptInputMentionsProps {
) => Promise<PromptInputMentionItem[]>;

/** Called when the menu opens or closes. */
onOpenChange?: (open: boolean) => void;
onOpenChange?: (
open: boolean,
details: { reason: 'none'; event: Event }
) => void;

/** Shown when nothing matches. @defaultValue "No results" */
emptyMessage?: React.ReactNode;
Expand Down
5 changes: 4 additions & 1 deletion apps/www/src/content/docs/ai-elements/reasoning/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ export interface ReasoningProps {
defaultOpen?: boolean;

/** Called when the panel is opened or closed. */
onOpenChange?: (open: boolean) => void;
onOpenChange?: (
open: boolean,
eventDetails: { reason: 'trigger-press' | 'none'; event: Event }
) => void;

/** Custom CSS class names. */
className?: string;
Expand Down
7 changes: 5 additions & 2 deletions apps/www/src/content/docs/components/accordion/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export interface AccordionRootProps<Value = string> {
/**
* Event handler called when the value changes. In single mode it receives `''` when every item closes.
*
* @remarks `(value: Value | '') => void` in single mode, `(value: Value[]) => void` with `multiple`
* @remarks `(value: Value | '', eventDetails) => void` in single mode, `(value: Value[], eventDetails) => void` with `multiple`
*/
onValueChange?: (value: Value | '' | Value[]) => void;
onValueChange?: (
value: Value | '' | Value[],
eventDetails: { reason: string; event: Event }
) => void;

/**
* Whether the accordion is disabled
Expand Down
5 changes: 4 additions & 1 deletion apps/www/src/content/docs/components/code-block/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export interface CodeBlockProps {
/**
* Callback when collapse state changes
*/
onCollapseChange?: (collapsed: boolean) => void;
onCollapseChange?: (
collapsed: boolean,
details: { reason: 'trigger-press'; event: Event }
) => void;

/**
* The code content to display
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The picker is composed from an area pad, sliders and an input, so you can drop i

### Basic usage

An uncontrolled picker. Pass `defaultValue` for the starting color and read changes from `onValueChange`.
An uncontrolled picker. Pass `defaultValue` for the starting color and read changes from `onValueChange`. Its second argument is a `details` object: `details.mode` is the mode of the emitted string, and `details.reason` tells which control changed it.

<Demo data={basicDemo} />

Expand Down
11 changes: 9 additions & 2 deletions apps/www/src/content/docs/components/color-picker/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ export interface ColorPickerProps {
*/
defaultValue?: string;
/**
* Callback fired when the color value changes.
* Callback fired when the color value changes. `details.mode` is the active mode.
*/
onValueChange?: (value: string, mode: string) => void;
onValueChange?: (
value: string,
details: {
reason: 'track-press' | 'drag' | 'keyboard' | 'input-change' | 'none';
event: Event;
mode: 'hex' | 'rgb' | 'hsl' | 'oklch';
}
) => void;
/**
* The initial color mode (hex, rgb, hsl, oklch).
* @default 'hex'
Expand Down
14 changes: 10 additions & 4 deletions apps/www/src/content/docs/components/combobox/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ export interface ComboboxRootProps {
defaultValue?: string | string[];

/** Callback fired when the value changes.
* For single selection: (value: string) => void
* For multiple selection: (value: string[]) => void
* For single selection: (value: string, eventDetails) => void
* For multiple selection: (value: string[], eventDetails) => void
*/
onValueChange?: (value: string | string[]) => void;
onValueChange?: (
value: string | string[],
eventDetails: { reason: string; event: Event }
) => void;

/** The controlled input value of the combobox. */
inputValue?: string;
Expand All @@ -30,7 +33,10 @@ export interface ComboboxRootProps {
defaultInputValue?: string;

/** Callback fired when the input value changes. */
onInputValueChange?: (inputValue: string) => void;
onInputValueChange?: (
inputValue: string,
eventDetails: { reason: string; event: Event }
) => void;

/** Whether the combobox is open.
* @default false
Expand Down
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/context-menu/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export interface ContextMenuRootProps {
defaultInputValue?: string;

/** Callback fired when the search input value changes */
onInputValueChange?: (value: string) => void;
onInputValueChange?: (
value: string,
eventDetails: { reason: string; event: Event }
) => void;

/** Control the open state of the menu */
open?: boolean;
Expand Down Expand Up @@ -155,7 +158,10 @@ export interface ContextMenuSubMenuProps {
defaultInputValue?: string;

/** Callback fired when the search input value changes */
onInputValueChange?: (value: string) => void;
onInputValueChange?: (
value: string,
eventDetails: { reason: string; event: Event }
) => void;

/** Control the open state of the submenu */
open?: boolean;
Expand Down
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/editor/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ export interface EditorSlashMenuProps {
emptyMessage?: React.ReactNode;

/** Observes the menu's open state. */
onOpenChange?: (open: boolean) => void;
onOpenChange?: (
open: boolean,
details: { reason: 'none'; event: Event }
) => void;
}

export interface EditorMentionsProps {
Expand Down Expand Up @@ -250,7 +253,10 @@ export interface EditorMentionsProps {
loadingRowCount?: number;

/** Observes the menu's open state. */
onOpenChange?: (open: boolean) => void;
onOpenChange?: (
open: boolean,
details: { reason: 'none'; event: Event }
) => void;
}

export interface EditorMentionItem {
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/filter-chip/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ FilterChip supports custom filter operations through the `operations` prop. When

### Controlled

Pass `value` with `onValueChange` to own the filter. Do this when the chip drives a query and the value has to survive a refetch or a shared URL.
Pass `value` with `onValueChange` to own the filter. Do this when the chip drives a query and the value has to survive a refetch or a shared URL. `onValueChange` receives the value and a `details` object, and `details.operation` is the active operation.

<Demo data={controlledDemo} />

Expand Down
4 changes: 2 additions & 2 deletions apps/www/src/content/docs/components/filter-chip/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ export interface FilterChipProps {
/** Optional array of operations for the type of filter operation */
operations?: { label: string; value: string }[];

/** Callback when the filter value changes; receives the value and the active
/** Callback when the filter value changes. `details.operation` is the active
* operation. For `select` and `multiselect`, the value is the picked
* option's `value`, with its type kept. For `number`, non-numeric input is
* rejected and never reported, and the value arrives as a `number`, except
* for the intermediate states `""`, `"-"`, `"."` and `"-."`, which are
* reported as-is so the field stays editable. */
onValueChange?: (
value: string | number | boolean | Array<string | number | boolean> | Date,
operation: string
details: { reason: string; event: Event; operation: string }
) => void;

/** Callback when the filter operation changes */
Expand Down
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/menu/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export interface MenuRootProps {
defaultInputValue?: string;

/** Callback fired when the search input value changes */
onInputValueChange?: (value: string) => void;
onInputValueChange?: (
value: string,
eventDetails: { reason: string; event: Event }
) => void;

/** Control the open state of the menu */
open?: boolean;
Expand Down Expand Up @@ -157,7 +160,10 @@ export interface MenuSubMenuProps {
defaultInputValue?: string;

/** Callback fired when the search input value changes */
onInputValueChange?: (value: string) => void;
onInputValueChange?: (
value: string,
eventDetails: { reason: string; event: Event }
) => void;

/** Control the open state of the submenu */
open?: boolean;
Expand Down
12 changes: 12 additions & 0 deletions apps/www/src/content/docs/components/select/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export interface SelectRootProps {
*/
autocompleteMode?: 'auto' | 'manual';

/** Callback fired when the value changes. The value is a `string[]` with `multiple`. */
onValueChange?: (
value: string | string[],
eventDetails: { reason: string; event: Event }
) => void;

/** Callback fired when the popup opens or closes. */
onOpenChange?: (
open: boolean,
eventDetails: { reason: string; event: Event }
) => void;

/** Current search value for autocomplete */
searchValue?: string;

Expand Down
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/sidebar/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export interface SidebarRootProps {
open?: boolean;

/** Callback when expanded/collapsed state changes. */
onOpenChange?: (open: boolean) => void;
onOpenChange?: (
open: boolean,
details: { reason: 'trigger-press' | 'none'; event: Event }
) => void;

/** Default expanded/collapsed state.
* @default true
Expand Down Expand Up @@ -80,7 +83,10 @@ export interface SidebarGroupProps {
defaultOpen?: boolean;

/** Callback when the group's expanded/collapsed state changes. */
onOpenChange?: (open: boolean) => void;
onOpenChange?: (
open: boolean,
eventDetails: { reason: string; event: Event }
) => void;

/** Optional ReactNode for group icon. */
leadingIcon?: ReactNode;
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/textarea/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Pass `value` with `onChange` to own the state, the same contract as a native tex

### Using `onValueChange`

`onValueChange` is a convenience callback that fires alongside `onChange` and receives just the new string value. Use it when you don't need the full React change event.
`onValueChange` fires alongside `onChange` with the new string value and an `eventDetails` object, as on `Input`. `eventDetails.event` is the native input event. Use it when you don't need the React change event.

<Demo data={onValueChangeDemo} />

Expand Down
4 changes: 2 additions & 2 deletions apps/www/src/content/docs/components/textarea/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ export interface TextAreaProps {

/**
* Convenience callback fired alongside `onChange` with the new string value.
* Use this when you only need the value rather than the full event.
* `eventDetails.event` is the native input event.
*/
onValueChange?: (
value: string,
event: React.ChangeEvent<HTMLTextAreaElement>
eventDetails: { reason: 'none'; event: Event }
) => void;

/** Additional CSS class names. */
Expand Down
5 changes: 4 additions & 1 deletion apps/www/src/content/docs/dataview/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export interface DataViewProps {
view?: string;

/** Called when the active view changes. */
onViewChange?: (view: string) => void;
onViewChange?: (
view: string,
details: { reason: string; event: Event }
) => void;

/**
* Optional local resolvers for non-accessor `group_by` keys.
Expand Down
11 changes: 11 additions & 0 deletions packages/react/components/accordion/__tests__/accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ describe('Accordion', () => {
expect(trigger).toHaveAttribute('aria-expanded', 'false');
});

it('passes eventDetails to onValueChange', () => {
const onValueChange = vi.fn();
render(<BasicAccordion onValueChange={onValueChange} />);

fireEvent.click(screen.getByRole('button', { name: ITEM_1_TEXT }));
expect(onValueChange).toHaveBeenCalledWith(
'item-1',
expect.objectContaining({ reason: 'trigger-press' })
);
});

it('supports keyboard navigation', async () => {
const user = userEvent.setup();
render(<BasicAccordion />);
Expand Down
Loading
Loading