diff --git a/.react-compiler.rec.json b/.react-compiler.rec.json index 6c27c5cf..ed8cd246 100644 --- a/.react-compiler.rec.json +++ b/.react-compiler.rec.json @@ -1,30 +1,30 @@ { - "recordVersion": 1, - "react-compiler-version": "1.0.0", - "files": { - "src/checkbox-field/checkbox-field.tsx": { - "CompileError": 1 - }, - "src/checkbox-field/use-fork-ref.ts": { - "CompileError": 1 - }, - "src/components/keyboard-shortcut/keyboard-shortcut.tsx": { - "CompileError": 1 - }, - "src/hooks/use-previous/use-previous.ts": { - "CompileError": 1 - }, - "src/menu/menu.tsx": { - "CompileError": 2 - }, - "src/tabs/tabs.tsx": { - "CompileError": 4 - }, - "src/tooltip/tooltip.tsx": { - "CompileError": 1 - }, - "src/utils/common-helpers.ts": { - "CompileError": 2 - } + "recordVersion": 1, + "react-compiler-version": "1.0.0", + "files": { + "src/checkbox-field/checkbox-field.tsx": { + "CompileError": 1 + }, + "src/checkbox-field/use-fork-ref.ts": { + "CompileError": 1 + }, + "src/components/keyboard-shortcut/keyboard-shortcut.tsx": { + "CompileError": 1 + }, + "src/hooks/use-previous/use-previous.ts": { + "CompileError": 1 + }, + "src/menu/menu.tsx": { + "CompileError": 2 + }, + "src/tabs/tabs.tsx": { + "CompileError": 4 + }, + "src/tooltip/tooltip.tsx": { + "CompileError": 1 + }, + "src/utils/common-helpers.ts": { + "CompileError": 2 } -} + } +} \ No newline at end of file diff --git a/src/control-presentation/control-action-button.tsx b/src/control-presentation/control-action-button.tsx new file mode 100644 index 00000000..25c6b0b3 --- /dev/null +++ b/src/control-presentation/control-action-button.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' + +import classNames from 'classnames' + +import { Button, IconButton } from '../button' + +import styles from './control-presentation.module.css' + +import type { ComponentProps } from 'react' + +export type ControlActionButtonProps = + | ({ + children: React.ReactElement + } & Omit, 'variant' | 'size'>) + | ({ + icon?: React.ReactElement + } & Omit, 'variant' | 'size'>) + +/** + * A compact action button intended for `ControlPresentation`'s `endSlot`. Wraps + * Reactist's `Button` / `IconButton` with a 24×24, 3px-radius variant sized to fit + * the field chrome alongside a 16px icon glyph. + */ +export const ControlActionButton = React.forwardRef( + function ControlActionButton({ exceptionallySetClassName, ...props }, ref) { + return 'children' in props ? ( + + , + ) + const control = screen.getByTestId('subject') + expect(control.tagName).toBe('BUTTON') + expect(control).toHaveTextContent('Choose') + }) + + it('does not alter attributes on the control', () => { + render( + + + , + ) + const control = screen.getByTestId('subject') + expect(control).toHaveAttribute('type', 'email') + expect(control).toHaveAttribute('placeholder', 'you@example.com') + expect(control).toHaveAttribute('data-custom', 'yes') + expect(control).toHaveAttribute('readonly') + expect(control).toBeDisabled() + }) + + it('focuses the control when a non-interactive startSlot is clicked', () => { + render( + }> + + , + ) + const control = screen.getByTestId('subject') + expect(control).not.toHaveFocus() + + userEvent.click(screen.getByTestId('test-icon')) + expect(control).toHaveFocus() + }) + + it('merges exceptionallySetClassName onto the wrapper', () => { + const { container } = render( + + + , + ) + expect(container.firstElementChild).toHaveClass('custom-class') + }) + + it('endSlot accepts multi-child composition', () => { + render( + + + + + } + > + + , + ) + expect(screen.getByTestId('a')).toBeInTheDocument() + expect(screen.getByTestId('b')).toBeInTheDocument() + }) + + describe('a11y', () => { + it('renders with no a11y violations', async () => { + const { container } = render( + <> + + + + + + } + endSlot={ + + } + > + + + , + ) + expect(await axe(container)).toHaveNoViolations() + }) + }) +}) diff --git a/src/control-presentation/control-presentation.tsx b/src/control-presentation/control-presentation.tsx new file mode 100644 index 00000000..b55ab497 --- /dev/null +++ b/src/control-presentation/control-presentation.tsx @@ -0,0 +1,72 @@ +import * as React from 'react' +import { type ComponentProps, forwardRef } from 'react' + +import classNames from 'classnames' + +import { Box } from '../box' + +import { OutlinedControlContainer } from './outlined-control-container' + +import styles from './control-presentation.module.css' + +type SlotContent = React.ReactElement | string | number + +export type ControlPresentationProps = { + /** + * A leading element rendered before the control — a decorative icon, a row + * of chips/tags, or any other content that should sit on the leading edge. + */ + startSlot?: SlotContent + + /** + * Trailing content rendered immediately after the control (e.g. a unit, + * counter, supplementary text, or an action button such as a clear button + * or dropdown-trigger chevron). + */ + endSlot?: SlotContent +} & Omit, 'borderRadius'> + +/** + * The visual chrome of an inline, single-row, text-field-style input: a + * 32px-tall row with optional start/end slots around a control element. + * + * Slot order (left to right): `startSlot` → control (children) → `endSlot`. + * + * Click handlers belong on the control itself, not on this wrapper. + * Clicking the wrapper focuses the control. + */ +export const ControlPresentation = forwardRef( + function ControlPresentation( + { startSlot, endSlot, exceptionallySetClassName, onClick, children }, + ref, + ) { + return ( + + {startSlot ? ( + {startSlot} + ) : null} +
{children}
+ {endSlot ? {endSlot} : null} +
+ ) + }, +) + +function Slot(props: ComponentProps) { + return ( + + ) +} diff --git a/src/control-presentation/index.ts b/src/control-presentation/index.ts new file mode 100644 index 00000000..5bd30a66 --- /dev/null +++ b/src/control-presentation/index.ts @@ -0,0 +1,2 @@ +export * from './control-action-button' +export * from './control-presentation' diff --git a/src/control-presentation/outlined-control-container.module.css b/src/control-presentation/outlined-control-container.module.css new file mode 100644 index 00000000..725792e2 --- /dev/null +++ b/src/control-presentation/outlined-control-container.module.css @@ -0,0 +1,72 @@ +.container { + display: flex; + align-items: center; + overflow: hidden; + + /* border */ + border: 1px solid var(--reactist-inputs-idle); + + /* color */ + background: var(--reactist-field-background); + color: var(--reactist-field-content); + + /* Read-only: matches native :read-only on form controls (scoped to + * /