@proyecto-viviana/solid-stately 0.4.0 → 0.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.
Files changed (34) hide show
  1. package/dist/calendar/createDateFieldState.d.ts +56 -18
  2. package/dist/calendar/createRangeCalendarState.d.ts +10 -0
  3. package/dist/calendar/createTimeFieldState.d.ts +38 -67
  4. package/dist/calendar/intl/index.d.ts +10 -0
  5. package/dist/collections/ListCollection.d.ts +8 -0
  6. package/dist/collections/createListState.d.ts +12 -0
  7. package/dist/collections/getChildNodes.d.ts +8 -0
  8. package/dist/collections/index.d.ts +2 -2
  9. package/dist/collections/types.d.ts +12 -0
  10. package/dist/color/intl/index.d.ts +14 -0
  11. package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +3451 -1309
  14. package/dist/index.js.map +1 -1
  15. package/dist/selection/SelectionManager.d.ts +9 -0
  16. package/package.json +1 -1
  17. package/src/calendar/createCalendarState.ts +26 -4
  18. package/src/calendar/createDateFieldState.ts +614 -459
  19. package/src/calendar/createRangeCalendarState.ts +212 -11
  20. package/src/calendar/createTimeFieldState.ts +179 -467
  21. package/src/calendar/intl/index.ts +364 -0
  22. package/src/collections/ListCollection.ts +40 -1
  23. package/src/collections/createListState.ts +105 -2
  24. package/src/collections/getChildNodes.ts +31 -0
  25. package/src/collections/index.ts +3 -0
  26. package/src/collections/types.ts +13 -0
  27. package/src/color/Color.ts +104 -133
  28. package/src/color/intl/index.ts +1409 -0
  29. package/src/combobox/createComboBoxState.ts +48 -16
  30. package/src/dnd/createDroppableCollectionState.ts +125 -6
  31. package/src/index.ts +3 -0
  32. package/src/selection/SelectionManager.ts +15 -0
  33. package/src/steplist/createStepListState.ts +27 -4
  34. package/src/tabs/createTabListState.ts +9 -1
@@ -6,7 +6,7 @@
6
6
  * either type to filter options or select from a list.
7
7
  */
8
8
 
9
- import { createSignal, createMemo, createEffect, type Accessor } from "solid-js";
9
+ import { createSignal, createMemo, createEffect, untrack, type Accessor } from "solid-js";
10
10
  import { access, type MaybeAccessor } from "../utils";
11
11
  import { createListState } from "../collections/createListState";
12
12
  import { createOverlayTriggerState } from "../overlays";
@@ -373,6 +373,28 @@ export function createComboBoxState<T = unknown>(
373
373
  };
374
374
 
375
375
  // ---- Open/Toggle Logic ----
