Skip to content
Closed
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
19 changes: 11 additions & 8 deletions src/components/ui/Disclosure/stories/Disclosure.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React, { JSX } from 'react';
import type { Meta } from '@storybook/react';
import Disclosure, { DisclosureProps } from '../Disclosure';
import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';

export default {
const meta: Meta<typeof Disclosure> = {
title: 'WIP/Disclosure',
component: Disclosure,
render: (args: JSX.IntrinsicAttributes & DisclosureProps) => <SandboxEditor>

<div>
<Disclosure {...args} />
</div>

</SandboxEditor>
render: (args: JSX.IntrinsicAttributes & DisclosureProps) => (
<SandboxEditor>
<div>
<Disclosure {...args} />
</div>
</SandboxEditor>
)
};

export default meta;

export const All = {
args: {
className: '',
Expand Down
41 changes: 14 additions & 27 deletions src/components/ui/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React from 'react';
import React, {
forwardRef,
ElementRef,
ComponentPropsWithoutRef
} from 'react';
import { clsx } from 'clsx';
import { customClassSwitcher } from '~/core';
import useControllableState from '~/core/hooks/useControllableState';
Expand All @@ -7,30 +11,12 @@ import TogglePrimitive from '~/core/primitives/Toggle';

const COMPONENT_NAME = 'Toggle';

/**
* Props for the Toggle component
* @typedef ToggleProps
*/
export type ToggleProps = {
/** Initial state when in uncontrolled mode */
defaultPressed?: boolean;
/** Current pressed state (for controlled mode) */
pressed?: boolean;
/** Optional custom root class name to override default styling */
customRootClass? : string;
/** Whether the toggle is disabled */
disabled? : boolean;
/** Content to render inside the toggle */
children? : React.ReactNode;
/** Additional class names to apply */
className? : string;
/** Accent color for the toggle */
export type ToggleElement = ElementRef<typeof TogglePrimitive>;
export interface ToggleProps extends ComponentPropsWithoutRef<typeof TogglePrimitive> {
customRootClass?: string;
color?: string;
/** Callback fired when toggle state changes */
onPressedChange : (isPressed:boolean) => void;
/** Whether to render as a child element instead of a button */
asChild?: boolean;
};
onPressedChange: (isPressed: boolean) => void;
}

/**
* Toggle component that can be used in either controlled or uncontrolled mode.
Expand All @@ -47,7 +33,7 @@ export type ToggleProps = {
* @param {ToggleProps} props - The component props
* @returns {JSX.Element} The Toggle component
*/
const Toggle: React.FC<ToggleProps> = ({
const Toggle = forwardRef<ToggleElement, ToggleProps>(({
defaultPressed = false,
customRootClass = '',
children,
Expand All @@ -57,7 +43,7 @@ const Toggle: React.FC<ToggleProps> = ({
onPressedChange,
asChild = false,
...props
}) => {
}, ref) => {
// Use our new hook to handle controlled/uncontrolled state
const [isPressed, setIsPressed] = useControllableState<boolean>(
pressed,
Expand All @@ -78,6 +64,7 @@ const Toggle: React.FC<ToggleProps> = ({

return (
<TogglePrimitive
ref={ref}
className={clsx(rootClass, className)}
pressed={isPressed}
onPressedChange={setIsPressed}
Expand All @@ -90,7 +77,7 @@ const Toggle: React.FC<ToggleProps> = ({
{children}
</TogglePrimitive>
);
};
});

Toggle.displayName = COMPONENT_NAME;

Expand Down
8 changes: 4 additions & 4 deletions src/components/ui/Toggle/stories/Toggle.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const MultipleControlledToggles = () => {
<Toggle
key={index}
pressed={activeToggles.includes(index)}
onPressedChange={(state) => handleToggle(index, state)}
onPressedChange={(state: boolean) => handleToggle(index, state)}
color={index === 0 ? 'red' : index === 1 ? 'blue' : 'green'}
>
{index + 1}
Expand Down Expand Up @@ -183,7 +183,7 @@ export const FormIntegration = () => {
<label className="text-sm">Enable Notifications</label>
<Toggle
pressed={formData.notifications}
onPressedChange={(value) => handleToggle('notifications', value)}
onPressedChange={(value: boolean) => handleToggle('notifications', value)}
color="blue"
/>
</div>
Expand All @@ -192,7 +192,7 @@ export const FormIntegration = () => {
<label className="text-sm">Dark Mode</label>
<Toggle
pressed={formData.darkMode}
onPressedChange={(value) => handleToggle('darkMode', value)}
onPressedChange={(value: boolean) => handleToggle('darkMode', value)}
color="purple"
/>
</div>
Expand All @@ -201,7 +201,7 @@ export const FormIntegration = () => {
<label className="text-sm">Auto-Save</label>
<Toggle
pressed={formData.autoSave}
onPressedChange={(value) => handleToggle('autoSave', value)}
onPressedChange={(value: boolean) => handleToggle('autoSave', value)}
color="green"
/>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/components/ui/Toggle/tests/Toggle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ describe('Toggle component', () => {
expect(getByText('Test Toggle')).toBeInTheDocument();
});

test('forwards ref to the button element', () => {
const ref = React.createRef();
render(<Toggle ref={ref} onPressedChange={() => {}}>Ref Toggle</Toggle>);
expect(ref.current instanceof HTMLButtonElement).toBe(true);
});

test('applies customRootClass correctly', () => {
const { container } = render(<Toggle customRootClass="custom-class" onPressedChange={() => {}}>Test Toggle</Toggle>);
expect(container.firstChild).toHaveClass('custom-class-toggle');
Expand Down
34 changes: 25 additions & 9 deletions src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React, { useState, useEffect } from 'react';
import React, {
forwardRef,
ElementRef,
ComponentPropsWithoutRef
} from 'react';
import { clsx } from 'clsx';
import { customClassSwitcher } from '~/core';
import useControllableState from '~/core/hooks/useControllableState';
Expand All @@ -7,7 +11,8 @@ import RovingFocusGroup from '~/core/utils/RovingFocusGroup';

import { ToggleContext } from '../contexts/toggleContext';

type ToggleGroupRootProps ={
type ToggleGroupRootElement = ElementRef<'div'>;
type ToggleGroupRootProps = {
/** Selection mode - 'single' allows only one item to be selected, 'multiple' allows many */
type?: 'single' | 'multiple';
/** Additional CSS class names to apply */
Expand Down Expand Up @@ -36,11 +41,11 @@ type ToggleGroupRootProps ={
asChild?: boolean;
/** Child elements */
children?: React.ReactNode;
}
} & ComponentPropsWithoutRef<'div'>;

const COMPONENT_NAME = 'ToggleGroup';

const ToggleGroupRoot: React.FC<ToggleGroupRootProps> = ({
const ToggleGroupRoot = forwardRef<ToggleGroupRootElement, ToggleGroupRootProps>(({
type = 'single',
className = '',
loop = true,
Expand All @@ -54,8 +59,9 @@ const ToggleGroupRoot: React.FC<ToggleGroupRootProps> = ({
dir = 'ltr',
rovingFocus = true,
asChild = false,
children
}) => {
children,
...props
}, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);

// Use controllable state for value management
Expand Down Expand Up @@ -91,9 +97,11 @@ const ToggleGroupRoot: React.FC<ToggleGroupRootProps> = ({
return (
<ToggleContext.Provider value={sendValues}>
<div
ref={ref}
className={clsx(rootClass, className)}
{...data_attributes}
dir={dir}
{...props}
>
{children}
</div>
Expand All @@ -109,14 +117,22 @@ const ToggleGroupRoot: React.FC<ToggleGroupRootProps> = ({
dir={dir}
>
<RovingFocusGroup.Group
className={clsx(rootClass, className)}
{...data_attributes}
{...({ asChild: true } as any)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove the as any escape hatch on RovingFocusGroup.Group.

Leaning on as any hides type issues and weakens safety. Prefer proper typing support for asChild on RovingFocusGroup.Group, then pass it directly.

Apply this diff here (and fix Group’s prop types accordingly):

-                    {...({ asChild: true } as any)}
+                    asChild
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{...({ asChild: true } as any)}
asChild

>
{children}
<div
ref={ref}
className={clsx(rootClass, className)}
{...props}
>
{children}
</div>
</RovingFocusGroup.Group>
</RovingFocusGroup.Root>
</ToggleContext.Provider>
);
};
});

ToggleGroupRoot.displayName = COMPONENT_NAME;

export default ToggleGroupRoot;
36 changes: 16 additions & 20 deletions src/components/ui/ToggleGroup/fragments/ToggleItem.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import React, { useContext } from 'react';
import React, {
useContext,
forwardRef,
ElementRef,
ComponentPropsWithoutRef
} from 'react';

import { ToggleContext } from '../contexts/toggleContext';
import TogglePrimitive from '~/core/primitives/Toggle';
import RovingFocusGroup from '~/core/utils/RovingFocusGroup';

/**
* Props for the ToggleItem component
* @typedef ToggleItemProps
*/
export type ToggleItemProps = {
/** Content to render inside the toggle item */
children?: React.ReactNode;
/** Value associated with this toggle item, used for selection state */
export type ToggleItemElement = ElementRef<typeof TogglePrimitive>;
export interface ToggleItemProps
extends ComponentPropsWithoutRef<typeof TogglePrimitive> {
value?: any;
/** Whether the toggle item is disabled */
disabled?: boolean;
/** Whether to render as a child element instead of a button */
asChild?: boolean;
/** Additional props to pass to the underlying TogglePrimitive */
[key: string]: any;
};
}

/**
* Individual toggle item to be used within a ToggleGroup.
Expand All @@ -35,14 +29,14 @@ export type ToggleItemProps = {
* @param {ToggleItemProps} props - Component props
* @returns {JSX.Element} The ToggleItem component
*/
const ToggleItem = ({
const ToggleItem = forwardRef<ToggleItemElement, ToggleItemProps>(({
children,
className = '',
value = null,
disabled = false,
asChild = false,
...props
}: ToggleItemProps) => {
}, ref) => {
const { type, activeToggles, setActiveToggles, rootClass, disabled: groupDisabled } = useContext(ToggleContext);
const isActive = activeToggles?.includes(value);

Expand Down Expand Up @@ -99,7 +93,7 @@ const ToggleItem = ({
dataProps['data-disabled'] = '';
}

return <RovingFocusGroup.Item>
return <RovingFocusGroup.Item ref={ref as React.Ref<HTMLButtonElement>}>
<TogglePrimitive
onClick={handleToggleSelect}
className={`${rootClass}-item ${className}`}
Expand All @@ -110,6 +104,8 @@ const ToggleItem = ({
{...props}
>{children}</TogglePrimitive>
</RovingFocusGroup.Item>;
};
});

ToggleItem.displayName = 'ToggleItem';

export default ToggleItem;
20 changes: 20 additions & 0 deletions src/components/ui/ToggleGroup/tests/ToggleGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ describe('ToggleGroup component', () => {
expect(buttons.length).toBe(3);
});

test('forwards ref to ToggleGroup.Root', () => {
const ref = React.createRef();
render(
<ToggleGroup.Root ref={ref}>
<ToggleGroup.Item value="item1">Item 1</ToggleGroup.Item>
</ToggleGroup.Root>
);
expect(ref.current instanceof HTMLDivElement).toBe(true);
});

test('forwards ref to ToggleGroup.Item', () => {
const ref = React.createRef();
render(
<ToggleGroup.Root>
<ToggleGroup.Item ref={ref} value="item1">Item 1</ToggleGroup.Item>
</ToggleGroup.Root>
);
expect(ref.current instanceof HTMLButtonElement).toBe(true);
});

test('handles multiple selection', () => {
const { getByText } = render(
<ToggleGroup.Root type="multiple">
Expand Down
Loading
Loading