@telefonica/mistica 17.6.0 → 17.7.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.
Files changed (214) hide show
  1. package/css/mistica.css +1 -1
  2. package/dist/accordion.css-mistica.js +6 -6
  3. package/dist/align.css-mistica.js +1 -1
  4. package/dist/autocomplete.css-mistica.js +1 -1
  5. package/dist/avatar.css-mistica.js +1 -1
  6. package/dist/badge.css-mistica.js +1 -1
  7. package/dist/box.css-mistica.js +13 -13
  8. package/dist/boxed.css-mistica.js +24 -24
  9. package/dist/button-group.css-mistica.js +1 -1
  10. package/dist/button-layout.css-mistica.js +14 -14
  11. package/dist/button.css-mistica.js +34 -34
  12. package/dist/callout.css-mistica.js +11 -11
  13. package/dist/card-internal.css-mistica.js +21 -21
  14. package/dist/carousel.css-mistica.js +8 -8
  15. package/dist/checkbox.css-mistica.js +11 -11
  16. package/dist/chip.css-mistica.js +15 -15
  17. package/dist/circle.css-mistica.js +1 -1
  18. package/dist/community/advanced-data-card.css-mistica.js +8 -8
  19. package/dist/community/ai-card.css-mistica.js +2 -2
  20. package/dist/community/blocks.css-mistica.js +1 -1
  21. package/dist/community/cyber/example-component.css-mistica.js +1 -1
  22. package/dist/community/example-component.css-mistica.js +2 -2
  23. package/dist/counter.css-mistica.js +1 -1
  24. package/dist/cover-hero.css-mistica.js +2 -2
  25. package/dist/credit-card-number-field.css-mistica.js +3 -3
  26. package/dist/date-field.css-mistica.js +1 -1
  27. package/dist/date-time-picker.css-mistica.js +1 -1
  28. package/dist/dialog.css-mistica.js +4 -4
  29. package/dist/divider.css-mistica.js +5 -5
  30. package/dist/double-field.css-mistica.js +2 -2
  31. package/dist/drawer.css-mistica.js +1 -1
  32. package/dist/empty-state-card.css-mistica.js +1 -1
  33. package/dist/empty-state.css-mistica.js +5 -5
  34. package/dist/fade-in.css-mistica.js +1 -1
  35. package/dist/fade-out.css-mistica.js +1 -1
  36. package/dist/feedback.css-mistica.js +1 -1
  37. package/dist/file-upload.css-mistica.js +7 -7
  38. package/dist/fixed-footer-layout.css-mistica.js +2 -2
  39. package/dist/form.css-mistica.js +1 -1
  40. package/dist/grid-layout.css-mistica.js +3 -3
  41. package/dist/grid.css-mistica.js +120 -120
  42. package/dist/header.css-mistica.js +1 -1
  43. package/dist/hero.css-mistica.js +2 -2
  44. package/dist/horizontal-scroll.css-mistica.js +1 -1
  45. package/dist/icon-button.css-mistica.js +53 -53
  46. package/dist/icons/icon-chevron.css-mistica.js +2 -2
  47. package/dist/icons/icon-error.css-mistica.js +1 -1
  48. package/dist/image.css-mistica.js +11 -11
  49. package/dist/inline.css-mistica.js +11 -11
  50. package/dist/list.css-mistica.js +7 -7
  51. package/dist/loading-bar.css-mistica.js +1 -1
  52. package/dist/loading-screen.css-mistica.js +4 -4
  53. package/dist/logo.css-mistica.js +5 -5
  54. package/dist/menu.css-mistica.js +26 -24
  55. package/dist/menu.css.d.ts +2 -2
  56. package/dist/menu.d.ts +15 -3
  57. package/dist/menu.js +184 -162
  58. package/dist/mosaic.css-mistica.js +1 -1
  59. package/dist/navigation-bar.css-mistica.js +9 -9
  60. package/dist/navigation-breadcrumbs.css-mistica.js +1 -1
  61. package/dist/navigation-shared.css-mistica.js +9 -9
  62. package/dist/package-version.js +1 -1
  63. package/dist/pagination.css-mistica.js +18 -18
  64. package/dist/pin-field.css-mistica.js +1 -1
  65. package/dist/popover.css-mistica.js +1 -1
  66. package/dist/popover.d.ts +1 -0
  67. package/dist/progress-bar.css-mistica.js +6 -6
  68. package/dist/radio-button.css-mistica.js +19 -19
  69. package/dist/rating.css-mistica.js +2 -2
  70. package/dist/responsive-layout.css-mistica.js +6 -6
  71. package/dist/screen-reader-only.css-mistica.js +1 -1
  72. package/dist/select.css-mistica.js +15 -15
  73. package/dist/sheet-action-row.css-mistica.js +1 -1
  74. package/dist/sheet-common.css-mistica.js +1 -1
  75. package/dist/sheet-info.css-mistica.js +1 -1
  76. package/dist/sidenav-bar-layout.css-mistica.js +1 -1
  77. package/dist/sidenav-bar.css-mistica.js +71 -71
  78. package/dist/sidenav-bar.js +55 -50
  79. package/dist/skeletons.css-mistica.js +6 -6
  80. package/dist/skins/skin-contract.css-mistica.js +738 -738
  81. package/dist/skip-link.css-mistica.js +1 -1
  82. package/dist/slider.css-mistica.js +18 -18
  83. package/dist/snackbar.css-mistica.js +4 -4
  84. package/dist/spinner.css-mistica.js +1 -1
  85. package/dist/square.css-mistica.js +1 -1
  86. package/dist/stack.css-mistica.js +5 -5
  87. package/dist/stacking-group.css-mistica.js +1 -1
  88. package/dist/stepper.css-mistica.js +2 -2
  89. package/dist/switch-component.css-mistica.js +35 -35
  90. package/dist/table.css-mistica.js +9 -9
  91. package/dist/tabs.css-mistica.js +17 -17
  92. package/dist/tag.css-mistica.js +1 -1
  93. package/dist/text-field-base.css-mistica.js +16 -16
  94. package/dist/text-field-components.css-mistica.js +3 -3
  95. package/dist/text-link.css-mistica.js +6 -6
  96. package/dist/text.css-mistica.js +6 -6
  97. package/dist/timeline.css-mistica.js +9 -9
  98. package/dist/timer.css-mistica.js +6 -6
  99. package/dist/tooltip.css-mistica.js +1 -1
  100. package/dist/tooltip.d.ts +3 -1
  101. package/dist/tooltip.js +158 -147
  102. package/dist/touchable.css-mistica.js +1 -1
  103. package/dist/utils/aspect-ratio-support.css-mistica.js +2 -2
  104. package/dist/video.css-mistica.js +1 -1
  105. package/dist/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
  106. package/dist-es/accordion.css-mistica.js +6 -6
  107. package/dist-es/align.css-mistica.js +1 -1
  108. package/dist-es/autocomplete.css-mistica.js +1 -1
  109. package/dist-es/avatar.css-mistica.js +1 -1
  110. package/dist-es/badge.css-mistica.js +1 -1
  111. package/dist-es/box.css-mistica.js +13 -13
  112. package/dist-es/boxed.css-mistica.js +23 -23
  113. package/dist-es/button-group.css-mistica.js +1 -1
  114. package/dist-es/button-layout.css-mistica.js +14 -14
  115. package/dist-es/button.css-mistica.js +34 -34
  116. package/dist-es/callout.css-mistica.js +11 -11
  117. package/dist-es/card-internal.css-mistica.js +21 -21
  118. package/dist-es/carousel.css-mistica.js +8 -8
  119. package/dist-es/checkbox.css-mistica.js +11 -11
  120. package/dist-es/chip.css-mistica.js +15 -15
  121. package/dist-es/circle.css-mistica.js +1 -1
  122. package/dist-es/community/advanced-data-card.css-mistica.js +8 -8
  123. package/dist-es/community/ai-card.css-mistica.js +2 -2
  124. package/dist-es/community/blocks.css-mistica.js +1 -1
  125. package/dist-es/community/cyber/example-component.css-mistica.js +1 -1
  126. package/dist-es/community/example-component.css-mistica.js +2 -2
  127. package/dist-es/counter.css-mistica.js +1 -1
  128. package/dist-es/cover-hero.css-mistica.js +2 -2
  129. package/dist-es/credit-card-number-field.css-mistica.js +3 -3
  130. package/dist-es/date-field.css-mistica.js +1 -1
  131. package/dist-es/date-time-picker.css-mistica.js +1 -1
  132. package/dist-es/dialog.css-mistica.js +4 -4
  133. package/dist-es/divider.css-mistica.js +5 -5
  134. package/dist-es/double-field.css-mistica.js +2 -2
  135. package/dist-es/drawer.css-mistica.js +1 -1
  136. package/dist-es/empty-state-card.css-mistica.js +1 -1
  137. package/dist-es/empty-state.css-mistica.js +5 -5
  138. package/dist-es/fade-in.css-mistica.js +1 -1
  139. package/dist-es/fade-out.css-mistica.js +1 -1
  140. package/dist-es/feedback.css-mistica.js +1 -1
  141. package/dist-es/file-upload.css-mistica.js +7 -7
  142. package/dist-es/fixed-footer-layout.css-mistica.js +2 -2
  143. package/dist-es/form.css-mistica.js +1 -1
  144. package/dist-es/grid-layout.css-mistica.js +3 -3
  145. package/dist-es/grid.css-mistica.js +120 -120
  146. package/dist-es/header.css-mistica.js +1 -1
  147. package/dist-es/hero.css-mistica.js +2 -2
  148. package/dist-es/horizontal-scroll.css-mistica.js +1 -1
  149. package/dist-es/icon-button.css-mistica.js +53 -53
  150. package/dist-es/icons/icon-chevron.css-mistica.js +2 -2
  151. package/dist-es/icons/icon-error.css-mistica.js +1 -1
  152. package/dist-es/image.css-mistica.js +4 -4
  153. package/dist-es/inline.css-mistica.js +11 -11
  154. package/dist-es/list.css-mistica.js +7 -7
  155. package/dist-es/loading-bar.css-mistica.js +1 -1
  156. package/dist-es/loading-screen.css-mistica.js +4 -4
  157. package/dist-es/logo.css-mistica.js +5 -5
  158. package/dist-es/menu.css-mistica.js +14 -15
  159. package/dist-es/menu.js +238 -216
  160. package/dist-es/mosaic.css-mistica.js +1 -1
  161. package/dist-es/navigation-bar.css-mistica.js +9 -9
  162. package/dist-es/navigation-breadcrumbs.css-mistica.js +1 -1
  163. package/dist-es/navigation-shared.css-mistica.js +9 -9
  164. package/dist-es/package-version.js +1 -1
  165. package/dist-es/pagination.css-mistica.js +18 -18
  166. package/dist-es/pin-field.css-mistica.js +1 -1
  167. package/dist-es/popover.css-mistica.js +1 -1
  168. package/dist-es/progress-bar.css-mistica.js +6 -6
  169. package/dist-es/radio-button.css-mistica.js +19 -19
  170. package/dist-es/rating.css-mistica.js +2 -2
  171. package/dist-es/responsive-layout.css-mistica.js +6 -6
  172. package/dist-es/screen-reader-only.css-mistica.js +1 -1
  173. package/dist-es/select.css-mistica.js +15 -15
  174. package/dist-es/sheet-action-row.css-mistica.js +1 -1
  175. package/dist-es/sheet-common.css-mistica.js +1 -1
  176. package/dist-es/sheet-info.css-mistica.js +1 -1
  177. package/dist-es/sidenav-bar-layout.css-mistica.js +1 -1
  178. package/dist-es/sidenav-bar.css-mistica.js +71 -71
  179. package/dist-es/sidenav-bar.js +70 -65
  180. package/dist-es/skeletons.css-mistica.js +6 -6
  181. package/dist-es/skins/skin-contract.css-mistica.js +738 -738
  182. package/dist-es/skip-link.css-mistica.js +1 -1
  183. package/dist-es/slider.css-mistica.js +18 -18
  184. package/dist-es/snackbar.css-mistica.js +4 -4
  185. package/dist-es/spinner.css-mistica.js +1 -1
  186. package/dist-es/square.css-mistica.js +1 -1
  187. package/dist-es/stack.css-mistica.js +5 -5
  188. package/dist-es/stacking-group.css-mistica.js +1 -1
  189. package/dist-es/stepper.css-mistica.js +2 -2
  190. package/dist-es/style.css +1 -1
  191. package/dist-es/switch-component.css-mistica.js +35 -35
  192. package/dist-es/table.css-mistica.js +9 -9
  193. package/dist-es/tabs.css-mistica.js +17 -17
  194. package/dist-es/tag.css-mistica.js +1 -1
  195. package/dist-es/text-field-base.css-mistica.js +16 -16
  196. package/dist-es/text-field-components.css-mistica.js +3 -3
  197. package/dist-es/text-link.css-mistica.js +6 -6
  198. package/dist-es/text.css-mistica.js +6 -6
  199. package/dist-es/timeline.css-mistica.js +9 -9
  200. package/dist-es/timer.css-mistica.js +6 -6
  201. package/dist-es/tooltip.css-mistica.js +1 -1
  202. package/dist-es/tooltip.js +198 -187
  203. package/dist-es/touchable.css-mistica.js +1 -1
  204. package/dist-es/utils/aspect-ratio-support.css-mistica.js +2 -2
  205. package/dist-es/video.css-mistica.js +1 -1
  206. package/dist-es/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
  207. package/package.json +1 -1
  208. package/src/menu.css.ts +5 -9
  209. package/src/menu.tsx +82 -64
  210. package/src/package-version.tsx +1 -1
  211. package/src/popover.tsx +1 -0
  212. package/src/sidenav-bar.tsx +9 -2
  213. package/src/tooltip.css.ts +1 -0
  214. package/src/tooltip.tsx +94 -90