376
+ // Auto-focus the menu on open the way upstream's listbox does via its
377
+ // `autoFocus: state.focusStrategy || true` prop (useComboBox.ts:490 → useSelectableCollection):
378
+ // the first selectable selected key, else the first/last key for an explicit
379
+ // strategy, else nothing. The port drives this from the state (createListBox
380
+ // reimplements nav inline and does not honor a listbox `autoFocus` prop), so
381
+ // BOTH open() and toggle() must apply it — a button/mouse open routes through
382
+ // toggle(), and omitting it there left the menu opened with no active option
383
+ // (no aria-activedescendant / no highlighted row) unlike upstream.
384
+ const applyOpenFocus = (strategy: FocusStrategy | null) => {
385
+ const key = selectedKey();
386
+ const nextFocusedKey =
387
+ key != null && !listState.isDisabled(key)
388
+ ? key
389
+ : strategy === "first"
390
+ ? displayedCollection().getFirstKey()
391
+ : strategy === "last"
392
+ ? displayedCollection().getLastKey()
393
+ : null;
394
+ listState.setFocused(true);
395
+ listState.setFocusedKey(nextFocusedKey);
396
+ };
397
+
376
398
  const open = (strategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
377
399
  const displayAll = trigger === "manual" || (trigger === "focus" && menuTrigger() === "focus");
378
400
 
@@ -391,17 +413,7 @@ export function createComboBoxState<T = unknown>(
391
413
  menuOpenTrigger = trigger ?? "focus";
392
414
  setFocusStrategy(strategy);
393
415
  if (!overlayState.isOpen()) {
394
- const key = selectedKey();
395
- const nextFocusedKey =
396
- key != null && !listState.isDisabled(key)
397
- ? key
398
- : strategy === "first"
399
- ? displayedCollection().getFirstKey()
400
- : strategy === "last"
401
- ? displayedCollection().getLastKey()
402
- : null;
403
- listState.setFocused(true);
404
- listState.setFocusedKey(nextFocusedKey);
416
+ applyOpenFocus(strategy);
405
417
  }
406
418
  overlayState.open();
407
419
  };
@@ -417,15 +429,20 @@ export function createComboBoxState<T = unknown>(
417
429
 
418
430
  if (!canOpen && !overlayState.isOpen()) return;
419
431
 
420
- if (displayAll && !overlayState.isOpen()) {
432
+ const willOpen = !overlayState.isOpen();
433
+
434
+ if (displayAll && willOpen) {
421
435
  setShowAllItems(true);
422
436
  }
423
437
 
424
- if (!overlayState.isOpen()) {
438
+ if (willOpen) {
425
439
  menuOpenTrigger = trigger ?? "focus";
426
440
  }
427
441
 
428
442
  setFocusStrategy(strategy);
443
+ if (willOpen) {
444
+ applyOpenFocus(strategy);
445
+ }
429
446
  overlayState.toggle();
430
447
  };
431
448
 
@@ -552,7 +569,22 @@ export function createComboBoxState<T = unknown>(
552
569
  }
553
570
  });
554
571
 
555
- // Update input when selection changes (only in single mode)
572
+ // Keep the input text in sync with the selected item (single mode only).
573
+ //
574
+ // This effect must react to the SELECTION and the COLLECTION — when the
575
+ // selected key changes, or when its item (and thus textValue) resolves as
576
+ // items load — but it must NOT react to the input value itself. The previous
577
+ // port compared `textValue !== inputValue()`, reading `inputValue()` inside
578
+ // the effect; in Solid that subscribes the effect to the input signal, so it
579
+ // re-ran on every keystroke and — while any selection was active — immediately
580
+ // reset the input back to the selected item's text. The result: the user
581
+ // could neither type nor delete in the field.
582
+ //
583
+ // The comparison is unchanged, but `inputValue()` is now read via `untrack`,
584
+ // so it is used only to decide whether a reset is needed and never becomes a
585
+ // dependency. `originalCollection()` stays tracked (the unfiltered collection
586
+ // only changes when `items` change, never on filtering) so a preset selection
587
+ // still gets its text once async items arrive.
556
588
  createEffect(() => {
557
589
  if (isMultiple()) return;
558
590
  const key = selectedKey();
@@ -561,7 +593,7 @@ export function createComboBoxState<T = unknown>(
561
593
 
562
594
  // Only update if selection changed and not fully controlled
563
595
  if (!isInputControlled() || !isSelectionControlled()) {
564
- if (key != null && textValue !== inputValue()) {
596
+ if (key != null && textValue !== untrack(inputValue)) {
565
597
  setInputValue(textValue);
566
598
  setLastValue(textValue);
567
599
  }
@@ -21,6 +21,7 @@ import type {
21
21
  DroppableCollectionReorderEvent,
22
22
  ItemDropTarget,
23
23
  } from "./types";
24
+ import type { Collection } from "../collections/types";
24
25
 
25
26
  export interface DroppableCollectionStateOptions {
26
27
  /**
@@ -56,6 +57,14 @@ export interface DroppableCollectionStateOptions {
56
57
  onMove?: (e: DroppableCollectionReorderEvent) => void;
57
58
  /** A function returning whether a given target is a valid "on" drop target. */
58
59
  shouldAcceptItemDrop?: (target: ItemDropTarget, types: DragTypes) => boolean;
60
+ /**
61
+ * The host collection. Used by the default drop-operation feature detection to
62
+ * confirm a reorder stays within the same parent and to prevent dropping an
63
+ * item onto itself or a descendant. Optional: a flat collection (e.g. a plain
64
+ * ListBox) is always "within parent" and has no descendants, so omitting it is
65
+ * faithful for flat hosts.
66
+ */
67
+ collection?: Collection<unknown>;
59
68
  /** Whether the droppable collection is disabled. */
60
69
  isDisabled?: boolean;
61
70
  }
@@ -86,11 +95,19 @@ export interface DroppableCollectionState {
86
95
  isInternal: boolean,
87
96
  draggingKeys?: Set<string | number>,
88
97
  ): void;
89
- /** Get the drop operation for a target. */
98
+ /**
99
+ * Get the drop operation for a target. `isInternal` is true when the active
100
+ * drag originated from this same collection; `draggingKeys` are the keys being
101
+ * dragged. Both feed the handler-aware feature detection that decides whether a
102
+ * given drop position (before/after/on/root) is a valid target — e.g. a
103
+ * reorder-only collection rejects "on" targets, matching React Aria.
104
+ */
90
105
  getDropOperation(
91
106
  target: DropTarget,
92
107
  types: DragTypes,
93
108
  allowedOperations: DropOperation[],
109
+ isInternal?: boolean,
110
+ draggingKeys?: Set<string | number>,
94
111
  ): DropOperation;
95
112
  /** Check if an item drop should be accepted. */
96
113
  shouldAcceptItemDrop(target: ItemDropTarget, types: DragTypes): boolean;
@@ -287,22 +304,124 @@ export function createDroppableCollectionState(
287
304
  }
288
305
  };
289
306
 
290
- const getDropOperation = (
291
- dropTarget: DropTarget,
307
+ // Faithful of react-stately's `isDraggingWithinParent`: every dragged item must
308
+ // share the target's parent for a reorder to be valid. A flat collection (no
309
+ // `collection` provided) has a single implicit parent, so this is always true.
310
+ const isDraggingWithinParent = (
311
+ target: ItemDropTarget,
312
+ draggingKeys: Set<string | number>,
313
+ ): boolean => {
314
+ const collection = getProps().collection;
315
+ if (!collection) return true;
316
+ const targetNode = collection.getItem(target.key);
317
+ for (const key of draggingKeys) {
318
+ const node = collection.getItem(key);
319
+ if (node?.parentKey !== targetNode?.parentKey) {
320
+ return false;
321
+ }
322
+ }
323
+ return true;
324
+ };
325
+
326
+ // Faithful of react-stately's `defaultGetDropOperation`: decide whether a drop
327
+ // position is a valid target from the handlers that are actually wired. A
328
+ // reorder-only collection (only `onReorder`) accepts before/after but rejects
329
+ // "on"; an item-drop collection accepts "on"; and so on.
330
+ const defaultGetDropOperation = (
331
+ target: DropTarget,
292
332
  types: DragTypes,
293
333
  allowedOperations: DropOperation[],
334
+ isInternal: boolean,
335
+ draggingKeys: Set<string | number>,
294
336
  ): DropOperation => {
295
337
  const p = getProps();
296
338
 
339
+ if (p.isDisabled || !target) {
340
+ return "cancel";
341
+ }
342
+
297
343
  if (!isAccepted(types)) {
298
344
  return "cancel";
299
345
  }
300
346
 
301
- if (typeof p.getDropOperation === "function") {
302
- return p.getDropOperation(dropTarget, types, allowedOperations);
347
+ const isValidInsert =
348
+ p.onInsert &&
349
+ target.type === "item" &&
350
+ !isInternal &&
351
+ (target.dropPosition === "before" || target.dropPosition === "after");
352
+ const isValidReorder =
353
+ p.onReorder &&
354
+ target.type === "item" &&
355
+ isInternal &&
356
+ (target.dropPosition === "before" || target.dropPosition === "after") &&
357
+ isDraggingWithinParent(target, draggingKeys);
358
+
359
+ const isItemDropAllowed =
360
+ target.type !== "item" ||
361
+ target.dropPosition !== "on" ||
362
+ !p.shouldAcceptItemDrop ||
363
+ p.shouldAcceptItemDrop(target, types);
364
+
365
+ const isValidMove = p.onMove && target.type === "item" && isInternal && isItemDropAllowed;
366
+
367
+ // Feedback was that internal root drop was weird, so it is prevented.
368
+ const isValidRootDrop = p.onRootDrop && target.type === "root" && !isInternal;
369
+
370
+ // Automatically prevent items (e.g. folders) from being dropped on themselves.
371
+ const isValidOnItemDrop =
372
+ p.onItemDrop &&
373
+ target.type === "item" &&
374
+ target.dropPosition === "on" &&
375
+ !(isInternal && target.key != null && draggingKeys.has(target.key)) &&
376
+ isItemDropAllowed;
377
+
378
+ if (
379
+ p.onDrop ||
380
+ isValidInsert ||
381
+ isValidReorder ||
382
+ isValidMove ||
383
+ isValidRootDrop ||
384
+ isValidOnItemDrop
385
+ ) {
386
+ if (typeof p.getDropOperation === "function") {
387
+ return p.getDropOperation(target, types, allowedOperations);
388
+ }
389
+ return allowedOperations[0] ?? "cancel";
390
+ }
391
+
392
+ return "cancel";
393
+ };
394
+
395
+ const getDropOperation = (
396
+ dropTarget: DropTarget,
397
+ types: DragTypes,
398
+ allowedOperations: DropOperation[],
399
+ isInternal: boolean = false,
400
+ draggingKeys: Set<string | number> = new Set(),
401
+ ): DropOperation => {
402
+ // Prevent dropping items onto themselves or their descendants (faithful of
403
+ // react-stately's `getDropOperation` guard). Only meaningful for a nested
404
+ // collection; a flat host has no descendants.
405
+ if (isInternal && dropTarget.type === "item" && draggingKeys.size > 0) {
406
+ if (draggingKeys.has(dropTarget.key) && dropTarget.dropPosition === "on") {
407
+ return "cancel";
408
+ }
409
+
410
+ const collection = getProps().collection;
411
+ if (collection) {
412
+ let currentKey: string | number | null = dropTarget.key;
413
+ while (currentKey != null) {
414
+ const item = collection.getItem(currentKey);
415
+ const parentKey = item?.parentKey;
416
+ if (parentKey != null && draggingKeys.has(parentKey)) {
417
+ return "cancel";
418
+ }
419
+ currentKey = parentKey ?? null;
420
+ }
421
+ }
303
422
  }
304
423
 
305
- return allowedOperations[0] ?? "cancel";
424
+ return defaultGetDropOperation(dropTarget, types, allowedOperations, isInternal, draggingKeys);
306
425
  };
307
426
 
308
427
  const shouldAcceptItemDrop = (dropTarget: ItemDropTarget, types: DragTypes): boolean => {
package/src/index.ts CHANGED
@@ -50,14 +50,17 @@ export {
50
50
  type MultipleSelection,
51
51
  ListCollection,
52
52
  createListCollection,
53
+ getItemCount,
53
54
  createSelectionState,
54
55
  type SelectionStateProps,
55
56
  type SelectionState,
56
57
  type SelectionPressEvent,
57
58
  createListState,
59
+ createFilteredListState,
58
60
  createSingleSelectListState,
59
61
  type ListStateProps,
60
62
  type ListState,
63
+ type ListFilterFn,
61
64
  type SingleSelectListStateProps,
62
65
  type SingleSelectListState,
63
66
  createMenuState,
@@ -523,4 +523,19 @@ export class SelectionManager<T = unknown> {
523
523
  fullCollection: this.fullCollection ?? this.collection,
524
524
  });
525
525
  }
526
+
527
+ /**
528
+ * Like {@link withCollection}, but the collection is supplied as a live
529
+ * accessor so the returned manager stays reactive as the collection recomputes
530
+ * (e.g. an Autocomplete filter narrowing the list as the user types). The
531
+ * shared underlying selection/focus state is preserved, and the original
532
+ * (unfiltered) collection is retained as `fullCollection` so a select-all
533
+ * still materializes the filtered-out keys.
534
+ */
535
+ withCollectionAccessor(collection: () => Collection<T>): SelectionManager<T> {
536
+ return new SelectionManager<T>(collection, this.state, {
537
+ layoutDelegate: this.layoutDelegate ?? undefined,
538
+ fullCollection: this.fullCollection ?? this.collection,
539
+ });
540
+ }
526
541
  }
@@ -3,7 +3,7 @@
3
3
  * Tracks selected step, completion status, and selectability.
4
4
  */
5
5
 
6
- import { createMemo, createSignal, type Accessor } from "solid-js";
6
+ import { createEffect, createMemo, createSignal, type Accessor } from "solid-js";
7
7
  import type { Key } from "../collections/types";
8
8
 
9
9
  export interface StepListStateProps {
@@ -112,13 +112,16 @@ export function createStepListState(props: StepListStateProps): StepListState {
112
112
  // First step is always selectable
113
113
  const keyIndex = indexMap().get(key);
114
114
  if (keyIndex === 0) return true;
115
- // A step is selectable if the previous step is completed OR is the currently selected step
115
+ // Otherwise a step is selectable only if the PREVIOUS step is completed —
116
+ // mirrors react-stately `useStepListState.isSelectable`
117
+ // (`isCompleted(prevStep) || step === firstKey`). There is no "step after the
118
+ // currently selected step" clause upstream: a fresh list exposes only the
119
+ // first step, and the immediate-next step becomes selectable when its
120
+ // predecessor is *completed*, not merely selected.
116
121
  const currentItems = items();
117
122
  if (keyIndex !== undefined && keyIndex > 0) {
118
123
  const prevKey = currentItems[keyIndex - 1].key;
119
124
  if (isCompleted(prevKey)) return true;
120
- // Allow advancing to the step immediately after the selected step
121
- if (prevKey === selectedKey()) return true;
122
125
  }
123
126
  return false;
124
127
  };
@@ -177,6 +180,26 @@ export function createStepListState(props: StepListStateProps): StepListState {
177
180
  props.onSelectionChange?.(key);
178
181
  };
179
182
 
183
+ // Mirror react-stately `useStepListState`'s effect: whenever the selected step
184
+ // sits more than one past the last completed step (e.g. mounted with a
185
+ // `defaultSelectedKey` ahead of progress), auto-complete its immediate
186
+ // predecessor. Because completion is cumulative (`isCompleted` = index ≤
187
+ // lastCompleted index), advancing `lastCompletedStep` to `selectedIdx - 1`
188
+ // marks every intermediate step complete. This runs regardless of
189
+ // `isDisabled` / `isReadOnly`, exactly as upstream (the effect is ungated).
190
+ createEffect(() => {
191
+ const selKey = selectedKey();
192
+ if (selKey === null) return;
193
+ const selIdx = indexMap().get(selKey);
194
+ if (selIdx === undefined || selIdx <= 0) return;
195
+ const completed = lastCompletedStep();
196
+ const lcs = completed !== null ? (indexMap().get(completed) ?? -1) : -1;
197
+ if (selIdx > lcs + 1) {
198
+ const prevKey = items()[selIdx - 1]?.key;
199
+ if (prevKey !== undefined) setLastCompletedStep(prevKey);
200
+ }
201
+ });
202
+
180
203
  return {
181
204
  selectedKey,
182
205
  lastCompletedStep,
@@ -88,7 +88,15 @@ export function createTabListState<T = unknown>(
88
88
  const nodes: CollectionNode<T>[] = items.map((item, index) => {
89
89
  const o = item as CollectionItemLike;
90
90
  const key = p.getKey?.(item) ?? o.key ?? o.id ?? index;
91
- const textValue = p.getTextValue?.(item) ?? o.textValue ?? o.label ?? String(item);
91
+ // See createListState: `String(item)` yields "[object Object]" for a plain
92
+ // object, so widen to name/title and fall back to the key for object items.
93
+ const textValue =
94
+ p.getTextValue?.(item) ??
95
+ o.textValue ??
96
+ o.label ??
97
+ o.name ??
98
+ o.title ??
99
+ (item != null && typeof item === "object" ? String(key) : String(item));
92
100
  const isDisabled = p.getDisabled?.(item) ?? o.isDisabled ?? false;
93
101
 
94
102
  return {