11import * as React from 'react'
22import { type ComponentProps , forwardRef } from 'react'
33
4+ import classNames from 'classnames'
5+
46import { Box } from '../box'
57
8+ import { FieldChromeContainer } from './field-chrome-container'
9+
610import styles from './control-presentation.module.css'
711
812import type { ObfuscatedClassName } from '../utils/common-types'
@@ -27,101 +31,46 @@ export type ControlPresentationProps = {
2731 * The control element (an `<input>`, an Ariakit Select trigger,
2832 * `<select>`, etc.). Attributes belonging to the control — `type`,
2933 * `value`, `onChange`, `readOnly`, `disabled`, `aria-invalid`, and so on —
30- * are set on this element directly. The wrapper chrome (read-only
31- * background, disabled state, error border) derives from those attributes.
34+ * are set on this element directly. The wrapper chrome derives from those
35+ * attributes.
3236 *
3337 * Click handlers belong on the control itself, not on this wrapper.
38+ * Clicking the wrapper focuses the control.
3439 */
3540 children : React . ReactNode
3641} & ObfuscatedClassName &
37- Omit < ComponentProps < typeof Box > , 'className ' >
42+ Omit < ComponentProps < typeof FieldChromeContainer > , 'borderRadius ' >
3843
3944/**
40- * A presentational primitive that renders the visual chrome of a
41- * text-field-style input (border, hover / focus / disabled / read-only / error
42- * states, plus an optional row of slots) around an arbitrary control element.
45+ * The visual chrome of an inline, single-row, text-field-style input: a
46+ * 32px-tall row with optional start/end slots around a control element.
4347 *
44- * Slot order (left to right):
45- * `startSlot` → control (children) → `endSlot`.
48+ * Slot order (left to right): `startSlot` → control (children) → `endSlot`.
4649 *
47- * Clicking anywhere on the wrapper focuses the control.
50+ * Click handlers belong on the control itself, not on this wrapper.
51+ * Clicking the wrapper focuses the control.
4852 */
4953export const ControlPresentation = forwardRef < HTMLDivElement , ControlPresentationProps > (
5054 function ControlPresentation (
5155 { startSlot, endSlot, exceptionallySetClassName, children, ...rest } ,
5256 ref ,
5357 ) {
54- const controlWrapperRef = React . useRef < HTMLDivElement > ( null )
55- // The synthetic .click() we dispatch below bubbles back up to this
56- // handler. Track that we're inside that re-entry so we can skip
57- // re-invoking the consumer onClick + activation for it.
58- const isDispatchedReentryRef = React . useRef ( false )
59-
60- // `onClick` isn't part of `ControlPresentationProps` — consumers
61- // should put click handlers on the inner control. This extraction
62- // exists only to compose with onClick that arrives via render-prop
63- // wrapping (e.g. Ariakit's Select trigger uses `render={<ControlPresentation/>}`
64- // and forwards its own onClick), where the wrapper IS the trigger.
65- const { onClick, ...restWithoutClick } = rest as typeof rest & {
66- onClick ?: React . MouseEventHandler < HTMLDivElement >
67- }
68-
69- function handleWrapperClick ( event : React . MouseEvent < HTMLDivElement > ) {
70- if ( isDispatchedReentryRef . current ) {
71- isDispatchedReentryRef . current = false
72- return
73- }
74-
75- onClick ?.( event )
76-
77- const control = controlWrapperRef . current ?. firstElementChild as HTMLElement | null
78- if ( ! control ) return
79-
80- // Don't re-fire when the user clicked directly on the control —
81- // it handled itself, and re-dispatching would double-activate.
82- if ( event . target instanceof Node && control . contains ( event . target ) ) return
83-
84- // .focus() handles inputs (cursor/keyboard focus). The activation
85- // hop differs by element type:
86- // - Native <select>: synthetic .click() is blocked by browsers
87- // for the native dropdown. .showPicker() is the correct API.
88- // - Everything else (inputs, native <button>, Ariakit Select
89- // triggers, anchors): .click() either activates the click
90- // handler or is harmlessly redundant.
91- control . focus ( )
92- if ( control instanceof HTMLSelectElement && typeof control . showPicker === 'function' ) {
93- try {
94- control . showPicker ( )
95- } catch {
96- // showPicker can throw (e.g. focus state issues) — fall back
97- // silently; the focus above is the minimum useful behavior.
98- }
99- return
100- }
101-
102- isDispatchedReentryRef . current = true
103- control . click ( )
104- }
105-
10658 return (
107- < Box
59+ < FieldChromeContainer
10860 ref = { ref }
109- { ...restWithoutClick }
110- className = { [ styles . container , exceptionallySetClassName ] }
61+ { ...rest }
62+ borderRadius = "small"
63+ exceptionallySetClassName = { classNames ( styles . container , exceptionallySetClassName ) }
11164 display = "flex"
11265 alignItems = "center"
11366 overflow = "hidden"
114- borderRadius = "standard"
115- onClick = { handleWrapperClick }
11667 >
11768 { startSlot ? (
11869 < Slot className = { [ styles . slot , styles . startSlot ] } > { startSlot } </ Slot >
11970 ) : null }
120- < div ref = { controlWrapperRef } className = { styles . control } >
121- { children }
122- </ div >
71+ < div className = { styles . control } > { children } </ div >
12372 { endSlot ? < Slot className = { [ styles . slot , styles . endSlot ] } > { endSlot } </ Slot > : null }
124- </ Box >
73+ </ FieldChromeContainer >
12574 )
12675 } ,
12776)
0 commit comments