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
18 changes: 18 additions & 0 deletions apps/www/src/content/docs/components/input/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ export const sizeChipDemo = {
</Flex>`
};

export const onValueChangeDemo = {
type: 'code',
code: `function ValueChangeExample() {
const [value, setValue] = React.useState("");

return (
<Flex direction="column" gap="medium" style={{ width: 400 }}>
<Input
placeholder="Type something..."
value={value}
onValueChange={setValue}
/>
<Text size="small">Current value: {value || "(empty)"}</Text>
</Flex>
);
}`
};

export const interactiveChipDemo = {
type: 'code',
style: {
Expand Down
7 changes: 7 additions & 0 deletions apps/www/src/content/docs/components/input/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
sizeChipDemo,
interactiveChipDemo,
withFieldDemo,
onValueChangeDemo,
} from "./demo.ts";

<Demo data={playground} />
Expand Down Expand Up @@ -60,6 +61,12 @@ Use Field to add label, description, and error handling.

<Demo data={withFieldDemo} />

### Controlled Value

Use `onValueChange` for a callback that receives only the new string value, or `onChange` for the full React change event. Both fire on every keystroke — pick whichever fits your needs.

<Demo data={onValueChangeDemo} />

### With Prefix/Suffix

Input with prefix and suffix text.
Expand Down
12 changes: 12 additions & 0 deletions apps/www/src/content/docs/components/input/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ export interface InputProps {
/** Ref to the outer container div. */
containerRef?: React.RefObject<HTMLDivElement | null>;

/** The controlled value of the input. */
value?: string;

/** Native change handler. Receives the React change event. */
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;

/**
* Convenience callback fired with the new string value.
* Provided by Base UI's Input primitive — use this when you only need the value.
*/
onValueChange?: (value: string, eventDetails: unknown) => void;

/** Additional CSS class names. */
className?: string;
}
20 changes: 20 additions & 0 deletions apps/www/src/content/docs/components/search/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,23 @@ export const clearDemo = {
<Search placeholder="Basic search..." />
</Flex>`
};

export const onValueChangeDemo = {
type: 'code',
code: `function SearchValueChangeExample() {
const [query, setQuery] = React.useState("");

return (
<Flex direction="column" gap="medium" style={{ width: 400 }}>
<Search
placeholder="Search items..."
value={query}
onValueChange={setQuery}
showClearButton
onClear={() => setQuery("")}
/>
<Text size="small">Query: {query || "(empty)"}</Text>
</Flex>
);
}`
};
8 changes: 7 additions & 1 deletion apps/www/src/content/docs/components/search/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A search input component with built-in search icon and optional cle
source: packages/raystack/components/search
---

import { playground, sizeDemo, clearDemo } from "./demo.ts";
import { playground, sizeDemo, clearDemo, onValueChangeDemo } from "./demo.ts";

<Demo data={playground} />

Expand Down Expand Up @@ -38,6 +38,12 @@ The Search component can include a clear button that appears when there is input

<Demo data={clearDemo} />

### Controlled Value

Use `onValueChange` to receive only the new query string, or `onChange` for the full React change event. The Search component forwards both to the underlying [Input](/docs/components/input).

<Demo data={onValueChangeDemo} />

## Accessibility

