@synerise/ds-select 1.3.34 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.5.0](https://github.com/synerise/synerise-design/compare/@synerise/ds-select@1.4.0...@synerise/ds-select@1.5.0) (2026-08-11)
7
+
8
+ ### Features
9
+
10
+ - **select:** support maxTagCount="responsive" fit-to-width chips ([cb113cf](https://github.com/synerise/synerise-design/commit/cb113cf9f38a7ae112e55a8d159185f128fa2e4a))
11
+
12
+ # [1.4.0](https://github.com/synerise/synerise-design/compare/@synerise/ds-select@1.3.34...@synerise/ds-select@1.4.0) (2026-07-24)
13
+
14
+ ### Bug Fixes
15
+
16
+ - **select:** add onKeyDown, maxLength, dropdownAlign, popupClassName, maxTagPlaceholder ([1cf5f39](https://github.com/synerise/synerise-design/commit/1cf5f394055c10caee8b2bef81c83d04e19dcd6e))
17
+ - **select:** add suffixIcon, tabIndex, option style/children parity shims ([10c0bbc](https://github.com/synerise/synerise-design/commit/10c0bbc584534f7950bd5093764d9363a526149b))
18
+ - **select:** color clear/remove icons via currentColor, not svg fill ([0573124](https://github.com/synerise/synerise-design/commit/05731249ebc2d6d291879f0cb0f7443d8966a2b6))
19
+ - **select:** fire onFocus/onBlur and split render into sub-components ([44f73ec](https://github.com/synerise/synerise-design/commit/44f73ec0dc705c920950048acfa61ea108ec8072))
20
+ - **select:** forward data-_ and aria-_ attributes to the select root ([49294a8](https://github.com/synerise/synerise-design/commit/49294a8b1ab2b17967fc9d8ce9879eb48c5844cb))
21
+ - **select:** mark the current option selected via ListItem selected prop ([b5cb80a](https://github.com/synerise/synerise-design/commit/b5cb80a9d6e8a0411752a58fb0e66770f5c19408))
22
+ - **select:** move onClick off the dropdown trigger; focus inputs in interaction tests ([ed1a79c](https://github.com/synerise/synerise-design/commit/ed1a79c45bf056304a11e959ceb086dc1258b55e))
23
+ - **select:** multiple chip keeps its label-defined width; X squeezes label on hover ([5c9908d](https://github.com/synerise/synerise-design/commit/5c9908d8ff86119aa3948a1f366497827598bd3d))
24
+ - **select:** overlay search input and blur it on selection ([f312354](https://github.com/synerise/synerise-design/commit/f31235487542b29f31b2435c2b9951716803d20c))
25
+ - **select:** restore antd-parity props for consumer back-compat ([ceac672](https://github.com/synerise/synerise-design/commit/ceac6729c4c4815ee918a927a98aa17de0407e8d))
26
+
27
+ ### Features
28
+
29
+ - **select:** forward per-option data-_/aria-_ to the rendered option row ([1a8162d](https://github.com/synerise/synerise-design/commit/1a8162d9dae98a5cf96b0f35f70f9148eb025e88))
30
+ - **select:** implement maxTagCount/maxTagTextLength/maxTagPlaceholder + onPopupScroll ([060d2c6](https://github.com/synerise/synerise-design/commit/060d2c6b74118c065d928786dad67aa1b88500c7))
31
+ - **select:** keyboard-arrow option navigation + combobox/listbox ARIA ([74e5466](https://github.com/synerise/synerise-design/commit/74e5466b23dc2d062c0bd734cc84334f55b99ab8))
32
+
6
33
  ## [1.3.34](https://github.com/synerise/synerise-design/compare/@synerise/ds-select@1.3.33...@synerise/ds-select@1.3.34) (2026-07-23)
7
34
 
8
35
  **Note:** Version bump only for package @synerise/ds-select
package/CLAUDE.md CHANGED
@@ -1,140 +1,184 @@
1
1
  # Select (`@synerise/ds-select`)
2
2
 
3
- > A DS-styled select dropdown wrapping Ant Design's Select with FormField integration, prefix/suffix addon support, `readOnly` mode, and a `raw` render variant.
3
+ > A DS-native select dropdown (no Ant Design): a selector trigger + floating options dropdown built
4
+ > on `@synerise/ds-dropdown` (floating-ui) and `@synerise/ds-list-item`, wrapped in `FormField`.
5
+ > Supports single-select, `multiple` (chips), `tags` (free-text) and in-selector search, with full
6
+ > keyboard navigation, combobox/listbox ARIA, prefix/suffix addons, `readOnly`, and a `raw` variant.
4
7
 
5
8
  ## Package structure
6
9
 
7
10
  ```
8
11
  src/
9
- Select.tsx — main component (forwardRef, compound with .Option and .OptGroup)
10
- Select.types.ts — Props type (extends Antd SelectProps + FormFieldCommonProps)
11
- Select.styles.ts — styled-components: SelectContainer, AntdSelect, SelectWrapper, PrefixWrapper, SuffixWrapper
12
- index.tsdefault export, SelectProps type, SelectStyles namespace
12
+ Select.tsx — main component (forwardRef, compound with .Option); composes the sub-components below
13
+ Select.types.ts — SelectProps, SelectValue, SelectOption, SelectMode, SelectHandler, RawValueType, FilterOptionFn
14
+ Select.styles.ts — styled-components (Selector = the .ds-select box, SelectWrapper, chips, dropdown, …)
15
+ Option.tsxdeclarative <Select.Option> marker (renders null) + OptionProps
16
+ index.ts — default export + types + SelectStyles namespace + getOptionsFromChildren/findOption
17
+ Select.figma.tsx — Figma Code Connect mapping
13
18
  modules.d.ts — imports @testing-library/jest-dom
14
- style/
15
- index.lessimports antd select LESS + ds-core variables + select.mixin.less
16
- select.mixin.lessDS overrides for all antd select class variants (dropdown, items, states)
19
+ hooks/
20
+ useSelectOptions.tsresolve options (prop children), client filtering, tags create-row
21
+ useResponsiveTagCount.tsmaxTagCount="responsive": fit-to-width chip count (ResizeObserver)
22
+ components/
23
+ OptionList.tsx — dropdown overlay: loading / empty / scrollable listbox of options
24
+ SelectorContent.tsx — selector inner content: chips / selected label / placeholder + search input
25
+ utils/
26
+ getOptionsFromChildren.ts — read <Select.Option> children into SelectOption[]; findOption()
27
+ helpers.ts — cx(), toArray(), defaultFilter(), DEFAULT_LIST_HEIGHT
17
28
  __specs__/
18
- Select.spec.tsx — Vitest + React Testing Library tests
29
+ Select.spec.tsx — Vitest + React Testing Library tests
19
30
  ```
20
31
 
32
+ > No `style/` dir and no `antd` peerDep — the component is styled purely with styled-components. The
33
+ > old antd-era LESS (`select.mixin.less`, `.ant-select-*` overrides) was relocated to `ds-table`
34
+ > (`table/src/style/`), the only consumer that still renders antd selects.
35
+
21
36
  ## Public exports
22
37
 
23
38
  ```ts
24
- export default SelectWithComponents; // compound: Select + .Option + .OptGroup
25
- export type { Props as SelectProps } from './Select.types';
39
+ export { default } from './Select'; // compound: Select + .Option
40
+ export { Option, type OptionProps } from './Option';
41
+ export type {
42
+ Props, SelectProps, SelectValue, SelectOption, SelectMode,
43
+ SelectHandler, RawValueType, FilterOptionFn,
44
+ } from './Select.types';
26
45
  export * as SelectStyles from './Select.styles';
46
+ export { getOptionsFromChildren, findOption } from './utils/getOptionsFromChildren';
27
47
  ```
28
48
 
29
49
  ### `Select` (default)
30
50
 
31
- The default export is a compound component. Sub-components come from Antd directly:
32
- - `Select.Option` `AntdSelect.Option`
33
- - `Select.OptGroup` `AntdSelect.OptGroup`
51
+ `forwardRef<HTMLDivElement, SelectProps>`, augmented into a compound component with a single
52
+ sub-component: **`Select.Option`** (the DS `Option` marker). `Select.OptGroup` is intentionally
53
+ **not** reimplemented (zero real usage).
34
54
 
35
- The component is wrapped in `forwardRef<HTMLDivElement, Props>`.
55
+ `SelectValue = string | number | (string | number)[] | undefined` — antd-free (no `LabeledValue`).
36
56
 
37
57
  #### DS-specific props
38
58
 
39
59
  | Prop | Type | Default | Description |
40
60
  |------|------|---------|-------------|
41
- | `label` | `ReactNode` | `undefined` | Label above the field (via `FormField`). |
42
- | `tooltip` | `ReactNode` | `undefined` | Info tooltip next to the label (renders InfoFillS icon trigger). |
43
- | `tooltipConfig` | `TooltipProps` | `undefined` | Extra config merged into the label Tooltip. |
44
- | `description` | `ReactNode` | `undefined` | Helper text below the field. |
45
- | `errorText` | `ReactNode` | `undefined` | Error message below the field; also activates error visual state. |
46
- | `error` | `boolean` | `undefined` | Activates error visual state without showing a message. |
47
- | `clearTooltip` | `string` | `undefined` | Tooltip text shown on hover of the clear (×) button. |
48
- | `prefixel` | `ReactNode` | `undefined` | Addon element attached to the left of the selector (shares border). |
49
- | `suffixel` | `ReactNode` | `undefined` | Addon element attached to the right of the selector (shares border). |
50
- | `grey` | `boolean` | `undefined` | Gives the selector a `grey-050` background when not in error state. |
51
- | `asFormElement` | `boolean` | `undefined` | Forces a 16 px bottom margin even when `errorText` and `description` are absent. |
52
- | `raw` | `boolean` | `undefined` | Skips the `FormField` wrapper entirely renders only the selector. `forwardedRef` attaches to `SelectWrapper` instead of `SelectContainer`. |
53
- | `readOnly` | `boolean` | `undefined` | Disables the antd select while styling it as readable (white bg, `default` cursor, `grey-600` text) instead of the standard disabled look. |
54
- | `disabled` | `boolean` | `undefined` | Standard disabled state; merged with `readOnly` — either flag disables the underlying Antd select. |
55
- | `listHeight` | `ReactText` | `256` (Antd default) | Max height of the dropdown list in px. Type is widened to `ReactText` (string or number), overriding Antd's `number`-only constraint. |
56
- | `selectorStyle` | `CSSObject` | `undefined` | Inline styled-components `css` applied to `.ant-select-selector`. Useful for custom widths or padding. |
57
- | `style` | `React.CSSProperties` | `undefined` | Applied to `SelectWrapper` (the flex row containing selector + addons). |
58
- | `className` | `string` | `undefined` | Added to `SelectWrapper`. |
59
- | `getPopupContainer` | `(triggerNode) => HTMLElement` | `defaultGetPopupContainer` from `@synerise/ds-utils` | Where the dropdown is rendered. |
60
-
61
- All Antd `SelectProps<T>` are also accepted and forwarded (mode, value, defaultValue, onChange, onSearch, filterOption, showSearch, allowClear, open, etc.).
61
+ | `label` / `description` / `tooltip` / `tooltipConfig` | `ReactNode` / `TooltipProps` | `undefined` | `FormField` chrome (from `FormFieldCommonProps`). |
62
+ | `errorText` | `ReactNode` | `undefined` | Error message below the field; also activates the error visual state. |
63
+ | `error` | `boolean` | `undefined` | Error visual state without a message. |
64
+ | `clearTooltip` | `string` | `undefined` | Tooltip on hover of the clear (×) control. |
65
+ | `prefixel` / `suffixel` | `ReactNode` | `undefined` | Addon nodes attached left/right of the selector (shared border). |
66
+ | `grey` | `boolean` | `undefined` | `grey-050` selector background when not in error state. |
67
+ | `asFormElement` | `boolean` | `undefined` | Forces a 16 px bottom margin even without `errorText`/`description`. |
68
+ | `raw` | `boolean` | `undefined` | Skips the `FormField` wrapper renders only the selector; `ref` attaches to `SelectWrapper`. |
69
+ | `readOnly` | `boolean` | `undefined` | Non-interactive with readable styling (white bg, `default` cursor, `grey-600` text). |
70
+ | `disabled` | `boolean` | `undefined` | Standard disabled state; ORed with `readOnly` to block interaction. |
71
+ | `selectorStyle` | `CSSObject` | `undefined` | Inline style object applied to the `Selector` box. |
72
+ | `listHeight` | `number \| string` | `256` | Max dropdown list height (px). |
73
+ | `style` | `CSSProperties` | `undefined` | Applied to `SelectWrapper` (the flex row: selector + addons). |
74
+ | `className` | `string` | `undefined` | Added to the outer `SelectContainer`. |
75
+ | `getPopupContainer` | `(node) => HTMLElement \| ParentNode \| null` | `defaultGetPopupContainer` (`@synerise/ds-utils`) | Container the dropdown mounts into. |
76
+ | `size` | `'default' \| 'middle' \| 'large'` | `'default'` | Selector height (`middle` maps to `default`). |
77
+
78
+ Native `data-*` / `aria-*` attributes are forwarded onto the select root (`.ds-select-wrapper`).
79
+
80
+ #### antd-parity props (additive back-compat)
81
+
82
+ Kept so antd-era consumers need no change: `searchValue`, `onClear`, `onClick`, `onInputKeyDown`,
83
+ `onKeyDown`, `suffixIcon`, `clearIcon`, `maxLength`, `maxTagCount`, `maxTagTextLength`,
84
+ `maxTagPlaceholder`, `onPopupScroll`, `popupClassName` (alias of `dropdownClassName`),
85
+ `dropdownMatchSelectWidth` (`boolean | number`), `dropdownRender`, `showArrow`,
86
+ `autoFocus`, `tabIndex`, `rowKey`, plus the standard
87
+ `mode`/`options`/`showSearch`/`filterOption`/`allowClear`/`open`/`onChange`/`onSearch`/… surface.
88
+ `SelectHandler` type and the `SelectStyles.Selector` styled export are re-exported for parity.
89
+
90
+ `maxTagCount` (`number | 'responsive'` — collapse extra chips into a `+N` overflow chip, or fit them
91
+ to the selector width on one line), `maxTagTextLength` (truncate chip labels) and `onPopupScroll` are
92
+ fully implemented. `listItemHeight`, `dropdownAlign` and `defaultActiveFirstOption` are accepted for
93
+ compatibility but have **no runtime effect**.
62
94
 
63
95
  ## Usage patterns
64
96
 
65
97
  ```tsx
66
98
  import Select from '@synerise/ds-select';
67
99
 
68
- const { Option, OptGroup } = Select;
69
-
70
- // Basic with FormField label / description
71
- <Select label="Platform" description="Choose your platform" defaultValue="insta">
72
- <OptGroup label="Social">
73
- <Option value="insta">Instagram</Option>
74
- <Option value="fb">Facebook</Option>
75
- </OptGroup>
76
- </Select>
77
-
78
- // Multiple mode
79
- <Select mode="multiple" placeholder="Select tags" allowClear>
80
- <Option value="a">Alpha</Option>
81
- <Option value="b">Beta</Option>
82
- </Select>
100
+ const { Option } = Select;
83
101
 
84
- // Error state with message
85
- <Select errorText="This field is required" value={undefined} />
102
+ // Options as data (preferred)
103
+ <Select label="Platform" options={[{ value: 'insta', label: 'Instagram' }]} defaultValue="insta" />
86
104
 
87
- // Error state without message (e.g. inline in a form row)
88
- <Select error />
105
+ // Declarative children (read only when `options` is absent)
106
+ <Select label="Platform"><Option value="insta">Instagram</Option></Select>
89
107
 
90
- // readOnly (looks like a regular input, not grayed out)
91
- <Select readOnly value="locked-value" />
108
+ // Multiple (removable chips) / tags (free-text) / search
109
+ <Select mode="multiple" placeholder="Select tags" allowClear options={opts} />
110
+ <Select mode="tags" tokenSeparators={[',']} />
111
+ <Select showSearch filterOption optionFilterProp="label" options={opts} />
92
112
 
93
- // Grey background variant
94
- <Select grey placeholder="Search.." showSearch />
113
+ // Remote search: filter server-side, feed options from onSearch
114
+ <Select showSearch filterOption={false} onSearch={fetchOptions} options={opts} />
95
115
 
96
- // Raw (no FormField wrapper, ref goes to SelectWrapper)
116
+ // readOnly / raw / prefix-suffix
117
+ <Select readOnly value="locked" />
97
118
  <Select raw ref={myRef} placeholder="Compact" />
98
-
99
- // Prefix / suffix addons
100
119
  <Select prefixel={<span>$</span>} suffixel={<span>USD</span>} />
101
-
102
- // Clear button tooltip
103
- <Select allowClear clearTooltip="Clear selection" />
104
120
  ```
105
121
 
106
122
  ## Styling
107
123
 
108
- Two-layer styling approach:
109
-
110
- 1. **LESS** (`style/index.less` + `style/select.mixin.less`) — overrides all Antd `.ant-select-*` class rules: dropdown shadows, item hover colors, selected-item checkmark (base64 SVG), arrow icon (base64 SVG), tag/multiple chip styles, focus ring (`blue-600` inset shadow, `blue-050` bg), disabled state.
111
-
112
- 2. **styled-components** (`Select.styles.ts`) — handles DS-specific structural and state variants:
113
- - `SelectContainer` `flex-direction: column`; adds `16px` bottom margin when `hasBottomMargin` is true.
114
- - `SelectWrapper` `display: flex` row; applies `grey-050` background via `grey` prop (only when not in error state).
115
- - `AntdSelect` — extends Antd Select; handles `large` size height/line-height overrides, `withPrefixel`/`withSuffixel` border-radius removal, error border/shadow/background (`red-600`/`red-050`), and `readOnly` vs `disabled` visual differentiation.
116
- - `PrefixWrapper` / `SuffixWrapper` `grey-050` background, `grey-300` inset box-shadow, rounded outer corners only; negative margin/padding creates flush border join with selector.
117
-
118
- The dropdown offset is hard-coded to `[0, 8]` px via `dropdownAlign` (STOR-588).
119
-
120
- The search icon in the selector is replaced with an inline SVG data-URL using the theme's `grey-400` color.
124
+ **styled-components (`Select.styles.ts`) is the source of truth.** Key styled parts:
125
+ - `SelectContainer` — column flex; adds a 16 px bottom margin when `hasBottomMargin`.
126
+ - `SelectWrapper` (`.ds-select-wrapper`) — the flex row (selector + addons); carries the root
127
+ `onFocus`/`onBlur`/`onClick` and `data-*`/`aria-*` passthrough.
128
+ - `Selector` (`.ds-select`) — the trigger box; `large` height, `withPrefixel`/`withSuffixel`
129
+ border-radius removal, open/focus ring (`blue-600`/`blue-050`), error (`red-600`/`red-050`),
130
+ `readOnly` vs `disabled` differentiation, and the `selectorStyle` interpolation.
131
+ - `PrefixWrapper` / `SuffixWrapper` `grey-050` addons with flush border join.
132
+ - Dropdown / options: `DropdownWrapper`, `ScrollList`, `Inner` (`role="listbox"`), `OptionItem`
133
+ (styled `ds-list-item`), `NotFound`, `Loading`. Selector content: `SelectionItem`, `Placeholder`,
134
+ `MultiValueArea`, `Chip`/`ChipLabel`/`ChipRemove`, `Arrow`, `ClearWrapper`, `SearchInputEl`.
135
+
136
+ Class hooks are `ds-select-*` (`.ds-select`, `.ds-select-selection-item`, `.ds-select-dropdown`,
137
+ `.ds-select-arrow`, `.ds-select-clear`, `.ds-select-search`, …).
121
138
 
122
139
  ## Key dependencies
123
140
 
124
- - `antd/lib/select` — base Select, Option, OptGroup
125
- - `@synerise/ds-form-field` — wraps label / description / error layout (skipped when `raw` is true)
126
- - `@synerise/ds-icon` `Close3M` for clear button, `CloseS` for tag remove icon
127
- - `@synerise/ds-tooltip` — wraps the clear button icon to show `clearTooltip`
128
- - `@synerise/ds-utils` — `getPopupContainer` default (renders dropdown in nearest scroll parent)
129
- - `@synerise/ds-core` — theme tokens used in styled-components; LESS variables imported in Less styles
141
+ - `@synerise/ds-dropdown` — floating-ui popover positioning + overlay (the `Selector` is the `asChild` trigger).
142
+ - `@synerise/ds-list-item` — the option row (`OptionItem`) and list wrapper (`ScrollList`).
143
+ - `@synerise/ds-scrollbar` / `@synerise/ds-loader` dropdown scroll container / loading spinner.
144
+ - `@synerise/ds-form-field` — label / description / error layout (skipped when `raw`).
145
+ - `@synerise/ds-icon` — `AngleDownS` (arrow), `Close3M` (clear), `CloseS` (chip remove).
146
+ - `@synerise/ds-tooltip` — wraps the clear control for `clearTooltip`.
147
+ - `@synerise/ds-utils` — default `getPopupContainer`.
148
+ - `@synerise/ds-core` — theme tokens (peerDep). **No `antd` peerDep** — the component imports zero
149
+ antd; the LESS that pulled `~antd/lib/select/style` was relocated to `ds-table` (see below).
130
150
 
131
151
  ## Implementation notes
132
152
 
133
- - **`readOnly` is implemented via `disabled`** both `readOnly` and `disabled` flags are ORed before passing to Antd's `disabled` prop. The visual distinction is achieved only through styled-components CSS on the `readOnly` transient prop.
134
- - **`it.only` in tests** — the `'should be empty'` test case uses `it.only`, which means the other tests in the file are skipped when running in isolation. This is likely unintentional.
135
- - **`listHeight` type widening** Antd types `listHeight` as `number`, but DS overrides it to `ReactText` (`string | number`) to allow string values like `"auto"`.
136
- - **`selectorStyle` is not in README** — the prop exists in `Select.types.ts` and is wired in `AntdSelect` styled component but is not documented in the README.
137
- - **`clearIcon` is always overridden** — even if `clearIcon` is passed via `antdProps`, it is re-set internally. Any consumer-provided `clearIcon` will be ignored.
138
- - **`removeIcon` is always overridden** — same as `clearIcon`; custom `removeIcon` from consumer props is ignored.
139
- - **Compound component typing** — `SelectWithComponents` is typed as `SelectCompoundComponent = typeof Select & { Option, OptGroup }` using `Object.assign`, so `.Option` and `.OptGroup` are fully typed.
140
- - **Uses Vitest** `package.json` has `"test": "jest"`.
153
+ - **Option resolution** — `useSelectOptions` returns `resolvedOptions` (from `options` prop, else
154
+ `getOptionsFromChildren(children)`) and `displayedOptions` (after client filtering and, in `tags`
155
+ mode, a create-row prepended for the typed text). `filterOption={false}` disables local filtering.
156
+ - **`Option.value` is optional** — falls back to the element's React `key` (antd parity); callbacks
157
+ expose `option.key`.
158
+ - **Keyboard nav is hand-rolled (not ds-dropdown's)** — Select is a *combobox*: focus stays on the
159
+ input/selector via `aria-activedescendant`, whereas ds-dropdown uses roving DOM focus. `activeIndex`
160
+ + `moveActive` + three effects drive highlight; `handleKeyDown` handles Arrow/Home/End/Enter/Escape/
161
+ Space and Backspace (drop last chip).
162
+ - **Focus / blur** — `onFocus`/`onBlur` fire on the root only when focus truly enters/leaves the
163
+ select (relatedTarget guard ignores selector↔input moves). `autoFocus` focuses the `Selector` in
164
+ select-only mode (no search input to receive it). The dropdown `onMouseDown` prevents focus loss so
165
+ selecting an option / scrolling isn't treated as a blur (needed by `subtle-form`'s revert-on-blur).
166
+ - **`readOnly` is implemented via `disabled`** — both flags are ORed into `isDisabled`; the visual
167
+ distinction comes from the `$readOnly` transient prop on `Selector`.
168
+ - **`maxTagCount="responsive"`** — `useResponsiveTagCount` measures an off-flow ghost row
169
+ (`TagMeasureRow`, one hidden chip per value + a worst-case `+N` chip) rather than the visible
170
+ chips: a hidden chip would measure 0, and the ghost's width never depends on the count derived
171
+ from it, so the `ResizeObserver` (on the chip row *and* the ghosts, for font/label changes) can't
172
+ feed itself. The row switches to `nowrap` and reserves 30 px of caret room unless disabled/readOnly.
173
+ Unmeasurable layout (SSR, `display: none`, no `ResizeObserver` → `window.resize` fallback) degrades
174
+ to showing every chip; a lone oversized chip ellipsis-clips instead of collapsing into `+ 1`.
175
+ - **Controlled/uncontrolled** — `value`/`open`/`searchValue` are controlled when defined, else backed
176
+ by internal state; `onSearch` still fires when `searchValue` is controlled.
177
+ - **antd-free; no LESS** — styling is entirely styled-components. The old antd-era LESS
178
+ (`style/index.less` + `select.mixin.less`, `.ant-select-*` overrides for antd selects) was
179
+ **relocated to `ds-table`** (`table/src/style/select.mixin.less` + a direct
180
+ `@import '~antd/lib/select/style'`), since `ds-table` (out of scope, stays on antd) was the only
181
+ remaining consumer. ds-select ships no `dist/style` and no longer declares an `antd` peerDep.
182
+ - **Tests** — Vitest + React Testing Library (`src/__specs__/Select.spec.tsx`): render/parity,
183
+ keyboard + ARIA, and focus/blur (autofocus, onBlur on leave / not on internal move, onFocus).
184
+ ```