// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file ToggleButtonGroup.tsx * @input Uses React Context, ToggleButton children * @output Exports ToggleButtonGroup component and types * @position Groups toggle buttons for single or multi-select behavior * * Uses a discriminated union on `type` to enforce type-safe value/onChange: * - type='single' → value: string | null, onChange: (v: string | null) => void * - type='multiple' → value: string[], onChange: (v: string[]) => void * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/ToggleButton/index.ts * - /apps/storybook/stories/ToggleButton.stories.tsx * - /packages/cli/assets/templates/blocks/components/ToggleButton/ (showcase blocks) */ import { createContext, useCallback, use, useMemo, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { spacingVars } from "../theme/tokens.stylex.js"; import type { ButtonSize } from "../Button/index.js"; import { mergeProps } from "../utils/index.js"; import type { StyleXStyles } from "../style-runtime.js"; import { themeProps } from "../utils/themeProps.js"; // ============================================================================= // Context // ============================================================================= interface ToggleButtonGroupContextValue { /** Currently selected value(s). */ selectedValues: Set; /** Toggle a value on/off. */ toggle: (value: string) => void; /** Group size default — individual buttons can override. */ size?: ButtonSize; /** Group disabled state. */ isDisabled?: boolean; } const ToggleButtonGroupContext = createContext(null); ToggleButtonGroupContext.displayName = 'ToggleButtonGroupContext'; /** * Hook for ToggleButton to read group context. * Returns null when used outside a group. */ export function useToggleButtonGroup(): ToggleButtonGroupContextValue | null { return use(ToggleButtonGroupContext); } // ============================================================================= // Styles // ============================================================================= const styles = { group: { k1xSpc: "x3nfvp2", kGNEyG: "x6s0dn4", kOIVth: "xzye2dw", $$css: true as const }, vertical: { kXwgrk: "xdt5ytf", kGNEyG: "x1qjc9v5", $$css: true as const } }; // ============================================================================= // Props — Discriminated Union // ============================================================================= interface ToggleButtonGroupBaseProps { /** Toggle button children. */ children: ReactNode; /** * Accessible label for the group (used as aria-label). */ label: string; /** * Orientation of the button group. * @default 'horizontal' */ orientation?: 'horizontal' | 'vertical'; /** * Default size for buttons in the group. * Individual buttons can override this with their own `size` prop. * @default 'md' */ size?: ButtonSize; /** * Whether all buttons in the group are disabled. * @default false */ isDisabled?: boolean; /** * StyleX styles for layout customization (margins, positioning, sizing). * Must be a `stylex.create()` value — not an inline style object. * * @example * ``` * const styles = stylex.create({ wrapper: { marginTop: 8 } }); * * ``` */ xstyle?: StyleXStyles; /** Test ID for testing frameworks. */ 'data-testid'?: string; } /** * Single-select group props. * Only one button can be active at a time. Clicking the active * button deselects it (value becomes null). */ export interface ToggleButtonGroupSingleProps extends ToggleButtonGroupBaseProps { /** * Single selection mode (default). * @default 'single' */ type?: 'single'; /** Currently selected value, or null if none selected. */ value: string | null; /** Called when selection changes. */ onChange: (value: string | null) => void; } /** * Multi-select group props. * Multiple buttons can be active simultaneously. */ export interface ToggleButtonGroupMultipleProps extends ToggleButtonGroupBaseProps { /** Multiple selection mode. */ type: 'multiple'; /** Currently selected values. */ value: string[]; /** Called when selection changes. */ onChange: (value: string[]) => void; } /** Discriminated union of single and multiple group props. */ export type ToggleButtonGroupProps = ToggleButtonGroupSingleProps | ToggleButtonGroupMultipleProps; // ============================================================================= // Component // ============================================================================= /** * Groups toggle buttons for exclusive (single) or multi-select behavior. * * Uses a discriminated union on `type` for type-safe value/onChange: * - `'single'` (default): `value: string | null`, click active deselects * - `'multiple'`: `value: string[]`, toggles individual items * * @example * ``` * const [view, setView] = useState('grid'); * * } /> * } /> * * ``` */ export function ToggleButtonGroup(props: ToggleButtonGroupProps): ReactNode { const { children, label, orientation = 'horizontal', size, isDisabled = false, xstyle, 'data-testid': testId } = props; const isMultiple = props.type === 'multiple'; const selectedValues = useMemo(() => { if (isMultiple) { return new Set(props.value); } const singleValue = props.value; return singleValue != null ? new Set([singleValue]) : new Set(); }, [isMultiple, props.value]); const toggle = useCallback((itemValue: string) => { if (isMultiple) { const current = props.value; const onChange = props.onChange; if (current.includes(itemValue)) { onChange(current.filter(v => v !== itemValue)); } else { onChange([...current, itemValue]); } } else { const current = props.value; const onChange = props.onChange; // Allow deselection: clicking active → null onChange(current === itemValue ? null : itemValue); } }, [isMultiple, props.value, props.onChange]); const contextValue = useMemo(() => ({ selectedValues, toggle, size, isDisabled }), [selectedValues, toggle, size, isDisabled]); return
{children}
; } ToggleButtonGroup.displayName = 'ToggleButtonGroup';