diff --git a/.changeset/feat-buttongroup-forward-ref.md b/.changeset/feat-buttongroup-forward-ref.md new file mode 100644 index 000000000..c6cf56f79 --- /dev/null +++ b/.changeset/feat-buttongroup-forward-ref.md @@ -0,0 +1,7 @@ +--- +'@clickhouse/click-ui': patch +--- + +Add forwardRef support to ButtonGroup component + +Exposes the wrapper div ref via React.forwardRef, following component library conventions. diff --git a/src/components/ButtonGroup/ButtonGroup.test.tsx b/src/components/ButtonGroup/ButtonGroup.test.tsx index 14b8206ac..b77384667 100644 --- a/src/components/ButtonGroup/ButtonGroup.test.tsx +++ b/src/components/ButtonGroup/ButtonGroup.test.tsx @@ -1,3 +1,4 @@ +import { createRef } from 'react'; import { fireEvent } from '@testing-library/react'; import { ButtonGroup } from '@/components/ButtonGroup'; import type { ButtonGroupProps, SelectionValue } from '@/components/ButtonGroup'; @@ -20,6 +21,17 @@ describe('ButtonGroup', () => { }); }); + it('forwards ref to the wrapper div', () => { + const ref = createRef(); + renderCUI( + + ); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + it('calls onClick handler when a button is clicked', () => { let counter = 0; const handleClick = () => (counter = 1); diff --git a/src/components/ButtonGroup/ButtonGroup.tsx b/src/components/ButtonGroup/ButtonGroup.tsx index f419b441c..deb38c11f 100644 --- a/src/components/ButtonGroup/ButtonGroup.tsx +++ b/src/components/ButtonGroup/ButtonGroup.tsx @@ -1,6 +1,6 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useState, forwardRef } from 'react'; import { styled } from 'styled-components'; -import { ButtonGroupProps, SelectionValue } from './ButtonGroup.types'; +import { ButtonGroupProps, ButtonGroupType, SelectionValue } from './ButtonGroup.types'; const normalizeToSet = (value: SelectionValue | undefined): Set => { if (value === undefined) { @@ -16,86 +16,92 @@ const isValueSelected = (value: string, selection: Set): boolean => { return selection.has(value); }; -export const ButtonGroup = ({ - options, - selected, - defaultSelected, - fillWidth = false, - onClick, - type = 'default', - multiple = false, - ...props -}: ButtonGroupProps) => { - const [internalSelection, setInternalSelection] = useState>(() => - normalizeToSet(defaultSelected) - ); - - // Use `selected` if the parent needs to own - // or sync the selection state management (controlled - // by consumer app) - // Use `defaultSelected` if the component can manage - // its own state independently (uncontrolled) - const isControlled = selected !== undefined; - const currentSelection = isControlled ? normalizeToSet(selected) : internalSelection; - - const onButtonGroupClickCommonHandler = useCallback( - (value: string) => { - let newSelection: Set; - - if (multiple) { - newSelection = new Set(currentSelection); - if (newSelection.has(value)) { - newSelection.delete(value); +export const ButtonGroup = forwardRef( + ( + { + options, + selected, + defaultSelected, + fillWidth = false, + onClick, + type = 'default', + multiple = false, + ...props + }, + ref + ) => { + const [internalSelection, setInternalSelection] = useState>(() => + normalizeToSet(defaultSelected) + ); + + // Use `selected` if the parent needs to own + // or sync the selection state management (controlled + // by consumer app) + // Use `defaultSelected` if the component can manage + // its own state independently (uncontrolled) + const isControlled = selected !== undefined; + const currentSelection = isControlled ? normalizeToSet(selected) : internalSelection; + + const onButtonGroupClickCommonHandler = useCallback( + (value: string) => { + let newSelection: Set; + + if (multiple) { + newSelection = new Set(currentSelection); + if (newSelection.has(value)) { + newSelection.delete(value); + } else { + newSelection.add(value); + } } else { - newSelection.add(value); + newSelection = new Set([value]); } - } else { - newSelection = new Set([value]); - } - if (!isControlled) { - setInternalSelection(newSelection); - } + if (!isControlled) { + setInternalSelection(newSelection); + } - // WARN: Single mode returns string - // while multiple mode returns Set (DS) - onClick?.(value, multiple ? newSelection : value); - }, - [currentSelection, multiple, isControlled, onClick] - ); + // WARN: Single mode returns string + // while multiple mode returns Set (DS) + onClick?.(value, multiple ? newSelection : value); + }, + [currentSelection, multiple, isControlled, onClick] + ); - const buttons = options.map(({ value, label, ...buttonProps }) => { - const isActive = isValueSelected(value, currentSelection); + const buttons = options.map(({ value, label, ...buttonProps }) => { + const isActive = isValueSelected(value, currentSelection); + + return ( + + ); + }); return ( - + {buttons} + ); - }); - - return ( - - {buttons} - - ); -}; + } +); -import { ButtonGroupType } from './ButtonGroup.types'; +ButtonGroup.displayName = 'ButtonGroup'; const ButtonGroupWrapper = styled.div<{ $fillWidth: boolean; $type: ButtonGroupType }>` display: inline-flex;