LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - segmented_control.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 146 146
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './segmented_control.module.css';
       3              : import { classNames } from '../../utils/classnames.js';
       4              : import { Tooltip } from './tooltip.js';
       5              : 
       6              : // ==================== Types ====================
       7              : 
       8            1 : export enum SegmentedControlSize {
       9            1 :     Tiny,
      10            1 :     Small,
      11            1 :     Medium,
      12            1 :     XSmall,
      13            1 :     XXSmall,
      14            1 : }
      15              : 
      16            1 : export enum SegmentedControlVariant {
      17              :     /** The default variant with an outer border and dividers between segments */
      18            1 :     Default,
      19              :     /** A borderless variant (no outer border, no dividers) — like the Button's invisible variant */
      20            1 :     Invisible,
      21            1 : }
      22              : 
      23              : export interface SegmentedControlProps {
      24              :     'aria-label'?: string;
      25              :     'aria-labelledby'?: string;
      26              :     'aria-describedby'?: string;
      27              :     /** A stable hook for tests */
      28              :     'data-testid'?: string;
      29              :     /** Whether the control fills the width of its parent */
      30              :     fullWidth?: boolean;
      31              :     /** The handler that gets called when a segment is selected */
      32              :     onChange?: (selectedIndex: number) => void;
      33              :     /** The size of the buttons */
      34              :     size?: SegmentedControlSize;
      35              :     /** The visual variant of the control */
      36              :     variant?: SegmentedControlVariant;
      37              :     className?: string;
      38              :     children?: React.ReactNode;
      39              : }
      40              : 
      41              : export interface SegmentedControlButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
      42              :     /** The visible label rendered in the button */
      43              :     children: string;
      44              :     /** Whether the segment is selected (for controlled components) */
      45              :     selected?: boolean;
      46              :     /** Whether the segment is selected initially (for uncontrolled components) */
      47              :     defaultSelected?: boolean;
      48              :     /** The leading visual icon */
      49              :     leadingVisual?: React.ElementType;
      50              :     /** Optional counter to display */
      51              :     count?: number | string;
      52              : }
      53              : 
      54              : export interface SegmentedControlIconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> {
      55              :     'aria-label': string;
      56              :     /** The icon element */
      57              :     icon: React.ElementType | React.ReactElement;
      58              :     /** Whether the segment is selected (for controlled components) */
      59              :     selected?: boolean;
      60              :     /** Whether the segment is selected initially (for uncontrolled components) */
      61              :     defaultSelected?: boolean;
      62              :     /** Supplementary description for tooltip */
      63              :     description?: string;
      64              : }
      65              : 
      66              : // ==================== Button Component ====================
      67              : 
      68            1 : const SegmentedControlButton: React.FC<SegmentedControlButtonProps> = ({
      69            1 :     children,
      70            1 :     leadingVisual: LeadingVisual,
      71            1 :     selected,
      72            1 :     defaultSelected: _defaultSelected,
      73            1 :     count,
      74            1 :     className,
      75            1 :     disabled,
      76            1 :     ...props
      77            1 : }) => {
      78            1 :     return (
      79            1 :         <li className={classNames(styles.item, { [styles.selected]: selected })} data-selected={selected ? '' : undefined}>
      80            1 :             <button
      81            1 :                 className={classNames(styles.button, className)}
      82            1 :                 type="button"
      83            1 :                 aria-current={selected}
      84            1 :                 aria-disabled={disabled || undefined}
      85            1 :                 disabled={disabled}
      86            1 :                 {...props}
      87              :             >
      88            1 :                 <span className={styles.content}>
      89            1 :                     {LeadingVisual && (
      90            1 :                         <span className={styles.leading_visual}>
      91            1 :                             <LeadingVisual />
      92            1 :                         </span>
      93              :                     )}
      94            1 :                     <span className={styles.text} data-text={children}>
      95            1 :                         {children}
      96            1 :                     </span>
      97            1 :                     {count !== undefined && (
      98            1 :                         <span className={styles.counter}>{count}</span>
      99              :                     )}
     100            1 :                 </span>
     101            1 :             </button>
     102            1 :         </li>
     103              :     );
     104            1 : };
     105              : 
     106              : // ==================== IconButton Component ====================
     107              : 
     108            1 : const SegmentedControlIconButton: React.FC<SegmentedControlIconButtonProps> = ({
     109            1 :     'aria-label': ariaLabel,
     110            1 :     icon: Icon,
     111            1 :     selected,
     112            1 :     defaultSelected: _defaultSelected,
     113            1 :     description,
     114            1 :     className,
     115            1 :     disabled,
     116            1 :     ...props
     117            1 : }) => {
     118            1 :     return (
     119            1 :         <li className={classNames(styles.item, styles.icon_item, { [styles.selected]: selected })} data-selected={selected ? '' : undefined}>
     120            1 :             <Tooltip
     121            1 :                 text={description ?? ariaLabel}
     122            1 :                 type={description ? undefined : 'label'}
     123              :             >
     124            1 :                 <button
     125            1 :                     className={classNames(styles.button, styles.icon_button, className)}
     126            1 :                     type="button"
     127            1 :                     aria-current={selected}
     128            1 :                     aria-label={description ? ariaLabel : undefined}
     129            1 :                     aria-disabled={disabled || undefined}
     130            1 :                     disabled={disabled}
     131            1 :                     {...props}
     132              :                 >
     133            1 :                     <span className={styles.content}>
     134            1 :                         {React.isValidElement(Icon) ? Icon : <Icon />}
     135            1 :                     </span>
     136            1 :                 </button>
     137            1 :             </Tooltip>
     138            1 :         </li>
     139              :     );
     140            1 : };
     141              : 
     142              : // ==================== Root Component ====================
     143              : 
     144            1 : const Root: React.FC<SegmentedControlProps> = ({
     145            1 :     'aria-label': ariaLabel,
     146            1 :     'aria-labelledby': ariaLabelledby,
     147            1 :     'aria-describedby': ariaDescribedby,
     148            1 :     'data-testid': testId,
     149            1 :     children,
     150            1 :     fullWidth,
     151            1 :     onChange,
     152            1 :     size,
     153            1 :     variant,
     154            1 :     className,
     155            1 : }) => {
     156            1 :     const isUncontrolled = onChange === undefined ||
     157            1 :         React.Children.toArray(children).some(
     158            1 :             child => React.isValidElement<SegmentedControlButtonProps | SegmentedControlIconButtonProps>(child) &&
     159            1 :                      child.props.defaultSelected !== undefined
     160            1 :         );
     161              : 
     162            1 :     const selectedSegments = React.Children.toArray(children).map(
     163            1 :         child =>
     164            1 :             React.isValidElement<SegmentedControlButtonProps | SegmentedControlIconButtonProps>(child) &&
     165            1 :             (child.props.defaultSelected || child.props.selected)
     166            1 :     );
     167              : 
     168            1 :     const hasSelectedButton = selectedSegments.some(isSelected => isSelected);
     169            1 :     const selectedIndexExternal = hasSelectedButton ? selectedSegments.indexOf(true) : 0;
     170            1 :     const [selectedIndexInternalState, setSelectedIndexInternalState] = React.useState<number>(selectedIndexExternal);
     171            1 :     const selectedIndex = isUncontrolled ? selectedIndexInternalState : selectedIndexExternal;
     172              : 
     173            1 :     if (!ariaLabel && !ariaLabelledby) {
     174            1 :         console.warn(
     175            1 :             'Use the `aria-label` or `aria-labelledby` prop to provide an accessible label for assistive technologies'
     176            1 :         );
     177              :     }
     178              : 
     179            1 :     return (
     180            1 :         <ul
     181            1 :             aria-label={ariaLabel}
     182            1 :             aria-labelledby={ariaLabelledby}
     183            1 :             aria-describedby={ariaDescribedby}
     184            1 :             data-testid={testId}
     185            1 :             className={classNames(
     186            1 :                 styles.segmented_control,
     187            1 :                 {
     188            1 :                     [styles.full_width]: fullWidth,
     189            1 :                     [styles.size_tiny]: size === SegmentedControlSize.Tiny,
     190            1 :                     [styles.size_small]: size === SegmentedControlSize.Small,
     191            1 :                     [styles.size_xsmall]: size === SegmentedControlSize.XSmall,
     192            1 :                     [styles.size_xxsmall]: size === SegmentedControlSize.XXSmall,
     193            1 :                     [styles.variant_invisible]: variant === SegmentedControlVariant.Invisible,
     194            1 :                 },
     195            1 :                 className
     196            1 :             )}
     197              :         >
     198            1 :             {React.Children.map(children, (child, index) => {
     199            1 :                 if (!React.isValidElement<SegmentedControlButtonProps | SegmentedControlIconButtonProps>(child)) {
     200            1 :                     return null;
     201              :                 }
     202              : 
     203            1 :                 const sharedChildProps = {
     204            1 :                     onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
     205              :                         const isDisabled =
     206            1 :                             child.props.disabled === true ||
     207            1 :                             child.props['aria-disabled'] === 'true' ||
     208            1 :                             child.props['aria-disabled'] === true;
     209              : 
     210            1 :                         if (!isDisabled) {
     211            1 :                             onChange?.(index);
     212            1 :                             isUncontrolled && setSelectedIndexInternalState(index);
     213            1 :                             child.props.onClick?.(event);
     214            1 :                         }
     215            1 :                     },
     216            1 :                     selected: index === selectedIndex,
     217            1 :                 };
     218              : 
     219            1 :                 return React.cloneElement(child, sharedChildProps);
     220            1 :             })}
     221            1 :         </ul>
     222              :     );
     223            1 : };
     224              : 
     225              : // ==================== Export ====================
     226              : 
     227            1 : export const SegmentedControl = Object.assign(Root, {
     228            1 :     Button: SegmentedControlButton,
     229            1 :     IconButton: SegmentedControlIconButton,
     230            1 : });
        

Generated by: LCOV version 2.0-1