@noya-app/noya-designsystem 0.1.73 → 0.1.75
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +13 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +112 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/combobox.test.ts +18 -0
- package/src/components/BaseToolbar.tsx +27 -13
- package/src/components/Combobox.tsx +48 -5
- package/src/components/InputField.tsx +36 -4
- package/src/components/Spacer.tsx +18 -4
- package/src/utils/combobox.ts +41 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@base-ui-components/react": "1.0.0-beta.4",
|
|
24
24
|
"@dnd-kit/core": "6.3.1",
|
|
25
25
|
"@dnd-kit/sortable": "10.0.0",
|
|
26
|
-
"@noya-app/noya-colorpicker": "0.1.
|
|
26
|
+
"@noya-app/noya-colorpicker": "0.1.31",
|
|
27
27
|
"@noya-app/noya-utils": "0.1.9",
|
|
28
28
|
"@noya-app/noya-geometry": "0.1.16",
|
|
29
29
|
"@noya-app/noya-icons": "0.1.16",
|
|
@@ -156,6 +156,24 @@ test("resets state", () => {
|
|
|
156
156
|
expect(snapshot.selectedIndex).toBe(0);
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
test("reset keeps selection aligned to provided title", () => {
|
|
160
|
+
const state = new ComboboxState(testItems, "", true);
|
|
161
|
+
state.setFilter("font");
|
|
162
|
+
state.moveSelection("down");
|
|
163
|
+
state.selectCurrentItem();
|
|
164
|
+
|
|
165
|
+
state.reset("", { selectedItemTitle: "font-italic" });
|
|
166
|
+
const snapshot = state.getSnapshot();
|
|
167
|
+
|
|
168
|
+
const matchedIndex = snapshot.filteredItems.findIndex(
|
|
169
|
+
(item) =>
|
|
170
|
+
isSelectableMenuItem(item) &&
|
|
171
|
+
item.title?.toString() === "font-italic"
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(snapshot.selectedIndex).toBe(matchedIndex);
|
|
175
|
+
});
|
|
176
|
+
|
|
159
177
|
test("handles empty query", () => {
|
|
160
178
|
const state = new ComboboxState(testItems, "", true);
|
|
161
179
|
state.setFilter("");
|
|
@@ -62,6 +62,23 @@ export function BaseToolbar({
|
|
|
62
62
|
innerClassName,
|
|
63
63
|
enableAbsoluteBreakpoint = true,
|
|
64
64
|
}: BaseToolbarProps) {
|
|
65
|
+
const childrenContainerClassName = cx(
|
|
66
|
+
"n-flex n-items-center n-justify-center n-text-text-muted n-pointer-events-none n-inset-0 n-pl-1 n-pr-1 n-min-w-0",
|
|
67
|
+
"@xl/toolbar:!n-absolute @xl/toolbar:!n-pl-0 @xl/toolbar:!n-pr-0"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const childrenElement = children && (
|
|
71
|
+
<div className={childrenContainerClassName}>
|
|
72
|
+
<div className="n-flex n-items-center n-justify-center n-pointer-events-auto">
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const leftElement = left && (
|
|
79
|
+
<div className="n-flex n-gap-toolbar-separator">{left}</div>
|
|
80
|
+
);
|
|
81
|
+
|
|
65
82
|
return (
|
|
66
83
|
<BaseToolbarContainer
|
|
67
84
|
showDivider={showDivider}
|
|
@@ -76,23 +93,20 @@ export function BaseToolbar({
|
|
|
76
93
|
</>
|
|
77
94
|
)}
|
|
78
95
|
<Spacer.Horizontal size={10} />
|
|
79
|
-
{
|
|
96
|
+
{childrenElement}
|
|
97
|
+
{leftElement && (
|
|
80
98
|
<>
|
|
81
|
-
|
|
99
|
+
{childrenElement && (
|
|
100
|
+
<>
|
|
101
|
+
<Spacer.Horizontal size={10} className="@xl/toolbar:!n-hidden" />
|
|
102
|
+
<DividerVertical className="@xl/toolbar:!n-hidden" />
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
<Spacer.Horizontal size={10} className="@xl/toolbar:!n-hidden" />
|
|
106
|
+
{leftElement}
|
|
82
107
|
<Spacer.Horizontal size={10} />
|
|
83
108
|
</>
|
|
84
109
|
)}
|
|
85
|
-
{children && (
|
|
86
|
-
<div
|
|
87
|
-
className={
|
|
88
|
-
"n-flex n-items-center n-justify-center n-text-text-muted n-pointer-events-none @xl/toolbar:!n-absolute n-inset-0"
|
|
89
|
-
}
|
|
90
|
-
>
|
|
91
|
-
<div className="n-flex n-items-center n-justify-center n-pointer-events-auto">
|
|
92
|
-
{children}
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
)}
|
|
96
110
|
<Spacer.Horizontal />
|
|
97
111
|
<Spacer.Horizontal size={10} />
|
|
98
112
|
<div className="n-flex n-gap-toolbar-separator">{right}</div>
|
|
@@ -154,6 +154,17 @@ export const Combobox = memoGeneric(
|
|
|
154
154
|
|
|
155
155
|
const initialValueString = getInitialValueString();
|
|
156
156
|
|
|
157
|
+
const resetSelectionOptions = useMemo(
|
|
158
|
+
() =>
|
|
159
|
+
props.mode === "string"
|
|
160
|
+
? { selectedItemTitle: initialValueString }
|
|
161
|
+
: {
|
|
162
|
+
selectedItemTitle: initialValueString,
|
|
163
|
+
selectedItemValue: props.value?.value?.toString(),
|
|
164
|
+
},
|
|
165
|
+
[initialValueString, props.mode, props.value]
|
|
166
|
+
);
|
|
167
|
+
|
|
157
168
|
const [comboboxState] = useState(
|
|
158
169
|
() =>
|
|
159
170
|
new ComboboxState(
|
|
@@ -165,6 +176,7 @@ export const Combobox = memoGeneric(
|
|
|
165
176
|
const state = useComboboxState(comboboxState);
|
|
166
177
|
const [isFocused, setIsFocused] = useState(false);
|
|
167
178
|
const listRef = useRef<ListView.VirtualizedList>(null);
|
|
179
|
+
const menuOpenedRef = useRef(false);
|
|
168
180
|
const [shouldBlur, setShouldBlur] = useState(false);
|
|
169
181
|
const { fieldId: id } = useLabel({
|
|
170
182
|
fieldId: rest.id,
|
|
@@ -190,9 +202,24 @@ export const Combobox = memoGeneric(
|
|
|
190
202
|
}
|
|
191
203
|
}, [selectedIndex]);
|
|
192
204
|
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!shouldShowMenu) {
|
|
207
|
+
menuOpenedRef.current = false;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (menuOpenedRef.current) return;
|
|
212
|
+
|
|
213
|
+
menuOpenedRef.current = true;
|
|
214
|
+
|
|
215
|
+
if (listRef.current) {
|
|
216
|
+
listRef.current.scrollToIndex(selectedIndex);
|
|
217
|
+
}
|
|
218
|
+
}, [shouldShowMenu, selectedIndex]);
|
|
219
|
+
|
|
193
220
|
const handleBlur = useCallback(() => {
|
|
194
221
|
ref.current?.blur();
|
|
195
|
-
comboboxState.reset(initialValueString);
|
|
222
|
+
comboboxState.reset(initialValueString, resetSelectionOptions);
|
|
196
223
|
if (props.mode === "string") {
|
|
197
224
|
props.onBlur?.(
|
|
198
225
|
state.filter === state.lastSubmittedValue.filter,
|
|
@@ -208,13 +235,21 @@ export const Combobox = memoGeneric(
|
|
|
208
235
|
}
|
|
209
236
|
}
|
|
210
237
|
setIsFocused(false);
|
|
211
|
-
}, [
|
|
238
|
+
}, [
|
|
239
|
+
filter,
|
|
240
|
+
initialValueString,
|
|
241
|
+
props,
|
|
242
|
+
resetSelectionOptions,
|
|
243
|
+
state,
|
|
244
|
+
comboboxState,
|
|
245
|
+
]);
|
|
212
246
|
|
|
213
247
|
const handleFocus: FocusEventHandler<HTMLInputElement> = useCallback(
|
|
214
248
|
(event) => {
|
|
215
249
|
setIsFocused(true);
|
|
216
250
|
comboboxState.reset(
|
|
217
|
-
openMenuBehavior === "showAllItems" ? "" : initialValueString
|
|
251
|
+
openMenuBehavior === "showAllItems" ? "" : initialValueString,
|
|
252
|
+
resetSelectionOptions
|
|
218
253
|
);
|
|
219
254
|
if (ref.current) {
|
|
220
255
|
const length = ref.current.value.length;
|
|
@@ -222,7 +257,13 @@ export const Combobox = memoGeneric(
|
|
|
222
257
|
}
|
|
223
258
|
onFocus?.(event);
|
|
224
259
|
},
|
|
225
|
-
[
|
|
260
|
+
[
|
|
261
|
+
initialValueString,
|
|
262
|
+
onFocus,
|
|
263
|
+
openMenuBehavior,
|
|
264
|
+
resetSelectionOptions,
|
|
265
|
+
comboboxState,
|
|
266
|
+
]
|
|
226
267
|
);
|
|
227
268
|
|
|
228
269
|
const handleUpdateSelection = useCallback(
|
|
@@ -415,7 +456,8 @@ export const Combobox = memoGeneric(
|
|
|
415
456
|
|
|
416
457
|
// Use openMenuBehavior consistently for both focus and chevron click
|
|
417
458
|
comboboxState.reset(
|
|
418
|
-
openMenuBehavior === "showAllItems" ? "" : initialValueString
|
|
459
|
+
openMenuBehavior === "showAllItems" ? "" : initialValueString,
|
|
460
|
+
resetSelectionOptions
|
|
419
461
|
);
|
|
420
462
|
ref.current?.focus();
|
|
421
463
|
},
|
|
@@ -424,6 +466,7 @@ export const Combobox = memoGeneric(
|
|
|
424
466
|
openMenuBehavior,
|
|
425
467
|
initialValueString,
|
|
426
468
|
handleBlur,
|
|
469
|
+
resetSelectionOptions,
|
|
427
470
|
comboboxState,
|
|
428
471
|
]
|
|
429
472
|
);
|
|
@@ -198,6 +198,11 @@ type InputElementProps = {
|
|
|
198
198
|
value?: string;
|
|
199
199
|
onSubmit?: (value: string) => void;
|
|
200
200
|
as?: "input" | "span";
|
|
201
|
+
/**
|
|
202
|
+
* If false, don't attach the shared inputRef to this element.
|
|
203
|
+
* Useful for decorative elements like the typeahead overlay.
|
|
204
|
+
*/
|
|
205
|
+
attachInputRef?: boolean;
|
|
201
206
|
};
|
|
202
207
|
|
|
203
208
|
const InputElement = forwardRef(
|
|
@@ -213,13 +218,17 @@ const InputElement = forwardRef(
|
|
|
213
218
|
onSubmit,
|
|
214
219
|
as = "input",
|
|
215
220
|
style,
|
|
221
|
+
attachInputRef = true,
|
|
216
222
|
...props
|
|
217
223
|
}: InputElementProps,
|
|
218
224
|
forwardedRef: ForwardedRef<any>
|
|
219
225
|
) => {
|
|
220
226
|
const { endWidth, inputRef, size, id, startWidth } = useInputFieldContext();
|
|
221
227
|
|
|
222
|
-
const ref = useMergeRefs(
|
|
228
|
+
const ref = useMergeRefs(
|
|
229
|
+
attachInputRef ? inputRef : null,
|
|
230
|
+
forwardedRef
|
|
231
|
+
);
|
|
223
232
|
|
|
224
233
|
const spacingStyles = getFieldSpacing({
|
|
225
234
|
endWidth,
|
|
@@ -322,6 +331,7 @@ const InputFieldTypeahead = (props: {
|
|
|
322
331
|
return (
|
|
323
332
|
<InputElement
|
|
324
333
|
as="span"
|
|
334
|
+
attachInputRef={false}
|
|
325
335
|
style={useMemo(
|
|
326
336
|
() => ({
|
|
327
337
|
position: "absolute",
|
|
@@ -551,8 +561,10 @@ function InputFieldRoot({
|
|
|
551
561
|
const startRef = React.useRef<HTMLSpanElement>(null);
|
|
552
562
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
553
563
|
const endRef = React.useRef<HTMLSpanElement>(null);
|
|
564
|
+
const rootRef = React.useRef<HTMLDivElement>(null);
|
|
554
565
|
|
|
555
566
|
const measuredInputSize = useSize(inputRef, "width");
|
|
567
|
+
const measuredRootSize = useSize(rootRef, "width");
|
|
556
568
|
const measuredStartSize = useSize(startRef, "width");
|
|
557
569
|
const measuredEndSize = useSize(endRef, "width");
|
|
558
570
|
const labelType = useLabelType();
|
|
@@ -596,7 +608,12 @@ function InputFieldRoot({
|
|
|
596
608
|
);
|
|
597
609
|
|
|
598
610
|
const rootElement = (
|
|
599
|
-
<RootContainer
|
|
611
|
+
<RootContainer
|
|
612
|
+
ref={rootRef}
|
|
613
|
+
$width={width}
|
|
614
|
+
style={style}
|
|
615
|
+
className={className}
|
|
616
|
+
>
|
|
600
617
|
{children}
|
|
601
618
|
{start && (
|
|
602
619
|
<span ref={startRef} className={cx(startBaseStyles, startClassName)}>
|
|
@@ -617,10 +634,25 @@ function InputFieldRoot({
|
|
|
617
634
|
</RootContainer>
|
|
618
635
|
);
|
|
619
636
|
|
|
637
|
+
const measuredWidth = useMemo(() => {
|
|
638
|
+
const inputWidth =
|
|
639
|
+
measuredInputSize && measuredInputSize.width > 0
|
|
640
|
+
? measuredInputSize.width
|
|
641
|
+
: undefined;
|
|
642
|
+
const rootWidth =
|
|
643
|
+
measuredRootSize && measuredRootSize.width > 0
|
|
644
|
+
? measuredRootSize.width
|
|
645
|
+
: undefined;
|
|
646
|
+
|
|
647
|
+
return inputWidth ?? rootWidth ?? width;
|
|
648
|
+
}, [measuredInputSize, measuredRootSize, width]);
|
|
649
|
+
|
|
620
650
|
const measuredWidthObject = useMemo(
|
|
621
651
|
() =>
|
|
622
|
-
|
|
623
|
-
|
|
652
|
+
measuredWidth && measuredWidth > 0
|
|
653
|
+
? { width: measuredWidth + 4 }
|
|
654
|
+
: undefined,
|
|
655
|
+
[measuredWidth]
|
|
624
656
|
);
|
|
625
657
|
|
|
626
658
|
return (
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ForwardedRef } from "react";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { cx } from "../utils/classNames";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
size?: number | string;
|
|
6
7
|
inline?: boolean;
|
|
8
|
+
className?: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/* ----------------------------------------------------------------------------
|
|
@@ -11,10 +13,16 @@ interface Props {
|
|
|
11
13
|
* ------------------------------------------------------------------------- */
|
|
12
14
|
|
|
13
15
|
const SpacerVertical = React.forwardRef<HTMLSpanElement, Props>(
|
|
14
|
-
(
|
|
16
|
+
(
|
|
17
|
+
{ size, inline, className, ...props },
|
|
18
|
+
ref: ForwardedRef<HTMLSpanElement>
|
|
19
|
+
) => {
|
|
15
20
|
return (
|
|
16
21
|
<span
|
|
17
|
-
className={
|
|
22
|
+
className={cx(
|
|
23
|
+
`${inline ? "n-inline-block" : "n-block"} ${size === undefined ? "n-flex n-flex-1" : ""}`,
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
18
26
|
style={
|
|
19
27
|
size
|
|
20
28
|
? {
|
|
@@ -34,10 +42,16 @@ const SpacerVertical = React.forwardRef<HTMLSpanElement, Props>(
|
|
|
34
42
|
* ------------------------------------------------------------------------- */
|
|
35
43
|
|
|
36
44
|
const SpacerHorizontal = React.forwardRef<HTMLSpanElement, Props>(
|
|
37
|
-
(
|
|
45
|
+
(
|
|
46
|
+
{ size, inline, className, ...props },
|
|
47
|
+
ref: ForwardedRef<HTMLSpanElement>
|
|
48
|
+
) => {
|
|
38
49
|
return (
|
|
39
50
|
<span
|
|
40
|
-
className={
|
|
51
|
+
className={cx(
|
|
52
|
+
`${inline ? "n-inline-block" : "n-block"} ${size === undefined ? "n-flex n-flex-1" : ""}`,
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
41
55
|
style={
|
|
42
56
|
size
|
|
43
57
|
? {
|
package/src/utils/combobox.ts
CHANGED
|
@@ -203,9 +203,48 @@ export class ComboboxState<T extends string> {
|
|
|
203
203
|
this.notifyChange();
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
reset(
|
|
206
|
+
reset(
|
|
207
|
+
newFilter = "",
|
|
208
|
+
options?: {
|
|
209
|
+
selectedItemTitle?: string;
|
|
210
|
+
selectedItemValue?: string | number;
|
|
211
|
+
}
|
|
212
|
+
) {
|
|
207
213
|
this.filter = newFilter;
|
|
208
|
-
|
|
214
|
+
|
|
215
|
+
const filteredItems = this.getFilteredItems();
|
|
216
|
+
const normalizedTitle = options?.selectedItemTitle
|
|
217
|
+
? options.selectedItemTitle.toString().toLowerCase()
|
|
218
|
+
: undefined;
|
|
219
|
+
const normalizedValue = options?.selectedItemValue
|
|
220
|
+
? options.selectedItemValue.toString().toLowerCase()
|
|
221
|
+
: undefined;
|
|
222
|
+
|
|
223
|
+
const matchedIndex =
|
|
224
|
+
normalizedTitle || normalizedValue
|
|
225
|
+
? filteredItems.findIndex((item) => {
|
|
226
|
+
if (!isSelectableMenuItem(item)) return false;
|
|
227
|
+
|
|
228
|
+
const title = item.title?.toString().toLowerCase();
|
|
229
|
+
const value = item.value?.toString().toLowerCase();
|
|
230
|
+
|
|
231
|
+
if (normalizedValue && value === normalizedValue) return true;
|
|
232
|
+
if (normalizedTitle && title === normalizedTitle) return true;
|
|
233
|
+
return false;
|
|
234
|
+
})
|
|
235
|
+
: -1;
|
|
236
|
+
|
|
237
|
+
if (matchedIndex !== -1) {
|
|
238
|
+
this.selectedIndex = matchedIndex;
|
|
239
|
+
this.notifyChange();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const firstSelectableIndex = filteredItems.findIndex((item): boolean =>
|
|
244
|
+
isSelectableMenuItem(item)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
this.selectedIndex = firstSelectableIndex === -1 ? 0 : firstSelectableIndex;
|
|
209
248
|
this.notifyChange();
|
|
210
249
|
}
|
|
211
250
|
|