package/src/menu.tsx CHANGED
@@ -270,8 +270,14 @@ export const MenuSection = ({children}: MenuSectionProps): JSX.Element => {
270
270
  );
271
271
  };
272
272
 
273
- const MARGIN_THRESHOLD = 12;
274
273
  const MENU_OFFSET_FROM_TARGET = 8;
274
+ const REFLECTED_PLACEMENT = {top: 'bottom', bottom: 'top', left: 'right', right: 'left'} as const;
275
+ const TRANSFORM_ORIGINS = {
276
+ top: 'center bottom',
277
+ bottom: 'center top',
278
+ left: 'right center',
279
+ right: 'left center',
280
+ } as const;
275
281
 
276
282
  type MenuRenderProps = {
277
283
  ref: (element: HTMLElement | null) => void;
@@ -286,19 +292,36 @@ type TargetRenderProps = {
286
292
  };
287
293
 
288
294
  export type MenuProps = {
295
+ /**
296
+ * Width in pixels. Values are clamped to the range 136–280.
297
+ * The final width never exceeds the viewport width, even if that
298
+ * means going below the 136px minimum.
299
+ *
300
+ * Examples: 100 → 136, 200 → 200, 500 → 280.
301
+ */
289
302
  width?: number;
290
303
  renderTarget: (props: TargetRenderProps) => React.ReactNode;
291
304
  renderMenu: (props: MenuRenderProps) => React.ReactNode;
292
305
  children?: void;
293
- position?: 'left' | 'right';
294
306
  dataAttributes?: DataAttributes;
295
- };
307
+ } & ExclusifyUnion<
308
+ | {
309
+ /** @deprecated Use placement and alignment instead. */
310
+ position?: 'left' | 'right';
311
+ }
312
+ | {
313
+ placement?: 'top' | 'bottom' | 'left' | 'right';
314
+ alignment?: 'start' | 'middle' | 'end';
315
+ }
316
+ >;
296
317
 
