selective-ui 1.2.4 → 1.2.6

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 (67) hide show
  1. package/README.md +7 -0
  2. package/dist/selective-ui.css +64 -58
  3. package/dist/selective-ui.css.map +1 -1
  4. package/dist/selective-ui.esm.js +4396 -1344
  5. package/dist/selective-ui.esm.js.map +1 -1
  6. package/dist/selective-ui.esm.min.js +2 -2
  7. package/dist/selective-ui.esm.min.js.br +0 -0
  8. package/dist/selective-ui.min.css +1 -1
  9. package/dist/selective-ui.min.css.br +0 -0
  10. package/dist/selective-ui.min.js +2 -2
  11. package/dist/selective-ui.min.js.br +0 -0
  12. package/dist/selective-ui.umd.js +4401 -1345
  13. package/dist/selective-ui.umd.js.map +1 -1
  14. package/package.json +3 -3
  15. package/src/css/components/accessorybox.css +1 -1
  16. package/src/css/components/directive.css +2 -2
  17. package/src/css/components/option-handle.css +4 -4
  18. package/src/css/components/placeholder.css +1 -1
  19. package/src/css/components/popup/empty-state.css +3 -3
  20. package/src/css/components/popup/loading-state.css +3 -3
  21. package/src/css/components/popup/popup.css +5 -5
  22. package/src/css/components/searchbox.css +2 -2
  23. package/src/css/components/selectbox.css +7 -7
  24. package/src/css/views/group-view.css +8 -8
  25. package/src/css/views/option-view.css +22 -22
  26. package/src/ts/adapter/mixed-adapter.ts +248 -92
  27. package/src/ts/components/accessorybox.ts +170 -73
  28. package/src/ts/components/directive.ts +55 -26
  29. package/src/ts/components/option-handle.ts +127 -60
  30. package/src/ts/components/placeholder.ts +73 -35
  31. package/src/ts/components/popup/empty-state.ts +71 -35
  32. package/src/ts/components/popup/loading-state.ts +73 -33
  33. package/src/ts/components/popup/popup.ts +19 -39
  34. package/src/ts/components/searchbox.ts +189 -50
  35. package/src/ts/components/selectbox.ts +401 -40
  36. package/src/ts/core/base/adapter.ts +160 -79
  37. package/src/ts/core/base/fenwick.ts +147 -0
  38. package/src/ts/core/base/lifecycle.ts +118 -35
  39. package/src/ts/core/base/model.ts +94 -36
  40. package/src/ts/core/base/recyclerview.ts +0 -1
  41. package/src/ts/core/base/view.ts +54 -23
  42. package/src/ts/core/base/virtual-recyclerview.ts +365 -283
  43. package/src/ts/core/model-manager.ts +172 -92
  44. package/src/ts/core/search-controller.ts +166 -93
  45. package/src/ts/global.ts +26 -5
  46. package/src/ts/index.ts +22 -3
  47. package/src/ts/models/group-model.ts +138 -32
  48. package/src/ts/models/option-model.ts +197 -53
  49. package/src/ts/services/dataset-observer.ts +72 -10
  50. package/src/ts/services/ea-observer.ts +87 -10
  51. package/src/ts/services/effector.ts +181 -32
  52. package/src/ts/services/refresher.ts +32 -7
  53. package/src/ts/services/resize-observer.ts +136 -19
  54. package/src/ts/services/select-observer.ts +115 -50
  55. package/src/ts/types/core/base/view.type.ts +3 -3
  56. package/src/ts/types/core/base/virtual-recyclerview.type.ts +1 -1
  57. package/src/ts/types/plugins/plugin.type.ts +46 -0
  58. package/src/ts/types/utils/ievents.type.ts +6 -1
  59. package/src/ts/types/utils/istorage.type.ts +8 -4
  60. package/src/ts/types/utils/libs.type.ts +2 -2
  61. package/src/ts/types/utils/selective.type.ts +14 -1
  62. package/src/ts/utils/callback-scheduler.ts +115 -37
  63. package/src/ts/utils/ievents.ts +91 -29
  64. package/src/ts/utils/libs.ts +41 -65
  65. package/src/ts/utils/selective.ts +412 -79
  66. package/src/ts/views/group-view.ts +142 -31
  67. package/src/ts/views/option-view.ts +272 -60
