Skip to content

[Toggle][ToggleGroup] Use useRenderElement #1707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 22, 2025
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
10 changes: 5 additions & 5 deletions docs/reference/generated/toggle-group.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"props": {
"defaultValue": {
"type": "any[]",
"description": "The open state of the ToggleGroup represented by an array of\nthe values of all pressed `<ToggleGroup.Item/>`s.\nThis is the uncontrolled counterpart of `value`."
"description": "The open state of the toggle group represented by an array of\nthe values of all pressed toggle buttons.\nThis is the uncontrolled counterpart of `value`."
},
"value": {
"type": "any[]",
"description": "The open state of the ToggleGroup represented by an array of\nthe values of all pressed `<ToggleGroup.Item/>`s\nThis is the controlled counterpart of `defaultValue`."
"description": "The open state of the toggle group represented by an array of\nthe values of all pressed toggle buttons.\nThis is the controlled counterpart of `defaultValue`."
},
"onValueChange": {
"type": "((groupValue: any[], event: Event) => void)",
"description": "Callback fired when the pressed states of the ToggleGroup changes."
"description": "Callback fired when the pressed states of the toggle group changes."
},
"toggleMultiple": {
"type": "boolean",
Expand All @@ -22,15 +22,15 @@
"disabled": {
"type": "boolean",
"default": "false",
"description": "Whether the component should ignore user interaction."
"description": "Whether the toggle group should ignore user interaction."
},
"loop": {
"type": "boolean",
"default": "true",
"description": "Whether to loop keyboard focus back to the first item\nwhen the end of the list is reached while using the arrow keys."
},
"orientation": {
"type": "ToggleGroupOrientation",
"type": "Orientation",
"default": "'horizontal'"
},
"className": {
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/generated/toggle.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
"props": {
"value": {
"type": "string",
"description": "A unique string that identifies the component when used\ninside a ToggleGroup."
"description": "A unique string that identifies the toggle when used\ninside a toggle group."
},
"defaultPressed": {
"type": "boolean",
"default": "false",
"description": "The default pressed state. Use when the component is not controlled."
"description": "Whether the toggle button is currently pressed.\nThis is the uncontrolled counterpart of `pressed`."
},
"pressed": {
"type": "boolean",
"description": "Whether the toggle button is currently active."
"description": "Whether the toggle button is currently pressed.\nThis is the controlled counterpart of `defaultPressed`."
},
"onPressedChange": {
"type": "((pressed: boolean, event: Event) => void)",
Expand Down
113 changes: 78 additions & 35 deletions packages/react/src/toggle-group/ToggleGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { NOOP } from '../utils/noop';
import { useComponentRenderer } from '../utils/useComponentRenderer';
import type { BaseUIComponentProps } from '../utils/types';
import { useRenderElement } from '../utils/useRenderElement';
import type { BaseUIComponentProps, Orientation } from '../utils/types';
import { CompositeRoot } from '../composite/root/CompositeRoot';
import { useControlled } from '../utils/useControlled';
import { useDirection } from '../direction-provider/DirectionContext';
import { useEventCallback } from '../utils/useEventCallback';
import { useToolbarRootContext } from '../toolbar/root/ToolbarRootContext';
import { useToggleGroup } from './useToggleGroup';
import { ToggleGroupContext } from './ToggleGroupContext';
import { ToggleGroupDataAttributes } from './ToggleGroupDataAttributes';

Expand All @@ -26,21 +26,21 @@ const customStyleHookMapping = {
* Documentation: [Base UI Toggle Group](https://base-ui.com/react/components/toggle-group)
*/
const ToggleGroup = React.forwardRef(function ToggleGroup(
props: ToggleGroup.Props,
componentProps: ToggleGroup.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const {
defaultValue: defaultValueProp,
disabled: disabledProp = false,
loop = true,
onValueChange: onValueChangeProp,
onValueChange,
orientation = 'horizontal',
toggleMultiple = false,
value: valueProp,
className,
render,
...otherProps
} = props;
...elementProps
} = componentProps;

const direction = useDirection();

Expand All @@ -54,12 +54,31 @@ const ToggleGroup = React.forwardRef(function ToggleGroup(
return undefined;
}, [valueProp, defaultValueProp]);

const { getRootProps, disabled, setGroupValue, value } = useToggleGroup({
value: valueProp,
defaultValue,
disabled: (toolbarContext?.disabled ?? false) || disabledProp,
toggleMultiple,
onValueChange: onValueChangeProp ?? NOOP,
const disabled = (toolbarContext?.disabled ?? false) || disabledProp;

const [groupValue, setValueState] = useControlled({
controlled: valueProp,
default: defaultValue,
name: 'ToggleGroup',
state: 'value',
});

const setGroupValue = useEventCallback((newValue: string, nextPressed: boolean, event: Event) => {
let newGroupValue: any[] | undefined;
if (toggleMultiple) {
newGroupValue = groupValue.slice();
if (nextPressed) {
newGroupValue.push(newValue);
} else {
newGroupValue.splice(groupValue.indexOf(newValue), 1);
}
} else {
newGroupValue = nextPressed ? [newValue] : [];
}
if (Array.isArray(newGroupValue)) {
setValueState(newGroupValue);
onValueChange?.(newGroupValue, event);
}
});

const state: ToggleGroup.State = React.useMemo(
Expand All @@ -72,19 +91,21 @@ const ToggleGroup = React.forwardRef(function ToggleGroup(
disabled,
orientation,
setGroupValue,
value,
value: groupValue,
}),
[disabled, orientation, setGroupValue, value],
[disabled, orientation, setGroupValue, groupValue],
);

const { renderElement } = useComponentRenderer({
propGetter: getRootProps,
render: render ?? 'div',
ref: forwardedRef,
const renderElement = useRenderElement('div', componentProps, {
state,
className,
ref: forwardedRef,
props: [
{
role: 'group',
},
elementProps,
],
customStyleHookMapping,
extraProps: otherProps,
});

return (
Expand All @@ -100,8 +121,6 @@ const ToggleGroup = React.forwardRef(function ToggleGroup(

export { ToggleGroup };

export type ToggleGroupOrientation = 'horizontal' | 'vertical';

namespace ToggleGroup {
export interface State {
/**
Expand All @@ -111,24 +130,48 @@ namespace ToggleGroup {
multiple: boolean;
}

export interface Props
extends Partial<useToggleGroup.Parameters>,
Omit<BaseUIComponentProps<'div', State>, 'defaultValue'> {
export interface Props extends BaseUIComponentProps<'div', State> {
/**
* Whether the component should ignore user interaction.
* The open state of the toggle group represented by an array of
* the values of all pressed toggle buttons.
* This is the controlled counterpart of `defaultValue`.
*/
value?: readonly any[];
/**
* The open state of the toggle group represented by an array of
* the values of all pressed toggle buttons.
* This is the uncontrolled counterpart of `value`.
*/
defaultValue?: readonly any[];
/**
* Callback fired when the pressed states of the toggle group changes.
*
* @param {any[]} groupValue An array of the `value`s of all the pressed items.
* @param {Event} event The corresponding event that initiated the change.
*/
onValueChange?: (groupValue: any[], event: Event) => void;
/**
* Whether the toggle group should ignore user interaction.
* @default false
*/
disabled?: boolean;
/**
* @default 'horizontal'
*/
orientation?: ToggleGroupOrientation;
orientation?: Orientation;
/**
* Whether to loop keyboard focus back to the first item
* when the end of the list is reached while using the arrow keys.
* @default true
*/
loop?: boolean;
/**
* When `false` only one item in the group can be pressed. If any item in
* the group becomes pressed, the others will become unpressed.
* When `true` multiple items can be pressed.
* @default false
*/
toggleMultiple?: boolean;
}
}

Expand All @@ -147,13 +190,13 @@ ToggleGroup.propTypes /* remove-proptypes */ = {
*/
className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/**
* The open state of the ToggleGroup represented by an array of
* the values of all pressed `<ToggleGroup.Item/>`s.
* The open state of the toggle group represented by an array of
* the values of all pressed toggle buttons.
* This is the uncontrolled counterpart of `value`.
*/
defaultValue: PropTypes.array,
/**
* Whether the component should ignore user interaction.
* Whether the toggle group should ignore user interaction.
* @default false
*/
disabled: PropTypes.bool,
Expand All @@ -164,7 +207,7 @@ ToggleGroup.propTypes /* remove-proptypes */ = {
*/
loop: PropTypes.bool,
/**
* Callback fired when the pressed states of the ToggleGroup changes.
* Callback fired when the pressed states of the toggle group changes.
*
* @param {any[]} groupValue An array of the `value`s of all the pressed items.
* @param {Event} event The corresponding event that initiated the change.
Expand All @@ -189,8 +232,8 @@ ToggleGroup.propTypes /* remove-proptypes */ = {
*/
toggleMultiple: PropTypes.bool,
/**
* The open state of the ToggleGroup represented by an array of
* the values of all pressed `<ToggleGroup.Item/>`s
* The open state of the toggle group represented by an array of
* the values of all pressed toggle buttons.
* This is the controlled counterpart of `defaultValue`.
*/
value: PropTypes.array,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/toggle-group/ToggleGroupContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';
import * as React from 'react';
import type { ToggleGroupOrientation } from './ToggleGroup';
import type { Orientation } from '../utils/types';

export interface ToggleGroupContext {
value: readonly any[];
setGroupValue: (newValue: string, nextPressed: boolean, event: Event) => void;
disabled: boolean;
orientation: ToggleGroupOrientation;
orientation: Orientation;
}

export const ToggleGroupContext = React.createContext<ToggleGroupContext | undefined>(undefined);
Expand Down
112 changes: 0 additions & 112 deletions packages/react/src/toggle-group/useToggleGroup.ts

This file was deleted.

Loading