From bb6e6d43654cc94faec5f3e7866ced24863e8ce5 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 15 Apr 2026 01:55:36 +0530 Subject: [PATCH 1/2] fix(input-field): fix pointer-events, disable chips when disabled, standardize CSS naming - Remove duplicate pointer-events declarations on leading/trailing icon styles, keeping only pointer-events: auto - Add disabled prop to Chip component with data-disabled attribute and CSS styling - Pass InputField disabled state to chips to prevent dismiss interaction - Rename .inputWrapper to .input-wrapper for consistent kebab-case naming - Add tests for disabled chip behavior in both Chip and InputField Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/chip/__tests__/chip.test.tsx | 29 +++++++++++++ .../raystack/components/chip/chip.module.css | 5 +++ packages/raystack/components/chip/chip.tsx | 5 ++- .../__tests__/input-field.test.tsx | 43 ++++++++++++++++--- .../input-field/input-field.module.css | 20 ++++----- .../components/input-field/input-field.tsx | 7 +-- 6 files changed, 87 insertions(+), 22 deletions(-) diff --git a/packages/raystack/components/chip/__tests__/chip.test.tsx b/packages/raystack/components/chip/__tests__/chip.test.tsx index 0f786a3b4..a15348866 100644 --- a/packages/raystack/components/chip/__tests__/chip.test.tsx +++ b/packages/raystack/components/chip/__tests__/chip.test.tsx @@ -165,6 +165,35 @@ describe('Chip', () => { }); }); + describe('Disabled State', () => { + it('sets data-disabled attribute when disabled', () => { + render(Disabled Chip); + + const chip = screen.getByRole('status'); + expect(chip).toHaveAttribute('data-disabled'); + }); + + it('does not set data-disabled when not disabled', () => { + render(Active Chip); + + const chip = screen.getByRole('status'); + expect(chip).not.toHaveAttribute('data-disabled'); + }); + + it('does not call onClick when disabled', () => { + const onClick = vi.fn(); + render( + + Disabled Chip + + ); + + const chip = screen.getByRole('status'); + fireEvent.click(chip); + expect(onClick).not.toHaveBeenCalled(); + }); + }); + describe('Accessibility', () => { it('uses status role by default', () => { render(Test Chip); diff --git a/packages/raystack/components/chip/chip.module.css b/packages/raystack/components/chip/chip.module.css index ff131ee85..4c01ce966 100644 --- a/packages/raystack/components/chip/chip.module.css +++ b/packages/raystack/components/chip/chip.module.css @@ -115,6 +115,11 @@ height: var(--rs-space-4); } +.chip[data-disabled] { + pointer-events: none; + opacity: 0.5; +} + .dismiss-button { background: none; border: none; diff --git a/packages/raystack/components/chip/chip.tsx b/packages/raystack/components/chip/chip.tsx index dfe459601..424298a4f 100644 --- a/packages/raystack/components/chip/chip.tsx +++ b/packages/raystack/components/chip/chip.tsx @@ -37,6 +37,7 @@ type ChipProps = VariantProps & { onClick?: () => void; role?: string; ariaLabel?: string; + disabled?: boolean; 'data-state'?: string; }; @@ -53,6 +54,7 @@ export const Chip = ({ onClick, role = 'status', ariaLabel, + disabled, 'data-state': dataState }: ChipProps) => { const handleDismiss = (e: React.MouseEvent) => { @@ -67,7 +69,8 @@ export const Chip = ({ aria-label={ ariaLabel || (typeof children === 'string' ? children : undefined) } - onClick={onClick} + onClick={disabled ? undefined : onClick} + data-disabled={disabled || undefined} data-state={dataState} > {leadingIcon && ( diff --git a/packages/raystack/components/input-field/__tests__/input-field.test.tsx b/packages/raystack/components/input-field/__tests__/input-field.test.tsx index 8e869ce9c..393e1137e 100644 --- a/packages/raystack/components/input-field/__tests__/input-field.test.tsx +++ b/packages/raystack/components/input-field/__tests__/input-field.test.tsx @@ -32,19 +32,19 @@ describe('InputField', () => { it('sets custom width', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveStyle({ width: '300px' }); }); it('sets numeric width as pixels', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveStyle({ width: '400px' }); }); it('defaults to 100% width', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveStyle({ width: '100%' }); }); @@ -58,13 +58,13 @@ describe('InputField', () => { describe('Sizes', () => { it('renders large size by default', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveClass(styles['size-large']); }); it('renders small size when specified', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveClass(styles['size-small']); }); }); @@ -72,13 +72,13 @@ describe('InputField', () => { describe('Variants', () => { it('renders default variant by default', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveClass(styles['variant-default']); }); it('renders borderless variant when specified', () => { const { container } = render(); - const wrapper = container.querySelector(`.${styles.inputWrapper}`); + const wrapper = container.querySelector(`.${styles['input-wrapper']}`); expect(wrapper).toHaveClass(styles['variant-borderless']); }); }); @@ -161,6 +161,35 @@ describe('InputField', () => { render(); expect(screen.getByText('+2')).toBeInTheDocument(); }); + + it('does not render dismiss button on chips when disabled', () => { + const handleRemove = vi.fn(); + const chips = [{ label: 'Tag1', onRemove: handleRemove }]; + render(); + const dismissButton = screen.queryByRole('button', { + name: 'Remove Tag1' + }); + expect(dismissButton).not.toBeInTheDocument(); + }); + + it('chips are non-interactive when disabled', () => { + const handleRemove = vi.fn(); + const chips = [{ label: 'Tag1', onRemove: handleRemove }]; + const { container } = render(); + const chip = container.querySelector(`.${styles.chip}`); + expect(chip).toHaveAttribute('data-disabled'); + }); + + it('chips remain interactive when not disabled', () => { + const handleRemove = vi.fn(); + const chips = [{ label: 'Tag1', onRemove: handleRemove }]; + render(); + const dismissButton = screen.getByRole('button', { + name: 'Remove Tag1' + }); + fireEvent.click(dismissButton); + expect(handleRemove).toHaveBeenCalledTimes(1); + }); }); describe('Event Handling', () => { diff --git a/packages/raystack/components/input-field/input-field.module.css b/packages/raystack/components/input-field/input-field.module.css index e4b29f69b..4ed256024 100644 --- a/packages/raystack/components/input-field/input-field.module.css +++ b/packages/raystack/components/input-field/input-field.module.css @@ -1,6 +1,6 @@ /* Note: If making changes here, possibly also need to make changes in text-area.module.css for design consistency. */ -.inputWrapper { +.input-wrapper { display: flex; align-items: center; width: 100%; @@ -13,32 +13,32 @@ overflow: hidden; } -.inputWrapper:hover { +.input-wrapper:hover { border-color: var(--rs-color-border-base-focus); background: var(--rs-color-background-base-primary-hover); } -.inputWrapper:focus-within, -.inputWrapper:has(.input-field[data-active="true"]) { +.input-wrapper:focus-within, +.input-wrapper:has(.input-field[data-active="true"]) { border-color: var(--rs-color-border-accent-emphasis); background-color: var(--rs-color-background-base-primary); outline: none; } -.inputWrapper[data-invalid] { +.input-wrapper[data-invalid] { border-color: var(--rs-color-border-danger-primary); } -.inputWrapper[data-invalid]:hover, -.inputWrapper[data-invalid]:focus-within { +.input-wrapper[data-invalid]:hover, +.input-wrapper[data-invalid]:focus-within { border-color: var(--rs-color-border-danger-emphasis-hover); } -.inputWrapper[data-disabled] { +.input-wrapper[data-disabled] { opacity: 0.5; } -.inputWrapper[data-disabled]:hover { +.input-wrapper[data-disabled]:hover { border-color: var(--rs-color-border-base-tertiary); background: var(--rs-color-background-base-primary); } @@ -50,7 +50,6 @@ width: var(--rs-space-5); height: var(--rs-space-5); color: var(--rs-color-foreground-base-secondary); - pointer-events: none; margin-left: var(--rs-space-3); pointer-events: auto; } @@ -62,7 +61,6 @@ width: var(--rs-space-5); height: var(--rs-space-5); color: var(--rs-color-foreground-base-secondary); - pointer-events: none; margin-right: var(--rs-space-3); pointer-events: auto; } diff --git a/packages/raystack/components/input-field/input-field.tsx b/packages/raystack/components/input-field/input-field.tsx index 9f56860f9..5cfce4aff 100644 --- a/packages/raystack/components/input-field/input-field.tsx +++ b/packages/raystack/components/input-field/input-field.tsx @@ -5,7 +5,7 @@ import { Chip } from '../chip'; import { useFieldContext } from '../field'; import styles from './input-field.module.css'; -const inputWrapper = cva(styles.inputWrapper, { +const inputWrapper = cva(styles['input-wrapper'], { variants: { size: { small: styles['size-small'], @@ -77,9 +77,10 @@ export function InputField({ {chip.label} From 9bbdc63a32794350096356cfabb0277c1f2c5c6c Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 16 Apr 2026 02:40:36 +0530 Subject: [PATCH 2/2] docs(input-field): update docs for disabled chips behavior and add containerRef prop Add demo showing disabled state with chips, update disabled prop description to document chip behavior, and add missing containerRef prop. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/content/docs/components/input-field/demo.ts | 13 +++++++++++++ .../content/docs/components/input-field/index.mdx | 7 +++++++ .../content/docs/components/input-field/props.ts | 5 ++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/www/src/content/docs/components/input-field/demo.ts b/apps/www/src/content/docs/components/input-field/demo.ts index 7e7916b90..65d89a6b7 100644 --- a/apps/www/src/content/docs/components/input-field/demo.ts +++ b/apps/www/src/content/docs/components/input-field/demo.ts @@ -73,6 +73,19 @@ export const disabledDemo = { />` }; +export const disabledChipsDemo = { + type: 'code', + code: ` console.log("Remove Tag1") }, + { label: "Tag2", onRemove: () => console.log("Remove Tag2") } + ]} +/>` +}; + export const widthDemo = { type: 'code', code: ` +### Disabled with Chips + +When disabled, chips become non-interactive and their dismiss buttons are hidden. + + + ### Custom Width Input field with custom width. diff --git a/apps/www/src/content/docs/components/input-field/props.ts b/apps/www/src/content/docs/components/input-field/props.ts index e383ab565..ed5a14eaf 100644 --- a/apps/www/src/content/docs/components/input-field/props.ts +++ b/apps/www/src/content/docs/components/input-field/props.ts @@ -5,7 +5,7 @@ export interface InputFieldProps { */ size?: 'small' | 'large'; - /** Whether the input is disabled. */ + /** Whether the input is disabled. When true, chips are also disabled and their dismiss buttons are hidden. */ disabled?: boolean; /** Icon element to display at the start of input. */ @@ -40,6 +40,9 @@ export interface InputFieldProps { */ variant?: 'default' | 'borderless'; + /** Ref to the outer container div. */ + containerRef?: React.RefObject; + /** Additional CSS class names. */ className?: string; }