@@ -4,35 +4,60 @@ import { SelectiveOptions } from "../../types/utils/selective.type";
4
4
  import { Libs } from "../../utils/libs";
5
5
 
6
6
  /**
7
- * UI component representing a loading state.
8
- *
9
- * The loading state is displayed while data is being fetched,
7
+ * Lightweight UI state box that renders a "loading" indicator while data is being fetched,
10
8
  * processed, or updated asynchronously.
11
9
  *
12
- * It manages a single DOM element and participates in the
13
- * standard lifecycle provided by `Lifecycle`.
10
+ * ### Responsibility
11
+ * - Owns a single DOM node representing the loading state.
12
+ * - Exposes an imperative API (`show`, `hide`, `isVisible`) to be driven by higher-level
13
+ * controllers (e.g., AJAX search / pagination) and containers (e.g., Popup).
14
+ *
15
+ * ### Lifecycle (Strict FSM)
16
+ * - Constructed in `NEW`. When `options` are provided, {@link initialize} is invoked and the
17
+ * instance transitions to `INITIALIZED` via {@link Lifecycle.init}.
18
+ * - This component does **not** attach itself to the DOM; consumers append {@link node} to the
19
+ * desired container.
20
+ * - {@link destroy} removes the node, clears references, and transitions to `DESTROYED`.
21
+ *
22
+ * ### Idempotency / No-ops
23
+ * - {@link show} and {@link hide} are **no-ops** until {@link node} exists.
24
+ * - {@link destroy} is idempotent once in {@link LifecycleState.DESTROYED}.
25
+ *
26
+ * ### Accessibility / DOM side effects
27
+ * - Uses `role="status"` and `aria-live="polite"` so assistive technologies announce changes
28
+ * without interrupting the user.
29
+ * - Visibility is controlled via the `"hide"` CSS class; hiding does not remove the element.
30
+ * - The `"small"` CSS class is toggled by {@link show} to support a compact loading indicator
31
+ * when items are already present.
14
32
  *
15
33
  * @extends Lifecycle
34
+ * @see {@link LifecycleState}
16
35
  */
