From e37963f506dd65d6f8c0494e6f09f950b0a3128d Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Mon, 13 Apr 2026 16:58:11 +0100 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20forwardRef=20?= =?UTF-8?q?to=20ButtonGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/feat-buttongroup-forward-ref.md | 7 + .../ButtonGroup/ButtonGroup.test.tsx | 12 ++ src/components/ButtonGroup/ButtonGroup.tsx | 144 +++++++++--------- 3 files changed, 95 insertions(+), 68 deletions(-) create mode 100644 .changeset/feat-buttongroup-forward-ref.md 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..841cd5477 100644 --- a/src/components/ButtonGroup/ButtonGroup.tsx +++ b/src/components/ButtonGroup/ButtonGroup.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useState, forwardRef } from 'react'; import { styled } from 'styled-components'; import { ButtonGroupProps, SelectionValue } from './ButtonGroup.types'; @@ -16,84 +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} - - ); -}; + } +); + +ButtonGroup.displayName = 'ButtonGroup'; import { ButtonGroupType } from './ButtonGroup.types'; From a89583146b6274125f345bec641c2cdda372acb6 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Mon, 13 Apr 2026 17:16:23 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20move=20it=20to=20the?= =?UTF-8?q?=20top=20with=20the=20other=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ButtonGroup/ButtonGroup.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ButtonGroup/ButtonGroup.tsx b/src/components/ButtonGroup/ButtonGroup.tsx index 841cd5477..deb38c11f 100644 --- a/src/components/ButtonGroup/ButtonGroup.tsx +++ b/src/components/ButtonGroup/ButtonGroup.tsx @@ -1,6 +1,6 @@ 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) { @@ -103,8 +103,6 @@ export const ButtonGroup = forwardRef( ButtonGroup.displayName = 'ButtonGroup'; -import { ButtonGroupType } from './ButtonGroup.types'; - const ButtonGroupWrapper = styled.div<{ $fillWidth: boolean; $type: ButtonGroupType }>` display: inline-flex; box-sizing: border-box;