stimeo-ui 0.2.1 → 0.4.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 +160 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +32 -5
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +32 -6
- package/dist/controllers/announcer_controller.js +255 -20
- package/dist/controllers/announcer_controller.js.map +1 -1
- 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 +16 -1
- package/dist/controllers/carousel_controller.js +47 -6
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.js +2 -2
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +14 -2
- package/dist/controllers/color_picker_controller.js +92 -7
- package/dist/controllers/color_picker_controller.js.map +1 -1
- 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 +226 -22
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +32 -5
- 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.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +19 -5
- package/dist/controllers/countdown_controller.js +112 -12
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +19 -1
- package/dist/controllers/data_grid_controller.js +82 -4
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +5 -1
- package/dist/controllers/date_range_picker_controller.js +77 -3
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +32 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +32 -5
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +1 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +9 -4
- package/dist/controllers/empty_state_controller.js +24 -10
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js +32 -5
- 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/frame_loading_controller.d.ts +14 -3
- package/dist/controllers/frame_loading_controller.js +177 -15
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- 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 +36 -11
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +31 -10
- 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.d.ts +19 -3
- package/dist/controllers/local_time_controller.js +102 -8
- package/dist/controllers/local_time_controller.js.map +1 -1
- 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 +91 -11
- package/dist/controllers/menubar_controller.js +415 -63
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +22 -2
- package/dist/controllers/meter_controller.js +145 -26
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +31 -5
- package/dist/controllers/multi_select_controller.js +312 -29
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +65 -18
- package/dist/controllers/navigation_menu_controller.js +154 -27
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +13 -1
- package/dist/controllers/network_status_controller.js +28 -8
- package/dist/controllers/network_status_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 +3 -2
- package/dist/controllers/overflow_indicator_controller.js +81 -13
- 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 +408 -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/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.d.ts +16 -1
- package/dist/controllers/progress_controller.js +116 -9
- 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 +18 -5
- package/dist/controllers/range_slider_controller.js +100 -9
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +4 -1
- package/dist/controllers/rating_controller.js +69 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js +63 -19
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +13 -0
- package/dist/controllers/relative_time_controller.js +133 -12
- package/dist/controllers/relative_time_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 +86 -12
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +1 -1
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +33 -3
- 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 +346 -73
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +9 -0
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +2 -12
- package/dist/controllers/sidebar_controller.js +37 -8
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +7 -2
- package/dist/controllers/skeleton_controller.js +73 -20
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +49 -8
- 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.d.ts +37 -4
- package/dist/controllers/spinner_controller.js +228 -27
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +12 -1
- package/dist/controllers/step_indicator_controller.js +82 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +31 -8
- package/dist/controllers/stick_to_bottom_controller.js +61 -10
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +32 -11
- 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 +11 -3
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js +22 -2
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +22 -3
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +20 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -5
- package/dist/controllers/toast_controller.js +4 -5
- 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.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +87 -6
- package/dist/controllers/tree_view_controller.js +467 -51
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +4507 -907
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +1009 -86
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1200 -120
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +28 -28
- package/dist/inspector/manifest.json +1060 -132
- package/package.json +5 -6
|
@@ -25,12 +25,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
25
25
|
*
|
|
26
26
|
* @remarks
|
|
27
27
|
* Behavior only. The consumer owns all layout (e.g. positioning the thumb from
|
|
28
|
-
* the fraction). Only the horizontal orientation is handled
|
|
28
|
+
* the fraction). Only the horizontal orientation is handled.
|
|
29
29
|
*
|
|
30
30
|
* Behavior provided:
|
|
31
31
|
* - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one
|
|
32
32
|
* step; `Home`/`End` jump to the min/max; `PageUp`/`PageDown` move by ten steps.
|
|
33
33
|
* - Pointer press/drag on the track sets the value from the pointer position.
|
|
34
|
+
*
|
|
35
|
+
* The fraction is a value ratio, not a position, so only the consumer knows
|
|
36
|
+
* whether their track mirrors under RTL. Set `logicalTrack` to declare that it
|
|
37
|
+
* does: the pointer mapping and the horizontal arrow pair then follow the
|
|
38
|
+
* writing direction. Left unset, nothing here reads `direction`.
|
|
34
39
|
*/
|
|
35
40
|
declare class SliderController extends Controller<HTMLElement> {
|
|
36
41
|
#private;
|
|
@@ -52,6 +57,10 @@ declare class SliderController extends Controller<HTMLElement> {
|
|
|
52
57
|
type: NumberConstructor;
|
|
53
58
|
default: number;
|
|
54
59
|
};
|
|
60
|
+
logicalTrack: {
|
|
61
|
+
type: BooleanConstructor;
|
|
62
|
+
default: boolean;
|
|
63
|
+
};
|
|
55
64
|
};
|
|
56
65
|
static actions: readonly ["onKeydown", "onPointerDown"];
|
|
57
66
|
static events: readonly ["change"];
|
|
@@ -63,6 +72,7 @@ declare class SliderController extends Controller<HTMLElement> {
|
|
|
63
72
|
maxValue: number;
|
|
64
73
|
stepValue: number;
|
|
65
74
|
valueValue: number;
|
|
75
|
+
logicalTrackValue: boolean;
|
|
66
76
|
/** Clamps the initial value and renders the starting position. */
|
|
67
77
|
connect(): void;
|
|
68
78
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/slider_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowKey(key, element) {
|
|
12
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
|
+
if (!isRtl(element)) return key;
|
|
14
|
+
return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
|
|
15
|
+
}
|
|
16
|
+
function isReservedArrowChord(event, allow = []) {
|
|
17
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
18
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/utils/range.ts
|
|
22
|
+
function rangeFraction(value, min, max) {
|
|
23
|
+
const span = max - min;
|
|
24
|
+
if (!(span > 0)) return 0;
|
|
25
|
+
const clamped = Math.min(max, Math.max(min, value));
|
|
26
|
+
let fraction;
|
|
27
|
+
if (Number.isFinite(span)) {
|
|
28
|
+
fraction = (clamped - min) / span;
|
|
29
|
+
} else {
|
|
30
|
+
const scale = Math.max(Math.abs(min), Math.abs(max));
|
|
31
|
+
fraction = (clamped / scale - min / scale) / (max / scale - min / scale);
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isFinite(fraction)) return 0;
|
|
34
|
+
return Math.min(1, Math.max(0, fraction));
|
|
35
|
+
}
|
|
36
|
+
|
|
3
37
|
// src/controllers/slider_controller.ts
|
|
4
38
|
var FRACTION_PROPERTY = "--stimeo--slider-fraction";
|
|
5
39
|
var SliderController = class extends Controller {
|
|
@@ -8,12 +42,17 @@ var SliderController = class extends Controller {
|
|
|
8
42
|
min: { type: Number, default: 0 },
|
|
9
43
|
max: { type: Number, default: 100 },
|
|
10
44
|
step: { type: Number, default: 1 },
|
|
11
|
-
value: { type: Number, default: 0 }
|
|
45
|
+
value: { type: Number, default: 0 },
|
|
46
|
+
logicalTrack: { type: Boolean, default: false }
|
|
12
47
|
};
|
|
13
48
|
static actions = ["onKeydown", "onPointerDown"];
|
|
14
49
|
static events = ["change"];
|
|
15
50
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
16
51
|
#dragAbort = null;
|
|
52
|
+
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
53
|
+
get #mirrored() {
|
|
54
|
+
return this.logicalTrackValue && isRtl(this.element);
|
|
55
|
+
}
|
|
17
56
|
/** Clamps the initial value and renders the starting position. */
|
|
18
57
|
connect() {
|
|
19
58
|
this.#setValue(this.valueValue, { silent: true });
|
|
@@ -25,9 +64,10 @@ var SliderController = class extends Controller {
|
|
|
25
64
|
}
|
|
26
65
|
/** Handles keyboard stepping per the APG slider model. */
|
|
27
66
|
onKeydown(event) {
|
|
67
|
+
if (isReservedArrowChord(event)) return;
|
|
28
68
|
const big = this.stepValue * 10;
|
|
29
69
|
let next = null;
|
|
30
|
-
switch (event.key) {
|
|
70
|
+
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
31
71
|
case "ArrowRight":
|
|
32
72
|
case "ArrowUp":
|
|
33
73
|
next = this.valueValue + this.stepValue;
|
|
@@ -58,12 +98,13 @@ var SliderController = class extends Controller {
|
|
|
58
98
|
onPointerDown(event) {
|
|
59
99
|
if (!this.hasTrackTarget) return;
|
|
60
100
|
event.preventDefault();
|
|
61
|
-
this.#
|
|
101
|
+
const mirrored = this.#mirrored;
|
|
102
|
+
this.#updateFromClientX(event.clientX, mirrored);
|
|
62
103
|
if (this.hasThumbTarget) this.thumbTarget.focus();
|
|
63
104
|
this.#dragAbort?.abort();
|
|
64
105
|
const abort = new AbortController();
|
|
65
106
|
this.#dragAbort = abort;
|
|
66
|
-
const onMove = (move) => this.#updateFromClientX(move.clientX);
|
|
107
|
+
const onMove = (move) => this.#updateFromClientX(move.clientX, mirrored);
|
|
67
108
|
const onUp = () => {
|
|
68
109
|
abort.abort();
|
|
69
110
|
this.#dragAbort = null;
|
|
@@ -73,10 +114,11 @@ var SliderController = class extends Controller {
|
|
|
73
114
|
document.addEventListener("pointercancel", onUp, { signal: abort.signal });
|
|
74
115
|
}
|
|
75
116
|
/** Maps a pointer X coordinate to a value using the track's geometry. */
|
|
76
|
-
#updateFromClientX(clientX) {
|
|
117
|
+
#updateFromClientX(clientX, mirrored) {
|
|
77
118
|
const rect = this.trackTarget.getBoundingClientRect();
|
|
78
119
|
if (rect.width === 0) return;
|
|
79
|
-
const
|
|
120
|
+
const offset = (clientX - rect.left) / rect.width;
|
|
121
|
+
const fraction = mirrored ? 1 - offset : offset;
|
|
80
122
|
this.#setValue(this.minValue + fraction * (this.maxValue - this.minValue));
|
|
81
123
|
}
|
|
82
124
|
/**
|
|
@@ -97,8 +139,7 @@ var SliderController = class extends Controller {
|
|
|
97
139
|
this.thumbTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
98
140
|
this.thumbTarget.setAttribute("aria-valuenow", String(value));
|
|
99
141
|
}
|
|
100
|
-
const
|
|
101
|
-
const fraction = span > 0 ? (value - this.minValue) / span : 0;
|
|
142
|
+
const fraction = rangeFraction(value, this.minValue, this.maxValue);
|
|
102
143
|
this.element.style.setProperty(FRACTION_PROPERTY, String(fraction));
|
|
103
144
|
if (changed && !silent) this.dispatch("change", { detail: { value } });
|
|
104
145
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/slider_controller.ts"],"names":[],"mappings":"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","file":"slider_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Name of the CSS custom property exposing the thumb position (0..1). */\nconst FRACTION_PROPERTY = \"--stimeo--slider-fraction\";\n\n/**\n * Headless, accessible slider (single-thumb range) behavior.\n *\n * Markup contract (identifier: `stimeo--slider`):\n * <div data-controller=\"stimeo--slider\"\n * data-stimeo--slider-min-value=\"0\"\n * data-stimeo--slider-max-value=\"100\"\n * data-stimeo--slider-step-value=\"1\"\n * data-stimeo--slider-value-value=\"40\">\n * <div data-stimeo--slider-target=\"track\"\n * data-action=\"pointerdown->stimeo--slider#onPointerDown\">\n * <div data-stimeo--slider-target=\"thumb\" role=\"slider\" tabindex=\"0\"\n * aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"40\"\n * data-action=\"keydown->stimeo--slider#onKeydown\"></div>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Slider** pattern. The current value is exposed\n * to assistive tech via `aria-valuenow`/`aria-valuemin`/`aria-valuemax` on the\n * thumb, and to the consumer's CSS via the `--stimeo--slider-fraction` custom\n * property (a number in `[0, 1]`) set on the controller element — the library\n * positions nothing itself.\n *\n * @remarks\n * Behavior only. The consumer owns all layout (e.g. positioning the thumb from\n * the fraction). Only the horizontal orientation is handled in this MVP.\n *\n * Behavior provided:\n * - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one\n * step; `Home`/`End` jump to the min/max; `PageUp`/`PageDown` move by ten steps.\n * - Pointer press/drag on the track sets the value from the pointer position.\n */\nexport class SliderController extends Controller<HTMLElement> {\n static override targets = [\"track\", \"thumb\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n value: { type: Number, default: 0 },\n };\n static actions = [\"onKeydown\", \"onPointerDown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly trackTarget: HTMLElement;\n declare readonly thumbTarget: HTMLElement;\n declare readonly hasTrackTarget: boolean;\n declare readonly hasThumbTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** Clamps the initial value and renders the starting position. */\n override connect(): void {\n this.#setValue(this.valueValue, { silent: true });\n }\n\n /** Cancels any active pointer drag so document listeners never leak. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /** Handles keyboard stepping per the APG slider model. */\n onKeydown(event: KeyboardEvent): void {\n const big = this.stepValue * 10;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n next = this.valueValue + this.stepValue;\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n next = this.valueValue - this.stepValue;\n break;\n case \"PageUp\":\n next = this.valueValue + big;\n break;\n case \"PageDown\":\n next = this.valueValue - big;\n break;\n case \"Home\":\n next = this.minValue;\n break;\n case \"End\":\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#setValue(next);\n }\n\n /** Begins a pointer drag: sets the value and tracks subsequent movement. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasTrackTarget) return;\n event.preventDefault();\n this.#updateFromClientX(event.clientX);\n if (this.hasThumbTarget) this.thumbTarget.focus();\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n const onMove = (move: PointerEvent): void => this.#updateFromClientX(move.clientX);\n const onUp = (): void => {\n abort.abort();\n this.#dragAbort = null;\n };\n document.addEventListener(\"pointermove\", onMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", onUp, { signal: abort.signal });\n // pointercancel fires when the gesture is interrupted (OS gesture, scroll\n // takeover, device switch); clean up the same way so no listener leaks.\n document.addEventListener(\"pointercancel\", onUp, { signal: abort.signal });\n }\n\n /** Maps a pointer X coordinate to a value using the track's geometry. */\n #updateFromClientX(clientX: number): void {\n const rect = this.trackTarget.getBoundingClientRect();\n if (rect.width === 0) return;\n const fraction = (clientX - rect.left) / rect.width;\n this.#setValue(this.minValue + fraction * (this.maxValue - this.minValue));\n }\n\n /**\n * Clamps `raw` to `[min, max]`, snaps it to the nearest step, stores it, and\n * reflects the new state on the thumb's ARIA attributes and the fraction\n * custom property. Dispatches `change` (detail `{ value }`) on a real value\n * change — symmetric with `range-slider` — unless `silent` (the initial\n * connect render, which is not a user edit).\n */\n #setValue(raw: number, { silent = false }: { silent?: boolean } = {}): void {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n const stepped =\n this.stepValue > 0\n ? Math.round((clamped - this.minValue) / this.stepValue) * this.stepValue + this.minValue\n : clamped;\n const value = Math.min(this.maxValue, Math.max(this.minValue, stepped));\n const changed = value !== this.valueValue;\n this.valueValue = value;\n\n if (this.hasThumbTarget) {\n this.thumbTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.thumbTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n this.thumbTarget.setAttribute(\"aria-valuenow\", String(value));\n }\n\n const span = this.maxValue - this.minValue;\n const fraction = span > 0 ? (value - this.minValue) / span : 0;\n this.element.style.setProperty(FRACTION_PROPERTY, String(fraction));\n\n if (changed && !silent) this.dispatch(\"change\", { detail: { value } });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/range.ts","../../src/controllers/slider_controller.ts"],"names":[],"mappings":"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","file":"slider_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Range normalization shared by the value-bearing controllers (progress, meter,\n * slider, range-slider).\n *\n * Each of them publishes \"where the value sits inside `[min, max]`\" as a CSS\n * custom property the consumer multiplies a track by, and each has to answer the\n * same degenerate cases: empty and inverted ranges cannot express progress, and\n * malformed Number Values can produce `NaN`. Keeping the rule in one place is\n * what makes those answers identical across the four.\n */\n\n/**\n * Fraction of `[min, max]` that `value` occupies, always within `[0, 1]`.\n *\n * `value` is clamped into the range first, so a value outside it reports a full\n * or empty track rather than pushing the fraction past the ends.\n *\n * An empty, inverted, or non-numeric range yields `0`. That is a deliberate\n * floor rather than a computed result: `min === max` would produce `NaN`, and\n * `min > max` would report a full track for a value that is really out of range.\n * Non-finite results are floored for the same reason. These results reach\n * assistive tech, because the same fraction drives the percentage substituted\n * into `aria-valuetext`.\n */\nexport function rangeFraction(value: number, min: number, max: number): number {\n const span = max - min;\n if (!(span > 0)) return 0;\n\n const clamped = Math.min(max, Math.max(min, value));\n let fraction: number;\n if (Number.isFinite(span)) {\n fraction = (clamped - min) / span;\n } else {\n // Scaling preserves the ratio when two finite endpoints straddle zero so\n // widely that their subtraction overflows to Infinity.\n const scale = Math.max(Math.abs(min), Math.abs(max));\n fraction = (clamped / scale - min / scale) / (max / scale - min / scale);\n }\n\n if (!Number.isFinite(fraction)) return 0;\n return Math.min(1, Math.max(0, fraction));\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { rangeFraction } from \"../utils/range\";\n\n/** Name of the CSS custom property exposing the thumb position (0..1). */\nconst FRACTION_PROPERTY = \"--stimeo--slider-fraction\";\n\n/**\n * Headless, accessible slider (single-thumb range) behavior.\n *\n * Markup contract (identifier: `stimeo--slider`):\n * <div data-controller=\"stimeo--slider\"\n * data-stimeo--slider-min-value=\"0\"\n * data-stimeo--slider-max-value=\"100\"\n * data-stimeo--slider-step-value=\"1\"\n * data-stimeo--slider-value-value=\"40\">\n * <div data-stimeo--slider-target=\"track\"\n * data-action=\"pointerdown->stimeo--slider#onPointerDown\">\n * <div data-stimeo--slider-target=\"thumb\" role=\"slider\" tabindex=\"0\"\n * aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"40\"\n * data-action=\"keydown->stimeo--slider#onKeydown\"></div>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Slider** pattern. The current value is exposed\n * to assistive tech via `aria-valuenow`/`aria-valuemin`/`aria-valuemax` on the\n * thumb, and to the consumer's CSS via the `--stimeo--slider-fraction` custom\n * property (a number in `[0, 1]`) set on the controller element — the library\n * positions nothing itself.\n *\n * @remarks\n * Behavior only. The consumer owns all layout (e.g. positioning the thumb from\n * the fraction). Only the horizontal orientation is handled.\n *\n * Behavior provided:\n * - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one\n * step; `Home`/`End` jump to the min/max; `PageUp`/`PageDown` move by ten steps.\n * - Pointer press/drag on the track sets the value from the pointer position.\n *\n * The fraction is a value ratio, not a position, so only the consumer knows\n * whether their track mirrors under RTL. Set `logicalTrack` to declare that it\n * does: the pointer mapping and the horizontal arrow pair then follow the\n * writing direction. Left unset, nothing here reads `direction`.\n */\nexport class SliderController extends Controller<HTMLElement> {\n static override targets = [\"track\", \"thumb\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n value: { type: Number, default: 0 },\n logicalTrack: { type: Boolean, default: false },\n };\n static actions = [\"onKeydown\", \"onPointerDown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly trackTarget: HTMLElement;\n declare readonly thumbTarget: HTMLElement;\n declare readonly hasTrackTarget: boolean;\n declare readonly hasThumbTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n declare logicalTrackValue: boolean;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** Whether the consumer declared a mirroring track and the direction mirrors it. */\n get #mirrored(): boolean {\n return this.logicalTrackValue && isRtl(this.element);\n }\n\n /** Clamps the initial value and renders the starting position. */\n override connect(): void {\n this.#setValue(this.valueValue, { silent: true });\n }\n\n /** Cancels any active pointer drag so document listeners never leak. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /** Handles keyboard stepping per the APG slider model. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const big = this.stepValue * 10;\n let next: number | null = null;\n // On a mirrored track the greater value sits at the visual left, so the\n // horizontal pair trades places; the vertical pair passes through.\n switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n next = this.valueValue + this.stepValue;\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n next = this.valueValue - this.stepValue;\n break;\n case \"PageUp\":\n next = this.valueValue + big;\n break;\n case \"PageDown\":\n next = this.valueValue - big;\n break;\n case \"Home\":\n next = this.minValue;\n break;\n case \"End\":\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#setValue(next);\n }\n\n /** Begins a pointer drag: sets the value and tracks subsequent movement. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasTrackTarget) return;\n event.preventDefault();\n // Resolve the direction once for the whole gesture: reading it per move\n // would query computed style on every frame, and a drag that flipped\n // mid-gesture would be incoherent anyway.\n const mirrored = this.#mirrored;\n this.#updateFromClientX(event.clientX, mirrored);\n if (this.hasThumbTarget) this.thumbTarget.focus();\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n const onMove = (move: PointerEvent): void => this.#updateFromClientX(move.clientX, mirrored);\n const onUp = (): void => {\n abort.abort();\n this.#dragAbort = null;\n };\n document.addEventListener(\"pointermove\", onMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", onUp, { signal: abort.signal });\n // pointercancel fires when the gesture is interrupted (OS gesture, scroll\n // takeover, device switch); clean up the same way so no listener leaks.\n document.addEventListener(\"pointercancel\", onUp, { signal: abort.signal });\n }\n\n /** Maps a pointer X coordinate to a value using the track's geometry. */\n #updateFromClientX(clientX: number, mirrored: boolean): void {\n const rect = this.trackTarget.getBoundingClientRect();\n if (rect.width === 0) return;\n const offset = (clientX - rect.left) / rect.width;\n const fraction = mirrored ? 1 - offset : offset;\n this.#setValue(this.minValue + fraction * (this.maxValue - this.minValue));\n }\n\n /**\n * Clamps `raw` to `[min, max]`, snaps it to the nearest step, stores it, and\n * reflects the new state on the thumb's ARIA attributes and the fraction\n * custom property. Dispatches `change` (detail `{ value }`) on a real value\n * change — symmetric with `range-slider` — unless `silent` (the initial\n * connect render, which is not a user edit).\n */\n #setValue(raw: number, { silent = false }: { silent?: boolean } = {}): void {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n const stepped =\n this.stepValue > 0\n ? Math.round((clamped - this.minValue) / this.stepValue) * this.stepValue + this.minValue\n : clamped;\n const value = Math.min(this.maxValue, Math.max(this.minValue, stepped));\n const changed = value !== this.valueValue;\n this.valueValue = value;\n\n if (this.hasThumbTarget) {\n this.thumbTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.thumbTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n this.thumbTarget.setAttribute(\"aria-valuenow\", String(value));\n }\n\n const fraction = rangeFraction(value, this.minValue, this.maxValue);\n this.element.style.setProperty(FRACTION_PROPERTY, String(fraction));\n\n if (changed && !silent) this.dispatch(\"change\", { detail: { value } });\n }\n}\n"]}
|
|
@@ -37,7 +37,9 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
37
37
|
*
|
|
38
38
|
* Pointer flow: dragging an item live-moves it whenever the pointer crosses a
|
|
39
39
|
* sibling's midpoint (per `orientation`). Keyboard flow: each synthetic
|
|
40
|
-
* `pointer-drag` move steps the item one position.
|
|
40
|
+
* `pointer-drag` move steps the item one position. A horizontal list under
|
|
41
|
+
* `dir="rtl"` reverses both, since DOM order then runs right-to-left while
|
|
42
|
+
* `pointer-drag` reports physical coordinates. Dropping dispatches
|
|
41
43
|
* `reorder` (`{ item, from, to }`, zero-based) when the position changed;
|
|
42
44
|
* Escape / `pointercancel` restores the pickup position. Every step is mirrored
|
|
43
45
|
* into the `status` live region (localizable via `data-grabbed` / `data-moved`
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/sortable_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
// src/controllers/sortable_controller.ts
|
|
4
11
|
var SortableController = class extends Controller {
|
|
5
12
|
static targets = ["list", "item", "status"];
|
|
@@ -67,6 +74,12 @@ var SortableController = class extends Controller {
|
|
|
67
74
|
* Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so
|
|
68
75
|
* the difference from the last consumed value is one arrow press — its sign is
|
|
69
76
|
* the direction. Cross-axis arrows never change the primary delta (no move).
|
|
77
|
+
*
|
|
78
|
+
* The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row
|
|
79
|
+
* has to invert it: there, moving the item rightward means moving it *earlier*
|
|
80
|
+
* in the DOM. Skipping this would also split the two halves of one keypress —
|
|
81
|
+
* `roving` already moves focus logically, so the same arrow would send the
|
|
82
|
+
* focus and the grabbed item opposite ways.
|
|
70
83
|
*/
|
|
71
84
|
#stepFromKeyboard(session, detail) {
|
|
72
85
|
const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;
|
|
@@ -75,7 +88,8 @@ var SortableController = class extends Controller {
|
|
|
75
88
|
if (delta === 0) return;
|
|
76
89
|
const items = this.#items();
|
|
77
90
|
const index = items.indexOf(session.item);
|
|
78
|
-
const
|
|
91
|
+
const step = (delta > 0 ? 1 : -1) * (this.#isReversed ? -1 : 1);
|
|
92
|
+
const next = Math.max(0, Math.min(index + step, items.length - 1));
|
|
79
93
|
if (next === index) return;
|
|
80
94
|
this.#moveTo(session.item, next);
|
|
81
95
|
this.#announce("moved", session.item);
|
|
@@ -91,11 +105,13 @@ var SortableController = class extends Controller {
|
|
|
91
105
|
if (others.length === 0) return;
|
|
92
106
|
let laidOut = false;
|
|
93
107
|
let target = 0;
|
|
108
|
+
const reversed = this.#isReversed;
|
|
94
109
|
for (const other of others) {
|
|
95
110
|
const rect = other.getBoundingClientRect();
|
|
96
111
|
if (rect.width > 0 || rect.height > 0) laidOut = true;
|
|
97
112
|
const midpoint = this.#isVertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;
|
|
98
|
-
|
|
113
|
+
const precedes = reversed ? pointer < midpoint : pointer > midpoint;
|
|
114
|
+
if (precedes) target += 1;
|
|
99
115
|
}
|
|
100
116
|
if (!laidOut) return;
|
|
101
117
|
const current = this.#items().indexOf(session.item);
|
|
@@ -118,7 +134,7 @@ var SortableController = class extends Controller {
|
|
|
118
134
|
* Mirrors a step into the `status` live region. Copy is localizable through
|
|
119
135
|
* `data-grabbed` / `data-moved` / `data-dropped` / `data-canceled` templates on
|
|
120
136
|
* the status element (`%{name}` / `%{position}` / `%{total}` placeholders);
|
|
121
|
-
* terse English is the fallback
|
|
137
|
+
* terse English is the fallback.
|
|
122
138
|
*/
|
|
123
139
|
#announce(key, item) {
|
|
124
140
|
if (!this.hasStatusTarget) return;
|
|
@@ -160,6 +176,21 @@ var SortableController = class extends Controller {
|
|
|
160
176
|
get #isVertical() {
|
|
161
177
|
return this.orientationValue !== "horizontal";
|
|
162
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether DOM order runs opposite to the primary coordinate — true only for a
|
|
181
|
+
* horizontal row under `dir="rtl"`, where the first item sits at the largest
|
|
182
|
+
* `x`. Both drag paths reach this controller in physical terms (`pointer-drag`
|
|
183
|
+
* documents its deltas as physical and hands RTL to its consumer, which is
|
|
184
|
+
* this controller), so both have to be mapped back onto DOM order here.
|
|
185
|
+
*
|
|
186
|
+
* Read from the list, the element that lays the items out — never from the
|
|
187
|
+
* dragged item, which may carry its own `dir`. The list inherits the computed
|
|
188
|
+
* direction, so authoring `dir` on the root works too. A vertical list is
|
|
189
|
+
* unaffected: writing direction does not mirror the block axis.
|
|
190
|
+
*/
|
|
191
|
+
get #isReversed() {
|
|
192
|
+
return !this.#isVertical && isRtl(this.#list);
|
|
193
|
+
}
|
|
163
194
|
};
|
|
164
195
|
|
|
165
196
|
export { SortableController };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/sortable_controller.ts"],"names":[],"mappings":";;;AAoEO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,OAAA,GAAU,CAAC,MAAA,EAAQ,QAAQ,QAAQ,CAAA;AAAA,EACnD,OAAgB,MAAA,GAAS;AAAA,IACvB,WAAA,EAAa,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,UAAA;AAAW,GACnD;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,SAAS,CAAA;AAAA,EAS1B,QAAA,GAA+B,IAAA;AAAA,EAEtB,OAAA,GAAgB;AAGvB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,wBAAwB,CAAA;AACrD,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,4BAAA,EAA8B,IAAA,CAAK,YAAY,CAAA;AAC7E,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,2BAAA,EAA6B,IAAA,CAAK,WAAW,CAAA;AAC3E,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,0BAAA,EAA4B,IAAA,CAAK,UAAU,CAAA;AACzE,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,6BAAA,EAA+B,IAAA,CAAK,aAAa,CAAA;AAAA,EACjF;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,4BAAA,EAA8B,IAAA,CAAK,YAAY,CAAA;AAChF,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,2BAAA,EAA6B,IAAA,CAAK,WAAW,CAAA;AAC9E,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,0BAAA,EAA4B,IAAA,CAAK,UAAU,CAAA;AAC5E,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,6BAAA,EAA+B,IAAA,CAAK,aAAa,CAAA;AAClF,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,wBAAwB,CAAA;AAAA,EACvD;AAAA;AAAA,EAGS,YAAA,GAAe,CAAC,KAAA,KAAuB;AAI9C,IAAA,IAAI,KAAK,QAAA,EAAU;AACnB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA;AACvC,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,IAAA,CAAK,QAAA,GAAW,EAAE,IAAA,EAAM,IAAA,EAAM,IAAA,CAAK,MAAA,EAAO,CAAE,OAAA,CAAQ,IAAI,CAAA,EAAG,WAAA,EAAa,CAAA,EAAE;AAC1E,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,wBAAA,EAA0B,MAAM,CAAA;AAC1D,IAAA,IAAA,CAAK,SAAA,CAAU,WAAW,IAAI,CAAA;AAAA,EAChC,CAAA;AAAA,EAES,WAAA,GAAc,CAAC,KAAA,KAAuB;AAC7C,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,MAAM,SAAU,KAAA,CAAuD,MAAA;AACvE,IAAA,IAAI,CAAC,WAAW,IAAA,CAAK,QAAA,CAAS,MAAM,MAAM,CAAA,KAAM,QAAQ,IAAA,EAAM;AAE9D,IAAA,IAAI,MAAA,CAAO,gBAAgB,UAAA,EAAY;AACrC,MAAA,IAAA,CAAK,iBAAA,CAAkB,SAAS,MAAM,CAAA;AAAA,IACxC,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,cAAA,CAAe,SAAS,MAAM,CAAA;AAAA,IACrC;AAAA,EACF,CAAA;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAAuB;AAC5C,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,IAAI,CAAC,WAAW,IAAA,CAAK,QAAA,CAAS,MAAM,MAAM,CAAA,KAAM,QAAQ,IAAA,EAAM;AAC9D,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,wBAAwB,CAAA;AACrD,IAAA,IAAA,CAAK,SAAA,CAAU,SAAA,EAAW,OAAA,CAAQ,IAAI,CAAA;AACtC,IAAA,MAAM,KAAK,IAAA,CAAK,MAAA,EAAO,CAAE,OAAA,CAAQ,QAAQ,IAAI,CAAA;AAC7C,IAAA,IAAI,EAAA,KAAO,QAAQ,IAAA,EAAM;AACvB,MAAA,IAAA,CAAK,QAAA,CAAS,SAAA,EAAW,EAAE,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,CAAQ,IAAA,EAAM,IAAA,EAAM,OAAA,CAAQ,IAAA,EAAM,EAAA,IAAM,CAAA;AAAA,IACrF;AAAA,EACF,CAAA;AAAA;AAAA,EAGS,aAAA,GAAgB,CAAC,KAAA,KAAuB;AAC/C,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,IAAI,CAAC,WAAW,IAAA,CAAK,QAAA,CAAS,MAAM,MAAM,CAAA,KAAM,QAAQ,IAAA,EAAM;AAC9D,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,wBAAwB,CAAA;AACrD,IAAA,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,OAAA,CAAQ,IAAI,CAAA;AACvC,IAAA,IAAA,CAAK,SAAA,CAAU,UAAA,EAAY,OAAA,CAAQ,IAAI,CAAA;AAAA,EACzC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAA,CAAkB,SAAsB,MAAA,EAA+C;AACrF,IAAA,MAAM,OAAA,GAAU,OAAO,IAAA,CAAK,WAAA,GAAc,OAAO,EAAA,GAAK,MAAA,CAAO,EAAE,CAAA,IAAK,CAAA;AACpE,IAAA,MAAM,KAAA,GAAQ,UAAU,OAAA,CAAQ,WAAA;AAChC,IAAA,OAAA,CAAQ,WAAA,GAAc,OAAA;AACtB,IAAA,IAAI,UAAU,CAAA,EAAG;AAEjB,IAAA,MAAM,KAAA,GAAQ,KAAK,MAAA,EAAO;AAC1B,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,IAAI,CAAA;AACxC,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,KAAA,IAAS,KAAA,GAAQ,CAAA,GAAI,CAAA,GAAI,EAAA,CAAA,EAAK,KAAA,CAAM,MAAA,GAAS,CAAC,CAAC,CAAA;AACjF,IAAA,IAAI,SAAS,KAAA,EAAO;AACpB,IAAA,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,IAAI,CAAA;AAC/B,IAAA,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,OAAA,CAAQ,IAAI,CAAA;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAA,CAAe,SAAsB,MAAA,EAA+C;AAClF,IAAA,MAAM,OAAA,GAAU,OAAO,IAAA,CAAK,WAAA,GAAc,OAAO,CAAA,GAAI,MAAA,CAAO,CAAC,CAAA,IAAK,CAAA;AAClE,IAAA,MAAM,MAAA,GAAS,KAAK,MAAA,EAAO,CAAE,OAAO,CAAC,IAAA,KAAS,IAAA,KAAS,OAAA,CAAQ,IAAI,CAAA;AACnE,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AAEzB,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,IAAI,MAAA,GAAS,CAAA;AACb,IAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,MAAA,MAAM,IAAA,GAAO,MAAM,qBAAA,EAAsB;AACzC,MAAA,IAAI,KAAK,KAAA,GAAQ,CAAA,IAAK,IAAA,CAAK,MAAA,GAAS,GAAG,OAAA,GAAU,IAAA;AACjD,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,WAAA,GAAc,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,MAAA,GAAS,CAAA,GAAI,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,KAAA,GAAQ,CAAA;AAC1F,MAAA,IAAI,OAAA,GAAU,UAAU,MAAA,IAAU,CAAA;AAAA,IACpC;AACA,IAAA,IAAI,CAAC,OAAA,EAAS;AAId,IAAA,MAAM,UAAU,IAAA,CAAK,MAAA,EAAO,CAAE,OAAA,CAAQ,QAAQ,IAAI,CAAA;AAClD,IAAA,IAAI,WAAW,OAAA,EAAS;AACtB,MAAA,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA;AACjC,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,OAAA,CAAQ,IAAI,CAAA;AAAA,IACtC;AAAA,EACF;AAAA;AAAA,EAGA,OAAA,CAAQ,MAAmB,KAAA,EAAqB;AAC9C,IAAA,MAAM,MAAA,GAAS,KAAK,MAAA,EAAO,CAAE,OAAO,CAAC,SAAA,KAAc,cAAc,IAAI,CAAA;AACrE,IAAA,MAAM,OAAA,GAAU,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC1D,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,OAAO,CAAA,IAAK,IAAA;AAIrC,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,MAAM,QAAA,GAAW,MAAA,YAAkB,WAAA,IAAe,IAAA,CAAK,SAAS,MAAM,CAAA;AACtE,IAAA,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,IAAA,EAAM,SAAS,CAAA;AACvC,IAAA,IAAI,QAAA,SAAiB,KAAA,EAAM;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAA,CAAU,KAAmD,IAAA,EAAyB;AACpF,IAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AAC3B,IAAA,MAAM,QAAA,GAAW,OAAO,IAAA,CAAK,MAAA,GAAS,OAAA,CAAQ,IAAI,IAAI,CAAC,CAAA;AACvD,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,MAAA,GAAS,MAAM,CAAA;AACzC,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAA;AAC9B,IAAA,MAAM,QAAA,GAAW;AAAA,MACf,SAAS,CAAA,QAAA,EAAW,IAAI,CAAA,WAAA,EAAc,QAAQ,OAAO,KAAK,CAAA,CAAA;AAAA,MAC1D,OAAO,CAAA,EAAG,IAAI,CAAA,WAAA,EAAc,QAAQ,OAAO,KAAK,CAAA,CAAA;AAAA,MAChD,SAAS,CAAA,QAAA,EAAW,IAAI,CAAA,aAAA,EAAgB,QAAQ,OAAO,KAAK,CAAA,CAAA;AAAA,MAC5D,UAAU,CAAA,kBAAA,EAAqB,IAAI,CAAA,sBAAA,EAAyB,QAAQ,OAAO,KAAK,CAAA;AAAA,KAClF;AAIA,IAAA,MAAM,MAAA,GAAiC,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAM;AAC/D,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAC9C,IAAA,IAAA,CAAK,YAAA,CAAa,cAAc,QAAA,GAC5B,QAAA,CAAS,QAAQ,6BAAA,EAA+B,CAAC,OAAO,KAAA,KAAkB;AACxE,MAAA,OAAO,MAAA,CAAO,KAAK,CAAA,IAAK,KAAA;AAAA,IAC1B,CAAC,CAAA,GACD,QAAA,CAAS,GAAG,CAAA;AAAA,EAClB;AAAA;AAAA,EAGA,QAAQ,IAAA,EAA2B;AACjC,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,YAAA,CAAa,4BAA4B,CAAA;AAC/D,IAAA,IAAI,UAAU,OAAO,QAAA;AACrB,IAAA,OAAA,CAAQ,KAAK,WAAA,IAAe,EAAA,EAAI,QAAQ,MAAA,EAAQ,GAAG,EAAE,IAAA,EAAK;AAAA,EAC5D;AAAA;AAAA,EAGA,SAAS,MAAA,EAAgD;AACvD,IAAA,MAAM,IAAA,GAAO,MAAA;AACb,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,OAAO,IAAA,CAAK,MAAA,EAAO,CAAE,IAAA,CAAK,CAAC,IAAA,KAAS,IAAA,KAAS,IAAA,IAAQ,IAAA,CAAK,QAAA,CAAS,IAAI,CAAC,CAAA,IAAK,IAAA;AAAA,EAC/E;AAAA;AAAA,EAGA,MAAA,GAAwB;AACtB,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,KAAA,GAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,aAAA,GAAgB,IAAA,CAAK,UAAA,GAAa,IAAA,CAAK,OAAA;AAAA,EACrD;AAAA,EAEA,IAAI,WAAA,GAAuB;AACzB,IAAA,OAAO,KAAK,gBAAA,KAAqB,YAAA;AAAA,EACnC;AACF","file":"sortable_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** A reorder in flight: one item picked up by pointer or keyboard. */\ninterface SortSession {\n item: HTMLElement;\n /** Index at pickup, for the `reorder` detail and the cancel restore. */\n from: number;\n /** Last primary-axis cumulative delta consumed by keyboard stepping. */\n lastPrimary: number;\n}\n\n/**\n * Headless, accessible **sortable** (single-list reorder) — the a11y-first\n * drag-and-drop that is the flagship consumer of the\n * `stimeo--pointer-drag` primitive. The composition is markup-level and each\n * layer keeps its job: `pointer-drag` (on every item) emits the normalized drag\n * signal with its built-in keyboard alternative, `roving` (on the list) keeps\n * the handles a single Tab stop, and this controller interprets the signal —\n * live-reordering the DOM, announcing each step through a `status` live region,\n * and reporting the final `reorder`. No dedicated APG pattern exists for\n * drag-and-drop; the keyboard model (grab → arrows → drop / Escape) comes from\n * `pointer-drag` and the announcements make it non-visually trackable\n * (WCAG 2.1.1 / 2.5.7 / 4.1.3). Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--sortable`):\n * <div data-controller=\"stimeo--sortable\">\n * <ul data-stimeo--sortable-target=\"list\" data-controller=\"stimeo--roving\"\n * data-stimeo--roving-orientation-value=\"vertical\">\n * <li data-stimeo--sortable-target=\"item\" data-stimeo--sortable-name=\"Card A\"\n * data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <span>Card A</span>\n * <button type=\"button\" aria-label=\"Reorder Card A\"\n * data-stimeo--pointer-drag-target=\"handle\"\n * data-stimeo--roving-target=\"item\">⠿</button>\n * </li>\n * …\n * </ul>\n * <span role=\"status\" data-stimeo--sortable-target=\"status\"></span>\n * </div>\n *\n * The composed values must follow the sort axis: roving's `orientation`\n * (which defaults to `horizontal`, so a vertical list has to author\n * `vertical`) and pointer-drag's `axis`. `stimeo check` enforces both\n * alignments as composition rules (`composition-mismatch`).\n *\n * Pointer flow: dragging an item live-moves it whenever the pointer crosses a\n * sibling's midpoint (per `orientation`). Keyboard flow: each synthetic\n * `pointer-drag` move steps the item one position. Dropping dispatches\n * `reorder` (`{ item, from, to }`, zero-based) when the position changed;\n * Escape / `pointercancel` restores the pickup position. Every step is mirrored\n * into the `status` live region (localizable via `data-grabbed` / `data-moved`\n * / `data-dropped` / `data-canceled` templates with `%{name}` / `%{position}` /\n * `%{total}` placeholders; terse English is the fallback).\n *\n * @remarks\n * Behavior only — the ghost/placeholder/drop-hint visuals are the consumer's\n * CSS, keyed off `pointer-drag`'s `data-dragging`/`data-grabbed` on the item\n * and this controller's `data-sortable-dragging` on the root. Persistence is\n * the consumer's `reorder` listener (post the new position; a Turbo Stream\n * broadcast makes the board collaborative). The DOM order is the single source\n * of truth: items are read live on every event, so rows appended by Turbo need\n * no rewiring, and a Turbo cache restore reconnects idempotently (`connect()`\n * clears the transient root hook; a drag cannot survive a navigation). Only the\n * event listeners are held, and they are removed on `disconnect()`. Multi-list\n * / kanban, nested DnD, auto-scroll at the edges, and virtualized lists are\n * deliberately out of this single-list scope.\n */\nexport class SortableController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"item\", \"status\"];\n static override values = {\n orientation: { type: String, default: \"vertical\" },\n };\n static events = [\"reorder\"] as const;\n\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasStatusTarget: boolean;\n declare readonly statusTarget: HTMLElement;\n declare orientationValue: string;\n\n #session: SortSession | null = null;\n\n override connect(): void {\n // A drag cannot survive a navigation: drop the hook a Turbo cache snapshot\n // may have preserved mid-drag (idempotent reconnect).\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.element.addEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.addEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.addEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.addEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.removeEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.removeEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.removeEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n }\n\n /** Picks the item up: remembers its origin and announces the grab. */\n readonly #onDragStart = (event: Event): void => {\n // One reorder at a time: a start from another item (each item has its own\n // pointer-drag instance) must not clobber the live session — its from\n // index and cancel restore would be lost.\n if (this.#session) return;\n const item = this.#itemFor(event.target);\n if (!item) return;\n this.#session = { item, from: this.#items().indexOf(item), lastPrimary: 0 };\n this.element.setAttribute(\"data-sortable-dragging\", \"true\");\n this.#announce(\"grabbed\", item);\n };\n\n readonly #onDragMove = (event: Event): void => {\n const session = this.#session;\n const detail = (event as CustomEvent<Record<string, number | string>>).detail;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n\n if (detail.pointerType === \"keyboard\") {\n this.#stepFromKeyboard(session, detail);\n } else {\n this.#followPointer(session, detail);\n }\n };\n\n /** Drops the item: announces, then reports `reorder` if the position changed. */\n readonly #onDragEnd = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#announce(\"dropped\", session.item);\n const to = this.#items().indexOf(session.item);\n if (to !== session.from) {\n this.dispatch(\"reorder\", { detail: { item: session.item, from: session.from, to } });\n }\n };\n\n /** Restores the pickup position (Escape / OS `pointercancel`). */\n readonly #onDragCancel = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#moveTo(session.item, session.from);\n this.#announce(\"canceled\", session.item);\n };\n\n /**\n * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so\n * the difference from the last consumed value is one arrow press — its sign is\n * the direction. Cross-axis arrows never change the primary delta (no move).\n */\n #stepFromKeyboard(session: SortSession, detail: Record<string, number | string>): void {\n const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;\n const delta = primary - session.lastPrimary;\n session.lastPrimary = primary;\n if (delta === 0) return;\n\n const items = this.#items();\n const index = items.indexOf(session.item);\n const next = Math.max(0, Math.min(index + (delta > 0 ? 1 : -1), items.length - 1));\n if (next === index) return;\n this.#moveTo(session.item, next);\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Pointer following: the item moves to the slot whose siblings' midpoints the\n * pointer has passed (per `orientation`). Skipped when the list has no layout\n * geometry (every rect is zero — nothing meaningful to compare against).\n */\n #followPointer(session: SortSession, detail: Record<string, number | string>): void {\n const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;\n const others = this.#items().filter((item) => item !== session.item);\n if (others.length === 0) return;\n\n let laidOut = false;\n let target = 0;\n for (const other of others) {\n const rect = other.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) laidOut = true;\n const midpoint = this.#isVertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;\n if (pointer > midpoint) target += 1;\n }\n if (!laidOut) return;\n\n // The item's index among all items equals the count of others before it,\n // so it doubles as the current insertion slot in others-space.\n const current = this.#items().indexOf(session.item);\n if (target !== current) {\n this.#moveTo(session.item, target);\n this.#announce(\"moved\", session.item);\n }\n }\n\n /** Reinserts `item` so it lands at `index` among the list's items. */\n #moveTo(item: HTMLElement, index: number): void {\n const others = this.#items().filter((candidate) => candidate !== item);\n const clamped = Math.max(0, Math.min(index, others.length));\n const reference = others[clamped] ?? null;\n // Real browsers drop focus when the focused node is re-inserted (the move\n // is a remove+insert), which would strand the keyboard grab after one\n // arrow press — restore it so focus rides the moved item.\n const active = document.activeElement;\n const hadFocus = active instanceof HTMLElement && item.contains(active);\n this.#list.insertBefore(item, reference);\n if (hadFocus) active.focus();\n }\n\n /**\n * Mirrors a step into the `status` live region. Copy is localizable through\n * `data-grabbed` / `data-moved` / `data-dropped` / `data-canceled` templates on\n * the status element (`%{name}` / `%{position}` / `%{total}` placeholders);\n * terse English is the fallback (the library's shared status-channel design).\n */\n #announce(key: \"grabbed\" | \"moved\" | \"dropped\" | \"canceled\", item: HTMLElement): void {\n if (!this.hasStatusTarget) return;\n const position = String(this.#items().indexOf(item) + 1);\n const total = String(this.#items().length);\n const name = this.#nameOf(item);\n const fallback = {\n grabbed: `Grabbed ${name}, position ${position} of ${total}`,\n moved: `${name}, position ${position} of ${total}`,\n dropped: `Dropped ${name} at position ${position} of ${total}`,\n canceled: `Reorder canceled, ${name} returned to position ${position} of ${total}`,\n } as const;\n // Single-pass function replacement keeps the substitution literal-safe:\n // `$`-sequences in the (author/content-derived) name never expand, and a\n // name that happens to contain a placeholder token is not re-substituted.\n const values: Record<string, string> = { name, position, total };\n const template = this.statusTarget.dataset[key];\n this.statusTarget.textContent = template\n ? template.replace(/%\\{(name|position|total)\\}/g, (match, token: string) => {\n return values[token] ?? match;\n })\n : fallback[key];\n }\n\n /** The announced item name: the authored override, else its collapsed text. */\n #nameOf(item: HTMLElement): string {\n const authored = item.getAttribute(\"data-stimeo--sortable-name\");\n if (authored) return authored;\n return (item.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n }\n\n /** Resolves the sortable item owning a bubbled `pointer-drag` event. */\n #itemFor(target: EventTarget | null): HTMLElement | null {\n const node = target as Node | null;\n if (!node) return null;\n return this.#items().find((item) => item === node || item.contains(node)) ?? null;\n }\n\n /** The items in live DOM order (targets re-query the DOM on every access). */\n #items(): HTMLElement[] {\n return this.itemTargets;\n }\n\n /** The reorder container: the `list` target when present, else the element. */\n get #list(): HTMLElement {\n return this.hasListTarget ? this.listTarget : this.element;\n }\n\n get #isVertical(): boolean {\n return this.orientationValue !== \"horizontal\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/controllers/sortable_controller.ts"],"names":[],"mappings":"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","file":"sortable_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isRtl } from \"../utils/logical_scroll\";\n\n/** A reorder in flight: one item picked up by pointer or keyboard. */\ninterface SortSession {\n item: HTMLElement;\n /** Index at pickup, for the `reorder` detail and the cancel restore. */\n from: number;\n /** Last primary-axis cumulative delta consumed by keyboard stepping. */\n lastPrimary: number;\n}\n\n/**\n * Headless, accessible **sortable** (single-list reorder) — the a11y-first\n * drag-and-drop that is the flagship consumer of the\n * `stimeo--pointer-drag` primitive. The composition is markup-level and each\n * layer keeps its job: `pointer-drag` (on every item) emits the normalized drag\n * signal with its built-in keyboard alternative, `roving` (on the list) keeps\n * the handles a single Tab stop, and this controller interprets the signal —\n * live-reordering the DOM, announcing each step through a `status` live region,\n * and reporting the final `reorder`. No dedicated APG pattern exists for\n * drag-and-drop; the keyboard model (grab → arrows → drop / Escape) comes from\n * `pointer-drag` and the announcements make it non-visually trackable\n * (WCAG 2.1.1 / 2.5.7 / 4.1.3). Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--sortable`):\n * <div data-controller=\"stimeo--sortable\">\n * <ul data-stimeo--sortable-target=\"list\" data-controller=\"stimeo--roving\"\n * data-stimeo--roving-orientation-value=\"vertical\">\n * <li data-stimeo--sortable-target=\"item\" data-stimeo--sortable-name=\"Card A\"\n * data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <span>Card A</span>\n * <button type=\"button\" aria-label=\"Reorder Card A\"\n * data-stimeo--pointer-drag-target=\"handle\"\n * data-stimeo--roving-target=\"item\">⠿</button>\n * </li>\n * …\n * </ul>\n * <span role=\"status\" data-stimeo--sortable-target=\"status\"></span>\n * </div>\n *\n * The composed values must follow the sort axis: roving's `orientation`\n * (which defaults to `horizontal`, so a vertical list has to author\n * `vertical`) and pointer-drag's `axis`. `stimeo check` enforces both\n * alignments as composition rules (`composition-mismatch`).\n *\n * Pointer flow: dragging an item live-moves it whenever the pointer crosses a\n * sibling's midpoint (per `orientation`). Keyboard flow: each synthetic\n * `pointer-drag` move steps the item one position. A horizontal list under\n * `dir=\"rtl\"` reverses both, since DOM order then runs right-to-left while\n * `pointer-drag` reports physical coordinates. Dropping dispatches\n * `reorder` (`{ item, from, to }`, zero-based) when the position changed;\n * Escape / `pointercancel` restores the pickup position. Every step is mirrored\n * into the `status` live region (localizable via `data-grabbed` / `data-moved`\n * / `data-dropped` / `data-canceled` templates with `%{name}` / `%{position}` /\n * `%{total}` placeholders; terse English is the fallback).\n *\n * @remarks\n * Behavior only — the ghost/placeholder/drop-hint visuals are the consumer's\n * CSS, keyed off `pointer-drag`'s `data-dragging`/`data-grabbed` on the item\n * and this controller's `data-sortable-dragging` on the root. Persistence is\n * the consumer's `reorder` listener (post the new position; a Turbo Stream\n * broadcast makes the board collaborative). The DOM order is the single source\n * of truth: items are read live on every event, so rows appended by Turbo need\n * no rewiring, and a Turbo cache restore reconnects idempotently (`connect()`\n * clears the transient root hook; a drag cannot survive a navigation). Only the\n * event listeners are held, and they are removed on `disconnect()`. Multi-list\n * / kanban, nested DnD, auto-scroll at the edges, and virtualized lists are\n * deliberately out of this single-list scope.\n */\nexport class SortableController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"item\", \"status\"];\n static override values = {\n orientation: { type: String, default: \"vertical\" },\n };\n static events = [\"reorder\"] as const;\n\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasStatusTarget: boolean;\n declare readonly statusTarget: HTMLElement;\n declare orientationValue: string;\n\n #session: SortSession | null = null;\n\n override connect(): void {\n // A drag cannot survive a navigation: drop the hook a Turbo cache snapshot\n // may have preserved mid-drag (idempotent reconnect).\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.element.addEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.addEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.addEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.addEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.removeEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.removeEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.removeEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n }\n\n /** Picks the item up: remembers its origin and announces the grab. */\n readonly #onDragStart = (event: Event): void => {\n // One reorder at a time: a start from another item (each item has its own\n // pointer-drag instance) must not clobber the live session — its from\n // index and cancel restore would be lost.\n if (this.#session) return;\n const item = this.#itemFor(event.target);\n if (!item) return;\n this.#session = { item, from: this.#items().indexOf(item), lastPrimary: 0 };\n this.element.setAttribute(\"data-sortable-dragging\", \"true\");\n this.#announce(\"grabbed\", item);\n };\n\n readonly #onDragMove = (event: Event): void => {\n const session = this.#session;\n const detail = (event as CustomEvent<Record<string, number | string>>).detail;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n\n if (detail.pointerType === \"keyboard\") {\n this.#stepFromKeyboard(session, detail);\n } else {\n this.#followPointer(session, detail);\n }\n };\n\n /** Drops the item: announces, then reports `reorder` if the position changed. */\n readonly #onDragEnd = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#announce(\"dropped\", session.item);\n const to = this.#items().indexOf(session.item);\n if (to !== session.from) {\n this.dispatch(\"reorder\", { detail: { item: session.item, from: session.from, to } });\n }\n };\n\n /** Restores the pickup position (Escape / OS `pointercancel`). */\n readonly #onDragCancel = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#moveTo(session.item, session.from);\n this.#announce(\"canceled\", session.item);\n };\n\n /**\n * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so\n * the difference from the last consumed value is one arrow press — its sign is\n * the direction. Cross-axis arrows never change the primary delta (no move).\n *\n * The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row\n * has to invert it: there, moving the item rightward means moving it *earlier*\n * in the DOM. Skipping this would also split the two halves of one keypress —\n * `roving` already moves focus logically, so the same arrow would send the\n * focus and the grabbed item opposite ways.\n */\n #stepFromKeyboard(session: SortSession, detail: Record<string, number | string>): void {\n const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;\n const delta = primary - session.lastPrimary;\n session.lastPrimary = primary;\n if (delta === 0) return;\n\n const items = this.#items();\n const index = items.indexOf(session.item);\n const step = (delta > 0 ? 1 : -1) * (this.#isReversed ? -1 : 1);\n const next = Math.max(0, Math.min(index + step, items.length - 1));\n if (next === index) return;\n this.#moveTo(session.item, next);\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Pointer following: the item moves to the slot whose siblings' midpoints the\n * pointer has passed (per `orientation`). Skipped when the list has no layout\n * geometry (every rect is zero — nothing meaningful to compare against).\n */\n #followPointer(session: SortSession, detail: Record<string, number | string>): void {\n const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;\n const others = this.#items().filter((item) => item !== session.item);\n if (others.length === 0) return;\n\n let laidOut = false;\n let target = 0;\n // `target` counts the siblings that precede the pointer *in DOM order*, and\n // the two agree only while DOM order runs the same way as the coordinate. In\n // a right-to-left row it runs the other way, so the comparison flips;\n // leaving it physical would mix two orderings into one number and drop the\n // item at a slot the pointer never crossed. Resolved once: it cannot change\n // mid-loop, and reading it per sibling would re-run `getComputedStyle` on\n // every pointermove.\n const reversed = this.#isReversed;\n for (const other of others) {\n const rect = other.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) laidOut = true;\n const midpoint = this.#isVertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;\n const precedes = reversed ? pointer < midpoint : pointer > midpoint;\n if (precedes) target += 1;\n }\n if (!laidOut) return;\n\n // The item's index among all items equals the count of others before it,\n // so it doubles as the current insertion slot in others-space.\n const current = this.#items().indexOf(session.item);\n if (target !== current) {\n this.#moveTo(session.item, target);\n this.#announce(\"moved\", session.item);\n }\n }\n\n /** Reinserts `item` so it lands at `index` among the list's items. */\n #moveTo(item: HTMLElement, index: number): void {\n const others = this.#items().filter((candidate) => candidate !== item);\n const clamped = Math.max(0, Math.min(index, others.length));\n const reference = others[clamped] ?? null;\n // Real browsers drop focus when the focused node is re-inserted (the move\n // is a remove+insert), which would strand the keyboard grab after one\n // arrow press — restore it so focus rides the moved item.\n const active = document.activeElement;\n const hadFocus = active instanceof HTMLElement && item.contains(active);\n this.#list.insertBefore(item, reference);\n if (hadFocus) active.focus();\n }\n\n /**\n * Mirrors a step into the `status` live region. Copy is localizable through\n * `data-grabbed` / `data-moved` / `data-dropped` / `data-canceled` templates on\n * the status element (`%{name}` / `%{position}` / `%{total}` placeholders);\n * terse English is the fallback.\n */\n #announce(key: \"grabbed\" | \"moved\" | \"dropped\" | \"canceled\", item: HTMLElement): void {\n if (!this.hasStatusTarget) return;\n const position = String(this.#items().indexOf(item) + 1);\n const total = String(this.#items().length);\n const name = this.#nameOf(item);\n const fallback = {\n grabbed: `Grabbed ${name}, position ${position} of ${total}`,\n moved: `${name}, position ${position} of ${total}`,\n dropped: `Dropped ${name} at position ${position} of ${total}`,\n canceled: `Reorder canceled, ${name} returned to position ${position} of ${total}`,\n } as const;\n // Single-pass function replacement keeps the substitution literal-safe:\n // `$`-sequences in the (author/content-derived) name never expand, and a\n // name that happens to contain a placeholder token is not re-substituted.\n const values: Record<string, string> = { name, position, total };\n const template = this.statusTarget.dataset[key];\n this.statusTarget.textContent = template\n ? template.replace(/%\\{(name|position|total)\\}/g, (match, token: string) => {\n return values[token] ?? match;\n })\n : fallback[key];\n }\n\n /** The announced item name: the authored override, else its collapsed text. */\n #nameOf(item: HTMLElement): string {\n const authored = item.getAttribute(\"data-stimeo--sortable-name\");\n if (authored) return authored;\n return (item.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n }\n\n /** Resolves the sortable item owning a bubbled `pointer-drag` event. */\n #itemFor(target: EventTarget | null): HTMLElement | null {\n const node = target as Node | null;\n if (!node) return null;\n return this.#items().find((item) => item === node || item.contains(node)) ?? null;\n }\n\n /** The items in live DOM order (targets re-query the DOM on every access). */\n #items(): HTMLElement[] {\n return this.itemTargets;\n }\n\n /** The reorder container: the `list` target when present, else the element. */\n get #list(): HTMLElement {\n return this.hasListTarget ? this.listTarget : this.element;\n }\n\n get #isVertical(): boolean {\n return this.orientationValue !== \"horizontal\";\n }\n\n /**\n * Whether DOM order runs opposite to the primary coordinate — true only for a\n * horizontal row under `dir=\"rtl\"`, where the first item sits at the largest\n * `x`. Both drag paths reach this controller in physical terms (`pointer-drag`\n * documents its deltas as physical and hands RTL to its consumer, which is\n * this controller), so both have to be mapped back onto DOM order here.\n *\n * Read from the list, the element that lays the items out — never from the\n * dragged item, which may carry its own `dir`. The list inherits the computed\n * direction, so authoring `dir` on the root works too. A vertical list is\n * unaffected: writing direction does not mirror the block axis.\n */\n get #isReversed(): boolean {\n return !this.#isVertical && isRtl(this.#list);\n }\n}\n"]}
|
|
@@ -8,6 +8,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
8
8
|
* <div data-controller="stimeo--spinner"
|
|
9
9
|
* data-stimeo--spinner-delay-value="150"
|
|
10
10
|
* data-stimeo--spinner-min-duration-value="500"
|
|
11
|
+
* data-stimeo--spinner-timeout-value="0"
|
|
11
12
|
* data-action="loading:start->stimeo--spinner#start
|
|
12
13
|
* loading:stop->stimeo--spinner#stop">
|
|
13
14
|
* <div role="status" aria-live="polite" hidden
|
|
@@ -21,17 +22,33 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
21
22
|
* alone) so screen readers announce loading; the controlled `region` mirrors the
|
|
22
23
|
* busy state via `aria-busy`. Two timers tame flicker: `delay` suppresses the
|
|
23
24
|
* spinner for fast operations, and `minDuration` keeps it visible long enough to
|
|
24
|
-
* be perceived once shown.
|
|
25
|
+
* be perceived once shown. `timeout` is the opt-in safety net for the case the
|
|
26
|
+
* consumer's `stop` never arrives.
|
|
27
|
+
*
|
|
28
|
+
* Events (all with an empty `detail`):
|
|
29
|
+
* - `stimeo--spinner:show` — the indicator became visible, after `delay`.
|
|
30
|
+
* - `stimeo--spinner:hide` — the indicator went away, after `minDuration`.
|
|
31
|
+
* - `stimeo--spinner:timeout` — `timeout` elapsed with the load still running;
|
|
32
|
+
* the controller then ends it exactly as `stop` would, so a `hide` follows.
|
|
25
33
|
*
|
|
26
34
|
* @remarks
|
|
27
35
|
* Behavior only — the visual spinner is the consumer's, alongside the text and
|
|
28
|
-
* `aria-hidden="true"`. Both timers are owned by {@link SafeTimeout}
|
|
29
|
-
*
|
|
36
|
+
* `aria-hidden="true"`. Both timers are owned by {@link SafeTimeout}, kept across
|
|
37
|
+
* an in-page move and dropped on a real detach via {@link DetachGate}, while the
|
|
38
|
+
* loading state a cached page would freeze is rewound by {@link BeforeCacheReset}.
|
|
30
39
|
*/
|
|
31
40
|
declare class SpinnerController extends Controller<HTMLElement> {
|
|
32
41
|
#private;
|
|
33
42
|
static targets: string[];
|
|
34
43
|
static values: {
|
|
44
|
+
announceText: {
|
|
45
|
+
type: StringConstructor;
|
|
46
|
+
default: string;
|
|
47
|
+
};
|
|
48
|
+
announceReadyText: {
|
|
49
|
+
type: StringConstructor;
|
|
50
|
+
default: string;
|
|
51
|
+
};
|
|
35
52
|
delay: {
|
|
36
53
|
type: NumberConstructor;
|
|
37
54
|
default: number;
|
|
@@ -40,9 +57,13 @@ declare class SpinnerController extends Controller<HTMLElement> {
|
|
|
40
57
|
type: NumberConstructor;
|
|
41
58
|
default: number;
|
|
42
59
|
};
|
|
60
|
+
timeout: {
|
|
61
|
+
type: NumberConstructor;
|
|
62
|
+
default: number;
|
|
63
|
+
};
|
|
43
64
|
};
|
|
44
65
|
static actions: readonly ["start", "stop"];
|
|
45
|
-
static events: readonly ["hide", "show"];
|
|
66
|
+
static events: readonly ["hide", "show", "timeout"];
|
|
46
67
|
readonly indicatorTarget: HTMLElement;
|
|
47
68
|
readonly regionTarget: HTMLElement;
|
|
48
69
|
readonly messageTarget: HTMLElement;
|
|
@@ -51,7 +72,19 @@ declare class SpinnerController extends Controller<HTMLElement> {
|
|
|
51
72
|
readonly hasMessageTarget: boolean;
|
|
52
73
|
delayValue: number;
|
|
53
74
|
minDurationValue: number;
|
|
75
|
+
timeoutValue: number;
|
|
76
|
+
announceTextValue: string;
|
|
77
|
+
announceReadyTextValue: string;
|
|
54
78
|
connect(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Re-applies the current phase to an indicator that arrived after `connect()`.
|
|
81
|
+
*
|
|
82
|
+
* A Turbo Stream can swap the indicator for a fresh node mid-load, and that node
|
|
83
|
+
* carries the markup contract's `hidden`. Without this the spinner would vanish
|
|
84
|
+
* while `data-state` still says `loading`, and nothing but the next cycle would
|
|
85
|
+
* bring it back.
|
|
86
|
+
*/
|
|
87
|
+
indicatorTargetConnected(target: HTMLElement): void;
|
|
55
88
|
disconnect(): void;
|
|
56
89
|
/** Begins loading. Honors `delay` before the spinner actually appears. */
|
|
57
90
|
start(): void;
|