@bluevolt-tech/lumen 2.1.0 → 2.3.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.
@@ -21,8 +21,7 @@ function adoptStyles(shadow, css) {
21
21
 
22
22
  // components/_shared/dropdown-controller.ts
23
23
  function bindDropdownController(host, options = {}) {
24
- const shadow = host.shadowRoot;
25
- if (!shadow) {
24
+ if (!host.shadowRoot) {
26
25
  throw new Error("bindDropdownController requires the host to have an open shadow root.");
27
26
  }
28
27
  const syncExpanded = () => {
@@ -37,24 +36,34 @@ function bindDropdownController(host, options = {}) {
37
36
  syncExpanded();
38
37
  host.dispatchEvent(new CustomEvent("bv-open", { bubbles: true, composed: true }));
39
38
  };
40
- const close = () => {
39
+ const closeWithReason = (reason) => {
40
+ var _a;
41
41
  if (!host.hasAttribute("open")) return;
42
+ if (options.canClose && !options.canClose(reason)) return;
42
43
  host.removeAttribute("open");
43
44
  syncExpanded();
45
+ host.dispatchEvent(
46
+ new CustomEvent("bv-dropdown-close", {
47
+ bubbles: true,
48
+ composed: true,
49
+ detail: { reason }
50
+ })
51
+ );
52
+ (_a = options.onAfterClose) == null ? void 0 : _a.call(options);
44
53
  };
54
+ const close = () => closeWithReason("programmatic");
45
55
  const toggle = () => {
46
- if (host.hasAttribute("open")) close();
56
+ if (host.hasAttribute("open")) closeWithReason("programmatic");
47
57
  else open();
48
58
  };
49
59
  const offClick = (e) => {
50
60
  if (!host.hasAttribute("open")) return;
51
- const target = e.target;
52
- if (host.contains(target) || shadow.contains(target)) return;
53
- close();
61
+ if (e.composedPath().includes(host)) return;
62
+ closeWithReason("outside-click");
54
63
  };
55
64
  document.addEventListener("click", offClick);
56
65
  const onKeyDown = (e) => {
57
- if (e.key === "Escape") close();
66
+ if (e.key === "Escape") closeWithReason("escape");
58
67
  };
59
68
  host.addEventListener("keydown", onKeyDown);
60
69
  const observer = new MutationObserver(syncExpanded);
@@ -341,9 +350,8 @@ var BvInputDropdown = class extends HTMLElement {
341
350
  trigger.setAttribute("aria-haspopup", "listbox");
342
351
  trigger.setAttribute("aria-expanded", "false");
343
352
  trigger.setAttribute("part", "trigger");
344
- trigger.addEventListener("click", (e) => {
353
+ trigger.addEventListener("click", () => {
345
354
  var _a;
346
- e.stopPropagation();
347
355
  if (!this.disabled) (_a = this._dropdown) == null ? void 0 : _a.toggle();
348
356
  });
349
357
  const triggerLabel = document.createElement("span");
@@ -0,0 +1,222 @@
1
+ import '../bv-searchbox/bv-searchbox.ts';
2
+ import '../bv-segmented-control/bv-segmented-control.ts';
3
+ import '../bv-toggle/bv-toggle.ts';
4
+ import '../bv-button/bv-button.ts';
5
+ export type SearchPickerMode = 'include' | 'exclude';
6
+ export interface SearchPickerItem {
7
+ id: string;
8
+ name: string;
9
+ inactive?: boolean;
10
+ }
11
+ /**
12
+ * Detail on the `bv-search` event.
13
+ *
14
+ * Note: `showInactive` is intentionally NOT on this payload — read it directly
15
+ * off the picker (`picker.showInactive`) inside the handler. It changes via its
16
+ * own event (`bv-show-inactive-change`), so mixing it into `bv-search` would
17
+ * emit an update on every keystroke rather than only when it actually changes.
18
+ */
19
+ export interface SearchPickerSearchDetail {
20
+ /** Trimmed user query. Only emitted past `min-query-length`. */
21
+ query: string;
22
+ /** Monotonic id the consumer echoes back via `results-request-id` so the picker can drop stale responses. */
23
+ requestId: number;
24
+ }
25
+ export interface SearchPickerBasketChangeDetail {
26
+ /** Staged mode at the moment of the change (before Apply). */
27
+ mode: SearchPickerMode;
28
+ /** Staged basket at the moment of the change (before Apply). Consumer sees every add/remove/clear. */
29
+ basket: SearchPickerItem[];
30
+ }
31
+ export interface SearchPickerModeChangeDetail {
32
+ /** Staged mode after the toggle (before Apply). */
33
+ mode: SearchPickerMode;
34
+ }
35
+ export interface SearchPickerShowInactiveChangeDetail {
36
+ showInactive: boolean;
37
+ }
38
+ export interface SearchPickerApplyDetail {
39
+ /** Committed mode. */
40
+ mode: SearchPickerMode;
41
+ /** Committed basket. */
42
+ basket: SearchPickerItem[];
43
+ }
44
+ export interface SearchPickerClearedDetail {
45
+ /** Consumer-supplied string identifying why the basket was cleared (e.g. `'category-changed'`, `'group-changed'`). */
46
+ reason: string;
47
+ }
48
+ export declare class BvSearchPicker extends HTMLElement {
49
+ static readonly observedAttributes: readonly ["label", "entity-label", "search-placeholder", "inactive-toggle-label", "mode", "min-query-length", "debounce-ms", "disabled", "loading", "show-inactive", "open", "results", "results-total", "results-request-id", "basket", "label-all", "label-count", "label-except", "label-only-these", "label-all-except", "label-empty-basket", "label-clear-all", "label-min-chars", "label-no-results", "label-results-cap", "label-cleared"];
50
+ private _rendered;
51
+ private _query;
52
+ /**
53
+ * BTN-10322 — staged basket edited inside the panel. Kept separate from the
54
+ * committed `basket` attribute so the trigger pill doesn't flicker mid-edit
55
+ * (add/remove doesn't touch it) and so a cascade reset (`reset()`) has a
56
+ * clean staged copy to reseed from. Committed on Apply, AND automatically
57
+ * whenever the panel closes any other way (Escape / outside-click / trigger
58
+ * toggle) — see `onAfterClose` in `_render()`. Closing is what "confirms"
59
+ * the edit; Apply is just the explicit, no-need-to-move-your-mouse way to
60
+ * do the same thing.
61
+ */
62
+ private _stagedBasket;
63
+ /** BTN-10322 — staged mode edited inside the panel. Committed on close, same as `_stagedBasket`. */
64
+ private _stagedMode;
65
+ /** BTN-10322 — monotonic id emitted with `bv-search`. The consumer echoes it via `results-request-id`; stale responses drop silently. */
66
+ private _searchRequestId;
67
+ /** BTN-10322 — timer handle for the internal debounce on `bv-search`. */
68
+ private _searchDebounceHandle;
69
+ /**
70
+ * BTN-10322 — the reason to display in the cascade cleared affordance,
71
+ * or `null` when the banner is hidden. Set by `reset({ reason })`,
72
+ * cleared when the user next opens the panel or explicitly dismisses.
73
+ */
74
+ private _clearedReason;
75
+ private _labelEl;
76
+ private _triggerEl;
77
+ private _panelEl;
78
+ private _pillEl;
79
+ private _pillTextEl;
80
+ private _pillClearEl;
81
+ private _modeControlEl;
82
+ private _inactiveToggleEl;
83
+ private _searchEl;
84
+ private _resultsRowsEl;
85
+ private _resultsEmptyEl;
86
+ private _resultsLimitNoteEl;
87
+ private _basketHeaderLabelEl;
88
+ private _basketClearAllEl;
89
+ private _basketListEl;
90
+ private _basketEmptyEl;
91
+ private _resultsListEl;
92
+ private _clearedAffordanceEl;
93
+ private _clearedAffordanceMsgEl;
94
+ private _dropdown;
95
+ constructor();
96
+ get mode(): SearchPickerMode;
97
+ /**
98
+ * Setting `mode` from outside is treated as a commit — it aligns the staged
99
+ * mode inside the panel with the new committed value. Useful for initialising
100
+ * from URL params or reverting from a saved report configuration. If the panel
101
+ * is currently open, the segmented control snaps to the new mode immediately;
102
+ * the user is expected to see this as an intentional external change, not an
103
+ * accidental override of their in-flight edit.
104
+ */
105
+ set mode(v: SearchPickerMode);
106
+ get entityLabel(): string;
107
+ set entityLabel(v: string);
108
+ get minQueryLength(): number;
109
+ set minQueryLength(v: number);
110
+ get disabled(): boolean;
111
+ set disabled(v: boolean);
112
+ get loading(): boolean;
113
+ set loading(v: boolean);
114
+ get showInactive(): boolean;
115
+ set showInactive(v: boolean);
116
+ get results(): SearchPickerItem[];
117
+ set results(v: SearchPickerItem[]);
118
+ get resultsTotal(): number | null;
119
+ set resultsTotal(v: number | null);
120
+ get debounceMs(): number;
121
+ set debounceMs(v: number);
122
+ get basket(): SearchPickerItem[];
123
+ set basket(v: SearchPickerItem[]);
124
+ connectedCallback(): void;
125
+ disconnectedCallback(): void;
126
+ attributeChangedCallback(name: string, _old: string | null, _val: string | null): void;
127
+ /**
128
+ * BTN-10322 — request-id guard. When `results` (or friends) are set from
129
+ * the consumer's async search, `results-request-id` must match the most
130
+ * recent id emitted by our own `bv-search`. Any mismatch means the
131
+ * response arrived out of order (user typed faster than the network) and
132
+ * we drop it silently to avoid rendering stale rows.
133
+ *
134
+ * `results-request-id` unset entirely is fine — consumers that don't
135
+ * bother with the guard behave as before (no correlation).
136
+ */
137
+ private _syncResultsWithGuard;
138
+ private _render;
139
+ private _positionPanel;
140
+ private _syncLabel;
141
+ private _syncModeOptions;
142
+ private _syncSearchPlaceholder;
143
+ private _syncInactiveToggleLabel;
144
+ private _syncMode;
145
+ private _syncDisabled;
146
+ private _syncShowInactive;
147
+ private _syncResults;
148
+ private _syncResultsEmptyText;
149
+ private _syncResultsLimitNote;
150
+ private _syncBasket;
151
+ private _syncBasketHeaderLabel;
152
+ private _syncBasketRowColors;
153
+ private _syncBasketEmptyText;
154
+ private _syncTriggerSummary;
155
+ private _interp;
156
+ private _addBasket;
157
+ private _removeBasket;
158
+ private _clearAllStaged;
159
+ /**
160
+ * The trigger's pill-clear (`x`) is an explicit user action that means "back
161
+ * to the neutral state". Unlike the basket's "Clear all" (which only touches
162
+ * staged), this commits immediately — the pill lives on the trigger, which
163
+ * only reflects committed state. Dispatches `bv-apply` because a commit
164
+ * happened.
165
+ */
166
+ private _clearCommitted;
167
+ private _stagedDiffersFromCommitted;
168
+ private _commitStaged;
169
+ private _apply;
170
+ private _dispatchBasketChange;
171
+ private _dispatchModeChange;
172
+ private _dispatchShowInactiveChange;
173
+ private _dispatchApply;
174
+ private _dispatchSearch;
175
+ private _scheduleSearchDispatch;
176
+ /**
177
+ * BTN-10322 — Arrow Up/Down navigation between result rows; Enter or Space
178
+ * adds the currently-focused row to the staged basket. Home/End jump to
179
+ * first/last. Escape delegates to the shared dropdown-controller (already
180
+ * wired on the host).
181
+ *
182
+ * `role="option"` rows use `aria-selected` only to communicate focus
183
+ * position to assistive tech — clicking / Enter also fires the add flow,
184
+ * which then removes the row from the results list.
185
+ */
186
+ private _onResultsKeyDown;
187
+ /**
188
+ * BTN-10322 — count of the currently committed basket. Consumers can read
189
+ * this to render an external counter badge without duplicating state.
190
+ */
191
+ get count(): number;
192
+ /**
193
+ * BTN-10322 — cascade reset from outside the panel. This is THE canonical
194
+ * way for a consumer to clear the picker in response to an upstream filter
195
+ * change (e.g. CET Report when Categories or Groups change). It:
196
+ *
197
+ * - clears the committed `basket` and the staged edit,
198
+ * - clears `results` + `resultsTotal` (post-cascade the last search's
199
+ * rows are stale — leaving them visible next to the cleared banner
200
+ * is confusing),
201
+ * - clears the internal search query,
202
+ * - surfaces the "cleared" affordance banner with `{ reason }` interpolated
203
+ * into the copy,
204
+ * - dispatches `bv-cleared` with `{ reason }`.
205
+ *
206
+ * Does NOT dispatch `bv-apply` — this is not a user commit, it's an
207
+ * out-of-band cascade. The consumer that triggered it already knows the
208
+ * basket was cleared.
209
+ *
210
+ * The affordance auto-hides the next time the user opens the panel or
211
+ * dismisses it explicitly via the banner's close button.
212
+ *
213
+ * **Prefer this over `element.basket = []`** — the setter is silent (no
214
+ * bv-cleared, no visible affordance) and is intended for state restoration
215
+ * from URL/deep-link, not for cascade reset.
216
+ */
217
+ reset({ reason }: {
218
+ reason: string;
219
+ }): void;
220
+ private _syncClearedAffordance;
221
+ private _dismissClearedAffordance;
222
+ }