17
36
  export class LoadingState extends Lifecycle {
18
-
19
37
  /**
20
- * Root DOM element of the loading state component.
21
- * Created during initialization and removed on destroy.
38
+ * Root DOM element for the loading state UI.
39
+ *
40
+ * - Created during {@link initialize}.
41
+ * - Intended to be appended by the parent container (component does not auto-attach).
42
+ * - Removed from DOM during {@link destroy}.
22
43
  */
23
44
  public node: HTMLDivElement | null = null;
24
45
 
25
46
  /**
26
- * Configuration options containing the loading message text.
47
+ * Configuration source for loading message text.
48
+ *
49
+ * Expected to provide:
50
+ * - `textLoading` (displayed while loading is active)
27
51
  */
28
52
  public options: SelectiveOptions | null = null;
29
53
 
30
54
  /**
31
- * Creates a new LoadingState instance.
55
+ * Creates a new {@link LoadingState}.
32
56
  *
33
- * If options are provided, the component is initialized immediately.
57
+ * If `options` are provided, initialization runs immediately (creates {@link node} and
58
+ * transitions to `INITIALIZED`).
34
59
  *
35
- * @param options - Configuration object containing the loading message text.
60
+ * @param {SelectiveOptions | null} [options=null] - Configuration containing the loading message text.
36
61
  */
37
62
  public constructor(options: SelectiveOptions | null = null) {
38
63
  super();
@@ -40,36 +65,43 @@ export class LoadingState extends Lifecycle {
40
65
  }
41
66
 
42
67
  /**
43
- * Initializes the loading state component.
68
+ * Initializes internal resources for this component.
44
69
  *
45
- * Creates the root DOM element, sets the initial loading text,
46
- * applies accessibility attributes, stores configuration options,
47
- * and starts the lifecycle.
70
+ * Side effects:
71
+ * - Creates the root `div` node with base CSS classes: `"seui-loading-state"` and `"hide"`.
72
+ * - Sets initial text to `options.textLoading`.
73
+ * - Applies `role="status"` and `aria-live="polite"`.
74
+ * - Stores the options reference and calls {@link Lifecycle.init}.
48
75
  *
49
- * @param options - Configuration object containing loading text.
76
+ * @param {SelectiveOptions} options - Configuration object containing loading text.
77
+ * @returns {void}
50
78
  */
51
79
  private initialize(options: SelectiveOptions): void {
52
80
  this.options = options;
53
81
 
54
- this.node = Libs.nodeCreator({
82
+ this.node = Libs.nodeCreator<HTMLDivElement>({
55
83
  node: "div",
56
- classList: ["selective-ui-loading-state", "hide"],
84
+ classList: ["seui-loading-state", "hide"],
57
85
  textContent: options.textLoading,
58
86
  role: "status",
59
87
  ariaLive: "polite",
60
- }) as HTMLDivElement;
88
+ });
61
89
 
62
90
  this.init();
63
91
  }
64
92
 
65
93
  /**
66
- * Displays the loading state.
94
+ * Shows the loading indicator.
95
+ *
96
+ * Behavior:
97
+ * - Updates the text to the latest `options.textLoading` (in case options changed).
98
+ * - Toggles the `"small"` CSS class when `hasItems` is true to display a compact variant.
99
+ * - Removes the `"hide"` class to make the node visible.
67
100
  *
68
- * When items are already present, the loading state can be shown
69
- * in a compact form by applying a reduced ("small") style.
101
+ * No-op if {@link node} or {@link options} are not initialized.
70
102
  *
71
- * @param hasItems - True if existing items are present,
72
- * enabling a compact loading indicator.
103
+ * @param {boolean} hasItems - Whether existing items are already present (enables compact loading style).
104
+ * @returns {void}
73
105
  */
74
106
  public show(hasItems: boolean): void {
75
107
  if (!this.node || !this.options) return;
@@ -80,10 +112,12 @@ export class LoadingState extends Lifecycle {
80
112
  }
81
113
 
82
114
  /**
83
- * Hides the loading state.
115
+ * Hides the loading indicator by applying the `"hide"` CSS class.
116
+ *
117
+ * This does not remove the element from the DOM.
118
+ * No-op if {@link node} is not initialized.
84
119
  *
85
- * This only toggles visibility via CSS and does not
86
- * remove the element from the DOM.
120
+ * @returns {void}
87
121
  */
88
122
  public hide(): void {
89
123
  if (!this.node) return;
@@ -91,19 +125,25 @@ export class LoadingState extends Lifecycle {
91
125
  }
92
126
 
93
127
  /**
94
- * Indicates whether the loading state is currently visible.
128
+ * Whether the loading indicator is currently visible.
95
129
  *
96
- * @returns True if the loading indicator is shown; otherwise false.
130
+ * @returns {boolean} `true` when {@link node} exists and does not have the `"hide"` class.
97
131
  */
98
132
  public get isVisible(): boolean {
99
133
  return !!this.node && !this.node.classList.contains("hide");
100
134
  }
101
135
 
102
136
  /**
103
- * Destroys the loading state component.
137
+ * Releases resources owned by this component.
138
+ *
139
+ * - Removes the root DOM node (if present).
140
+ * - Clears stored options and internal references.
141
+ * - Transitions to `DESTROYED`.
142
+ *
143
+ * Idempotent: returns early if already in {@link LifecycleState.DESTROYED}.
104
144
  *
105
- * Removes the DOM node, clears stored options,
106
- * and terminates the lifecycle.
145
+ * @returns {void}
146
+ * @override
107
147
  */
108
148
  public override destroy(): void {
109
149
  if (this.is(LifecycleState.DESTROYED)) {
@@ -13,6 +13,7 @@ import { SelectiveOptions } from "../../types/utils/selective.type";
13
13
  import { ParentBinderMapLike, VirtualRecyclerOptions } from "../../types/components/popup.type";
14
14
  import { Lifecycle } from "../../core/base/lifecycle";
15
15
  import { LifecycleState } from "../../types/core/base/lifecycle.type";
16
+ import { MountViewResult } from "src/ts/types/utils/libs.type";
16
17
 
17
18
  /**
18
19
  * Popup panel that renders and manages the dropdown surface.
@@ -129,12 +130,12 @@ export class Popup extends Lifecycle {
129
130
  this.emptyState = new EmptyState(options);
130
131
  this.loadingState = new LoadingState(options);
131
132
 
132
- const nodeMounted = Libs.mountNode(
133
+ const nodeMounted = Libs.mountNode<MountViewResult>(
133
134
  {
134
135
  PopupContainer: {
135
136
  tag: {
136
137
  node: "div",
137
- classList: "selective-ui-popup",
138
+ classList: "seui-popup",
138
139
  style: { maxHeight: options.panelHeight },
139
140
  },
140
141
  child: {
@@ -143,7 +144,7 @@ export class Popup extends Lifecycle {
143
144
  tag: {
144
145
  id: options.SEID_LIST,
145
146
  node: "div",
146
- classList: "selective-ui-options-container",
147
+ classList: "seui-options-container",
147
148
  role: "listbox",
148
149
  },
149
150
  },
@@ -158,7 +159,7 @@ export class Popup extends Lifecycle {
158
159
  this.node = nodeMounted.view as HTMLDivElement;
159
160
  this.optionsContainer = nodeMounted.tags.OptionsContainer as HTMLDivElement;
160
161
 
161
- this.parent = Libs.getBinderMap(select) as ParentBinderMapLike | null;
162
+ this.parent = Libs.getBinderMap<ParentBinderMapLike>(select);
162
163
  this.options = options;
163
164
  this.init();
164
165
 
@@ -174,10 +175,7 @@ export class Popup extends Lifecycle {
174
175
  // Load ModelManager resources into the list container
175
176
  this.modelManager.load<VirtualRecyclerOptions>(this.optionsContainer, { isMultiple: options.multiple }, recyclerViewOpt);
176
177
 
177
- const MMResources = this.modelManager.getResources() as {
178
- adapter: MixedAdapter;
179
- recyclerView: RecyclerViewContract<MixedAdapter>;
180
- };
178
+ const MMResources = this.modelManager.getResources();
181
179
 
182
180
  this.optionAdapter = MMResources.adapter;
183
181
  this.recyclerView = MMResources.recyclerView;
@@ -471,10 +469,8 @@ export class Popup extends Lifecycle {
471
469
  return;
472
470
  }
473
471
 
474
- if (this.hideLoadHandle) {
475
- clearTimeout(this.hideLoadHandle);
476
- this.hideLoadHandle = null;
477
- }
472
+ clearTimeout(this.hideLoadHandle!);
473
+ this.hideLoadHandle = null;
478
474
 
479
475
  if (this.node && this.scrollListener) {
480
476
  this.node.removeEventListener("scroll", this.scrollListener);
@@ -484,51 +480,35 @@ export class Popup extends Lifecycle {
484
480
  this.emptyState.destroy();
485
481
  this.loadingState.destroy();
486
482
  this.optionHandle.destroy();
487
- this.recyclerView.destroy();
488
-
489
- try {
490
- this.resizeObser?.disconnect();
491
- } catch (_) {}
492
- this.resizeObser = null;
493
-
494
- try {
495
- this.effSvc?.setElement?.(null);
496
- } catch (_) {}
497
- this.effSvc = null;
483
+ this.resizeObser?.disconnect?.();
484
+ this.effSvc?.setElement?.(null);
485
+ this.modelManager?.skipEvent?.(false);
486
+ this.recyclerView?.clear?.();
487
+ this.node?.remove?.();
498
488
 
499
489
  if (this.node) {
500
490
  try {
501
- const clone = this.node.cloneNode(true) as HTMLDivElement;
491
+ const clone = Libs.nodeCloner<HTMLDivElement>(this.node);
502
492
  this.node.replaceWith(clone);
503
493
  clone.remove();
504
494
  } catch (_) {
505
495
  this.node.remove();
506
496
  }
507
497
  }
498
+
508
499
  this.node = null;
509
500
  this.optionsContainer = null;
510
-
511
- try {
512
- this.modelManager?.skipEvent?.(false);
513
-
514
- this.recyclerView?.clear?.();
515
- this.recyclerView = null;
516
-
517
- this.optionAdapter = null;
518
-
519
- // Original behavior kept intentionally.
520
- this.node.remove();
521
- } catch (_) {}
522
-
523
501
  this.modelManager = null;
524
502
  this.optionHandle = null;
525
503
  this.emptyState = null;
526
504
  this.loadingState = null;
527
-
528
505
  this.parent = null;
529
506
  this.options = null;
530
-
531
507
  this.isCreated = false;
508
+ this.effSvc = null;
509
+ this.resizeObser = null;
510
+ this.recyclerView = null;
511
+ this.optionAdapter = null;
532
512
 
533
513
  super.destroy();
534
514
  }
@@ -6,25 +6,58 @@ import { Lifecycle } from "../core/base/lifecycle";
6
6
  import { LifecycleState } from "../types/core/base/lifecycle.type";
7
7
 
8
8
  /**
9
- * Searchable input component for the Select UI.
9
+ * SearchBox
10
10
  *
11
- * Responsibilities:
12
- * - Render a search input field with proper ARIA attributes
13
- * - Dispatch typed events for search, navigation, Enter, and Escape
14
- * - Support showing/hiding and dynamic placeholder updates
11
+ * DOM-driven, headless-friendly search input used by the Select UI to filter and
12
+ * navigate option lists. This component owns a small DOM subtree and exposes
13
+ * callback hooks for the host/controller layer to implement filtering, highlight,
14
+ * and commit/cancel behaviors.
15
15
  *
16
- * Lifecycle:
17
- * - Constructed with optional options initialized `init()`
18
- * - Consumers wire callbacks (`onSearch`, `onNavigate`, `onEnter`, `onEsc`)
16
+ * ### Responsibility
17
+ * - Render a `<input type="search">` wrapped by a container element.
18
+ * - Apply ARIA attributes used by the surrounding listbox/popup integration.
19
+ * - Convert DOM events into typed callbacks:
20
+ * - text input changes → {@link onSearch}
21
+ * - keyboard navigation (ArrowUp/ArrowDown/Tab) → {@link onNavigate}
22
+ * - commit (Enter) → {@link onEnter}
23
+ * - cancel (Escape) → {@link onEsc}
24
+ * - Provide imperative UI helpers:
25
+ * - {@link show}/{@link hide} (visibility + focus/readOnly behavior)
26
+ * - {@link clear} (reset query and optionally trigger the search hook)
27
+ * - {@link setPlaceHolder} (safe placeholder update)
28
+ * - {@link setActiveDescendant} (ARIA highlight binding)
29
+ *
30
+ * ### Lifecycle (Strict FSM)
31
+ * - Constructed in `NEW`.
32
+ * - If options are provided, {@link initialize} creates DOM and calls `init()`
33
+ * → transitions to `INITIALIZED`.
34
+ * - This class does not override `update()`: runtime changes are performed via
35
+ * its imperative methods (e.g., {@link show}, {@link clear}, {@link setPlaceHolder}).
36
+ * - {@link destroy} is terminal: removes DOM references and ends lifecycle.
37
+ * Subsequent calls become no-ops once {@link LifecycleState.DESTROYED}.
38
+ *
39
+ * ### Event Model / Ownership
40
+ * - This component does **not** own filtering logic or selection state.
41
+ * - All "meaningful actions" are emitted outward through callbacks (external events).
42
+ * - It also performs event containment (`stopPropagation`) to avoid parent-level
43
+ * handlers (e.g., popup/list container) from intercepting interactions.
44
+ *
45
+ * ### a11y / DOM Side Effects
46
+ * - Writes ARIA attributes such as `aria-controls`, `aria-autocomplete`, and
47
+ * `aria-activedescendant` onto the input element.
48
+ * - Intercepts keyboard events and may call `preventDefault()` for navigation keys.
19
49
  *
20
50
  * @extends Lifecycle
21
51
  */
22
52
  export class SearchBox extends Lifecycle {
23
53
  /**
24
- * Creates a searchable input box component with optional configuration
25
- * and initializes it if options are provided.
54
+ * Creates a new {@link SearchBox}.
55
+ *
56
+ * If `options` is provided, initialization is performed immediately (DOM is created
57
+ * and `init()` is called). If `options` is `null`, the instance stays in `NEW` until
58
+ * initialized elsewhere.
26
59
  *
27
- * @param options - Configuration (e.g., placeholder, accessibility IDs).
60
+ * @param options - Configuration such as placeholder, accessibility IDs, and flags.
28
61
  */
29
62
  constructor(options: SelectiveOptions | null = null) {
30
63
  super();
@@ -32,57 +65,127 @@ export class SearchBox extends Lifecycle {
32
65
  if (options) this.initialize(options);
33
66
  }
34
67
 
35
- /** Internal reference to the mounted node structure with typed tags. */
68
+ /**
69
+ * The mount result returned by {@link Libs.mountNode}.
70
+ *
71
+ * Provides typed access to created DOM tags (e.g., `SearchInput`) and the root view.
72
+ * `null` before initialization and after destruction.
73
+ *
74
+ * @internal
75
+ */
36
76
  private nodeMounted: MountViewResult<SearchBoxTags> | null = null;
37
77
 
38
- /** Root container element for the search box component. */
78
+ /**
79
+ * Root container node of this component.
80
+ *
81
+ * Created during {@link initialize} and removed during {@link destroy}.
82
+ * Visibility is controlled by adding/removing the `hide` class.
83
+ */
39
84
  public node: HTMLDivElement | null = null;
40
85
 
41
- /** Reference to the input element (`type="search"`). */
86
+ /**
87
+ * The `<input type="search">` element used to capture user queries.
88
+ *
89
+ * Cached for imperative operations (focus, placeholder updates, ARIA updates).
90
+ * `null` before initialization and after destruction.
91
+ *
92
+ * @internal
93
+ */
42
94
  private SearchInput: HTMLInputElement | null = null;
43
95
 
44
- /** Callback fired on input changes (when not a control key event). */
96
+ /**
97
+ * External "search changed" hook.
98
+ *
99
+ * Invoked when the user edits text (via the `input` event) and the edit is not
100
+ * part of a handled control-key sequence (e.g., ArrowUp/Down/Tab/Enter/Escape).
101
+ *
102
+ * Ownership:
103
+ * - Implementations typically filter adapter/model state and refresh the list.
104
+ */
45
105
  public onSearch: SearchHandler | null = null;
46
106
 
47
- /** Current configuration options (placeholder, IDs, searchable flag, etc.). */
107
+ /**
108
+ * Options snapshot used for behavior toggles and attributes.
109
+ *
110
+ * Key fields typically consumed here:
111
+ * - `placeholder`: initial placeholder string
112
+ * - `searchable`: toggles readOnly + focus behavior on {@link show}
113
+ * - `SEID_LIST`: used as `aria-controls` value to bind to listbox container
114
+ *
115
+ * Cleared during {@link destroy}.
116
+ *
117
+ * @internal
118
+ */
48
119
  private options: SelectiveOptions | null = null;
49
120
 
50
- /** Callback to handle list navigation: +1 for next, -1 for previous. */
121
+ /**
122
+ * External navigation hook for list traversal.
123
+ *
124
+ * Called with:
125
+ * - `+1` for forward (ArrowDown / Tab)
126
+ * - `-1` for backward (ArrowUp)
127
+ *
128
+ * Typical consumers update highlight/active option in Adapter/RecyclerView.
129
+ */
51
130
  public onNavigate: NavigateHandler | null = null;
52
131
 
53
- /** Callback fired on Enter. Typically used to confirm a selection. */
132
+ /**
133
+ * External "commit" hook (Enter key).
134
+ *
135
+ * Typical consumers confirm selection of the highlighted option or submit the current state.
136
+ */
54
137
  public onEnter: (() => void) | null = null;
55
138
 
56
- /** Callback fired on Escape. Typically used to dismiss a popup. */
139
+ /**
140
+ * External "cancel" hook (Escape key).
141
+ *
142
+ * Typical consumers close the popup, clear highlight, or reset interaction mode.
143
+ */
57
144
  public onEsc: (() => void) | null = null;
58
145
 
59
146
  /**
60
- * Initializes the search box DOM, sets ARIA attributes, and wires keyboard/mouse/input events.
147
+ * Initializes DOM, ARIA attributes, and interaction listeners.
61
148
  *
62
- * Accessibility:
63
- * - `role="searchbox"`
64
- * - `aria-controls` references the listbox container by ID
65
- * - `aria-autocomplete="list"` indicates list-based suggestions/results
149
+ * DOM structure (conceptually):
150
+ * - Root: `div.seui-searchbox.hide`
151
+ * - Child: `input[type="search"].seui-searchbox-input`
66
152
  *
67
- * Keyboard support:
68
- * - ArrowDown / Tab navigate forward
69
- * - ArrowUp navigate backward
70
- * - Enter confirm action
71
- * - Escape → cancel/close action
153
+ * Accessibility attributes set on the input:
154
+ * - `role="searchbox"`: announces search field semantics
155
+ * - `aria-controls=options.SEID_LIST`: points to the list container (listbox)
156
+ * - `aria-autocomplete="list"`: indicates suggestion results are list-driven
72
157
  *
73
- * @param options - Configuration including placeholder and `SEID_LIST` for `aria-controls`.
158
+ * Interaction model:
159
+ * - Mouse down/up: stops propagation to prevent container/popup listeners from interfering.
160
+ * - Keydown:
161
+ * - ArrowDown / Tab → emits {@link onNavigate}(+1)
162
+ * - ArrowUp → emits {@link onNavigate}(-1)
163
+ * - Enter → emits {@link onEnter}()
164
+ * - Escape → emits {@link onEsc}()
165
+ * Control keys are treated as "internal control events" and do not produce {@link onSearch}
166
+ * via the `input` listener (guarded by `isControlKey`).
167
+ * - Input:
168
+ * - Emits {@link onSearch}(value, true) for text edits that are not control-key sequences.
169
+ *
170
+ * Side effects:
171
+ * - Creates DOM nodes via {@link Libs.mountNode}.
172
+ * - Attaches event listeners to the input element.
173
+ * - Transitions lifecycle via `init()` at the end.
174
+ *
175
+ * @param options - Configuration including placeholder and listbox id used by `aria-controls`.
176
+ * @internal
74
177
  */
75
178
  private initialize(options: SelectiveOptions): void {
76
- this.nodeMounted = Libs.mountNode({
179
+ this.nodeMounted = Libs.mountNode<MountViewResult<SearchBoxTags>>({
77
180
  SearchBox: {
78
- tag: { node: "div", classList: ["selective-ui-searchbox", "hide"] },
181
+ tag: { node: "div", classList: ["seui-searchbox", "hide"] },
79
182
  child: {
80
183
  SearchInput: {
81
184
  tag: {
82
185
  id: Libs.randomString(),
83
186
  node: "input",
84
187
  type: "search",
85
- classList: ["selective-ui-searchbox-input"],
188
+ classList: ["seui-searchbox-input"],
86
189
  placeholder: options.placeholder,
87
190
  role: "searchbox",
88
191
  ariaControls: options.SEID_LIST,
@@ -91,7 +194,7 @@ export class SearchBox extends Lifecycle {
91
194
  },
92
195
  },
93
196
  },
94
- }) as MountViewResult<SearchBoxTags>;
197
+ });
95
198
 
96
199
  this.node = this.nodeMounted.view as HTMLDivElement;
97
200
  this.SearchInput = this.nodeMounted.tags.SearchInput;
@@ -99,7 +202,7 @@ export class SearchBox extends Lifecycle {
99
202
  let isControlKey = false;
100
203
  const inputEl = this.nodeMounted.tags.SearchInput;
101
204
 
102
- // Prevent parent listeners from intercepting mouse interactions.
205
+ // Prevent parent listeners (e.g., popup container) from intercepting mouse interactions.
103
206
  inputEl.addEventListener("mousedown", (e: MouseEvent) => {
104
207
  e.stopPropagation();
105
208
  });
@@ -108,7 +211,8 @@ export class SearchBox extends Lifecycle {
108
211
  e.stopPropagation();
109
212
  });
110
213
 
111
- // Keyboard handling: navigation, submit, and cancel.
214
+ // Keyboard handling: navigation, commit, and cancel.
215
+ // Control-key sequences are tracked to avoid emitting onSearch from the subsequent input event.
112
216
  inputEl.addEventListener("keydown", (e: KeyboardEvent) => {
113
217
  isControlKey = false;
114
218
 
@@ -138,7 +242,7 @@ export class SearchBox extends Lifecycle {
138
242
  e.stopPropagation();
139
243
  });
140
244
 
141
- // Text input changes (ignore control-key initiated sequences).
245
+ // Text edits (ignore those attributable to control-key flows).
142
246
  inputEl.addEventListener("input", () => {
143
247
  if (isControlKey) return;
144
248
  this.onSearch?.(inputEl.value, true);
@@ -148,8 +252,18 @@ export class SearchBox extends Lifecycle {
148
252
  }
149
253
 
150
254
  /**
151
- * Shows the search box, toggles `readOnly` based on `options.searchable`,
152
- * and focuses the input when searchable.
255
+ * Shows the search box and prepares the input for interaction.
256
+ *
257
+ * Behavior:
258
+ * - Removes the `hide` class from the root node.
259
+ * - Toggles `readOnly` according to `options.searchable`.
260
+ * - When searchable, schedules a focus on the next animation frame.
261
+ *
262
+ * No-ops if not initialized (missing {@link node}, {@link SearchInput}, or {@link options}).
263
+ *
264
+ * DOM side effects:
265
+ * - May change focus.
266
+ * - Mutates `readOnly` on the input element.
153
267
  */
154
268
  public show(): void {
155
269
  if (!this.node || !this.SearchInput || !this.options) return;
@@ -165,7 +279,9 @@ export class SearchBox extends Lifecycle {
165
279
  }
166
280
 
167
281
  /**
168
- * Hides the search box by adding the `hide` class.
282
+ * Hides the search box by adding the `hide` class to the root node.
283
+ *
284
+ * No-ops if {@link node} is `null`.
169
285
  */
170
286
  public hide(): void {
171
287
  if (!this.node) return;
@@ -173,9 +289,15 @@ export class SearchBox extends Lifecycle {
173
289
  }
174
290
 
175
291
  /**
176
- * Clears the current search value and optionally triggers the `onSearch` callback.
292
+ * Clears the current query and optionally notifies the host via {@link onSearch}.
293
+ *
294
+ * This method always resets the input's value to an empty string.
295
+ * The `isTrigger` flag is forwarded to {@link onSearch} and can be used by the host
296
+ * to differentiate external (programmatic) clearing from user-driven changes.
297
+ *
298
+ * No-ops if the component has not been initialized ({@link nodeMounted} is `null`).
177
299
  *
178
- * @param isTrigger - Whether to invoke `onSearch` with an empty string. Defaults to `true`.
300
+ * @param isTrigger - Whether to invoke {@link onSearch} with an empty string. Defaults to `true`.
179
301
  */
180
302
  public clear(isTrigger: boolean = true): void {
181
303
  if (!this.nodeMounted) return;
@@ -184,9 +306,14 @@ export class SearchBox extends Lifecycle {
184
306
  }
185
307
 
186
308
  /**
187
- * Updates the input's placeholder text, stripping any HTML for safety.
309
+ * Updates the input's placeholder text.
188
310
  *
189
- * @param value - The new placeholder text.
311
+ * Safety:
312
+ * - HTML is stripped via {@link Libs.stripHtml} to avoid rendering markup in an attribute.
313
+ *
314
+ * No-ops if {@link SearchInput} is `null`.
315
+ *
316
+ * @param value - New placeholder text (may contain markup, which will be stripped).
190
317
  */
191
318
  public setPlaceHolder(value: string): void {
192
319
  if (!this.SearchInput) return;
@@ -194,9 +321,15 @@ export class SearchBox extends Lifecycle {
194
321
  }
195
322
 
196
323
  /**
197
- * Sets the active descendant for ARIA to indicate the currently highlighted option.
324
+ * Sets `aria-activedescendant` to reflect the currently highlighted option in the list.
325
+ *
326
+ * This is typically used in conjunction with keyboard navigation to keep assistive
327
+ * technologies informed about the active/highlighted item without moving DOM focus away
328
+ * from the search input.
198
329
  *
199
- * @param id - The DOM id of the active option element.
330
+ * No-ops if {@link SearchInput} is `null`.
331
+ *
332
+ * @param id - DOM id of the active option element.
200
333
  */
201
334
  public setActiveDescendant(id: string): void {
202
335
  if (!this.SearchInput) return;
@@ -204,11 +337,17 @@ export class SearchBox extends Lifecycle {
204
337
  }
205
338
 
206
339
  /**
207
- * Destroys the search box and releases resources.
340
+ * Disposes DOM resources and terminates the lifecycle.
341
+ *
342
+ * Strict FSM / idempotency:
343
+ * - If already {@link LifecycleState.DESTROYED}, returns immediately.
344
+ *
345
+ * Side effects:
346
+ * - Removes the root DOM node from the document (if present).
347
+ * - Clears references to DOM nodes and callbacks to enable garbage collection.
348
+ * - Delegates to `super.destroy()` to finalize lifecycle transition.
208
349
  *
209
- * - Removes the root DOM node
210
- * - Clears references to DOM and callbacks
211
- * - Ends the lifecycle
350
+ * @override
212
351
  */
213
352
  public override destroy(): void {
214
353
  if (this.is(LifecycleState.DESTROYED)) {