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
2 changes: 1 addition & 1 deletion docs/src/app/(docs)/react/components/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1823,7 +1823,7 @@ A two-state button that can be on or off.
- Exports:
- Toggle
- Props: className, defaultPressed, disabled, nativeButton, onPressedChange, pressed, render, style, value
- Data Attributes: data-pressed
- Data Attributes: data-disabled, data-pressed
- Types: Toggle.ChangeEventDetails, Toggle.ChangeEventReason, Toggle.Props, Toggle.State

</details>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import styles from './index.module.css';

export default function ExampleToggleGroup() {
return (
<ToggleGroup defaultValue={['left']} className={styles.Panel}>
<ToggleGroup aria-label="Text alignment" defaultValue={['left']} className={styles.Panel}>
<Toggle aria-label="Align left" value="left" className={styles.Button}>
<AlignLeftIcon />
</Toggle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ToggleGroup } from '@base-ui/react/toggle-group';
export default function ExampleToggleGroup() {
return (
<ToggleGroup
aria-label="Text alignment"
defaultValue={['left']}
className="flex gap-px p-px border border-neutral-950 dark:border-white"
>
Expand Down
7 changes: 4 additions & 3 deletions docs/src/app/(docs)/react/components/toggle/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ Renders a `<button>` element.

**Toggle Data Attributes:**

| Attribute | Type | Description |
| :----------- | :--- | :----------------------------------------- |
| data-pressed | - | Present when the toggle button is pressed. |
| Attribute | Type | Description |
| :------------ | :--- | :------------------------------------------ |
| data-pressed | - | Present when the toggle button is pressed. |
| data-disabled | - | Present when the toggle button is disabled. |

### Toggle.Props

Expand Down
20 changes: 20 additions & 0 deletions packages/react/src/toggle-group/ToggleGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,26 @@ describe('<ToggleGroup />', () => {
expect(onValueChange.mock.calls[1][0]).toEqual(['two']);
});

it('does not change the value when the event is canceled', async () => {
const onValueChange = vi.fn((_value, eventDetails) => {
eventDetails.cancel();
});

const { user } = await render(
<ToggleGroup onValueChange={onValueChange}>
<Toggle value="one" />
<Toggle value="two" />
</ToggleGroup>,
);

const [button1] = screen.getAllByRole('button');

await user.pointer({ keys: '[MouseLeft]', target: button1 });

expect(onValueChange.mock.calls.length).toBe(1);
expect(button1).toHaveAttribute('aria-pressed', 'false');
});

['Enter', 'Space'].forEach((key) => {
it(`fires when the ${key} is pressed`, async ({ skip }) => {
if (isJSDOM) {
Expand Down
44 changes: 44 additions & 0 deletions packages/react/src/toggle/Toggle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,50 @@ describe('<Toggle />', () => {
expect(handlePressed.mock.calls.length).toBe(1);
expect(handlePressed.mock.calls[0][0]).toBe(true);
});

it('does not change the pressed state when the event is canceled', async () => {
await render(
<Toggle
defaultPressed={false}
onPressedChange={(_pressed, eventDetails) => {
eventDetails.cancel();
}}
/>,
);

const button = screen.getByRole('button');

await act(async () => {
button.click();
});

expect(button).toHaveAttribute('aria-pressed', 'false');
});

it('canceling in a grouped Toggle prevents the group value from changing', async () => {
const onValueChange = vi.fn();

await render(
<ToggleGroup onValueChange={onValueChange}>
<Toggle
value="one"
onPressedChange={(_pressed, eventDetails) => {
eventDetails.cancel();
}}
/>
<Toggle value="two" />
</ToggleGroup>,
);

const [button1] = screen.getAllByRole('button');

await act(async () => {
button1.click();
});

expect(button1).toHaveAttribute('aria-pressed', 'false');
expect(onValueChange.mock.calls.length).toBe(0);
});
});

describe('prop: disabled', () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/react/src/toggle/Toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,17 @@ export const Toggle = React.forwardRef(function Toggle<Value extends string>(
const nextPressed = !pressed;
const details = createChangeEventDetails(REASONS.none, event.nativeEvent);

// `onPressedChange` runs before the group commits so that canceling here
// can also veto the group value change, which shares this `details` object.
onPressedChange?.(nextPressed, details);

if (details.isCanceled) {
return;
}

if (value) {
groupContext?.setGroupValue?.(value, nextPressed, details);
}
onPressedChange?.(nextPressed, details);

if (details.isCanceled) {
return;
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/toggle/ToggleDataAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ export enum ToggleDataAttributes {
* Present when the toggle button is pressed.
*/
pressed = 'data-pressed',
/**
* Present when the toggle button is disabled.
*/
disabled = 'data-disabled',
}
Loading