stimeo-ui 0.2.0 → 0.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.
- package/CHANGELOG.md +163 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +97 -10
- package/dist/controllers/breadcrumb_controller.js +225 -13
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +11 -1
- package/dist/controllers/calendar_controller.js +89 -22
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +115 -0
- package/dist/controllers/carousel_controller.js +313 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +87 -0
- package/dist/controllers/color_picker_controller.js +252 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +14 -1
- package/dist/controllers/combobox_controller.js +162 -23
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +194 -17
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +7 -6
- package/dist/controllers/context_menu_controller.js +32 -10
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +89 -0
- package/dist/controllers/data_grid_controller.js +246 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +88 -0
- package/dist/controllers/date_range_picker_controller.js +441 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +169 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +67 -19
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +54 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +9 -2
- package/dist/controllers/listbox_controller.js +257 -53
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -2
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +29 -11
- package/dist/controllers/menu_controller.js +104 -17
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +142 -0
- package/dist/controllers/menubar_controller.js +785 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +141 -0
- package/dist/controllers/multi_select_controller.js +755 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +146 -0
- package/dist/controllers/navigation_menu_controller.js +511 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +1 -1
- package/dist/controllers/number_input_controller.js +7 -0
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +18 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
- package/dist/controllers/overflow_indicator_controller.js +246 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +78 -19
- package/dist/controllers/overflow_menu_controller.js +381 -57
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +36 -5
- package/dist/controllers/pagination_controller.js +163 -32
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/persist_controller.js +6 -6
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +2 -3
- package/dist/controllers/pointer_drag_controller.js +9 -1
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -2
- package/dist/controllers/popover_controller.js +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +4 -4
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +22 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +97 -0
- package/dist/controllers/range_slider_controller.js +192 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/rating_controller.js +16 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +238 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -1
- package/dist/controllers/resizable_controller.js +65 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +17 -2
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +101 -14
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +40 -6
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +64 -15
- package/dist/controllers/scrollspy_controller.js +369 -74
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +69 -0
- package/dist/controllers/separator_controller.js +96 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.js +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +32 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +3 -1
- package/dist/controllers/sortable_controller.js +34 -3
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
- package/dist/controllers/stick_to_bottom_controller.js +9 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +109 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -2
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js +1 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -4
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +295 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +42 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +231 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +5 -14
- package/dist/controllers/toast_controller.js +40 -14
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +23 -2
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +53 -13
- package/dist/controllers/toolbar_controller.js +230 -31
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +135 -0
- package/dist/controllers/tree_view_controller.js +691 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +4256 -915
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +228 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +419 -48
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +26 -26
- package/dist/inspector/manifest.json +1054 -127
- package/package.json +5 -6
|
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/overflow_menu_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/focus_candidate.ts
|
|
6
|
+
function inheritsFieldsetDisabled(control) {
|
|
7
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
8
|
+
while (fieldset) {
|
|
9
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
10
|
+
if (!legend?.contains(control)) return true;
|
|
11
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function canTakeFocus(element) {
|
|
16
|
+
if (element.closest("[hidden]")) return false;
|
|
17
|
+
if (!("disabled" in element)) return true;
|
|
18
|
+
if (element.disabled) return false;
|
|
19
|
+
return !inheritsFieldsetDisabled(element);
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
// src/utils/layout_observer.ts
|
|
6
23
|
var LayoutObserver = class {
|
|
7
24
|
#callback;
|
|
@@ -111,7 +128,42 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
111
128
|
}
|
|
112
129
|
};
|
|
113
130
|
|
|
131
|
+
// src/utils/tabindex_loan.ts
|
|
132
|
+
var TabindexLoan = class {
|
|
133
|
+
#value;
|
|
134
|
+
#lent = /* @__PURE__ */ new Set();
|
|
135
|
+
/**
|
|
136
|
+
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
137
|
+
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
138
|
+
* which a scroll region with no focusable content of its own needs.
|
|
139
|
+
*/
|
|
140
|
+
constructor(value = "-1") {
|
|
141
|
+
this.#value = value;
|
|
142
|
+
}
|
|
143
|
+
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
144
|
+
lend(element) {
|
|
145
|
+
if (element.hasAttribute("tabindex")) return;
|
|
146
|
+
element.setAttribute("tabindex", this.#value);
|
|
147
|
+
this.#lent.add(element);
|
|
148
|
+
}
|
|
149
|
+
/** Takes back every loan whose value is still the one that was lent. */
|
|
150
|
+
returnAll() {
|
|
151
|
+
for (const element of this.#lent) {
|
|
152
|
+
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
153
|
+
}
|
|
154
|
+
this.#lent.clear();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
114
158
|
// src/controllers/overflow_menu_controller.ts
|
|
159
|
+
var BANKED = "data-stimeo--overflow-menu-banked";
|
|
160
|
+
var INDEX = "data-stimeo--overflow-menu-index";
|
|
161
|
+
var BOUNDARY = "data-stimeo--overflow-menu-boundary";
|
|
162
|
+
var SAVED_ROLE = "data-stimeo--overflow-menu-role";
|
|
163
|
+
var SAVED_TABINDEX = "data-stimeo--overflow-menu-tabindex";
|
|
164
|
+
var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-menu-target";
|
|
165
|
+
var MENU_TARGET = "data-stimeo--menu-target";
|
|
166
|
+
var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
|
|
115
167
|
var OverflowMenuController = class extends Controller {
|
|
116
168
|
static targets = ["items", "more"];
|
|
117
169
|
static values = {
|
|
@@ -122,24 +174,26 @@ var OverflowMenuController = class extends Controller {
|
|
|
122
174
|
static events = ["change"];
|
|
123
175
|
#layout = new LayoutObserver(() => this.#scheduleUpdate());
|
|
124
176
|
#timers = new SafeTimeout();
|
|
125
|
-
/** The managed items in canonical (authored) order,
|
|
177
|
+
/** The managed items in canonical (authored) order, rebuilt from the DOM each pass. */
|
|
126
178
|
#items = [];
|
|
127
179
|
#index = /* @__PURE__ */ new WeakMap();
|
|
128
|
-
/**
|
|
129
|
-
* Cached bar-context width per item. An item's natural width is location-independent,
|
|
130
|
-
* so this lets a measure pass read only the items currently in the bar (menu items
|
|
131
|
-
* reuse their last value) instead of pulling everything back to measure — one reflow,
|
|
132
|
-
* not two.
|
|
133
|
-
*/
|
|
180
|
+
/** Cached bar-context width per item; see the class remarks for the staleness caveat. */
|
|
134
181
|
#widths = /* @__PURE__ */ new WeakMap();
|
|
135
182
|
/** Last reported overflow count, so `change` fires only on transitions. */
|
|
136
183
|
#lastHidden = null;
|
|
184
|
+
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
185
|
+
#tabindex = new TabindexLoan();
|
|
186
|
+
/** Hands Turbo a pristine snapshot: the cache is written after this event. */
|
|
187
|
+
#onBeforeCache = () => {
|
|
188
|
+
this.#restoreAll();
|
|
189
|
+
};
|
|
137
190
|
connect() {
|
|
138
191
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
139
192
|
const trigger = this.#trigger();
|
|
140
|
-
if (trigger && (trigger
|
|
193
|
+
if (trigger !== null && this.#isBareTrigger(trigger)) {
|
|
141
194
|
trigger.textContent = this.moreLabelValue;
|
|
142
195
|
}
|
|
196
|
+
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
143
197
|
this.#layout.observe(this.element);
|
|
144
198
|
this.#layout.observeViewport();
|
|
145
199
|
this.update();
|
|
@@ -147,6 +201,8 @@ var OverflowMenuController = class extends Controller {
|
|
|
147
201
|
disconnect() {
|
|
148
202
|
this.#layout.disconnect();
|
|
149
203
|
this.#timers.clearAll();
|
|
204
|
+
document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
205
|
+
this.#restoreAll();
|
|
150
206
|
this.#lastHidden = null;
|
|
151
207
|
}
|
|
152
208
|
/** Re-measures and rebalances items between the bar and the More menu. */
|
|
@@ -154,36 +210,67 @@ var OverflowMenuController = class extends Controller {
|
|
|
154
210
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
155
211
|
this.#syncItems();
|
|
156
212
|
this.moreTarget.hidden = false;
|
|
213
|
+
if (this.#items.some((item) => item.parentElement !== this.itemsTarget && !this.#widths.has(item))) {
|
|
214
|
+
this.#removeBoundary();
|
|
215
|
+
for (const item of this.#items) this.#unbank(item);
|
|
216
|
+
this.#reorder(this.itemsTarget, this.#items);
|
|
217
|
+
}
|
|
157
218
|
for (const item of this.#items) {
|
|
158
219
|
if (item.parentElement === this.itemsTarget) this.#widths.set(item, item.offsetWidth);
|
|
159
220
|
}
|
|
160
221
|
const moreWidth = (this.#trigger() ?? this.moreTarget).offsetWidth;
|
|
161
|
-
const
|
|
222
|
+
const itemGap = this.#gap(this.itemsTarget);
|
|
223
|
+
const barGap = this.#gap(this.element);
|
|
162
224
|
const widthOf = (el) => this.#widths.get(el) ?? el.offsetWidth;
|
|
163
|
-
const containerWidth = this.element.clientWidth;
|
|
164
|
-
const
|
|
165
|
-
|
|
225
|
+
const containerWidth = this.element.clientWidth - this.#paddingX(this.element);
|
|
226
|
+
const rendered = this.#items.filter((item) => this.#rendered(item));
|
|
227
|
+
const itemsWidth = rendered.reduce((sum, el) => sum + widthOf(el), 0);
|
|
228
|
+
let visibleWidth = itemsWidth + Math.max(0, rendered.length - 1) * itemGap;
|
|
166
229
|
const hidden = /* @__PURE__ */ new Set();
|
|
167
230
|
if (visibleWidth > containerWidth) {
|
|
168
|
-
const budget = containerWidth - moreWidth -
|
|
169
|
-
const dropOrder = [...
|
|
231
|
+
const budget = containerWidth - moreWidth - barGap;
|
|
232
|
+
const dropOrder = [...rendered].sort(
|
|
170
233
|
(a, b) => this.#rank(b) - this.#rank(a) || this.#indexOf(b) - this.#indexOf(a)
|
|
171
234
|
);
|
|
172
235
|
for (const item of dropOrder) {
|
|
173
236
|
if (visibleWidth <= budget) break;
|
|
174
237
|
hidden.add(item);
|
|
175
|
-
visibleWidth -= widthOf(item) +
|
|
238
|
+
visibleWidth -= widthOf(item) + itemGap;
|
|
176
239
|
}
|
|
177
240
|
}
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
241
|
+
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
242
|
+
const focused = active === null ? null : this.#ownerOf(active);
|
|
243
|
+
const retreating = focused !== null && hidden.has(focused) && focused.parentElement === this.itemsTarget && // i.e. *starting* to retreat
|
|
244
|
+
!this.#menuExpanded();
|
|
245
|
+
const returning = focused !== null && !hidden.has(focused) && focused.parentElement !== this.itemsTarget && // i.e. *leaving* the menu
|
|
246
|
+
this.#menuExpanded();
|
|
247
|
+
if (hidden.size === 0 || hidden.size !== this.#items.length) this.#removeBoundary();
|
|
181
248
|
for (const item of this.#items) {
|
|
182
|
-
if (hidden.has(item)) this.#
|
|
183
|
-
else this.#
|
|
249
|
+
if (hidden.has(item)) this.#bank(item);
|
|
250
|
+
else this.#unbank(item);
|
|
184
251
|
}
|
|
185
|
-
|
|
252
|
+
this.#reorder(
|
|
253
|
+
this.#menuList(),
|
|
254
|
+
this.#items.filter((item) => hidden.has(item))
|
|
255
|
+
);
|
|
256
|
+
this.#reorder(
|
|
257
|
+
this.itemsTarget,
|
|
258
|
+
this.#items.filter((item) => !hidden.has(item))
|
|
259
|
+
);
|
|
260
|
+
if (retreating) (this.#trigger() ?? this.moreTarget).focus();
|
|
261
|
+
else if (active !== null && this.#lostFocus(active)) active.focus();
|
|
186
262
|
const count = hidden.size;
|
|
263
|
+
for (const item of this.#items) {
|
|
264
|
+
if (count > 0) item.setAttribute(INDEX, String(this.#indexOf(item)));
|
|
265
|
+
else item.removeAttribute(INDEX);
|
|
266
|
+
}
|
|
267
|
+
if (count > 0 && count === this.#items.length) this.#ensureBoundary();
|
|
268
|
+
else this.#removeBoundary();
|
|
269
|
+
const inMore = document.activeElement instanceof HTMLElement && this.moreTarget.contains(document.activeElement);
|
|
270
|
+
if (count === 0 || returning) {
|
|
271
|
+
this.#closeMenu();
|
|
272
|
+
if (inMore) this.#rescueFocus();
|
|
273
|
+
}
|
|
187
274
|
this.moreTarget.hidden = count === 0;
|
|
188
275
|
if (count > 0) this.element.setAttribute("data-overflowing", "true");
|
|
189
276
|
else this.element.removeAttribute("data-overflowing");
|
|
@@ -193,30 +280,142 @@ var OverflowMenuController = class extends Controller {
|
|
|
193
280
|
this.dispatch("change", { detail: { visible: this.#items.length - count, hidden: count } });
|
|
194
281
|
}
|
|
195
282
|
}
|
|
196
|
-
/** The flex `column-gap`
|
|
197
|
-
#gap() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
283
|
+
/** The flex `column-gap` on `el` in px (0 when none / unsupported). */
|
|
284
|
+
#gap(el) {
|
|
285
|
+
const style = this.#styleOf(el);
|
|
286
|
+
return style === null ? 0 : this.#px(style.columnGap || style.gap);
|
|
287
|
+
}
|
|
288
|
+
/** Horizontal padding on `el` in px — `clientWidth` counts it, the content box cannot. */
|
|
289
|
+
#paddingX(el) {
|
|
290
|
+
const style = this.#styleOf(el);
|
|
291
|
+
return style === null ? 0 : this.#px(style.paddingLeft) + this.#px(style.paddingRight);
|
|
292
|
+
}
|
|
293
|
+
#styleOf(el) {
|
|
294
|
+
return typeof window.getComputedStyle === "function" ? window.getComputedStyle(el) : null;
|
|
295
|
+
}
|
|
296
|
+
#px(value) {
|
|
297
|
+
const parsed = Number.parseFloat(value ?? "");
|
|
298
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
202
299
|
}
|
|
203
300
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* and
|
|
207
|
-
* live in the menu (still within the controller element) and are retained.
|
|
301
|
+
* Rebuilds the managed set and the canonical order from the DOM. The bar's children
|
|
302
|
+
* give the order of everything inline (so a leading insert is not silently appended)
|
|
303
|
+
* and {@link #merge} weaves the banked items back among them.
|
|
208
304
|
*/
|
|
209
305
|
#syncItems() {
|
|
210
|
-
const
|
|
306
|
+
const previous = this.#items;
|
|
307
|
+
const bar = [];
|
|
308
|
+
let boundaryAt;
|
|
211
309
|
for (const el of this.itemsTarget.children) {
|
|
212
|
-
if (el instanceof
|
|
310
|
+
if (el instanceof HTMLTemplateElement && el.hasAttribute(BOUNDARY)) {
|
|
311
|
+
boundaryAt ??= bar.length;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (el instanceof HTMLElement) bar.push(el);
|
|
213
315
|
}
|
|
214
|
-
|
|
316
|
+
const banked = [];
|
|
317
|
+
for (const el of this.#menuList().children) {
|
|
318
|
+
if (el instanceof HTMLElement && el.hasAttribute(BANKED)) banked.push(el);
|
|
319
|
+
}
|
|
320
|
+
banked.sort((a, b) => this.#bankedIndex(a) - this.#bankedIndex(b));
|
|
321
|
+
const items = this.#merge(bar, banked, boundaryAt);
|
|
322
|
+
for (const el of previous) {
|
|
323
|
+
if (!items.includes(el)) {
|
|
324
|
+
this.#unbank(el);
|
|
325
|
+
el.removeAttribute(INDEX);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.#items = items;
|
|
215
329
|
this.#index = /* @__PURE__ */ new WeakMap();
|
|
216
|
-
|
|
330
|
+
items.forEach((el, i) => {
|
|
217
331
|
this.#index.set(el, i);
|
|
218
332
|
});
|
|
219
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Interleaves the banked items back among the bar's children.
|
|
336
|
+
*
|
|
337
|
+
* A saved index is a position in the *canonical* order, so it can only be compared
|
|
338
|
+
* against another canonical position — never used as an offset into the current bar,
|
|
339
|
+
* which is a shorter and differently-indexed list. (Doing so survives an untouched
|
|
340
|
+
* round trip and silently scrambles the order as soon as the consumer inserts into a
|
|
341
|
+
* bar that is already short, and `#bank()` then burns the scrambled index back into
|
|
342
|
+
* the attribute, so widening never heals it.) Each bar child the controller already
|
|
343
|
+
* knows carries its canonical index from the previous pass; a banked item goes in
|
|
344
|
+
* front of the first known child whose index is higher.
|
|
345
|
+
*
|
|
346
|
+
* Children never seen before — inserted by the consumer since the last pass — have no
|
|
347
|
+
* canonical position yet, so they keep their DOM neighbours. The exception is a
|
|
348
|
+
* *trailing* run of them: appending to the bar means appending to the toolbar, so
|
|
349
|
+
* everything banked is emitted before it.
|
|
350
|
+
*
|
|
351
|
+
* With no known child at all (a fresh instance connecting to markup that already
|
|
352
|
+
* holds banked items), a fully-banked snapshot's inert boundary preserves whether an
|
|
353
|
+
* unindexed run was prepended or appended before connect. Older or server-rendered
|
|
354
|
+
* markup has no boundary; for that compatibility path the saved index is used as the
|
|
355
|
+
* offset, the inverse of the move that banked it.
|
|
356
|
+
*/
|
|
357
|
+
#merge(bar, banked, boundaryAt) {
|
|
358
|
+
const canonical = bar.map((el) => this.#index.get(el) ?? this.#savedIndex(el));
|
|
359
|
+
let lastKnown = -1;
|
|
360
|
+
canonical.forEach((index, i) => {
|
|
361
|
+
if (index !== void 0) lastKnown = i;
|
|
362
|
+
});
|
|
363
|
+
if (lastKnown === -1) {
|
|
364
|
+
if (boundaryAt !== void 0) {
|
|
365
|
+
return [...bar.slice(0, boundaryAt), ...banked, ...bar.slice(boundaryAt)];
|
|
366
|
+
}
|
|
367
|
+
const restored = [...bar];
|
|
368
|
+
for (const item of banked) {
|
|
369
|
+
restored.splice(Math.min(this.#bankedIndex(item), restored.length), 0, item);
|
|
370
|
+
}
|
|
371
|
+
return restored;
|
|
372
|
+
}
|
|
373
|
+
const items = [];
|
|
374
|
+
let next = 0;
|
|
375
|
+
const drainWhile = (accept) => {
|
|
376
|
+
while (next < banked.length) {
|
|
377
|
+
const item = banked[next];
|
|
378
|
+
if (item === void 0 || !accept(this.#bankedIndex(item))) break;
|
|
379
|
+
items.push(item);
|
|
380
|
+
next++;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
bar.forEach((el, i) => {
|
|
384
|
+
const index = canonical[i];
|
|
385
|
+
if (index !== void 0) drainWhile((saved) => saved < index);
|
|
386
|
+
else if (i > lastKnown) drainWhile(() => true);
|
|
387
|
+
items.push(el);
|
|
388
|
+
});
|
|
389
|
+
drainWhile(() => true);
|
|
390
|
+
return items;
|
|
391
|
+
}
|
|
392
|
+
/** The canonical index a banked item saved, or ∞ (append) when it carries none. */
|
|
393
|
+
#bankedIndex(item) {
|
|
394
|
+
return this.#savedIndex(item) ?? Number.POSITIVE_INFINITY;
|
|
395
|
+
}
|
|
396
|
+
/** A finite canonical index stored on either side of the overflow split. */
|
|
397
|
+
#savedIndex(item) {
|
|
398
|
+
const raw = item.getAttribute(INDEX);
|
|
399
|
+
if (raw === null) return void 0;
|
|
400
|
+
const value = Number(raw);
|
|
401
|
+
return Number.isFinite(value) ? value : void 0;
|
|
402
|
+
}
|
|
403
|
+
/** The inert split point written only when every managed item lives in More. */
|
|
404
|
+
#boundary() {
|
|
405
|
+
for (const el of this.itemsTarget.children) {
|
|
406
|
+
if (el instanceof HTMLTemplateElement && el.hasAttribute(BOUNDARY)) return el;
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
#ensureBoundary() {
|
|
411
|
+
if (this.#boundary() !== null) return;
|
|
412
|
+
const boundary = document.createElement("template");
|
|
413
|
+
boundary.setAttribute(BOUNDARY, "");
|
|
414
|
+
this.itemsTarget.appendChild(boundary);
|
|
415
|
+
}
|
|
416
|
+
#removeBoundary() {
|
|
417
|
+
this.#boundary()?.remove();
|
|
418
|
+
}
|
|
220
419
|
/** Debounced re-measure for resize-driven churn. */
|
|
221
420
|
#scheduleUpdate() {
|
|
222
421
|
this.#timers.clearAll();
|
|
@@ -229,41 +428,166 @@ var OverflowMenuController = class extends Controller {
|
|
|
229
428
|
#trigger() {
|
|
230
429
|
return this.moreTarget.querySelector('[data-stimeo--menu-target="trigger"]');
|
|
231
430
|
}
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Whether the More trigger is bare enough for the label to be safe to write.
|
|
433
|
+
* Assigning `textContent` replaces *every* child node, so a trigger holding an
|
|
434
|
+
* icon would silently lose it — and the loss is permanent, since the restore
|
|
435
|
+
* pass returns items but not authored trigger content. A trigger that already
|
|
436
|
+
* carries an accessible name is left alone too: injecting visible text under a
|
|
437
|
+
* different `aria-label` would make the label and the name disagree. A trigger
|
|
438
|
+
* with neither text, nor children, nor a name has nothing to lose and gets the
|
|
439
|
+
* fallback; one that is empty *and* unnamed is the only case the label rescues.
|
|
440
|
+
*/
|
|
441
|
+
#isBareTrigger(trigger) {
|
|
442
|
+
return (trigger.textContent ?? "").trim() === "" && trigger.firstElementChild === null && !trigger.hasAttribute("aria-label") && !trigger.hasAttribute("aria-labelledby");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Whether the More menu is expanded. Menu owns that state and reflects it on the
|
|
446
|
+
* trigger's `aria-expanded`, so read the contract rather than guess from `hidden`
|
|
447
|
+
* (which is absent until Menu connects).
|
|
448
|
+
*/
|
|
449
|
+
#menuExpanded() {
|
|
450
|
+
return this.#trigger()?.getAttribute("aria-expanded") === "true";
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Collapses the More menu before its wrapper is hidden. Menu owns the state (and the
|
|
454
|
+
* Escape-stack membership that goes with it), so ask it; the DOM fallback covers markup
|
|
455
|
+
* that reached an expanded state with no Menu mounted.
|
|
456
|
+
*/
|
|
457
|
+
#closeMenu() {
|
|
458
|
+
if (!this.#menuExpanded()) return;
|
|
459
|
+
const menu = this.application.getControllerForElementAndIdentifier(
|
|
460
|
+
this.moreTarget,
|
|
461
|
+
"stimeo--menu"
|
|
462
|
+
);
|
|
463
|
+
if (menu) {
|
|
464
|
+
menu.close();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this.#trigger()?.setAttribute("aria-expanded", "false");
|
|
468
|
+
const list = this.#menuList();
|
|
469
|
+
if (list !== this.moreTarget) list.hidden = true;
|
|
470
|
+
}
|
|
471
|
+
/** The managed item that is, or contains, `el` — focus rescue works on either. */
|
|
472
|
+
#ownerOf(el) {
|
|
473
|
+
return this.#items.find((item) => item === el || item.contains(el)) ?? null;
|
|
474
|
+
}
|
|
475
|
+
/** Whether the item takes up room in the bar; an authored `hidden` one does not. */
|
|
476
|
+
#rendered(item) {
|
|
477
|
+
return !item.hasAttribute("hidden");
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Moves focus out of the More wrapper just before it is hidden, to the last
|
|
481
|
+
* item in canonical order that can hold it. When every item refuses, the root
|
|
482
|
+
* takes focus instead of letting the browser drop it to `<body>`, borrowing a
|
|
483
|
+
* `tabindex="-1"` just-in-time — not a Tab stop, and handed back on restore
|
|
484
|
+
* when this controller was the one that added it.
|
|
485
|
+
*/
|
|
486
|
+
#rescueFocus() {
|
|
487
|
+
for (let i = this.#items.length - 1; i >= 0; i--) {
|
|
488
|
+
const item = this.#items[i];
|
|
489
|
+
if (item !== void 0 && canTakeFocus(item)) {
|
|
490
|
+
item.focus();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
this.#tabindex.lend(this.element);
|
|
495
|
+
this.element.focus();
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Whether `el` held focus, is still in a visible part of this controller, and lost it —
|
|
499
|
+
* what a re-insert does in a real browser. An item that retreated into a collapsed menu
|
|
500
|
+
* is excluded: it sits in a `hidden` subtree and must not be focused back.
|
|
501
|
+
*/
|
|
502
|
+
#lostFocus(el) {
|
|
503
|
+
return el.isConnected && document.activeElement !== el && this.element.contains(el) && el.closest("[hidden]") === null;
|
|
504
|
+
}
|
|
505
|
+
/** Returns every item to the bar and removes all traces of this controller. */
|
|
506
|
+
#restoreAll() {
|
|
507
|
+
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
508
|
+
this.#syncItems();
|
|
509
|
+
this.#removeBoundary();
|
|
510
|
+
this.#reorder(this.itemsTarget, this.#items);
|
|
511
|
+
for (const item of this.#items) {
|
|
512
|
+
this.#unbank(item);
|
|
513
|
+
item.removeAttribute(INDEX);
|
|
514
|
+
}
|
|
515
|
+
this.#closeMenu();
|
|
516
|
+
this.moreTarget.hidden = true;
|
|
517
|
+
this.element.removeAttribute("data-overflowing");
|
|
518
|
+
this.element.removeAttribute("data-overflow-count");
|
|
519
|
+
this.#tabindex.returnAll();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Makes `parent`'s managed children match `sequence`, moving only what is out of place.
|
|
523
|
+
* Walking backwards lets each item be compared against the element that should follow
|
|
524
|
+
* it, so an unchanged run costs zero DOM mutations — the point of the exercise, since
|
|
525
|
+
* every re-insert blurs the focused node.
|
|
526
|
+
*/
|
|
527
|
+
#reorder(parent, sequence) {
|
|
528
|
+
let next = null;
|
|
529
|
+
for (let i = sequence.length - 1; i >= 0; i--) {
|
|
530
|
+
const item = sequence[i];
|
|
531
|
+
if (item === void 0) continue;
|
|
532
|
+
if (item.parentElement !== parent || item.nextElementSibling !== next) {
|
|
533
|
+
this.#insert(parent, item, next);
|
|
534
|
+
}
|
|
535
|
+
next = item;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** Relocates `item`, preferring `moveBefore` (keeps focus and element state). */
|
|
539
|
+
#insert(parent, item, before) {
|
|
540
|
+
const move = parent.moveBefore;
|
|
541
|
+
if (typeof move === "function") {
|
|
542
|
+
try {
|
|
543
|
+
move.call(parent, item, before);
|
|
544
|
+
return;
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
parent.insertBefore(item, before);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Gives an item menuitem semantics for Menu to drive and records its canonical index,
|
|
552
|
+
* saving every authored value it overwrites ("" means the attribute was absent).
|
|
553
|
+
*/
|
|
554
|
+
#bank(item) {
|
|
555
|
+
if (!item.hasAttribute(BANKED)) {
|
|
556
|
+
item.setAttribute(BANKED, "true");
|
|
557
|
+
item.setAttribute(SAVED_ROLE, item.getAttribute("role") ?? "");
|
|
558
|
+
item.setAttribute(SAVED_TABINDEX, item.getAttribute("tabindex") ?? "");
|
|
559
|
+
item.setAttribute(SAVED_MENU_TARGET, item.getAttribute(MENU_TARGET) ?? "");
|
|
249
560
|
item.setAttribute("role", "menuitem");
|
|
250
561
|
item.setAttribute("tabindex", "-1");
|
|
251
|
-
item.setAttribute(
|
|
562
|
+
item.setAttribute(MENU_TARGET, "item");
|
|
252
563
|
}
|
|
253
|
-
|
|
564
|
+
item.setAttribute(INDEX, String(this.#indexOf(item)));
|
|
565
|
+
}
|
|
566
|
+
/** Undoes the banking: restores every authored value and drops the bookkeeping. */
|
|
567
|
+
#unbank(item) {
|
|
568
|
+
if (!item.hasAttribute(BANKED)) return;
|
|
569
|
+
this.#restoreAttr(item, "role", item.getAttribute(SAVED_ROLE));
|
|
570
|
+
this.#restoreAttr(item, "tabindex", item.getAttribute(SAVED_TABINDEX));
|
|
571
|
+
this.#restoreAttr(item, MENU_TARGET, item.getAttribute(SAVED_MENU_TARGET));
|
|
572
|
+
for (const name of BOOKKEEPING) item.removeAttribute(name);
|
|
254
573
|
}
|
|
255
574
|
/** Re-applies a saved attribute value, or removes the attribute when it was absent. */
|
|
256
575
|
#restoreAttr(item, name, original) {
|
|
257
|
-
if (original ===
|
|
576
|
+
if (original === null || original === "") item.removeAttribute(name);
|
|
258
577
|
else item.setAttribute(name, original);
|
|
259
578
|
}
|
|
260
579
|
#indexOf(item) {
|
|
261
580
|
return this.#index.get(item) ?? 0;
|
|
262
581
|
}
|
|
263
|
-
/**
|
|
582
|
+
/**
|
|
583
|
+
* Retention rank: lower keeps longer. An absent, empty, or non-numeric `data-priority`
|
|
584
|
+
* ranks lowest (drops first) — empty is the common ERB accident
|
|
585
|
+
* (`data-priority="<%= maybe_nil %>"`), and `Number("")` is 0, which would otherwise
|
|
586
|
+
* read as the *highest* retention.
|
|
587
|
+
*/
|
|
264
588
|
#rank(item) {
|
|
265
589
|
const raw = item.getAttribute("data-priority");
|
|
266
|
-
if (raw === null) return Number.POSITIVE_INFINITY;
|
|
590
|
+
if (raw === null || raw.trim() === "") return Number.POSITIVE_INFINITY;
|
|
267
591
|
const value = Number(raw);
|
|
268
592
|
return Number.isNaN(value) ? Number.POSITIVE_INFINITY : value;
|
|
269
593
|
}
|