The Search component is built with accessibility in mind, following ARIA best practices:
Expand Down
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/search/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ export interface SearchProps {
/** The controlled value of the input. */
value?: string;

/** Callback when input value changes. */
onChange?: (value: string) => void;
/** Native change handler. Receives the React change event. */
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;

/**
* Convenience callback fired with the new string value.
* Forwarded to the underlying Input — use this when you only need the value.
*/
onValueChange?: (value: string, eventDetails: unknown) => void;

/** Callback when clear button is clicked. */
onClear?: () => void;
Expand Down
17 changes: 17 additions & 0 deletions apps/www/src/content/docs/components/textarea/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ export const controlledDemo = {
}`
};

export const onValueChangeDemo = {
type: 'code',
code: `function TextAreaValueChangeExample() {
const [value, setValue] = React.useState('');

return (
<Field label="Bio" description={\`\${value.length} characters\`}>
<TextArea
value={value}
onValueChange={setValue}
placeholder="Tell us about yourself..."
/>
</Field>
);
}`
};

export const sizeDemo = {
type: 'code',
code: `<Flex direction="column" gap="medium" style={{ width: 400 }}>
Expand Down
7 changes: 7 additions & 0 deletions apps/www/src/content/docs/components/textarea/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
playground,
basicDemo,
controlledDemo,
onValueChangeDemo,
sizeDemo,
variantDemo,
rowsDemo,
Expand Down Expand Up @@ -63,6 +64,12 @@ Example of TextArea in controlled mode.

<Demo data={controlledDemo} />

### 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.

<Demo data={onValueChangeDemo} />

### Size Variants

TextArea comes in two sizes: `large` (default) and `small`.
Expand Down
11 changes: 10 additions & 1 deletion apps/www/src/content/docs/components/textarea/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,18 @@ export interface TextAreaProps {
/** Controlled value for the textarea. */
value?: string;

/** Change handler for controlled usage. */
/** Change handler for controlled usage. Receives the React change event. */
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;

/**
* Convenience callback fired alongside `onChange` with the new string value.
* Use this when you only need the value rather than the full event.
*/
onValueChange?: (
value: string,
event: React.ChangeEvent<HTMLTextAreaElement>
) => void;

/** Additional CSS class names. */
className?: string;
}
16 changes: 16 additions & 0 deletions docs/V1-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,16 @@ Unchanged props: `size`, `variant`, `disabled`, `leadingIcon`, `trailingIcon`, `
- `DatePicker.inputFieldProps` → `DatePicker.inputProps`
- `RangePicker.inputFieldsProps` → `RangePicker.inputsProps`

#### New Features

- `onValueChange` callback — provided by Base UI's Input primitive. Fires with the new string value (and an `eventDetails` second arg) alongside the standard `onChange`. Use it when you only need the value:

```tsx
<Input value={value} onValueChange={setValue} placeholder="Enter text" />
```

The `Search` component forwards `onValueChange` to the underlying Input, so it works there as well.

---

### Label
Expand Down Expand Up @@ -1827,6 +1837,12 @@ Unchanged props: `disabled`, `placeholder`, `width`, `value`, `onChange`, `rows`
<TextArea rows={6} placeholder="Taller textarea" />
```

- `onValueChange` callback — fires alongside `onChange` with the new string value (and the React change event as the second arg). Use it when you only need the value:

```tsx
<TextArea value={value} onValueChange={setValue} placeholder="Write something..." />
```

---

### Toast
Expand Down
4 changes: 0 additions & 4 deletions packages/raystack/components/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ export interface SearchProps extends Omit<InputProps, 'leadingIcon'> {
}

export function Search({
className,
disabled,
placeholder = 'Search',
size,
showClearButton,
onClear,
value,
onChange,
width = '100%',
variant = 'default',
...props
Expand Down Expand Up @@ -54,9 +52,7 @@ export function Search({
placeholder={placeholder}
disabled={disabled}
value={value}
onChange={onChange}
size={size}
className={className}
aria-label={placeholder}
variant={variant}
{...props}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ describe('TextArea', () => {
});

describe('Event Handling', () => {
it('calls onValueChange with the new string value', () => {
const handleValueChange = vi.fn();
render(<TextArea onValueChange={handleValueChange} />);
const textarea = screen.getByRole('textbox');

fireEvent.change(textarea, { target: { value: 'hello' } });
expect(handleValueChange).toHaveBeenCalledTimes(1);
expect(handleValueChange.mock.calls[0][0]).toBe('hello');
expect(handleValueChange.mock.calls[0][1]).toBeDefined();
});

it('calls both onChange and onValueChange', () => {
const handleChange = vi.fn();
const handleValueChange = vi.fn();
render(
<TextArea onChange={handleChange} onValueChange={handleValueChange} />
);
const textarea = screen.getByRole('textbox');

fireEvent.change(textarea, { target: { value: 'hi' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleValueChange).toHaveBeenCalledTimes(1);
});

it('handles onFocus event', () => {
const handleFocus = vi.fn();
render(<TextArea onFocus={handleFocus} />);
Expand Down
12 changes: 11 additions & 1 deletion packages/raystack/components/text-area/text-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export interface TextAreaProps
width?: string | number;
value?: string;
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void;
onValueChange?: (
value: string,
event: ChangeEvent<HTMLTextAreaElement>
) => void;
}

export function TextArea({
Expand All @@ -39,6 +43,7 @@ export function TextArea({
width = '100%',
value,
onChange,
onValueChange,
placeholder,
required,
size,
Expand All @@ -48,6 +53,11 @@ export function TextArea({
const fieldContext = useFieldContext();
const resolvedRequired = required ?? fieldContext?.required;

const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
onChange?.(event);
onValueChange?.(event.target.value, event);
};

const textarea = (
<textarea
rows={3}
Expand All @@ -57,7 +67,7 @@ export function TextArea({
className
)}
value={value}
onChange={onChange}
onChange={handleChange}
disabled={disabled}
placeholder={placeholder}
required={resolvedRequired}
Expand Down
Loading