@synerise/ds-select 2.0.0 → 2.0.2
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 +10 -0
- package/CLAUDE.md +29 -6
- package/README.md +8 -3
- package/dist/Select.js +6 -14
- package/dist/Select.styles.d.ts +19 -1
- package/dist/Select.styles.js +16 -11
- package/dist/Select.types.d.ts +5 -1
- package/dist/components/OptionList.d.ts +6 -2
- package/dist/components/OptionList.js +202 -29
- package/dist/hooks/useSelectOptions.js +18 -7
- package/dist/utils/areOptionChildrenEqual.d.ts +7 -0
- package/dist/utils/areOptionChildrenEqual.js +31 -0
- package/dist/utils/helpers.d.ts +14 -0
- package/dist/utils/helpers.js +6 -0
- package/package.json +10 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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
|
+
## [2.0.2](https://github.com/synerise/synerise-design/compare/@synerise/ds-select@2.0.1...@synerise/ds-select@2.0.2) (2026-09-02)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @synerise/ds-select
|
|
9
|
+
|
|
10
|
+
## [2.0.1](https://github.com/synerise/synerise-design/compare/@synerise/ds-select@2.0.0...@synerise/ds-select@2.0.1) (2026-09-01)
|
|
11
|
+
|
|
12
|
+
### Performance Improvements
|
|
13
|
+
|
|
14
|
+
- **select:** virtualize the dropdown option list ([f95de4e](https://github.com/synerise/synerise-design/commit/f95de4e6bbe392def14e010c0556e2c56c90ddc7))
|
|
15
|
+
|
|
6
16
|
# [2.0.0](https://github.com/synerise/synerise-design/compare/@synerise/ds-select@1.5.3...@synerise/ds-select@2.0.0) (2026-08-26)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package @synerise/ds-select
|
package/CLAUDE.md
CHANGED
|
@@ -20,11 +20,12 @@ src/
|
|
|
20
20
|
useSelectOptions.ts — resolve options (prop → children), client filtering, tags create-row
|
|
21
21
|
useResponsiveTagCount.ts — maxTagCount="responsive": fit-to-width chip count (ResizeObserver)
|
|
22
22
|
components/
|
|
23
|
-
OptionList.tsx — dropdown overlay: loading / empty /
|
|
23
|
+
OptionList.tsx — dropdown overlay: loading / empty / windowed (react-window) listbox of options
|
|
24
24
|
SelectorContent.tsx — selector inner content: chips / selected label / placeholder + search input
|
|
25
25
|
utils/
|
|
26
26
|
getOptionsFromChildren.ts — read <Select.Option> children into SelectOption[]; findOption()
|
|
27
|
-
|
|
27
|
+
areOptionChildrenEqual.ts — structural compare of <Select.Option> children (memo guard)
|
|
28
|
+
helpers.ts — cx(), toArray(), defaultFilter(), DEFAULT_LIST_HEIGHT, DEFAULT_LIST_ITEM_HEIGHT, OVERSCAN_COUNT, MAX_MEASURED_ROWS
|
|
28
29
|
__specs__/
|
|
29
30
|
Select.spec.tsx — Vitest + React Testing Library tests
|
|
30
31
|
```
|
|
@@ -69,7 +70,8 @@ sub-component: **`Select.Option`** (the DS `Option` marker). `Select.OptGroup` i
|
|
|
69
70
|
| `readOnly` | `boolean` | `undefined` | Non-interactive with readable styling (white bg, `default` cursor, `grey-600` text). |
|
|
70
71
|
| `disabled` | `boolean` | `undefined` | Standard disabled state; ORed with `readOnly` to block interaction. |
|
|
71
72
|
| `selectorStyle` | `CSSObject` | `undefined` | Inline style object applied to the `Selector` box. |
|
|
72
|
-
| `listHeight` | `number \| string` | `256` | Max dropdown list height (px). |
|
|
73
|
+
| `listHeight` | `number \| string` | `256` | Max dropdown list height (px); also the windowed viewport height. |
|
|
74
|
+
| `listItemHeight` | `number` | `32` | Height per option row the window is sized from; taller rows are measured. |
|
|
73
75
|
| `style` | `CSSProperties` | `undefined` | Applied to `SelectWrapper` (the flex row: selector + addons). |
|
|
74
76
|
| `className` | `string` | `undefined` | Added to the outer `SelectContainer`. |
|
|
75
77
|
| `getPopupContainer` | `(node) => HTMLElement \| ParentNode \| null` | `defaultGetPopupContainer` (`@synerise/ds-utils`) | Container the dropdown mounts into. |
|
|
@@ -88,9 +90,9 @@ Kept so antd-era consumers need no change: `searchValue`, `onClear`, `onClick`,
|
|
|
88
90
|
`SelectHandler` type and the `SelectStyles.Selector` styled export are re-exported for parity.
|
|
89
91
|
|
|
90
92
|
`maxTagCount` (`number | 'responsive'` — collapse extra chips into a `+N` overflow chip, or fit them
|
|
91
|
-
to the selector width on one line), `maxTagTextLength`
|
|
92
|
-
fully implemented. `
|
|
93
|
-
compatibility but have **no runtime effect**.
|
|
93
|
+
to the selector width on one line), `maxTagTextLength`, `onPopupScroll` and `listItemHeight` (the row
|
|
94
|
+
height the windowed list is sized from) are fully implemented. `dropdownAlign` and
|
|
95
|
+
`defaultActiveFirstOption` are accepted for compatibility but have **no runtime effect**.
|
|
94
96
|
|
|
95
97
|
## Usage patterns
|
|
96
98
|
|
|
@@ -145,6 +147,8 @@ Class hooks are `ds-select-*` (`.ds-select`, `.ds-select-selection-item`, `.ds-s
|
|
|
145
147
|
- `@synerise/ds-icon` — `AngleDownS` (arrow), `Close3M` (clear), `CloseS` (chip remove).
|
|
146
148
|
- `@synerise/ds-tooltip` — wraps the clear control for `clearTooltip`.
|
|
147
149
|
- `@synerise/ds-utils` — default `getPopupContainer`.
|
|
150
|
+
- `react-window` — `VariableSizeList`, the windowing engine behind `OptionList` (already a DS-wide
|
|
151
|
+
dependency: `dropdown`, `context-selector`, `item-picker`, `table`, …).
|
|
148
152
|
- `@synerise/ds-core` — theme tokens (peerDep). **No `antd` peerDep** — the component imports zero
|
|
149
153
|
antd; the LESS that pulled `~antd/lib/select/style` was relocated to `ds-table` (see below).
|
|
150
154
|
|
|
@@ -153,6 +157,25 @@ Class hooks are `ds-select-*` (`.ds-select`, `.ds-select-selection-item`, `.ds-s
|
|
|
153
157
|
- **Option resolution** — `useSelectOptions` returns `resolvedOptions` (from `options` prop, else
|
|
154
158
|
`getOptionsFromChildren(children)`) and `displayedOptions` (after client filtering and, in `tags`
|
|
155
159
|
mode, a create-row prepended for the typed text). `filterOption={false}` disables local filtering.
|
|
160
|
+
The children path is cached against `areOptionChildrenEqual` rather than `useMemo([children])`:
|
|
161
|
+
JSX hands over a fresh array of fresh elements on every parent render, so a reference-keyed memo
|
|
162
|
+
would rebuild the options — and invalidate every memo below them — on renders that changed nothing.
|
|
163
|
+
- **Windowed option list** — `OptionList` renders `displayedOptions` through react-window's
|
|
164
|
+
`VariableSizeList`, so only the visible rows plus `OVERSCAN_COUNT` are in the DOM. Following the
|
|
165
|
+
DS idiom (`item-picker`, `context-selector`): the surrounding `Scrollbar` owns the scrolling
|
|
166
|
+
(`overflow: unset` on the list, `onScroll` → `list.scrollTo`), which keeps `onPopupScroll` firing
|
|
167
|
+
from the same element as before. `listItemHeight` (default 32) is the size estimate; each mounted
|
|
168
|
+
row reports its real `offsetHeight` so rows with taller JSX (a flag + a wrapping label) lay out
|
|
169
|
+
correctly. Keyboard navigation can target a row that is not mounted, so `OptionList` scrolls by
|
|
170
|
+
computed offset instead of `scrollIntoView`, and a changed query resets the window to the top.
|
|
171
|
+
Because only a slice of the options is ever in the DOM, each row states `aria-setsize` /
|
|
172
|
+
`aria-posinset` — AT can no longer count the set for itself.
|
|
173
|
+
- **Row measurement invalidates through `resetAfterIndex`** — react-window memoises row offsets, so a
|
|
174
|
+
measured height only takes effect once the list is told to drop that memo. Two consequences worth
|
|
175
|
+
keeping in mind when touching `OptionList`: a row's own layout effect cannot reach `listRef` on the
|
|
176
|
+
mount commit (rows are descendants of the list, so their effects run first), and a change to
|
|
177
|
+
`listItemHeight` must invalidate *and* force a render — a passive `resetAfterIndex(index, false)`
|
|
178
|
+
leaves the stale layout on screen until something else happens to re-render the list.
|
|
156
179
|
- **Row vs selector display (antd parity)** — the dropdown row renders `option.children ?? label ??
|
|
157
180
|
value`, the selector renders the field named by `optionLabelProp` (`label`, `children`, `value`,
|
|
158
181
|
`title`, …) and falls back to `label`. So `<Option label={compact}>{rich}</Option>` shows `rich` in
|
package/README.md
CHANGED
|
@@ -104,8 +104,8 @@ select root.
|
|
|
104
104
|
| dropdownStyle | Inline style on the dropdown overlay. | `CSSProperties` | - |
|
|
105
105
|
| dropdownMatchSelectWidth | Match dropdown width to the selector; a number fixes the width (px). | `boolean \| number` | `true` |
|
|
106
106
|
| dropdownRender | Wrap the rendered option menu (custom footer / scroll container). | `(menu: ReactElement) => ReactNode` | - |
|
|
107
|
-
| listHeight | Max dropdown list height (px). | `number \| string` | `256` |
|
|
108
|
-
| listItemHeight |
|
|
107
|
+
| listHeight | Max dropdown list height (px) — also the height of the virtualised viewport. | `number \| string` | `256` |
|
|
108
|
+
| listItemHeight | Height per option row the virtualised window is sized from. Rows with taller content are measured and lay out at their real height. | `number` | `32` |
|
|
109
109
|
|
|
110
110
|
### Display & tags
|
|
111
111
|
|
|
@@ -191,7 +191,12 @@ import type {
|
|
|
191
191
|
`.ds-select`, `.ds-select-wrapper`, `.ds-select-selection-item`, `.ds-select-dropdown`, etc.
|
|
192
192
|
- **Remote search** — set `filterOption={false}` and update `options` from your `onSearch`
|
|
193
193
|
handler; the component won't filter locally in that mode.
|
|
194
|
+
- **Virtualised list** — the dropdown renders only the visible rows (plus overscan) via
|
|
195
|
+
`react-window`, so a few hundred options cost the same as a few. `listHeight` sizes the viewport
|
|
196
|
+
and `listItemHeight` the row estimate.
|
|
194
197
|
- **Keyboard & ARIA** — Arrow / Home / End / Enter / Escape / Space (and Backspace to drop the
|
|
195
|
-
last chip in multiple mode), with combobox / listbox `aria-activedescendant`.
|
|
198
|
+
last chip in multiple mode), with combobox / listbox `aria-activedescendant`. Because the list
|
|
199
|
+
is virtualised, each option also carries `aria-setsize` / `aria-posinset` so assistive tech
|
|
200
|
+
announces its place in the whole list, not in the mounted window.
|
|
196
201
|
- **Not reimplemented from antd** — `OptGroup`, `labelInValue` / `LabeledValue`,
|
|
197
202
|
`autoClearSearchValue`, `firstActiveValue`, `menuItemSelectedIcon`.
|
package/dist/Select.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useState, useRef, useId, useEffect } from "react";
|
|
2
|
+
import { forwardRef, useState, useMemo, useRef, useId, useCallback, useEffect } from "react";
|
|
3
3
|
import Dropdown from "@synerise/ds-dropdown";
|
|
4
4
|
import FormField from "@synerise/ds-form-field";
|
|
5
5
|
import Icon, { AngleDownS, Close3M } from "@synerise/ds-icon";
|
|
@@ -11,7 +11,7 @@ import { OptionList } from "./components/OptionList.js";
|
|
|
11
11
|
import { SelectorContent } from "./components/SelectorContent.js";
|
|
12
12
|
import { useSelectOptions } from "./hooks/useSelectOptions.js";
|
|
13
13
|
import { findOption } from "./utils/getOptionsFromChildren.js";
|
|
14
|
-
import { DEFAULT_LIST_HEIGHT,
|
|
14
|
+
import { DEFAULT_LIST_HEIGHT, toArray, cx } from "./utils/helpers.js";
|
|
15
15
|
const SelectInner = forwardRef((props, ref) => {
|
|
16
16
|
const {
|
|
17
17
|
value,
|
|
@@ -60,6 +60,7 @@ const SelectInner = forwardRef((props, ref) => {
|
|
|
60
60
|
dropdownMatchSelectWidth = true,
|
|
61
61
|
dropdownRender,
|
|
62
62
|
listHeight = DEFAULT_LIST_HEIGHT,
|
|
63
|
+
listItemHeight,
|
|
63
64
|
notFoundContent = "No data",
|
|
64
65
|
clearIcon,
|
|
65
66
|
clearTooltip,
|
|
@@ -88,7 +89,7 @@ const SelectInner = forwardRef((props, ref) => {
|
|
|
88
89
|
const passthroughAttrs = Object.fromEntries(Object.entries(props).filter(([key]) => key.startsWith("data-") || key.startsWith("aria-")));
|
|
89
90
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
90
91
|
const currentValue = value !== void 0 ? value : internalValue;
|
|
91
|
-
const selectedValues = toArray(currentValue);
|
|
92
|
+
const selectedValues = useMemo(() => toArray(currentValue), [currentValue]);
|
|
92
93
|
const [internalOpen, setInternalOpen] = useState(!!defaultOpen);
|
|
93
94
|
const isControlledOpen = openProp !== void 0;
|
|
94
95
|
const isOpen = isControlledOpen ? !!openProp : internalOpen;
|
|
@@ -100,7 +101,7 @@ const SelectInner = forwardRef((props, ref) => {
|
|
|
100
101
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
101
102
|
const baseId = useId();
|
|
102
103
|
const listboxId = `${baseId}-listbox`;
|
|
103
|
-
const optionDomId = (index) => `${baseId}-option-${index}
|
|
104
|
+
const optionDomId = useCallback((index) => `${baseId}-option-${index}`, [baseId]);
|
|
104
105
|
const {
|
|
105
106
|
resolvedOptions,
|
|
106
107
|
displayedOptions
|
|
@@ -157,15 +158,6 @@ const SelectInner = forwardRef((props, ref) => {
|
|
|
157
158
|
}
|
|
158
159
|
setActiveIndex(firstEnabledIndex());
|
|
159
160
|
}, [effectiveQuery]);
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
if (!isOpen || activeIndex < 0) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const activeEl = document.getElementById(optionDomId(activeIndex));
|
|
165
|
-
activeEl?.scrollIntoView?.({
|
|
166
|
-
block: "nearest"
|
|
167
|
-
});
|
|
168
|
-
}, [activeIndex, isOpen]);
|
|
169
161
|
useEffect(() => {
|
|
170
162
|
if (autoFocus && !hasInput) {
|
|
171
163
|
selectorRef.current?.focus();
|
|
@@ -369,7 +361,7 @@ const SelectInner = forwardRef((props, ref) => {
|
|
|
369
361
|
const hasValue = selectedValues.length > 0;
|
|
370
362
|
const showClear = allowClear && hasValue && !isDisabled;
|
|
371
363
|
const placeholderStr = typeof placeholder === "string" ? placeholder : void 0;
|
|
372
|
-
const menu = /* @__PURE__ */ jsx(OptionList, { loading, options: displayedOptions, notFoundContent, listHeight, isMultiple, listboxId, selectedValues, activeIndex, rowKey, optionDomId, onOptionActivate: setActiveIndex, onOptionSelect: handleSelect, onPopupScroll });
|
|
364
|
+
const menu = /* @__PURE__ */ jsx(OptionList, { loading, options: displayedOptions, notFoundContent, listHeight, listItemHeight, isMultiple, listboxId, selectedValues, activeIndex, searchQuery: effectiveQuery, rowKey, optionDomId, onOptionActivate: setActiveIndex, onOptionSelect: handleSelect, onPopupScroll });
|
|
373
365
|
const handleSearchKeyDown = (event) => {
|
|
374
366
|
onInputKeyDown?.(event);
|
|
375
367
|
onKeyDown?.(event);
|
package/dist/Select.styles.d.ts
CHANGED
|
@@ -29,7 +29,25 @@ export declare const Arrow: import('styled-components').StyledComponent<"span",
|
|
|
29
29
|
export declare const ClearWrapper: import('styled-components').StyledComponent<"span", any, {}, never>;
|
|
30
30
|
export declare const DropdownWrapper: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
31
31
|
export declare const ScrollList: import('styled-components').StyledComponent<({ children, onClick, maxToShowItems, texts, ...htmlAttributes }: import('@synerise/ds-list-item').ListWrapperProps) => React.JSX.Element, any, {}, never>;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The listbox. Its only child is the windowed list, styled from here (rather than
|
|
34
|
+
* via `styled(VariableSizeList)`, which would erase react-window's `itemData`
|
|
35
|
+
* generic): `height` is a react-window *prop* sizing the render window, not the
|
|
36
|
+
* box, so the CSS height is handed back to the content — the dropdown shrinks to
|
|
37
|
+
* fit a short list — while `overflow: unset` leaves scrolling to the surrounding
|
|
38
|
+
* `Scrollbar`.
|
|
39
|
+
*
|
|
40
|
+
* `max-height` does not clip (overflow is visible by design); it keeps the *box*
|
|
41
|
+
* at `listHeight` so nothing downstream measures a list of 500 rows as a
|
|
42
|
+
* 16000px-tall element. The rows below it overflow into the `Scrollbar`, whose
|
|
43
|
+
* scrollable region covers them — which holds only as long as no element between
|
|
44
|
+
* this one and that scroll node introduces its own `overflow: hidden`.
|
|
45
|
+
*/
|
|
46
|
+
export declare const Inner: import('styled-components').StyledComponent<"div", any, {
|
|
47
|
+
$maxHeight?: number;
|
|
48
|
+
}, never>;
|
|
49
|
+
/** Row wrapper carrying react-window's absolute offset; height follows content. */
|
|
50
|
+
export declare const VirtualRow: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
33
51
|
export declare const NotFound: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
34
52
|
export declare const Loading: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
35
53
|
export declare const OptionItem: StyledListItem;
|
package/dist/Select.styles.js
CHANGED
|
@@ -55,42 +55,46 @@ const ScrollList = /* @__PURE__ */ styled(ListWrapper).withConfig({
|
|
|
55
55
|
const Inner = /* @__PURE__ */ styled.div.withConfig({
|
|
56
56
|
displayName: "Selectstyles__Inner",
|
|
57
57
|
componentId: "sc-n9lk0v-11"
|
|
58
|
-
})(["padding-right:8px;"]);
|
|
58
|
+
})(["padding-right:8px;> .ds-select-option-list{overflow-x:unset;overflow-y:unset;height:auto !important;", "}"], (props) => props.$maxHeight !== void 0 && css(["max-height:", "px;"], props.$maxHeight));
|
|
59
|
+
const VirtualRow = /* @__PURE__ */ styled.div.withConfig({
|
|
60
|
+
displayName: "Selectstyles__VirtualRow",
|
|
61
|
+
componentId: "sc-n9lk0v-12"
|
|
62
|
+
})(["box-sizing:border-box;"]);
|
|
59
63
|
const NotFound = /* @__PURE__ */ styled.div.withConfig({
|
|
60
64
|
displayName: "Selectstyles__NotFound",
|
|
61
|
-
componentId: "sc-n9lk0v-
|
|
65
|
+
componentId: "sc-n9lk0v-13"
|
|
62
66
|
})(["display:flex;align-items:center;justify-content:center;padding:8px 12px;color:", ";font-weight:normal;"], (props) => props.theme.palette["grey-600"]);
|
|
63
67
|
const Loading = /* @__PURE__ */ styled.div.withConfig({
|
|
64
68
|
displayName: "Selectstyles__Loading",
|
|
65
|
-
componentId: "sc-n9lk0v-
|
|
69
|
+
componentId: "sc-n9lk0v-14"
|
|
66
70
|
})(["display:flex;align-items:center;justify-content:center;padding:16px;"]);
|
|
67
71
|
const OptionItem = /* @__PURE__ */ styled(DSListItem).withConfig({
|
|
68
72
|
displayName: "Selectstyles__OptionItem",
|
|
69
|
-
componentId: "sc-n9lk0v-
|
|
73
|
+
componentId: "sc-n9lk0v-15"
|
|
70
74
|
})(["min-width:auto;font-weight:normal;&&:hover:not(.ds-list-item-disabled),&&.ds-select-item-option-active:not(.ds-list-item-disabled){background-color:", ";}"], (props) => props.theme.palette["blue-050"]);
|
|
71
75
|
const SearchInputEl = /* @__PURE__ */ styled.input.withConfig({
|
|
72
76
|
displayName: "Selectstyles__SearchInputEl",
|
|
73
|
-
componentId: "sc-n9lk0v-
|
|
77
|
+
componentId: "sc-n9lk0v-16"
|
|
74
78
|
})(["flex:1 1 30px;min-width:30px;width:100%;border:none;outline:none;background:transparent;padding:0;margin:0;font-family:inherit;font-size:13px;color:", ";cursor:inherit;&::placeholder{color:", ";}&:disabled{cursor:not-allowed;}", ""], (props) => props.theme.palette["grey-700"], (props) => props.theme.palette["grey-500"], (props) => props.$overlay && css(["position:absolute;top:0;left:0;width:100%;height:100%;flex:none;box-sizing:border-box;padding:0 30px 0 12px;"]));
|
|
75
79
|
const MultiValueArea = /* @__PURE__ */ styled.div.withConfig({
|
|
76
80
|
displayName: "Selectstyles__MultiValueArea",
|
|
77
|
-
componentId: "sc-n9lk0v-
|
|
81
|
+
componentId: "sc-n9lk0v-17"
|
|
78
82
|
})(["display:flex;flex-wrap:wrap;align-items:center;gap:4px;flex:1;min-width:0;", ""], (props) => props.$responsive && css(["position:relative;flex-wrap:nowrap;overflow:hidden;> .ds-select-selection-item,> .ds-select-selection-overflow{flex:0 0 auto;}> .ds-select-search{flex:1 1 0;min-width:0;}"]));
|
|
79
83
|
const TagMeasureRow = /* @__PURE__ */ styled.div.withConfig({
|
|
80
84
|
displayName: "Selectstyles__TagMeasureRow",
|
|
81
|
-
componentId: "sc-n9lk0v-
|
|
85
|
+
componentId: "sc-n9lk0v-18"
|
|
82
86
|
})(["position:absolute;top:0;left:0;width:100%;height:0;display:flex;flex-wrap:nowrap;gap:4px;overflow:hidden;visibility:hidden;pointer-events:none;> *{flex:0 0 auto;}"]);
|
|
83
87
|
const Chip = /* @__PURE__ */ styled.span.withConfig({
|
|
84
88
|
displayName: "Selectstyles__Chip",
|
|
85
|
-
componentId: "sc-n9lk0v-
|
|
89
|
+
componentId: "sc-n9lk0v-19"
|
|
86
90
|
})(["position:relative;display:inline-flex;align-items:center;box-sizing:border-box;width:max-content;max-width:100%;min-height:24px;padding:0 8px;background:", ";border:none;border-radius:3px;font-size:13px;line-height:1;color:", ";&:hover{background:", ";color:", ";}&:hover .ds-select-selection-item-label{max-width:calc(100% - 24px);}&:hover .ds-select-selection-item-remove{display:inline-flex;}"], (props) => props.theme.palette["grey-200"], (props) => props.theme.palette["grey-600"], (props) => props.theme.palette["grey-300"], (props) => props.theme.palette["grey-800"]);
|
|
87
91
|
const ChipLabel = /* @__PURE__ */ styled.span.withConfig({
|
|
88
92
|
displayName: "Selectstyles__ChipLabel",
|
|
89
|
-
componentId: "sc-n9lk0v-
|
|
93
|
+
componentId: "sc-n9lk0v-20"
|
|
90
94
|
})(["max-width:100%;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;"]);
|
|
91
95
|
const ChipRemove = /* @__PURE__ */ styled.span.withConfig({
|
|
92
96
|
displayName: "Selectstyles__ChipRemove",
|
|
93
|
-
componentId: "sc-n9lk0v-
|
|
97
|
+
componentId: "sc-n9lk0v-21"
|
|
94
98
|
})(["position:absolute;right:4px;top:50%;transform:translateY(-50%);display:none;align-items:center;justify-content:center;width:24px;height:24px;cursor:pointer;color:", ";"], (props) => props.theme.palette["red-600"]);
|
|
95
99
|
export {
|
|
96
100
|
Arrow,
|
|
@@ -113,5 +117,6 @@ export {
|
|
|
113
117
|
SelectionItem,
|
|
114
118
|
Selector,
|
|
115
119
|
SuffixWrapper,
|
|
116
|
-
TagMeasureRow
|
|
120
|
+
TagMeasureRow,
|
|
121
|
+
VirtualRow
|
|
117
122
|
};
|
package/dist/Select.types.d.ts
CHANGED
|
@@ -120,7 +120,11 @@ export type SelectProps<VT extends SelectValue = SelectValue> = {
|
|
|
120
120
|
dropdownRender?: (menu: ReactElement) => ReactNode;
|
|
121
121
|
/** Max dropdown list height (px). */
|
|
122
122
|
listHeight?: number | string;
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Height (px) of a single option row, used to window the list. Rows that render
|
|
125
|
+
* taller content are measured and lay out at their real height, so this is the
|
|
126
|
+
* estimate the window starts from rather than a hard cap. Defaults to 32.
|
|
127
|
+
*/
|
|
124
128
|
listItemHeight?: number;
|
|
125
129
|
notFoundContent?: ReactNode;
|
|
126
130
|
clearIcon?: ReactNode;
|
|
@@ -6,11 +6,15 @@ type OptionListProps = {
|
|
|
6
6
|
options: SelectOption[];
|
|
7
7
|
notFoundContent?: ReactNode;
|
|
8
8
|
listHeight?: number | string;
|
|
9
|
+
/** Estimated height of one option row; measured rows override it. */
|
|
10
|
+
listItemHeight?: number;
|
|
9
11
|
isMultiple: boolean;
|
|
10
12
|
listboxId: string;
|
|
11
13
|
selectedValues: RawValueType[];
|
|
12
14
|
/** Keyboard-highlighted option index. */
|
|
13
15
|
activeIndex: number;
|
|
16
|
+
/** Current query — a change scrolls the window back to the top. */
|
|
17
|
+
searchQuery: string;
|
|
14
18
|
rowKey?: (option: SelectOption) => Key;
|
|
15
19
|
optionDomId: (index: number) => string;
|
|
16
20
|
onOptionActivate: (index: number) => void;
|
|
@@ -18,6 +22,6 @@ type OptionListProps = {
|
|
|
18
22
|
/** antd parity: fired as the option list scrolls (e.g. to page in more options). */
|
|
19
23
|
onPopupScroll?: (event: UIEvent<HTMLDivElement>) => void;
|
|
20
24
|
};
|
|
21
|
-
/** The dropdown overlay: loading / empty / the
|
|
22
|
-
export declare const OptionList: ({ loading, options, notFoundContent, listHeight, isMultiple, listboxId, selectedValues, activeIndex, rowKey, optionDomId, onOptionActivate, onOptionSelect, onPopupScroll, }: OptionListProps) => ReactElement;
|
|
25
|
+
/** The dropdown overlay: loading / empty / the windowed listbox of options. */
|
|
26
|
+
export declare const OptionList: ({ loading, options, notFoundContent, listHeight, listItemHeight, isMultiple, listboxId, selectedValues, activeIndex, searchQuery, rowKey, optionDomId, onOptionActivate, onOptionSelect, onPopupScroll, }: OptionListProps) => ReactElement;
|
|
23
27
|
export {};
|
|
@@ -1,50 +1,223 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, memo, useRef, useLayoutEffect, useCallback, useEffect, useMemo } from "react";
|
|
3
|
+
import { VariableSizeList } from "react-window";
|
|
2
4
|
import Loader from "@synerise/ds-loader";
|
|
3
5
|
import Scrollbar from "@synerise/ds-scrollbar";
|
|
4
|
-
import { DropdownWrapper, Loading, NotFound, ScrollList, Inner
|
|
5
|
-
import { DEFAULT_LIST_HEIGHT,
|
|
6
|
+
import { VirtualRow, OptionItem, DropdownWrapper, Loading, NotFound, ScrollList, Inner } from "../Select.styles.js";
|
|
7
|
+
import { cx, DEFAULT_LIST_ITEM_HEIGHT, MAX_MEASURED_ROWS, DEFAULT_LIST_HEIGHT, OVERSCAN_COUNT } from "../utils/helpers.js";
|
|
8
|
+
const LIST_STYLE = {
|
|
9
|
+
overflowX: "unset",
|
|
10
|
+
overflowY: "unset"
|
|
11
|
+
};
|
|
12
|
+
const PresentationDiv = forwardRef((props, ref) => /* @__PURE__ */ jsx("div", { ref, role: "presentation", ...props }));
|
|
13
|
+
PresentationDiv.displayName = "SelectListWrapper";
|
|
14
|
+
const OptionRow = memo(({
|
|
15
|
+
index,
|
|
16
|
+
style,
|
|
17
|
+
data
|
|
18
|
+
}) => {
|
|
19
|
+
const {
|
|
20
|
+
options,
|
|
21
|
+
selectedValues,
|
|
22
|
+
activeIndex,
|
|
23
|
+
optionDomId,
|
|
24
|
+
onOptionActivate,
|
|
25
|
+
onOptionSelect,
|
|
26
|
+
measureRow
|
|
27
|
+
} = data;
|
|
28
|
+
const option = options[index];
|
|
29
|
+
const rowRef = useRef(null);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const height = rowRef.current?.offsetHeight ?? 0;
|
|
32
|
+
measureRow(index, height);
|
|
33
|
+
});
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
const node = rowRef.current;
|
|
36
|
+
if (!node || typeof ResizeObserver === "undefined") {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
const observer = new ResizeObserver(() => measureRow(index, node.offsetHeight));
|
|
40
|
+
observer.observe(node);
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, [index, measureRow]);
|
|
43
|
+
const isSelected = selectedValues.includes(option.value);
|
|
44
|
+
const optionAttrs = Object.fromEntries(Object.entries(option).filter(([key]) => key.startsWith("data-") || key.startsWith("aria-")));
|
|
45
|
+
return /* @__PURE__ */ jsx(
|
|
46
|
+
VirtualRow,
|
|
47
|
+
{
|
|
48
|
+
ref: rowRef,
|
|
49
|
+
role: "presentation",
|
|
50
|
+
style: {
|
|
51
|
+
...style,
|
|
52
|
+
height: "auto",
|
|
53
|
+
minHeight: style.height
|
|
54
|
+
},
|
|
55
|
+
children: /* @__PURE__ */ jsx(
|
|
56
|
+
OptionItem,
|
|
57
|
+
{
|
|
58
|
+
id: optionDomId(index),
|
|
59
|
+
role: "option",
|
|
60
|
+
className: cx("ds-select-item-option", isSelected && "ds-select-item-option-selected", index === activeIndex && "ds-select-item-option-active"),
|
|
61
|
+
selected: isSelected,
|
|
62
|
+
"aria-selected": isSelected,
|
|
63
|
+
"aria-setsize": options.length,
|
|
64
|
+
"aria-posinset": index + 1,
|
|
65
|
+
"data-testid": "select-option",
|
|
66
|
+
title: typeof option.title === "string" ? option.title : void 0,
|
|
67
|
+
text: option.children ?? option.label ?? option.value,
|
|
68
|
+
style: option.style,
|
|
69
|
+
disabled: option.disabled,
|
|
70
|
+
onMouseEnter: () => onOptionActivate(index),
|
|
71
|
+
onClick: () => onOptionSelect(option),
|
|
72
|
+
...optionAttrs
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
OptionRow.displayName = "SelectOptionRow";
|
|
6
79
|
const OptionList = ({
|
|
7
80
|
loading,
|
|
8
81
|
options,
|
|
9
82
|
notFoundContent,
|
|
10
83
|
listHeight,
|
|
84
|
+
listItemHeight,
|
|
11
85
|
isMultiple,
|
|
12
86
|
listboxId,
|
|
13
87
|
selectedValues,
|
|
14
88
|
activeIndex,
|
|
89
|
+
searchQuery,
|
|
15
90
|
rowKey,
|
|
16
91
|
optionDomId,
|
|
17
92
|
onOptionActivate,
|
|
18
93
|
onOptionSelect,
|
|
19
94
|
onPopupScroll
|
|
20
|
-
}) =>
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
95
|
+
}) => {
|
|
96
|
+
const listRef = useRef(null);
|
|
97
|
+
const scrollRef = useRef(null);
|
|
98
|
+
const scrollOffsetRef = useRef(0);
|
|
99
|
+
const sizeCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
100
|
+
const windowHeight = Number(listHeight) || DEFAULT_LIST_HEIGHT;
|
|
101
|
+
const rowHeight = listItemHeight && listItemHeight > 0 ? listItemHeight : DEFAULT_LIST_ITEM_HEIGHT;
|
|
102
|
+
const keyFor = useCallback((option) => rowKey ? rowKey(option) : option.key ?? option.value, [rowKey]);
|
|
103
|
+
const getItemSize = useCallback((index) => {
|
|
104
|
+
const option = options[index];
|
|
105
|
+
if (!option) {
|
|
106
|
+
return rowHeight;
|
|
107
|
+
}
|
|
108
|
+
return sizeCacheRef.current.get(keyFor(option)) ?? rowHeight;
|
|
109
|
+
}, [options, keyFor, rowHeight]);
|
|
110
|
+
const measureRow = useCallback((index, height) => {
|
|
111
|
+
const option = options[index];
|
|
112
|
+
if (!option || !height) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const key = keyFor(option);
|
|
116
|
+
if (sizeCacheRef.current.get(key) === height) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
sizeCacheRef.current.set(key, height);
|
|
120
|
+
listRef.current?.resetAfterIndex(index);
|
|
121
|
+
}, [options, keyFor]);
|
|
122
|
+
const prevRowHeightRef = useRef(rowHeight);
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
if (prevRowHeightRef.current === rowHeight) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
prevRowHeightRef.current = rowHeight;
|
|
128
|
+
sizeCacheRef.current.clear();
|
|
129
|
+
listRef.current?.resetAfterIndex(0);
|
|
130
|
+
}, [rowHeight]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const cache = sizeCacheRef.current;
|
|
133
|
+
if (cache.size > MAX_MEASURED_ROWS) {
|
|
134
|
+
const live = new Set(options.map(keyFor));
|
|
135
|
+
cache.forEach((_height, key) => {
|
|
136
|
+
if (!live.has(key)) {
|
|
137
|
+
cache.delete(key);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
listRef.current?.resetAfterIndex(0, false);
|
|
142
|
+
}, [options, keyFor]);
|
|
143
|
+
const scrollTo = useCallback((offset) => {
|
|
144
|
+
scrollOffsetRef.current = offset;
|
|
145
|
+
if (scrollRef.current) {
|
|
146
|
+
scrollRef.current.scrollTop = offset;
|
|
147
|
+
}
|
|
148
|
+
listRef.current?.scrollTo(offset);
|
|
149
|
+
}, []);
|
|
150
|
+
const isFirstQuery = useRef(true);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (isFirstQuery.current) {
|
|
153
|
+
isFirstQuery.current = false;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
scrollTo(0);
|
|
157
|
+
}, [searchQuery, scrollTo]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (activeIndex < 0 || activeIndex >= options.length) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
let top = 0;
|
|
163
|
+
for (let index = 0; index < activeIndex; index += 1) {
|
|
164
|
+
top += getItemSize(index);
|
|
165
|
+
}
|
|
166
|
+
const bottom = top + getItemSize(activeIndex);
|
|
167
|
+
const viewport = scrollRef.current?.clientHeight || windowHeight;
|
|
168
|
+
const current = scrollOffsetRef.current;
|
|
169
|
+
let next = current;
|
|
170
|
+
if (top < current) {
|
|
171
|
+
next = top;
|
|
172
|
+
} else if (bottom > current + viewport) {
|
|
173
|
+
next = bottom - viewport;
|
|
174
|
+
}
|
|
175
|
+
if (next !== current) {
|
|
176
|
+
scrollTo(Math.max(0, next));
|
|
177
|
+
}
|
|
178
|
+
}, [activeIndex, options, getItemSize, windowHeight, scrollTo]);
|
|
179
|
+
const handleScroll = useCallback((event) => {
|
|
180
|
+
const {
|
|
181
|
+
scrollTop
|
|
182
|
+
} = event.currentTarget;
|
|
183
|
+
scrollOffsetRef.current = scrollTop;
|
|
184
|
+
listRef.current?.scrollTo(scrollTop);
|
|
185
|
+
onPopupScroll?.(event);
|
|
186
|
+
}, [onPopupScroll]);
|
|
187
|
+
const itemData = useMemo(() => ({
|
|
188
|
+
options,
|
|
189
|
+
selectedValues,
|
|
190
|
+
activeIndex,
|
|
191
|
+
optionDomId,
|
|
192
|
+
onOptionActivate,
|
|
193
|
+
onOptionSelect,
|
|
194
|
+
measureRow
|
|
195
|
+
}), [options, selectedValues, activeIndex, optionDomId, onOptionActivate, onOptionSelect, measureRow]);
|
|
196
|
+
return /* @__PURE__ */ jsx(DropdownWrapper, { onMouseDown: (event) => {
|
|
197
|
+
const target = event.target;
|
|
198
|
+
if (!target.closest('input, textarea, [contenteditable="true"]')) {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
}
|
|
201
|
+
}, children: loading ? /* @__PURE__ */ jsx(Loading, { className: "ds-select-loading", children: /* @__PURE__ */ jsx(Loader, { size: "M" }) }) : options.length === 0 ? /* @__PURE__ */ jsx(NotFound, { className: "ds-select-empty", children: notFoundContent }) : /* @__PURE__ */ jsx(ScrollList, { children: /* @__PURE__ */ jsx(Scrollbar, { absolute: true, maxHeight: windowHeight, onScroll: handleScroll, ref: scrollRef, children: /* @__PURE__ */ jsx(Inner, { role: "listbox", id: listboxId, "aria-multiselectable": isMultiple || void 0, $maxHeight: windowHeight, children: /* @__PURE__ */ jsx(
|
|
202
|
+
VariableSizeList,
|
|
30
203
|
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
}
|
|
204
|
+
ref: listRef,
|
|
205
|
+
className: "ds-select-option-list",
|
|
206
|
+
width: "100%",
|
|
207
|
+
height: windowHeight,
|
|
208
|
+
itemCount: options.length,
|
|
209
|
+
itemSize: getItemSize,
|
|
210
|
+
estimatedItemSize: rowHeight,
|
|
211
|
+
itemData,
|
|
212
|
+
itemKey: (index, data) => keyFor(data.options[index]),
|
|
213
|
+
overscanCount: OVERSCAN_COUNT,
|
|
214
|
+
outerElementType: PresentationDiv,
|
|
215
|
+
innerElementType: PresentationDiv,
|
|
216
|
+
style: LIST_STYLE,
|
|
217
|
+
children: OptionRow
|
|
218
|
+
}
|
|
219
|
+
) }) }) }) });
|
|
220
|
+
};
|
|
48
221
|
export {
|
|
49
222
|
OptionList
|
|
50
223
|
};
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useMemo, useRef } from "react";
|
|
2
|
+
import { areOptionChildrenEqual } from "../utils/areOptionChildrenEqual.js";
|
|
2
3
|
import { getOptionsFromChildren } from "../utils/getOptionsFromChildren.js";
|
|
3
4
|
import { defaultFilter } from "../utils/helpers.js";
|
|
5
|
+
const useOptionsFromChildren = (children) => {
|
|
6
|
+
const cacheRef = useRef(null);
|
|
7
|
+
const cached = cacheRef.current;
|
|
8
|
+
if (cached && areOptionChildrenEqual(cached.children, children)) {
|
|
9
|
+
return cached.options;
|
|
10
|
+
}
|
|
11
|
+
const options = getOptionsFromChildren(children);
|
|
12
|
+
cacheRef.current = {
|
|
13
|
+
children,
|
|
14
|
+
options
|
|
15
|
+
};
|
|
16
|
+
return options;
|
|
17
|
+
};
|
|
4
18
|
const useSelectOptions = ({
|
|
5
19
|
options,
|
|
6
20
|
children,
|
|
@@ -10,12 +24,9 @@ const useSelectOptions = ({
|
|
|
10
24
|
isTags,
|
|
11
25
|
selectedValues
|
|
12
26
|
}) => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return getOptionsFromChildren(children);
|
|
18
|
-
}, [options, children]);
|
|
27
|
+
const hasOptionsProp = Boolean(options && options.length > 0);
|
|
28
|
+
const childrenOptions = useOptionsFromChildren(hasOptionsProp ? null : children);
|
|
29
|
+
const resolvedOptions = hasOptionsProp ? options : childrenOptions;
|
|
19
30
|
const displayedOptions = useMemo(() => {
|
|
20
31
|
let list = resolvedOptions;
|
|
21
32
|
if (filterOption !== false && effectiveQuery) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* True when two `children` node lists describe the same `<Select.Option>` set.
|
|
4
|
+
* Lets the option list keep its identity across a parent re-render that did not
|
|
5
|
+
* actually change the options (see `useSelectOptions`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const areOptionChildrenEqual: (a: ReactNode, b: ReactNode) => boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Children, isValidElement } from "react";
|
|
2
|
+
const MAX_DEPTH = 6;
|
|
3
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
|
|
4
|
+
const isEqualValue = (a, b, depth) => {
|
|
5
|
+
if (Object.is(a, b)) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (depth >= MAX_DEPTH) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
12
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => isEqualValue(item, b[index], depth + 1));
|
|
13
|
+
}
|
|
14
|
+
if (isValidElement(a) || isValidElement(b)) {
|
|
15
|
+
return isValidElement(a) && isValidElement(b) && a.type === b.type && a.key === b.key && isEqualValue(a.props, b.props, depth + 1);
|
|
16
|
+
}
|
|
17
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
18
|
+
const keysA = Object.keys(a);
|
|
19
|
+
const keysB = Object.keys(b);
|
|
20
|
+
return keysA.length === keysB.length && keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && isEqualValue(a[key], b[key], depth + 1));
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
const areOptionChildrenEqual = (a, b) => {
|
|
25
|
+
const listA = Children.toArray(a);
|
|
26
|
+
const listB = Children.toArray(b);
|
|
27
|
+
return listA.length === listB.length && listA.every((child, index) => isEqualValue(child, listB[index], 0));
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
areOptionChildrenEqual
|
|
31
|
+
};
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { FilterOptionFn, RawValueType, SelectValue } from '../Select.types';
|
|
2
2
|
/** Default max height (px) of the option list. */
|
|
3
3
|
export declare const DEFAULT_LIST_HEIGHT = 256;
|
|
4
|
+
/**
|
|
5
|
+
* Default height (px) of a single option row — `ds-list-item`'s `default` size.
|
|
6
|
+
* Used as the windowing estimate until a row reports its real height.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_LIST_ITEM_HEIGHT = 32;
|
|
9
|
+
/** Rows rendered above/below the visible window so scrolling never shows a gap. */
|
|
10
|
+
export declare const OVERSCAN_COUNT = 8;
|
|
11
|
+
/**
|
|
12
|
+
* Upper bound on remembered row measurements. Heights are cached per option (not
|
|
13
|
+
* per index) so filtering a list down and back keeps them usable — which means a
|
|
14
|
+
* remote-search select accumulates an entry for every option it has ever shown.
|
|
15
|
+
* Past this many, the cache is pruned to the options currently in the list.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_MEASURED_ROWS = 1000;
|
|
4
18
|
/** Join truthy class names into a single string. */
|
|
5
19
|
export declare const cx: (...classes: (string | false | undefined)[]) => string;
|
|
6
20
|
/** Normalise a `SelectValue` to an array of raw values. */
|
package/dist/utils/helpers.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const DEFAULT_LIST_HEIGHT = 256;
|
|
2
|
+
const DEFAULT_LIST_ITEM_HEIGHT = 32;
|
|
3
|
+
const OVERSCAN_COUNT = 8;
|
|
4
|
+
const MAX_MEASURED_ROWS = 1e3;
|
|
2
5
|
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
3
6
|
const toArray = (value) => {
|
|
4
7
|
if (value === void 0 || value === null) {
|
|
@@ -13,6 +16,9 @@ const defaultFilter = (optionFilterProp) => (input, option) => {
|
|
|
13
16
|
};
|
|
14
17
|
export {
|
|
15
18
|
DEFAULT_LIST_HEIGHT,
|
|
19
|
+
DEFAULT_LIST_ITEM_HEIGHT,
|
|
20
|
+
MAX_MEASURED_ROWS,
|
|
21
|
+
OVERSCAN_COUNT,
|
|
16
22
|
cx,
|
|
17
23
|
defaultFilter,
|
|
18
24
|
toArray
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synerise/ds-select",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Select UI Component for the Synerise Design System",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"repository": "synerise/synerise-design",
|
|
@@ -42,15 +42,16 @@
|
|
|
42
42
|
],
|
|
43
43
|
"types": "dist/index.d.ts",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@synerise/ds-dropdown": "^2.0.
|
|
46
|
-
"@synerise/ds-form-field": "^2.0.
|
|
47
|
-
"@synerise/ds-icon": "^2.
|
|
48
|
-
"@synerise/ds-list-item": "^2.0.
|
|
45
|
+
"@synerise/ds-dropdown": "^2.0.1",
|
|
46
|
+
"@synerise/ds-form-field": "^2.0.1",
|
|
47
|
+
"@synerise/ds-icon": "^2.1.0",
|
|
48
|
+
"@synerise/ds-list-item": "^2.0.1",
|
|
49
49
|
"@synerise/ds-loader": "^2.0.0",
|
|
50
|
-
"@synerise/ds-scrollbar": "^2.0.
|
|
51
|
-
"@synerise/ds-tooltip": "^2.0.
|
|
50
|
+
"@synerise/ds-scrollbar": "^2.0.1",
|
|
51
|
+
"@synerise/ds-tooltip": "^2.0.1",
|
|
52
52
|
"@synerise/ds-utils": "^2.0.0",
|
|
53
|
-
"classnames": "^2.5.1"
|
|
53
|
+
"classnames": "^2.5.1",
|
|
54
|
+
"react-window": "^1.8.11"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
57
|
"@synerise/ds-core": "*",
|
|
@@ -58,5 +59,5 @@
|
|
|
58
59
|
"styled-components": "^5.3.3",
|
|
59
60
|
"vitest": "4"
|
|
60
61
|
},
|
|
61
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "1dc97a2bae9f1d20c3f35e6672d059d502c90d1a"
|
|
62
63
|
}
|