@noya-app/noya-designsystem 0.1.74 → 0.1.76

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
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.31",
26
+ "@noya-app/noya-colorpicker": "0.1.32",
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("");
@@ -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
- }, [filter, initialValueString, props, state, comboboxState]);
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
- [initialValueString, onFocus, openMenuBehavior, comboboxState]
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
  );
@@ -203,9 +203,48 @@ export class ComboboxState<T extends string> {
203
203
  this.notifyChange();
204
204
  }
205
205
 
206
- reset(newFilter = "") {
206
+ reset(
207
+ newFilter = "",
208
+ options?: {
209
+ selectedItemTitle?: string;
210
+ selectedItemValue?: string | number;
211
+ }
212
+ ) {
207
213
  this.filter = newFilter;
208
- this.selectedIndex = 0;
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