ng-hub-ui-utils 22.9.3 → 22.11.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.
@@ -610,8 +610,7 @@ const hubFocusTrap = (zone, element, stopFocusTrap$, refocusOnClick = false) =>
610
610
  .pipe(takeUntil(stopFocusTrap$), filter((e) => e.key === 'Tab'), withLatestFrom(lastFocusedElement$))
611
611
  .subscribe(([tabEvent, focusedElement]) => {
612
612
  const [first, last] = getFocusableBoundaryElements(element);
613
- if ((focusedElement === first || focusedElement === element) &&
614
- tabEvent.shiftKey) {
613
+ if ((focusedElement === first || focusedElement === element) && tabEvent.shiftKey) {
615
614
  last.focus();
616
615
  tabEvent.preventDefault();
617
616
  }
@@ -653,9 +652,7 @@ function isNumber(value) {
653
652
  return !isNaN(toInteger(value));
654
653
  }
655
654
  function isInteger(value) {
656
- return (typeof value === 'number' &&
657
- isFinite(value) &&
658
- Math.floor(value) === value);
655
+ return typeof value === 'number' && isFinite(value) && Math.floor(value) === value;
659
656
  }
660
657
  function isDefined(value) {
661
658
  return value !== undefined && value !== null;
@@ -826,9 +823,7 @@ function getValue(target, key) {
826
823
  key = '';
827
824
  do {
828
825
  key += keys.shift();
829
- if (isDefined(target) &&
830
- isDefined(target[key]) &&
831
- (typeof target[key] === 'object' || !keys.length)) {
826
+ if (isDefined(target) && isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
832
827
  target = target[key];
833
828
  key = '';
834
829
  }
@@ -925,9 +920,7 @@ function getActiveElement(root = document) {
925
920
  if (!activeEl) {
926
921
  return null;
927
922
  }
928
- return activeEl.shadowRoot
929
- ? getActiveElement(activeEl.shadowRoot)
930
- : activeEl;
923
+ return activeEl.shadowRoot ? getActiveElement(activeEl.shadowRoot) : activeEl;
931
924
  }
932
925
 
933
926
  const HUB_TRANSLATION_CONFIG = new InjectionToken('HUB_TRANSLATION_CONFIG');
@@ -1084,6 +1077,21 @@ function provideHubTranslationAdapter(factory) {
1084
1077
  class OverlayPosition {
1085
1078
  _origin = null;
1086
1079
  _positions = [];
1080
+ _direction = null;
1081
+ /**
1082
+ * Forces the writing direction used to resolve `start` and `end`.
1083
+ *
1084
+ * Rarely needed: left unset, the direction is read from the origin element itself, which is
1085
+ * what a field inside an RTL container reports. Set it only when the overlay must follow a
1086
+ * direction its origin does not have.
1087
+ *
1088
+ * @param direction Writing direction, or `null` to go back to reading the origin's.
1089
+ * @returns This position instance for chaining.
1090
+ */
1091
+ withDirection(direction) {
1092
+ this._direction = direction;
1093
+ return this;
1094
+ }
1087
1095
  /**
1088
1096
  * Sets the origin element used to position the overlay.
1089
1097
  *
@@ -1116,9 +1124,13 @@ class OverlayPosition {
1116
1124
  }
1117
1125
  const originElement = this._origin instanceof ElementRef ? this._origin.nativeElement : this._origin;
1118
1126
  const originRect = originElement.getBoundingClientRect();
1127
+ // `start` and `end` are logical: under RTL they name the right and the left edge. Read from
1128
+ // the origin rather than from the document, so a field inside an RTL island is positioned by
1129
+ // the direction it is actually laid out in.
1130
+ const isRtl = this._direction === 'rtl' || (this._direction === null && getComputedStyle(originElement).direction === 'rtl');
1119
1131
  // Try each position until we find one that fits in the viewport
1120
1132
  for (const position of this._positions) {
1121
- const coords = this._calculatePosition(originRect, overlayElement, position);
1133
+ const coords = this._calculatePosition(originRect, overlayElement, position, isRtl);
1122
1134
  if (this._fitsInViewport(coords, overlayElement)) {
1123
1135
  this._applyPosition(overlayElement, coords);
1124
1136
  return;
@@ -1126,7 +1138,7 @@ class OverlayPosition {
1126
1138
  }
1127
1139
  // If no position fits perfectly, use the first one
1128
1140
  if (this._positions.length > 0) {
1129
- const coords = this._calculatePosition(originRect, overlayElement, this._positions[0]);
1141
+ const coords = this._calculatePosition(originRect, overlayElement, this._positions[0], isRtl);
1130
1142
  this._applyPosition(overlayElement, coords);
1131
1143
  }
1132
1144
  }
@@ -1138,13 +1150,13 @@ class OverlayPosition {
1138
1150
  * @param position Position configuration.
1139
1151
  * @returns Calculated x and y coordinates.
1140
1152
  */
1141
- _calculatePosition(originRect, overlayElement, position) {
1153
+ _calculatePosition(originRect, overlayElement, position, isRtl) {
1142
1154
  const overlayRect = overlayElement.getBoundingClientRect();
1143
1155
  // Calculate origin point
1144
- let x = this._getOriginX(originRect, position.originX);
1156
+ let x = this._getOriginX(originRect, position.originX, isRtl);
1145
1157
  let y = this._getOriginY(originRect, position.originY);
1146
1158
  // Adjust for overlay alignment
1147
- x -= this._getOverlayX(overlayRect, position.overlayX);
1159
+ x -= this._getOverlayX(overlayRect, position.overlayX, isRtl);
1148
1160
  y -= this._getOverlayY(overlayRect, position.overlayY);
1149
1161
  // Apply offsets
1150
1162
  if (position.offsetX) {
@@ -1158,14 +1170,14 @@ class OverlayPosition {
1158
1170
  /**
1159
1171
  * Gets the X coordinate for the origin point.
1160
1172
  */
1161
- _getOriginX(rect, position) {
1173
+ _getOriginX(rect, position, isRtl) {
1162
1174
  switch (position) {
1163
1175
  case 'start':
1164
- return rect.left;
1176
+ return isRtl ? rect.right : rect.left;
1165
1177
  case 'center':
1166
1178
  return rect.left + rect.width / 2;
1167
1179
  case 'end':
1168
- return rect.right;
1180
+ return isRtl ? rect.left : rect.right;
1169
1181
  }
1170
1182
  }
1171
1183
  /**
@@ -1184,14 +1196,14 @@ class OverlayPosition {
1184
1196
  /**
1185
1197
  * Gets the X offset for the overlay alignment.
1186
1198
  */
1187
- _getOverlayX(rect, position) {
1199
+ _getOverlayX(rect, position, isRtl) {
1188
1200
  switch (position) {
1189
1201
  case 'start':
1190
- return 0;
1202
+ return isRtl ? rect.width : 0;
1191
1203
  case 'center':
1192
1204
  return rect.width / 2;
1193
1205
  case 'end':
1194
- return rect.width;
1206
+ return isRtl ? 0 : rect.width;
1195
1207
  }
1196
1208
  }
1197
1209
  /**
@@ -1228,6 +1240,37 @@ class OverlayPosition {
1228
1240
  }
1229
1241
  }
1230
1242
 
1243
+ const stack = [];
1244
+ let cleanup = null;
1245
+ function dispatch(event) {
1246
+ stack[stack.length - 1]?.handler(event);
1247
+ }
1248
+ /**
1249
+ * Adds an overlay to the top of the stack.
1250
+ *
1251
+ * @param handler Called with every keydown while this overlay is the topmost one.
1252
+ * @returns A function that removes it again; safe to call more than once.
1253
+ */
1254
+ function registerOverlayKeydown(handler) {
1255
+ const entry = { handler };
1256
+ stack.push(entry);
1257
+ if (!cleanup && typeof document !== 'undefined') {
1258
+ document.addEventListener('keydown', dispatch);
1259
+ cleanup = () => document.removeEventListener('keydown', dispatch);
1260
+ }
1261
+ return () => {
1262
+ const index = stack.indexOf(entry);
1263
+ if (index === -1) {
1264
+ return;
1265
+ }
1266
+ stack.splice(index, 1);
1267
+ if (!stack.length) {
1268
+ cleanup?.();
1269
+ cleanup = null;
1270
+ }
1271
+ };
1272
+ }
1273
+
1231
1274
  /**
1232
1275
  * Manages a single overlay instance created by {@link OverlayService}.
1233
1276
  * Creates a container and optional backdrop in `document.body` and attaches
@@ -1242,6 +1285,10 @@ class OverlayRef {
1242
1285
  _viewRef = null;
1243
1286
  _componentRef = null;
1244
1287
  _isAttached = false;
1288
+ _repositionHandler;
1289
+ _repositionFrame = 0;
1290
+ _keydownCallback;
1291
+ _unregisterKeydown;
1245
1292
  _backdropClickCallback;
1246
1293
  _backdropClickHandler;
1247
1294
  constructor(_config, _appRef) {
@@ -1290,8 +1337,7 @@ class OverlayRef {
1290
1337
  environmentInjector: this._appRef.injector
1291
1338
  });
1292
1339
  this._appRef.attachView(this._componentRef.hostView);
1293
- contentElement = this._componentRef.hostView
1294
- .rootNodes[0];
1340
+ contentElement = this._componentRef.hostView.rootNodes[0];
1295
1341
  }
1296
1342
  this._contentElement = contentElement;
1297
1343
  // Only append if not already in container
@@ -1302,7 +1348,9 @@ class OverlayRef {
1302
1348
  // Apply position strategy
1303
1349
  if (this._config.positionStrategy) {
1304
1350
  this._config.positionStrategy.apply(this._containerElement);
1351
+ this._listenForReposition();
1305
1352
  }
1353
+ this._listenForKeys();
1306
1354
  return contentElement;
1307
1355
  }
1308
1356
  /**
@@ -1312,11 +1360,75 @@ class OverlayRef {
1312
1360
  if (!this._isAttached) {
1313
1361
  return;
1314
1362
  }
1363
+ this._stopListeningForReposition();
1364
+ this._unregisterKeydown?.();
1365
+ this._unregisterKeydown = undefined;
1315
1366
  if (this._contentElement && this._containerElement) {
1316
- this._containerElement.removeChild(this._contentElement);
1367
+ this._contentElement?.remove();
1317
1368
  }
1318
1369
  this._isAttached = false;
1319
1370
  }
1371
+ /**
1372
+ * Registers a handler for keys pressed while this overlay is the topmost open one.
1373
+ *
1374
+ * Needed because an overlay rarely holds focus: opened from a click it leaves focus where it
1375
+ * was, so a component listening on its own host never hears Escape. Set it before `attach()`,
1376
+ * or right after — the listener is registered on attach and released on detach.
1377
+ *
1378
+ * @param callback Invoked with each keydown; decide there what to act on.
1379
+ */
1380
+ onKeydown(callback) {
1381
+ this._keydownCallback = callback;
1382
+ if (this._isAttached && !this._unregisterKeydown) {
1383
+ this._listenForKeys();
1384
+ }
1385
+ }
1386
+ /** Puts this overlay on top of the keyboard stack for as long as it is attached. */
1387
+ _listenForKeys() {
1388
+ if (!this._keydownCallback || this._unregisterKeydown) {
1389
+ return;
1390
+ }
1391
+ const callback = this._keydownCallback;
1392
+ this._unregisterKeydown = registerOverlayKeydown((event) => callback(event));
1393
+ }
1394
+ /**
1395
+ * Keeps the overlay glued to its origin while the page moves under it.
1396
+ *
1397
+ * Registered on `window` in the CAPTURE phase, which is the whole point: a `scroll` event on an
1398
+ * element does not bubble, so a listener on `document` never hears an application that scrolls
1399
+ * an inner container rather than the page. Capture sees both.
1400
+ *
1401
+ * Coalesced into an animation frame, because a scroll fires far more often than a paint.
1402
+ */
1403
+ _listenForReposition() {
1404
+ if (this._repositionHandler || typeof window === 'undefined') {
1405
+ return;
1406
+ }
1407
+ this._repositionHandler = () => {
1408
+ if (this._repositionFrame) {
1409
+ return;
1410
+ }
1411
+ this._repositionFrame = requestAnimationFrame(() => {
1412
+ this._repositionFrame = 0;
1413
+ this.updatePosition();
1414
+ });
1415
+ };
1416
+ window.addEventListener('scroll', this._repositionHandler, { capture: true, passive: true });
1417
+ window.addEventListener('resize', this._repositionHandler, { passive: true });
1418
+ }
1419
+ /** Removes the listeners registered by {@link _listenForReposition}. */
1420
+ _stopListeningForReposition() {
1421
+ if (!this._repositionHandler || typeof window === 'undefined') {
1422
+ return;
1423
+ }
1424
+ window.removeEventListener('scroll', this._repositionHandler, { capture: true });
1425
+ window.removeEventListener('resize', this._repositionHandler);
1426
+ this._repositionHandler = undefined;
1427
+ if (this._repositionFrame) {
1428
+ cancelAnimationFrame(this._repositionFrame);
1429
+ this._repositionFrame = 0;
1430
+ }
1431
+ }
1320
1432
  /**
1321
1433
  * Disposes the overlay and cleans up all allocated resources.
1322
1434
  */
@@ -1333,7 +1445,7 @@ class OverlayRef {
1333
1445
  this._componentRef = null;
1334
1446
  }
1335
1447
  if (this._containerElement) {
1336
- document.body.removeChild(this._containerElement);
1448
+ this._containerElement?.remove();
1337
1449
  this._containerElement = null;
1338
1450
  }
1339
1451
  if (this._backdropElement) {
@@ -1342,7 +1454,7 @@ class OverlayRef {
1342
1454
  this._backdropElement.removeEventListener('click', this._backdropClickHandler);
1343
1455
  this._backdropClickHandler = undefined;
1344
1456
  }
1345
- document.body.removeChild(this._backdropElement);
1457
+ this._backdropElement?.remove();
1346
1458
  this._backdropElement = null;
1347
1459
  }
1348
1460
  this._contentElement = null;
@@ -1381,22 +1493,16 @@ class OverlayRef {
1381
1493
  this._containerElement = document.createElement('div');
1382
1494
  this._containerElement.classList.add('hub-overlay-container');
1383
1495
  if (this._config.panelClass) {
1384
- const classes = Array.isArray(this._config.panelClass)
1385
- ? this._config.panelClass
1386
- : [this._config.panelClass];
1496
+ const classes = Array.isArray(this._config.panelClass) ? this._config.panelClass : [this._config.panelClass];
1387
1497
  classes.forEach((cls) => this._containerElement.classList.add(cls));
1388
1498
  }
1389
1499
  if (this._config.width) {
1390
1500
  this._containerElement.style.width =
1391
- typeof this._config.width === 'number'
1392
- ? `${this._config.width}px`
1393
- : this._config.width;
1501
+ typeof this._config.width === 'number' ? `${this._config.width}px` : this._config.width;
1394
1502
  }
1395
1503
  if (this._config.height) {
1396
1504
  this._containerElement.style.height =
1397
- typeof this._config.height === 'number'
1398
- ? `${this._config.height}px`
1399
- : this._config.height;
1505
+ typeof this._config.height === 'number' ? `${this._config.height}px` : this._config.height;
1400
1506
  }
1401
1507
  // A content-sized overlay must never clip: with no configured size the
1402
1508
  // container computes to 0x0 whenever its content is absolutely positioned
@@ -1411,9 +1517,7 @@ class OverlayRef {
1411
1517
  // overlay (e.g. above a modal) without `!important`; the literal fallback
1412
1518
  // keeps today's behavior when the overlay stylesheet is not imported.
1413
1519
  this._containerElement.style.zIndex =
1414
- this._config.zIndex != null
1415
- ? String(this._config.zIndex)
1416
- : 'var(--hub-overlay-zindex, 1000)';
1520
+ this._config.zIndex != null ? String(this._config.zIndex) : 'var(--hub-overlay-zindex, 1000)';
1417
1521
  document.body.appendChild(this._containerElement);
1418
1522
  }
1419
1523
  /**
@@ -1477,6 +1581,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1477
1581
  }]
1478
1582
  }] });
1479
1583
 
1584
+ /**
1585
+ * The fallback chain a dropdown wants: below the origin, aligned to its start edge, flipping above
1586
+ * it when there is no room, then trying the end edge for the same pair.
1587
+ *
1588
+ * `start` and `end` are logical, so this list mirrors itself under RTL without a second copy.
1589
+ * Matches the order Angular Material's own connected overlay defaults to, because a reader who
1590
+ * knows one should not have to learn the other.
1591
+ */
1592
+ const HUB_DROPDOWN_POSITIONS = [
1593
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
1594
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
1595
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' },
1596
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' }
1597
+ ];
1598
+
1480
1599
  class GetPipe {
1481
1600
  /**
1482
1601
  * @param value The object to retrieve the property from.
@@ -1489,9 +1608,7 @@ class GetPipe {
1489
1608
  }
1490
1609
  return path
1491
1610
  .split('.')
1492
- .reduce((a, c) => a && a[c] !== null && a[c] !== undefined
1493
- ? a[c]
1494
- : defaultValue || null, value);
1611
+ .reduce((a, c) => (a && a[c] !== null && a[c] !== undefined ? a[c] : defaultValue || null), value);
1495
1612
  }
1496
1613
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: GetPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1497
1614
  static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: GetPipe, isStandalone: true, name: "get" });
@@ -1761,8 +1878,7 @@ const hubRunTransition = (zone, element, startFn, options) => {
1761
1878
  // If animations are disabled, we have to emit a value and complete the observable
1762
1879
  // In this case we have to call the end function, but can finish immediately by emitting a value,
1763
1880
  // completing the observable and executing end functions synchronously.
1764
- if (!options.animation ||
1765
- window.getComputedStyle(element).transitionProperty === 'none') {
1881
+ if (!options.animation || window.getComputedStyle(element).transitionProperty === 'none') {
1766
1882
  zone.run(() => endFn());
1767
1883
  return of(undefined).pipe(runInZone(zone));
1768
1884
  }
@@ -1863,9 +1979,7 @@ class PopupService {
1863
1979
  return new ContentRef([viewRef.rootNodes], viewRef);
1864
1980
  }
1865
1981
  else {
1866
- return new ContentRef([
1867
- [this._document.createTextNode(`${content}`)]
1868
- ]);
1982
+ return new ContentRef([[this._document.createTextNode(`${content}`)]]);
1869
1983
  }
1870
1984
  }
1871
1985
  }
@@ -1933,7 +2047,14 @@ const TOOLTIP_THEME_VARS = [
1933
2047
  '--hub-tooltip-zindex',
1934
2048
  '--hub-tooltip-transition-duration',
1935
2049
  '--hub-tooltip-shadow',
1936
- '--hub-tooltip-font-family'
2050
+ '--hub-tooltip-font-family',
2051
+ // How the label breaks and sits. Forwarded like the rest so a tooltip that carries a
2052
+ // sentence or two — a field's explanation rather than its name — can be asked for from
2053
+ // the host, which is the only element a consumer can reach: the tooltip itself is on
2054
+ // `<body>`, outside any component's styles. Without them the only way to widen one
2055
+ // tooltip was a global rule that widened every tooltip in the product.
2056
+ '--hub-tooltip-white-space',
2057
+ '--hub-tooltip-text-align'
1937
2058
  ];
1938
2059
  /**
1939
2060
  * Framework-agnostic tooltip engine.
@@ -2397,5 +2518,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
2397
2518
  * Generated bundle index. Do not edit.
2398
2519
  */
2399
2520
 
2400
- export { ContentRef, FOCUSABLE_ELEMENTS_SELECTOR, GetPipe, HUB_TOOLTIP_ADAPTER, HUB_TRANSLATION_CONFIG, HUB_TRANSLATION_PREFIX, HUB_TRANSLATION_SOURCE, HubDragDropService, HubOverflowTooltipDirective, HubTooltipController, HubTooltipDirective, HubTranslationService, IsObjectPipe, IsObservablePipe, IsStringPipe, OverlayPosition, OverlayRef, OverlayService, PopupService, ScrollBar, TooltipDirective, TranslatePipe, UcfirstPipe, UnwrapAsyncPipe, clamp, closest, computeTargetIndex, containsNode, copyArrayItem, createNativeDragImage, createPointerDragSession, debouncedSignal, equals, generateUniqueId, getActiveElement, getFocusableBoundaryElements, getValue, getValueInRange, hubCompleteTransition, hubFocusTrap, hubRunTransition, hubTooltipAdapter, interpolateString, isDefined, isInteger, isNumber, isObject, isPromise, isString, mergeDeep, moveItemInArray, padNumber, provideHubTooltip, provideHubTranslation, provideHubTranslationAdapter, reflow, regExpEscape, removeAccents, resolveDropPosition, resolveHubAccent, runInZone, toAbsoluteIndex, toInteger, toString, transferArrayItem };
2521
+ export { ContentRef, FOCUSABLE_ELEMENTS_SELECTOR, GetPipe, HUB_DROPDOWN_POSITIONS, HUB_TOOLTIP_ADAPTER, HUB_TRANSLATION_CONFIG, HUB_TRANSLATION_PREFIX, HUB_TRANSLATION_SOURCE, HubDragDropService, HubOverflowTooltipDirective, HubTooltipController, HubTooltipDirective, HubTranslationService, IsObjectPipe, IsObservablePipe, IsStringPipe, OverlayPosition, OverlayRef, OverlayService, PopupService, ScrollBar, TooltipDirective, TranslatePipe, UcfirstPipe, UnwrapAsyncPipe, clamp, closest, computeTargetIndex, containsNode, copyArrayItem, createNativeDragImage, createPointerDragSession, debouncedSignal, equals, generateUniqueId, getActiveElement, getFocusableBoundaryElements, getValue, getValueInRange, hubCompleteTransition, hubFocusTrap, hubRunTransition, hubTooltipAdapter, interpolateString, isDefined, isInteger, isNumber, isObject, isPromise, isString, mergeDeep, moveItemInArray, padNumber, provideHubTooltip, provideHubTranslation, provideHubTranslationAdapter, reflow, regExpEscape, registerOverlayKeydown, removeAccents, resolveDropPosition, resolveHubAccent, runInZone, toAbsoluteIndex, toInteger, toString, transferArrayItem };
2401
2522
  //# sourceMappingURL=ng-hub-ui-utils.mjs.map