Skip to content
Draft
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
56 changes: 28 additions & 28 deletions .react-compiler.rec.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
48 changes: 48 additions & 0 deletions src/control-presentation/control-action-button.tsx
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] This wrapper narrows the underlying button APIs more than intended: Button accepts any ReactNode label and IconButton accepts its own icon type, but this type only allows ReactElement for both. That means supported cases like <ControlActionButton>Clear</ControlActionButton> stop type-checking even though the inner component can render them. Reuse the underlying prop types (or widen children/icon) so the wrapper doesn't drop valid Button/IconButton inputs.

} & Omit<ComponentProps<typeof Button>, 'variant' | 'size'>)
| ({
icon?: React.ReactElement
} & Omit<ComponentProps<typeof IconButton>, '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<HTMLButtonElement, ControlActionButtonProps>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] ControlActionButton creates a second compact field-action implementation alongside the existing TextField/PasswordField path: PasswordField still renders a raw IconButton, and text-field.module.css already shrinks slotted buttons to 24px. That leaves two parallel ways to express the same field affordance, with styling drift already possible. Please migrate the existing field action usage to this wrapper here, or move the 24px field-action treatment into a shared button size/token that both paths reuse.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] This new exported primitive ships without any dedicated test coverage. Please add tests for both branches of the union (children -> Button, icon -> IconButton) so regressions in the prop discrimination and the injected compact styling don't slip through unnoticed.

function ControlActionButton({ exceptionallySetClassName, ...props }, ref) {
return 'children' in props ? (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P3] The shared props between Button and IconButton can be extracted to avoid duplicating the variant and exceptionallySetClassName setup.

const sharedProps = {
    ref,
    variant: 'quaternary' as const,
    exceptionallySetClassName: classNames([
        styles.controlActionButton,
        exceptionallySetClassName,
    ]),
}

return 'children' in props ? (
    <Button {...props} {...sharedProps} />
) : (
    <IconButton {...props} {...sharedProps} />
)

<Button
ref={ref}
{...props}
variant="quaternary"
exceptionallySetClassName={classNames([
styles.controlActionButton,
exceptionallySetClassName,
])}
/>
) : (
<IconButton
ref={ref}
{...props}
variant="quaternary"
exceptionallySetClassName={classNames([
styles.controlActionButton,
exceptionallySetClassName,
])}
/>
)
},
)
64 changes: 64 additions & 0 deletions src/control-presentation/control-presentation.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
:root {
--reactist-field-height: 32px;
}

.container {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] This adds a second source of truth for the input chrome that still exists in text-field.module.css and select-field.module.css (32px field height, idle/hover/focus/error borders, read-only styling, slot spacing). Until those components are migrated, any visual/state fix will have to be made in multiple places. Please either switch the existing field components to consume ControlPresentation in this PR, or extract the shared chrome rules into one reused path before publishing the new primitive.

/* sizing */
height: var(--reactist-field-height);

/* slot-to-control gap (only takes effect between rendered children) */
gap: 6px;

/* default outer padding; shrunk on the side(s) where a slot is present */
padding-inline: 10px;
}

/* Conditional outer padding. When a slot is present on a side, shrink
* the outer padding on that side; the wrapper's flex `gap` then provides
* the visual spacing between the slot and the control. */
.container:has(.startSlot) {
padding-left: 6px;
}

.container:has(.endSlot) {
padding-right: 4px;
}

.control {
display: contents;

& > * {
/* layout */
flex: 1;
box-sizing: border-box;
margin: 0;
padding: 0;
width: 100%;

/* border */
border: none;
outline: none; /* focus state is handled by the wrapper border */

/* color */
background: transparent;
color: inherit;
}
}

.slot {
/* color */
color: var(--reactist-field-slot-content);
}

/*
* Compact 24×24 action button variant for use inside `endSlot`. The 3px
* border-radius and reduced min-width make it fit the field chrome alongside a
* 16px icon glyph.
*/
.controlActionButton.controlActionButton {
&.controlActionButton {
--reactist-btn-height: 24px;
}
border-radius: 3px;
min-width: var(--reactist-btn-height);
}
Loading
Loading