@aurodesignsystem/auro-library 5.7.0 → 5.9.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Semantic Release Automated Changelog
2
2
 
3
+ # [5.9.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.8.0...v5.9.0) (2026-01-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * move popover positioner to library ([6d69131](https://github.com/AlaskaAirlines/auro-library/commit/6d69131a3ade2922cde25169d2dec6543264d11b))
9
+
10
+ # [5.8.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.7.0...v5.8.0) (2026-01-22)
11
+
12
+
13
+ ### Features
14
+
15
+ * add StringBoolean lit converter for supporting string boolean attributes ([025cde2](https://github.com/AlaskaAirlines/auro-library/commit/025cde2fad3b12fa113c11a123268a816de4c009))
16
+
3
17
  # [5.7.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.6.0...v5.7.0) (2026-01-22)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem/auro-library",
3
- "version": "5.7.0",
3
+ "version": "5.9.0",
4
4
  "description": "This repository holds shared scripts, utilities, and workflows utilized across repositories along the Auro Design System.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,9 +15,9 @@
15
15
  "generateDocs": "./bin/generateDocs.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@floating-ui/dom": "^1.6.11",
19
18
  "handlebars": "^4.7.8",
20
- "markdown-magic": "^2.6.1"
19
+ "markdown-magic": "^2.6.1",
20
+ "@floating-ui/dom": "^1.7.4"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@aurodesignsystem/auro-cli": "^3.5.1",
@@ -0,0 +1,8 @@
1
+ export const StringBoolean = {
2
+ fromAttribute: (value) => {
3
+ if (value === null || value === undefined) return false;
4
+ if (value === "") return true;
5
+ return String(value).trim().toLowerCase() === "true";
6
+ },
7
+ toAttribute: (value) => (value ? "" : null),
8
+ };
@@ -0,0 +1,268 @@
1
+ import {
2
+ arrow,
3
+ autoPlacement,
4
+ autoUpdate,
5
+ computePosition,
6
+ flip,
7
+ hide,
8
+ inline,
9
+ offset,
10
+ } from "@floating-ui/dom";
11
+
12
+ /**
13
+ * Positions a popover element relative to a target element using Floating UI.
14
+ * @param {Object} config
15
+ * @param {HTMLElement} config.target - The reference element (trigger)
16
+ * @param {HTMLElement} config.popover - The floating element (dropdown)
17
+ * @param {Object} config.options - Configuration options
18
+ * @param {string} [config.options.placement='bottom-start'] - Positioning placement of the floating element
19
+ * @param {number} [config.options.offset=0] - Distance in pixels from the reference element
20
+ * @param {boolean} [config.options.autoStart=true] - Whether to start auto-updating the position immediately
21
+ * @param {HTMLElement} [config.options.arrowEl=null] - Optional arrow element to position (e.g., <div class="arrow"></div>)
22
+ * @param {boolean} [config.options.useAutoPlacement=true] - Whether to use auto placement to find optimal position
23
+ * @param {boolean} [config.options.useFlip=true] - Whether to flip the floating element if it doesn't fit
24
+ * @param {boolean} [config.options.useHide=true] - Whether to hide the floating element if it doesn't fit
25
+ * @param {string} [config.options.strategy='absolute'] - Positioning strategy ('absolute' or 'fixed')
26
+ * @param {boolean} [config.options.inline=false] - Whether to position inline (needed for hyperlinks and inline elements)
27
+ */
28
+ export class PopoverPositioner {
29
+ #currentPlacement;
30
+ #currentSide;
31
+ #currentArrowSide;
32
+ #currentArrowDirection;
33
+
34
+ #referenceEl;
35
+ #floatingEl;
36
+ #options;
37
+ #onPlacementChange;
38
+ #cleanup;
39
+
40
+ constructor({
41
+ target,
42
+ popover,
43
+ options = {},
44
+ onPlacementChange = () => {},
45
+ } = {}) {
46
+ this.#referenceEl = target;
47
+ this.#floatingEl = popover;
48
+ this.#options = options;
49
+ this.#onPlacementChange = onPlacementChange;
50
+ this.#setInitialFloatingElementStyles();
51
+ this.#cleanup = this.#options.autoStart !== false ? this.start() : null;
52
+ }
53
+
54
+ /**
55
+ * Start auto-updating the position
56
+ * @returns {Function} Cleanup function
57
+ */
58
+ start() {
59
+ if (this.cleanup) {
60
+ this.#cleanup();
61
+ }
62
+
63
+ const cleanup = autoUpdate(
64
+ this.#referenceEl,
65
+ this.#floatingEl,
66
+ this.#updatePosition.bind(this),
67
+ );
68
+
69
+ return cleanup;
70
+ }
71
+
72
+ /**
73
+ * Stop auto-updating the position
74
+ */
75
+ stop() {
76
+ if (this.#cleanup) {
77
+ this.#cleanup();
78
+ this.#cleanup = null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Destroy the instance and clean up
84
+ */
85
+ disconnect() {
86
+ this.stop();
87
+ this.referenceEl = null;
88
+ this.floatingEl = null;
89
+ }
90
+
91
+ /**
92
+ * Builds the list of Floating UI middleware used to position the popover. This configures how the popover behaves in response to space, visibility, and arrow requirements.
93
+ * @returns {Array} An array of middleware instances that control offset, flipping, auto placement, hiding, inline behavior, and arrow positioning.
94
+ */
95
+ get #middleware() {
96
+ return [].concat(
97
+ this.#options.offset && this.#options.offset !== 0
98
+ ? offset(this.#options.offset)
99
+ : [],
100
+ this.#options.useFlip ? flip() : [],
101
+ this.#options.useAutoPlacement ? autoPlacement() : [],
102
+ this.#options.useHide ? hide() : [],
103
+ this.#options.inline ? inline() : [],
104
+ this.#options.arrowEl ? arrow({ element: this.#options.arrowEl }) : [],
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Prepares the arguments used when calling Floating UI to compute the popover position. This encapsulates the reference, floating element, and core configuration in a single structure.
110
+ * @returns {Array} An ordered array containing the reference element, floating element, and configuration object for Floating UI.
111
+ */
112
+ get #floatingUiArgs() {
113
+ return [
114
+ this.#referenceEl,
115
+ this.#floatingEl,
116
+ {
117
+ strategy: this.#options.strategy || "absolute",
118
+ placement: this.#options.placement || "bottom-start",
119
+ middleware: this.#middleware,
120
+ },
121
+ ];
122
+ }
123
+
124
+ /**
125
+ * Primary callback for FloatingUI updates
126
+ * Recomputes and applies the current popover and arrow position. This keeps the popover layout in sync with its reference element as the page or viewport changes.
127
+ */
128
+ #updatePosition() {
129
+ if (!this.#referenceEl || !this.#floatingEl) return;
130
+
131
+ computePosition(...this.#floatingUiArgs).then(
132
+ ({ x, y, middlewareData, placement }) => {
133
+ // Handle the new placement and get the direction the arrow should point
134
+ const { arrowSide } = this.#handlePlacementChange({ placement });
135
+
136
+ // Upload the position of the floating element
137
+ this.#updateFloatingElPosition({ x, y, middlewareData });
138
+
139
+ // Update the position of the arrow element
140
+ this.#updateArrowPosition({
141
+ arrowData: middlewareData.arrow,
142
+ arrowSide,
143
+ });
144
+ },
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Applies the base positioning styles to the floating element before any calculations run. This ensures the popover is ready to be positioned consistently by Floating UI.
150
+ */
151
+ #setInitialFloatingElementStyles() {
152
+ if (!this.#floatingEl) return;
153
+ const strategy = this.#options.strategy || "absolute";
154
+ const defaultStyles = {
155
+ position: strategy,
156
+ margin: "0",
157
+ };
158
+ Object.assign(this.#floatingEl.style, defaultStyles);
159
+ }
160
+
161
+ /**
162
+ * Derives normalized placement information from the raw Floating UI placement value. This determines how the popover and its arrow should be oriented relative to the reference element.
163
+ * @param {Object} params
164
+ * @param {string} params.placement - The full placement value reported by Floating UI (e.g., "bottom-start").
165
+ * @returns {{placement: string, side: string, arrowDirection: string, arrowSide: string}} Normalized placement data including side and arrow orientation.
166
+ */
167
+ #handlePlacementChange({ placement }) {
168
+ // Get the side of the placement (top, right, bottom, left)
169
+ const placementSide = placement.split("-")[0];
170
+ const arrowSide = {
171
+ top: "bottom",
172
+ right: "left",
173
+ bottom: "top",
174
+ left: "right",
175
+ }[placementSide];
176
+
177
+ // Get the direction the arrow is pointing
178
+ const arrowDirection = {
179
+ top: "down",
180
+ right: "left",
181
+ bottom: "up",
182
+ left: "right",
183
+ }[placementSide];
184
+
185
+ this.#updateInternalState({
186
+ placement,
187
+ arrowDirection,
188
+ arrowSide: arrowSide,
189
+ side: placementSide,
190
+ });
191
+
192
+ return { placement, side: placementSide, arrowDirection, arrowSide };
193
+ }
194
+
195
+ /**
196
+ * Caches the latest placement state and triggers any placement change callback. This keeps consumers informed when the popover or its arrow orientation changes.
197
+ * @param {Object} params
198
+ * @param {string} params.placement - The current full placement string (e.g., "bottom-start").
199
+ * @param {string} params.side - The side of the reference element where the popover is placed.
200
+ * @param {string} params.arrowDirection - The direction the arrow visually points.
201
+ * @param {string} params.arrowSide - The side of the popover where the arrow is anchored.
202
+ */
203
+ #updateInternalState({ placement, side, arrowDirection, arrowSide }) {
204
+ if (
205
+ this.#currentPlacement !== placement ||
206
+ this.#currentArrowDirection !== arrowDirection ||
207
+ this.#currentArrowSide !== arrowSide ||
208
+ this.#currentSide !== side
209
+ ) {
210
+ this.#currentPlacement = placement;
211
+ this.#currentArrowDirection = arrowDirection;
212
+ this.#currentArrowSide = arrowSide;
213
+ this.#currentSide = side;
214
+ this.#onPlacementChange?.({ placement, arrowDirection, arrowSide, side });
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Applies the latest computed position to the floating element. This keeps the popover visually aligned with the reference element and updates its visibility state.
220
+ * @param {Object} params
221
+ * @param {number} params.x - The horizontal position in pixels for the floating element.
222
+ * @param {number} params.y - The vertical position in pixels for the floating element.
223
+ * @param {Object} params.middlewareData - Data returned from Floating UI middleware, including visibility information.
224
+ */
225
+ #updateFloatingElPosition({ x, y, middlewareData }) {
226
+ if (!this.#floatingEl) return;
227
+ Object.assign(this.#floatingEl.style, {
228
+ visibility: middlewareData.hide?.referenceHidden ? "hidden" : "visible",
229
+ left: `${x}px`,
230
+ top: `${y}px`,
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Updates the visual position of the arrow element relative to the popover. This keeps the arrow aligned with the reference element and clears any stale positioning styles.
236
+ * @param {Object} params
237
+ * @param {{x: number|null, y: number|null}|null} params.arrowData - The computed arrow coordinates from Floating UI, or null if no arrow data is available.
238
+ * @param {string} params.arrowSide - The side of the popover where the arrow should be anchored (top, right, bottom, or left).
239
+ */
240
+ #updateArrowPosition({ arrowData, arrowSide }) {
241
+ // Update the styles for the arrow element
242
+ if (arrowData && this.#options.arrowEl) {
243
+ // Get current arrow styles
244
+ const arrowStyle = this.#options.arrowEl?.style;
245
+
246
+ // Reset arrow style properties modified by this middleware below to clean up old styles before applying new ones
247
+ const propsToReset = [
248
+ "top",
249
+ "bottom",
250
+ "left",
251
+ "right",
252
+ ...(arrowData ? [] : ["position"]),
253
+ ];
254
+ propsToReset.forEach((prop) => {
255
+ arrowStyle[prop] = "";
256
+ });
257
+
258
+ // Update the arrow position
259
+ const { x, y } = arrowData;
260
+ Object.assign(arrowStyle ?? {}, {
261
+ left: x != null ? `${x}px` : "",
262
+ top: y != null ? `${y}px` : "",
263
+ [arrowSide]: `${-this.#options.arrowEl.offsetWidth}px`,
264
+ position: "absolute",
265
+ });
266
+ }
267
+ }
268
+ }