297
318
  export const Menu = ({
298
319
  renderTarget,
299
320
  renderMenu,
300
321
  width,
301
322
  position = 'left',
323
+ placement = 'bottom',
324
+ alignment = position === 'left' ? 'start' : 'end',
302
325
  dataAttributes,
303
326
  }: MenuProps): JSX.Element => {
304
327
  const [isMenuOpen, setIsMenuOpen] = React.useState(false);
@@ -309,11 +332,9 @@ export const Menu = ({
309
332
  const menuRef = React.useRef<HTMLDivElement | null>(null);
310
333
 
311
334
  const [itemsComputedProps, setItemsComputedProps] = React.useState<{
312
- left?: number;
313
- right?: number;
314
- top: string;
315
- bottom: string;
316
- maxHeight?: number;
335
+ left: number;
336
+ top: number;
337
+ maxHeight: number;
317
338
  transformOrigin: string;
318
339
  } | null>(null);
319
340
 
@@ -326,44 +347,52 @@ export const Menu = ({
326
347
  return;
327
348
  }
328
349
 
329
- const {top: topTarget, right: rightTarget, left: leftTarget, bottom: bottomTarget} = targetRect;
330
-
331
- const heightMenu = menu.scrollHeight;
332
-
333
- const leftDirection = position === 'left' ? leftTarget : undefined;
334
- const rightDirection = position === 'right' ? windowSize.width - rightTarget : undefined;
335
-
336
- const topTargetWithOffset = topTarget - MENU_OFFSET_FROM_TARGET;
337
- const bottomTargetWithOffset = bottomTarget + MENU_OFFSET_FROM_TARGET;
338
-
339
- const availableSpaceOnBottom = windowSize.height - bottomTargetWithOffset - MARGIN_THRESHOLD;
340
- const availableSpaceOnTop = topTargetWithOffset - MARGIN_THRESHOLD;
341
- const menuFitsOnBottom = availableSpaceOnBottom > heightMenu;
342
- const menuFitsOnTop = availableSpaceOnTop > heightMenu;
343
-
344
- const isMenuOnBottom =
345
- menuFitsOnBottom || (!menuFitsOnTop && availableSpaceOnBottom > availableSpaceOnTop);
346
-
347
- if (isMenuOnBottom) {
348
- setItemsComputedProps({
349
- left: leftDirection,
350
- right: rightDirection,
351
- top: `${bottomTargetWithOffset}px`,
352
- bottom: 'auto',
353
- maxHeight: menuFitsOnBottom ? undefined : availableSpaceOnBottom,
354
- transformOrigin: 'center top',
355
- });
356
- } else {
357
- setItemsComputedProps({
358
- left: leftDirection,
359
- right: rightDirection,
360
- top: 'auto',
361
- bottom: `${windowSize.height - topTargetWithOffset}px`,
362
- maxHeight: menuFitsOnTop ? undefined : availableSpaceOnTop,
363
- transformOrigin: 'center bottom',
364
- });
350
+ const availableSpace = {
351
+ top: targetRect.top - MENU_OFFSET_FROM_TARGET,
352
+ bottom: windowSize.height - targetRect.bottom - MENU_OFFSET_FROM_TARGET,
353
+ left: targetRect.left - MENU_OFFSET_FROM_TARGET,
354
+ right: windowSize.width - targetRect.right - MENU_OFFSET_FROM_TARGET,
355
+ };
356
+ const menuSize = placement === 'top' || placement === 'bottom' ? menu.scrollHeight : menu.offsetWidth;
357
+ const candidateReflectedPlacement = REFLECTED_PLACEMENT[placement];
358
+ let finalPlacement =
359
+ availableSpace[placement] < menuSize &&
360
+ availableSpace[candidateReflectedPlacement] > availableSpace[placement]
361
+ ? candidateReflectedPlacement
362
+ : placement;
363
+ if (
364
+ (finalPlacement === 'left' || finalPlacement === 'right') &&
365
+ availableSpace[finalPlacement] < menu.offsetWidth
366
+ ) {
367
+ finalPlacement = availableSpace.bottom >= availableSpace.top ? 'bottom' : 'top';
365
368
  }
366
- }, [position, isMenuOpen, menu, target, width, windowSize]);
369
+
370
+ const isVertical = finalPlacement === 'top' || finalPlacement === 'bottom';
371
+ const maxHeight = Math.max(0, isVertical ? availableSpace[finalPlacement] : windowSize.height);
372
+ const heightMenu = Math.min(menu.scrollHeight, maxHeight);
373
+ const widthMenu = menu.offsetWidth;
374
+ const targetStart = isVertical ? targetRect.left : targetRect.top;
375
+ const targetSize = isVertical ? targetRect.width : targetRect.height;
376
+ const alignmentSize = isVertical ? widthMenu : heightMenu;
377
+ const alignmentOffset = alignment === 'start' ? 0 : alignment === 'end' ? 1 : 0.5;
378
+ const alignedPosition = targetStart + (targetSize - alignmentSize) * alignmentOffset;
379
+ const shiftedPosition = Math.max(
380
+ 0,
381
+ Math.min(alignedPosition, (isVertical ? windowSize.width : windowSize.height) - alignmentSize)
382
+ );
383
+ const positions = {
384
+ top: {left: shiftedPosition, top: targetRect.top - MENU_OFFSET_FROM_TARGET - heightMenu},
385
+ bottom: {left: shiftedPosition, top: targetRect.bottom + MENU_OFFSET_FROM_TARGET},
386
+ left: {left: targetRect.left - MENU_OFFSET_FROM_TARGET - widthMenu, top: shiftedPosition},
387
+ right: {left: targetRect.right + MENU_OFFSET_FROM_TARGET, top: shiftedPosition},
388
+ };
389
+
390
+ setItemsComputedProps({
391
+ ...positions[finalPlacement],
392
+ maxHeight,
393
+ transformOrigin: TRANSFORM_ORIGINS[finalPlacement],
394
+ });
395
+ }, [placement, alignment, isMenuOpen, menu, target, width, windowSize]);
367
396
 
368
397
  const targetProps = React.useMemo(
369
398
  () => ({
@@ -504,29 +533,18 @@ export const Menu = ({
504
533
  <div
505
534
  style={{
506
535
  ...applyCssVars({
536
+ [styles.vars.maxWidth]:
537
+ `${Math.min(styles.MENU_MAX_WIDTH, Math.max(0, windowSize.width))}px`,
538
+ ...(width && {
539
+ [styles.vars.width]: `${width}px`,
540
+ }),
507
541
  ...(itemsComputedProps
508
542
  ? {
509
- [styles.vars.top]: itemsComputedProps.top,
510
- [styles.vars.bottom]: itemsComputedProps.bottom,
543
+ [styles.vars.top]: `${itemsComputedProps.top}px`,
544
+ [styles.vars.left]: `${itemsComputedProps.left}px`,
545
+ [styles.vars.maxHeight]: `${itemsComputedProps.maxHeight}px`,
511
546
  [styles.vars.transformOrigin]:
512
547
  itemsComputedProps.transformOrigin,
513
-
514
- ...(itemsComputedProps.left !== undefined && {
515
- [styles.vars.left]: `${itemsComputedProps.left}px`,
516
- }),
517
-
518
- ...(itemsComputedProps.right !== undefined && {
519
- [styles.vars.right]: `${itemsComputedProps.right}px`,
520
- }),
521
-
522
- ...(itemsComputedProps.maxHeight !== undefined && {
523
- [styles.vars.maxHeight]:
524
- `${itemsComputedProps.maxHeight}px`,
525
- }),
526
-
527
- ...(width !== undefined && {
528
- [styles.vars.width]: width ? `${width}px` : '',
529
- }),
530
548
  }
531
549
  : {}),
532
550
  }),
@@ -1,2 +1,2 @@
1
1
  // DO NOT EDIT THIS FILE. It's autogenerated by set-version.js
2
- export const PACKAGE_VERSION = '17.6.0' as string;
2
+ export const PACKAGE_VERSION = '17.7.0' as string;
package/src/popover.tsx CHANGED
@@ -21,6 +21,7 @@ type Props = {
21
21
  onClose?: () => void;
22
22
  closeButtonLabel?: string;
23
23
  position?: Position;
24
+ alignment?: 'start' | 'middle' | 'end';
24
25
  width?: number;
25
26
  trackingEvent?: TrackingEvent | ReadonlyArray<TrackingEvent>;
26
27
  open?: boolean;
@@ -446,8 +446,15 @@ const SidenavBar = ({
446
446
  const footerDividerSentinelRef = React.useRef<HTMLDivElement>(null);
447
447
  const bodyRef = React.useRef<HTMLDivElement>(null);
448
448
 
449
+ // The mobile treatment replaces the whole rail, so a trip to a narrow screen unmounts the body and
450
+ // its sentinels. Both dividers go back to their rest state, and the observers of the new body decide
451
+ // again: a divider earned by a scroll before the trip does not belong to the body that comes back.
449
452
  React.useEffect(() => {
450
- if (!bodyRef.current) return;
453
+ if (!bodyRef.current) {
454
+ setShowHeaderDivider(false);
455
+ setShowFooterDivider(false);
456
+ return;
457
+ }
451
458
 
452
459
  // The sentinels have no height, and at rest they sit exactly on the edge of the scrollport. A
453
460
  // fractional layout (a boxed sidenav measures 100vh minus its margins) can leave them half a
@@ -478,7 +485,7 @@ const SidenavBar = ({
478
485
  headerObserver.disconnect();
479
486
  footerObserver.disconnect();
480
487
  };
481
- }, []);
488
+ }, [isTabletOrSmaller]);
482
489
 
483
490
  // The rail is travelling between its two widths. See `columnsWhileMoving`.
484
491
  const isMoving = isSidenavMoving(collapseState);
@@ -53,6 +53,7 @@ export const contentContainer = style([
53
53
  overflow: 'hidden',
54
54
  }),
55
55
  {
56
+ overflowY: 'auto',
56
57
  minWidth: CONTENT_MIN_WIDTH - 2 * BORDER_SIZE, // border is not included in this container
57
58
  },
58
59
  ]);
package/src/tooltip.tsx CHANGED
@@ -6,7 +6,6 @@ import {Transition} from 'react-transition-group';
6
6
  import * as styles from './tooltip.css';
7
7
  import Stack from './stack';
8
8
  import {Text2} from './text';
9
- import {getCssVarValue} from './utils/dom';
10
9
  import {ESC, TAB} from './utils/keys';
11
10
  import {isTouchableDevice} from './utils/environment';
12
11
  import {isEqual} from './utils/helpers';
@@ -41,7 +40,9 @@ const TOOLTIP_ENTER_TRANSITION_DELAY_IN_MS = 500;
41
40
  const TOOLTIP_EXIT_TRANSITION_DURATION_IN_MS = 100;
42
41
  const ARROW_SIZE = 20;
43
42
  const TOOLTIP_OFFSET_FROM_TARGET = 6;
44
- const TOOLTIP_PADDING_FROM_TARGET = TOOLTIP_OFFSET_FROM_TARGET + ARROW_SIZE / 2 + 1;
43
+ const TOOLTIP_BORDER_SIZE = 1;
44
+ const TOOLTIP_PADDING_FROM_TARGET = TOOLTIP_OFFSET_FROM_TARGET + ARROW_SIZE / 2 + TOOLTIP_BORDER_SIZE;
45
+ const REFLECTED_POSITION = {top: 'bottom', bottom: 'top', left: 'right', right: 'left'} as const;
45
46
 
46
47
  type Position = 'top' | 'bottom' | 'left' | 'right';
47
48
 
@@ -79,62 +80,36 @@ const getFinalPosition = (
79
80
  tooltip: HTMLElement | undefined | null,
80
81
  position: Position,
81
82
  windowHeight: number,
82
- windowWidth: number
83
+ windowWidth: number,
84
+ contentHeight?: number
83
85
  ): Position | undefined => {
84
86
  if (!targetRect || !tooltip) {
85
87
  return undefined;
86
88
  }
87
- const {top, bottom, left, right} = targetRect;
88
-
89
- const availableSpaceOnBottom = windowHeight - bottom;
90
- const availableSpaceOnRight = windowWidth - right;
91
- const availableSpaceOnTop = top;
92
- const availableSpaceOnLeft = left;
93
-
94
- const {width, height} = getElementDimensionsWithoutPadding(tooltip);
95
-
96
- const tooltipWidth = width + TOOLTIP_PADDING_FROM_TARGET;
97
- const tooltipHeight = height + TOOLTIP_PADDING_FROM_TARGET;
98
-
99
- const fitsHorizontal = tooltipWidth <= Math.max(availableSpaceOnLeft, availableSpaceOnRight);
100
- const fitsVertical = tooltipHeight <= Math.max(availableSpaceOnBottom, availableSpaceOnTop);
101
-
102
- if (!fitsVertical && !fitsHorizontal) {
103
- return undefined;
89
+ const availableSpace = {
90
+ top: targetRect.top,
91
+ bottom: windowHeight - targetRect.bottom,
92
+ left: targetRect.left,
93
+ right: windowWidth - targetRect.right,
94
+ };
95
+ const dimensions = getElementDimensionsWithoutPadding(tooltip);
96
+ const width = dimensions.width;
97
+ const height = contentHeight === undefined ? dimensions.height : contentHeight + 2 * TOOLTIP_BORDER_SIZE;
98
+ const size = (position === 'top' || position === 'bottom' ? height : width) + TOOLTIP_PADDING_FROM_TARGET;
99
+ const reflected = REFLECTED_POSITION[position];
100
+ const candidateFinalPosition =
101
+ availableSpace[position] < size && availableSpace[reflected] > availableSpace[position]
102
+ ? reflected
103
+ : position;
104
+
105
+ if (
106
+ (candidateFinalPosition === 'left' || candidateFinalPosition === 'right') &&
107
+ availableSpace[candidateFinalPosition] < size
108
+ ) {
109
+ return availableSpace.bottom > availableSpace.top ? 'bottom' : 'top';
104
110
  }
105
111
 
106
- switch (position) {
107
- case 'left':
108
- if (fitsHorizontal) {
109
- return tooltipWidth <= availableSpaceOnLeft ? 'left' : 'right';
110
- } else {
111
- return availableSpaceOnBottom > availableSpaceOnTop ? 'bottom' : 'top';
112
- }
113
-
114
- case 'right':
115
- if (fitsHorizontal) {
116
- return tooltipWidth <= availableSpaceOnRight ? 'right' : 'left';
117
- } else {
118
- return availableSpaceOnBottom > availableSpaceOnTop ? 'bottom' : 'top';
119
- }
120
-
121
- case 'top':
122
- if (fitsVertical) {
123
- return tooltipHeight <= availableSpaceOnTop ? 'top' : 'bottom';
124
- } else {
125
- return availableSpaceOnLeft > availableSpaceOnRight ? 'left' : 'right';
126
- }
127
-
128
- case 'bottom':
129
- if (fitsVertical) {
130
- return tooltipHeight <= availableSpaceOnBottom ? 'bottom' : 'top';
131
- } else {
132
- return availableSpaceOnLeft > availableSpaceOnRight ? 'left' : 'right';
133
- }
134
-
135
- default:
136
- return undefined;
137
- }
112
+ return candidateFinalPosition;
138
113
  };
139
114
 
140
115
  type Props = {
@@ -145,6 +120,7 @@ type Props = {
145
120
  targetStyle?: React.CSSProperties;
146
121
  title?: string;
147
122
  position?: Position;
123
+ alignment?: 'start' | 'middle' | 'end';
148
124
  width?: number;
149
125
  delay?: boolean;
150
126
  dataAttributes?: DataAttributes;
@@ -157,6 +133,7 @@ type BaseTooltipProps = {
157
133
  target: React.ReactNode;
158
134
  targetStyle?: React.CSSProperties;
159
135
  position?: Position;
136
+ alignment?: 'start' | 'middle' | 'end';
160
137
  width?: number;
161
138
  delay?: boolean;
162
139
  dataAttributes?: DataAttributes;
@@ -174,6 +151,7 @@ export const BaseTooltip = ({
174
151
  targetStyle,
175
152
  width,
176
153
  position = 'top',
154
+ alignment = 'middle',
177
155
  dataAttributes,
178
156
  delay = true,
179
157
  centerContent,
@@ -183,7 +161,12 @@ export const BaseTooltip = ({
183
161
  hasPointerInteractionOnly = false,
184
162
  trackingEvent,
185
163
  }: BaseTooltipProps): JSX.Element => {
186
- const {texts, t} = useTheme();
164
+ const {texts, t, borderRadii} = useTheme();
165
+ const arrowOffsetFromBorder = parseFloat(borderRadii.popup);
166
+ const minSize = Math.max(
167
+ styles.CONTENT_MIN_WIDTH,
168
+ 2 * (arrowOffsetFromBorder + TOOLTIP_BORDER_SIZE) + ARROW_SIZE
169
+ );
187
170
  const tooltipId = React.useId();
188
171
  const {openTooltipId} = useTooltipState();
189
172
  const {openTooltip, closeTooltip} = useSetTooltipState();
@@ -194,6 +177,7 @@ export const BaseTooltip = ({
194
177
 
195
178
  const targetRef = React.useRef<Element | null>(null);
196
179
  const tooltipRef = React.useRef<HTMLDivElement | null>(null);
180
+ const contentRef = React.useRef<HTMLDivElement | null>(null);
197
181
  const [tooltip, setTooltip] = React.useState<HTMLElement | null>(null);
198
182
  const isTouchable = isTouchableDevice();
199
183
  const tooltipEnterDelay = delay ? TOOLTIP_ENTER_TRANSITION_DELAY_IN_MS : 0;
@@ -208,7 +192,26 @@ export const BaseTooltip = ({
208
192
 
209
193
  const targetRect = useBoundingRect(targetRef, isTooltipOpen);
210
194
  const tooltipRect = useBoundingRect(tooltipRef, isTooltipOpen, true);
195
+ const contentRect = useBoundingRect(contentRef, isTooltipOpen, true);
211
196
  const windowSize = useWindowSize();
197
+ const currentPosition = getFinalPosition(
198
+ targetRect,
199
+ tooltip,
200
+ position,
201
+ windowSize.height,
202
+ windowSize.width,
203
+ contentRect?.height
204
+ );
205
+ const maxHeight = Math.max(
206
+ 0,
207
+ (currentPosition === 'top'
208
+ ? targetRect?.top ?? windowSize.height
209
+ : currentPosition === 'bottom'
210
+ ? windowSize.height - (targetRect?.bottom ?? 0)
211
+ : windowSize.height) -
212
+ (currentPosition === 'top' || currentPosition === 'bottom' ? TOOLTIP_PADDING_FROM_TARGET : 0) -
213
+ 2 * TOOLTIP_BORDER_SIZE
214
+ );
212
215
 
213
216
  const resetTooltipInteractions = React.useCallback(() => {
214
217
  setIsFocused(false);
@@ -227,15 +230,7 @@ export const BaseTooltip = ({
227
230
  return;
228
231
  }
229
232
 
230
- const finalPosition = getFinalPosition(
231
- targetRect,
232
- tooltip,
233
- position,
234
- windowSize.height,
235
- windowSize.width
236
- );
237
-
238
- if (!finalPosition || !targetRect) {
233
+ if (!currentPosition || !targetRect) {
239
234
  setTooltipComputedStyles(undefined);
240
235
  setArrowComputedStyles(undefined);
241
236
  resetTooltipInteractions();
@@ -250,22 +245,29 @@ export const BaseTooltip = ({
250
245
 
251
246
  const maxLeftOffset = windowSize.width - tooltipWidth;
252
247
  const maxTopOffset = windowSize.height - tooltipHeight;
248
+ const alignmentRatio = alignment === 'start' ? 0 : alignment === 'end' ? 1 : 0.5;
249
+ const leftPropInPx = Math.max(
250
+ 0,
251
+ Math.min(maxLeftOffset, left + (right - left - tooltipWidth) * alignmentRatio)
252
+ );
253
+ const topPropInPx = Math.max(
254
+ 0,
255
+ Math.min(maxTopOffset, top + (bottom - top - tooltipHeight) * alignmentRatio)
256
+ );
253
257
 
254
- const arrowOffsetFromViewport = parseInt(getCssVarValue(vars.borderRadii.popup)) ?? 8;
255
-
256
- switch (finalPosition) {
258
+ switch (currentPosition) {
257
259
  case 'top':
258
260
  tooltipStyles = {
259
- left: Math.max(0, Math.min(maxLeftOffset, (left + right - tooltipWidth) / 2)),
261
+ left: leftPropInPx,
260
262
  top: top - tooltipHeight - ARROW_SIZE / 2,
261
263
  padding: `0px 0px ${TOOLTIP_PADDING_FROM_TARGET}px 0px`,
262
264
  };
263
265
 
264
266
  arrowStyles = {
265
267
  left: Math.max(
266
- arrowOffsetFromViewport,
268
+ leftPropInPx + arrowOffsetFromBorder,
267
269
  Math.min(
268
- windowSize.width - arrowOffsetFromViewport - ARROW_SIZE,
270
+ leftPropInPx + tooltipWidth - arrowOffsetFromBorder - ARROW_SIZE,
269
271
  (left + right - ARROW_SIZE) / 2
270
272
  )
271
273
  ),
@@ -276,16 +278,16 @@ export const BaseTooltip = ({
276
278
 
277
279
  case 'bottom':
278
280
  tooltipStyles = {
279
- left: Math.max(0, Math.min(maxLeftOffset, (left + right - tooltipWidth) / 2)),
281
+ left: leftPropInPx,
280
282
  top: bottom - TOOLTIP_OFFSET_FROM_TARGET,
281
283
  padding: `${TOOLTIP_PADDING_FROM_TARGET}px 0px 0px 0px`,
282
284
  };
283
285
 
284
286
  arrowStyles = {
285
287
  left: Math.max(
286
- arrowOffsetFromViewport,
288
+ leftPropInPx + arrowOffsetFromBorder,
287
289
  Math.min(
288
- windowSize.width - arrowOffsetFromViewport - ARROW_SIZE,
290
+ leftPropInPx + tooltipWidth - arrowOffsetFromBorder - ARROW_SIZE,
289
291
  (left + right - ARROW_SIZE) / 2
290
292
  )
291
293
  ),
@@ -298,15 +300,15 @@ export const BaseTooltip = ({
298
300
  case 'left':
299
301
  tooltipStyles = {
300
302
  left: left - tooltipWidth - ARROW_SIZE / 2,
301
- top: Math.max(0, Math.min(maxTopOffset, (top + bottom - tooltipHeight) / 2)),
303
+ top: topPropInPx,
302
304
  padding: `0px ${TOOLTIP_PADDING_FROM_TARGET}px 0px 0px`,
303
305
  };
304
306
 
305
307
  arrowStyles = {
306
308
  top: Math.max(
307
- arrowOffsetFromViewport,
309
+ topPropInPx + arrowOffsetFromBorder,
308
310
  Math.min(
309
- windowSize.height - arrowOffsetFromViewport - ARROW_SIZE,
311
+ topPropInPx + tooltipHeight - arrowOffsetFromBorder - ARROW_SIZE,
310
312
  (top + bottom - ARROW_SIZE) / 2
311
313
  )
312
314
  ),
@@ -318,18 +320,17 @@ export const BaseTooltip = ({
318
320
  break;
319
321
 
320
322
  case 'right':
321
- default:
322
323
  tooltipStyles = {
323
324
  left: right - TOOLTIP_OFFSET_FROM_TARGET,
324
- top: Math.max(0, Math.min(maxTopOffset, (top + bottom - tooltipHeight) / 2)),
325
+ top: topPropInPx,
325
326
  padding: `0px 0px 0px ${TOOLTIP_PADDING_FROM_TARGET}px`,
326
327
  };
327
328
 
328
329
  arrowStyles = {
329
330
  top: Math.max(
330
- arrowOffsetFromViewport,
331
+ topPropInPx + arrowOffsetFromBorder,
331
332
  Math.min(
332
- windowSize.height - arrowOffsetFromViewport - ARROW_SIZE,
333
+ topPropInPx + tooltipHeight - arrowOffsetFromBorder - ARROW_SIZE,
333
334
  (top + bottom - ARROW_SIZE) / 2
334
335
  )
335
336
  ),
@@ -339,6 +340,10 @@ export const BaseTooltip = ({
339
340
  };
340
341
 
341
342
  break;
343
+ default: {
344
+ const exhaustiveCheck: never = currentPosition;
345
+ throw new Error(`Invalid tooltip position: ${exhaustiveCheck}`);
346
+ }
342
347
  }
343
348
 
344
349
  /**
@@ -366,10 +371,12 @@ export const BaseTooltip = ({
366
371
  targetRect,
367
372
  tooltipRect,
368
373
  isTooltipOpen,
369
- position,
374
+ currentPosition,
375
+ alignment,
370
376
  windowSize,
371
377
  tooltipComputedStyles,
372
378
  arrowComputedStyles,
379
+ arrowOffsetFromBorder,
373
380
  isTouchable,
374
381
  tooltipId,
375
382
  resetTooltipInteractions,
@@ -438,17 +445,8 @@ export const BaseTooltip = ({
438
445
  hasControlledValue,
439
446
  ]);
440
447
 
441
- const currentPosition = getFinalPosition(
442
- targetRect,
443
- tooltip,
444
- position,
445
- windowSize.height,
446
- windowSize.width
447
- );
448
-
449
448
  // by default, center content only if tooltip has minimum possible width
450
- const hasCenteredContent =
451
- centerContent !== undefined ? centerContent : tooltipRect?.width === styles.CONTENT_MIN_WIDTH;
449
+ const hasCenteredContent = centerContent !== undefined ? centerContent : tooltipRect?.width === minSize;
452
450
 
453
451
  return (
454
452
  <>
@@ -568,17 +566,23 @@ export const BaseTooltip = ({
568
566
  className={styles.tooltip}
569
567
  style={{
570
568
  width,
569
+ minWidth: minSize,
570
+ minHeight:
571
+ currentPosition === 'left' || currentPosition === 'right'
572
+ ? minSize
573
+ : undefined,
571
574
  ...getBorderStyle(themeVariant),
572
575
  maxWidth: Math.min(TOOLTIP_MAX_WIDTH, windowSize.width),
573
576
  }}
574
577
  >
575
578
  <div
579
+ style={{maxHeight}}
576
580
  className={classNames(styles.contentContainer, {
577
581
  [styles.tooltipCenter]: hasCenteredContent,
578
582
  })}
579
583
  >
580
584
  <ThemeVariant variant="default">
581
- {content}
585
+ <div ref={contentRef}>{content}</div>
582
586
 
583
587
  {onClose && (
584
588
  <div className={styles.closeButtonIcon}>