@justeattakeaway/pie-tooltip 0.5.2 → 0.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.
package/dist/react.d.ts CHANGED
@@ -41,7 +41,7 @@ export declare const PieTooltip: React_2.ForwardRefExoticComponent<React_2.Props
41
41
  declare class PieTooltip_2 extends PieElement implements TooltipProps {
42
42
  trigger: TooltipProps['trigger'];
43
43
  isOpen: boolean;
44
- position: "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end";
44
+ position: "top-centre" | "top-start" | "top-end" | "bottom-centre" | "bottom-start" | "bottom-end" | "left-centre" | "left-start" | "left-end" | "right-centre" | "right-start" | "right-end";
45
45
  size: "default" | "fit-to-content" | "fill-container";
46
46
  variant: "default" | "inverse";
47
47
  type: "default" | "icon";
@@ -55,11 +55,8 @@ declare class PieTooltip_2 extends PieElement implements TooltipProps {
55
55
  private _hasActionContent;
56
56
  private _isPositioned;
57
57
  private _isAnchorVisible;
58
- private _resolvedPosition;
59
58
  private _clippedTriggerElement;
60
59
  private _triggerClippers;
61
- private _overlayClippers;
62
- private _collisionSignature;
63
60
  private _overlayModeDirty;
64
61
  private _triggerTrackingController;
65
62
  private _interactionController;
@@ -81,10 +78,6 @@ declare class PieTooltip_2 extends PieElement implements TooltipProps {
81
78
  private resolveMode;
82
79
  private resolveOverlayMode;
83
80
  private projectOverTrigger;
84
- private resolveCollision;
85
- private _setResolvedPosition;
86
- private _getCollisionBoundary;
87
- private _getCandidatePositions;
88
81
  private _refreshTriggerClippers;
89
82
  private handleCloseButtonClick;
90
83
  private _getTriggerElement;
@@ -104,7 +97,7 @@ declare type PieTooltipEvents = {
104
97
  onPieTooltipClose?: (event: Event) => void;
105
98
  };
106
99
 
107
- export declare const positions: readonly ["top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", "right", "right-start", "right-end"];
100
+ export declare const positions: readonly ["top-centre", "top-start", "top-end", "bottom-centre", "bottom-start", "bottom-end", "left-centre", "left-start", "left-end", "right-centre", "right-start", "right-end"];
108
101
 
109
102
  declare type ReactBaseType = React_2.HTMLAttributes<HTMLElement>;
110
103
 
@@ -117,7 +110,7 @@ export declare interface TooltipProps {
117
110
  trigger?: string;
118
111
  /** When true, the panel is visible. The component never writes to this property. */
119
112
  isOpen?: boolean;
120
- /** The preferred side and alignment; the panel repositions itself to avoid collisions. */
113
+ /** The side of the trigger the panel sits on, and its alignment along the cross axis. */
121
114
  position?: typeof positions[number];
122
115
  /** How the panel sizes itself. Not applied when `type` is `icon`. */
123
116
  size?: typeof sizes[number];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-tooltip",
3
3
  "description": "PIE Design System Tooltip built using Web Components",
4
- "version": "0.5.2",
4
+ "version": "0.7.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
package/src/defs.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core';
2
2
 
3
3
  export const positions = [
4
- 'top',
4
+ 'top-centre',
5
5
  'top-start',
6
6
  'top-end',
7
- 'bottom',
7
+ 'bottom-centre',
8
8
  'bottom-start',
9
9
  'bottom-end',
10
- 'left',
10
+ 'left-centre',
11
11
  'left-start',
12
12
  'left-end',
13
- 'right',
13
+ 'right-centre',
14
14
  'right-start',
15
15
  'right-end',
16
16
  ] as const;
@@ -42,7 +42,7 @@ export interface TooltipProps {
42
42
  /** When true, the panel is visible. The component never writes to this property. */
43
43
  isOpen?: boolean;
44
44
 
45
- /** The preferred side and alignment; the panel repositions itself to avoid collisions. */
45
+ /** The side of the trigger the panel sits on, and its alignment along the cross axis. */
46
46
  position?: typeof positions[number];
47
47
 
48
48
  /** How the panel sizes itself. Not applied when `type` is `icon`. */
@@ -80,7 +80,7 @@ export type DefaultProps = ComponentDefaultProps<TooltipProps, keyof Omit<Toolti
80
80
 
81
81
  export const defaultProps: DefaultProps = {
82
82
  isOpen: false,
83
- position: 'top',
83
+ position: 'top-centre',
84
84
  size: 'default',
85
85
  variant: 'default',
86
86
  type: 'default',
package/src/index.ts CHANGED
@@ -125,115 +125,6 @@ const intersectRects = (a: DOMRect, b: DOMRect): DOMRect | null => {
125
125
  return new DOMRect(left, top, right - left, bottom - top);
126
126
  };
127
127
 
128
- type TooltipSide = 'top' | 'bottom' | 'left' | 'right';
129
- type TooltipAlignment = '' | '-start' | '-end';
130
-
131
- interface CandidateRect {
132
- left: number;
133
- top: number;
134
- right: number;
135
- bottom: number;
136
- }
137
-
138
- const oppositeSide: Record<TooltipSide, TooltipSide> = {
139
- top: 'bottom',
140
- bottom: 'top',
141
- left: 'right',
142
- right: 'left',
143
- };
144
-
145
- const crossSides: Record<TooltipSide, Array<TooltipSide>> = {
146
- top: ['left', 'right'],
147
- bottom: ['left', 'right'],
148
- left: ['top', 'bottom'],
149
- right: ['top', 'bottom'],
150
- };
151
-
152
- const parsePosition = (position: TooltipProps['position']): { side: TooltipSide; alignment: TooltipAlignment } => {
153
- const match = /^(top|bottom|left|right)(-start|-end)?$/.exec(position ?? '');
154
-
155
- if (!match) {
156
- return { side: 'top', alignment: '' };
157
- }
158
-
159
- return { side: match[1] as TooltipSide, alignment: (match[2] ?? '') as TooltipAlignment };
160
- };
161
-
162
- const toPhysicalSide = (side: TooltipSide, isRtl: boolean): TooltipSide => {
163
- if (!isRtl) {
164
- return side;
165
- }
166
-
167
- if (side === 'left') {
168
- return 'right';
169
- }
170
-
171
- if (side === 'right') {
172
- return 'left';
173
- }
174
-
175
- return side;
176
- };
177
-
178
- const getCandidateRect = (
179
- side: TooltipSide,
180
- alignment: TooltipAlignment,
181
- anchor: DOMRect,
182
- panelWidth: number,
183
- panelHeight: number,
184
- offset: number,
185
- isRtl: boolean,
186
- ): CandidateRect => {
187
- const physicalSide = toPhysicalSide(side, isRtl);
188
- const isVerticalSide = physicalSide === 'top' || physicalSide === 'bottom';
189
-
190
- let left = 0;
191
- let top = 0;
192
-
193
- if (physicalSide === 'top') {
194
- top = anchor.top - offset - panelHeight;
195
- } else if (physicalSide === 'bottom') {
196
- top = anchor.bottom + offset;
197
- } else if (physicalSide === 'left') {
198
- left = anchor.left - offset - panelWidth;
199
- } else {
200
- left = anchor.right + offset;
201
- }
202
-
203
- if (isVerticalSide) {
204
- if (alignment === '') {
205
- left = anchor.left + (anchor.width / 2) - (panelWidth / 2);
206
- } else if (alignment === '-start') {
207
- left = isRtl ? anchor.right - panelWidth : anchor.left;
208
- } else {
209
- left = isRtl ? anchor.left : anchor.right - panelWidth;
210
- }
211
- } else if (alignment === '') {
212
- top = anchor.top + (anchor.height / 2) - (panelHeight / 2);
213
- } else {
214
- top = alignment === '-start' ? anchor.top : anchor.bottom - panelHeight;
215
- }
216
-
217
- return {
218
- left,
219
- top,
220
- right: left + panelWidth,
221
- bottom: top + panelHeight,
222
- };
223
- };
224
-
225
- const containsRect = (boundary: DOMRect, rect: CandidateRect): boolean => rect.left >= boundary.left &&
226
- rect.right <= boundary.right &&
227
- rect.top >= boundary.top &&
228
- rect.bottom <= boundary.bottom;
229
-
230
- const getVisibleArea = (boundary: DOMRect, rect: CandidateRect): number => {
231
- const width = Math.min(boundary.right, rect.right) - Math.max(boundary.left, rect.left);
232
- const height = Math.min(boundary.bottom, rect.bottom) - Math.max(boundary.top, rect.top);
233
-
234
- return Math.max(0, width) * Math.max(0, height);
235
- };
236
-
237
128
  /**
238
129
  * @tagname pie-tooltip
239
130
  * @event {Event} pie-tooltip-open - When a configured trigger asks for the panel. Set `isOpen` to `true` in response.
@@ -291,16 +182,10 @@ export class PieTooltip extends PieElement implements TooltipProps {
291
182
 
292
183
  @state() private _isAnchorVisible = true;
293
184
 
294
- @state() private _resolvedPosition: TooltipProps['position'] | undefined;
295
-
296
185
  private _clippedTriggerElement: Element | null = null;
297
186
 
298
187
  private _triggerClippers: Array<Element> = [];
299
188
 
300
- private _overlayClippers: Array<Element> = [];
301
-
302
- private _collisionSignature: string | null = null;
303
-
304
189
  private _overlayModeDirty = true;
305
190
 
306
191
  private _triggerTrackingController: AbortController | undefined;
@@ -355,15 +240,9 @@ export class PieTooltip extends PieElement implements TooltipProps {
355
240
  this._overlayModeDirty = true;
356
241
  }
357
242
 
358
- const isAnchoringChange = anchoringProperties.some((prop) => changedProperties.has(prop));
359
-
360
- if (isAnchoringChange) {
361
- this._collisionSignature = null;
362
- }
363
-
364
243
  this.resolveOverlayMode();
365
244
 
366
- if (isAnchoringChange) {
245
+ if (anchoringProperties.some((prop) => changedProperties.has(prop))) {
367
246
  this.projectOverTrigger();
368
247
  }
369
248
 
@@ -552,7 +431,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
552
431
  const useFixed = fixedClippingAncestors.length < absoluteClippingAncestors.length;
553
432
 
554
433
  this.style.position = useFixed ? 'fixed' : '';
555
- this._overlayClippers = useFixed ? fixedClippingAncestors : absoluteClippingAncestors;
556
434
  }
557
435
 
558
436
  private projectOverTrigger (): void {
@@ -603,121 +481,9 @@ export class PieTooltip extends PieElement implements TooltipProps {
603
481
 
604
482
  this.style.setProperty('--tooltip-container-inline-size', `${containerInlineSize}px`);
605
483
 
606
- this.resolveCollision(visibleRect);
607
-
608
484
  this._isPositioned = true;
609
485
  }
610
486
 
611
- private resolveCollision (anchorRect: DOMRect): void {
612
- const panel = this.renderRoot.querySelector<HTMLElement>(`.${componentClass}`);
613
-
614
- if (!panel) {
615
- return;
616
- }
617
-
618
- const boundary = this._getCollisionBoundary();
619
-
620
- if (!boundary) {
621
- this._collisionSignature = null;
622
- this._setResolvedPosition(this.position ?? defaultProps.position);
623
-
624
- return;
625
- }
626
-
627
- const isIconType = this.type === 'icon';
628
- const hostStyles = getComputedStyle(this);
629
- const isRtl = hostStyles.direction === 'rtl';
630
-
631
- const panelWidth = panel.offsetWidth;
632
- const panelHeight = panel.offsetHeight;
633
- const signature = [
634
- anchorRect.left, anchorRect.top, anchorRect.width, anchorRect.height,
635
- boundary.left, boundary.top, boundary.width, boundary.height,
636
- panelWidth, panelHeight,
637
- isRtl,
638
- ].join(':');
639
-
640
- if (this._collisionSignature === signature) {
641
- return;
642
- }
643
-
644
- this._collisionSignature = signature;
645
-
646
- const offset = parseFloat(hostStyles.getPropertyValue('--tooltip-offset')) || 0;
647
- const arrowSize = parseFloat(hostStyles.getPropertyValue('--tooltip-arrow-size')) || 0;
648
- const layerOffset = isIconType ? offset : offset + arrowSize;
649
-
650
- const { side: preferredSide, alignment: preferredAlignment } = parsePosition(this.position ?? defaultProps.position);
651
-
652
- let fitting: { side: TooltipSide; alignment: TooltipAlignment } | undefined;
653
- let bestFallback: { side: TooltipSide; alignment: TooltipAlignment } | undefined;
654
- let bestArea = -1;
655
-
656
- this._getCandidatePositions(preferredSide, preferredAlignment).some(({ side, alignment }) => {
657
- const rect = getCandidateRect(
658
- side,
659
- alignment,
660
- anchorRect,
661
- panelWidth,
662
- panelHeight,
663
- layerOffset,
664
- isRtl,
665
- );
666
-
667
- if (containsRect(boundary, rect)) {
668
- fitting = { side, alignment };
669
-
670
- return true;
671
- }
672
-
673
- const area = getVisibleArea(boundary, rect);
674
-
675
- if (area > bestArea) {
676
- bestArea = area;
677
- bestFallback = { side, alignment };
678
- }
679
-
680
- return false;
681
- });
682
-
683
- const resolved = fitting ?? bestFallback;
684
- const resolvedPosition = resolved
685
- ? `${resolved.side}${resolved.alignment}` as TooltipProps['position']
686
- : this.position ?? defaultProps.position;
687
-
688
- this._setResolvedPosition(resolvedPosition);
689
- }
690
-
691
- private _setResolvedPosition (position: TooltipProps['position']): void {
692
- if (this._resolvedPosition !== position) {
693
- this._resolvedPosition = position;
694
- }
695
- }
696
-
697
- private _getCollisionBoundary (): DOMRect | null {
698
- const { documentElement } = this.ownerDocument;
699
- const viewport = new DOMRect(0, 0, documentElement.clientWidth, documentElement.clientHeight);
700
-
701
- return this._overlayClippers.reduce<DOMRect | null>(
702
- (rect, clipper) => (rect ? intersectRects(rect, getClipRect(clipper)) : null),
703
- viewport,
704
- );
705
- }
706
-
707
- private _getCandidatePositions (
708
- preferredSide: TooltipSide,
709
- preferredAlignment: TooltipAlignment,
710
- ): Array<{ side: TooltipSide; alignment: TooltipAlignment }> {
711
- const sideOrder: Array<TooltipSide> = [preferredSide, oppositeSide[preferredSide], ...crossSides[preferredSide]];
712
- const allAlignments: Array<TooltipAlignment> = ['', '-start', '-end'];
713
-
714
- return sideOrder.flatMap((side) => {
715
- const alignments = [preferredAlignment, ...allAlignments.filter((alignment) => alignment !== preferredAlignment)];
716
-
717
- return alignments.map((alignment) => ({ side, alignment }));
718
- });
719
- }
720
-
721
487
  private _refreshTriggerClippers (): void {
722
488
  const triggerElement = this._getTriggerElement();
723
489
 
@@ -886,15 +652,13 @@ export class PieTooltip extends PieElement implements TooltipProps {
886
652
  heading,
887
653
  isDismissible,
888
654
  isOpen,
889
- position: preferredPosition,
655
+ position,
890
656
  size,
891
657
  type,
892
658
  variant,
893
- _resolvedPosition,
894
659
  _mode: mode,
895
660
  } = this;
896
661
 
897
- const position = _resolvedPosition ?? preferredPosition;
898
662
  const isIconType = type === 'icon';
899
663
 
900
664
  const layerClasses = {
package/src/tooltip.scss CHANGED
@@ -1,10 +1,6 @@
1
1
  @use '@justeattakeaway/pie-css/scss' as p;
2
2
 
3
- // All twelve position values are generated explicitly, none inferred.
4
- $tooltip-alignments: ('', '-start', '-end');
5
-
6
- // Zero-size, pinned to the containing block's origin. Trigger position is measured relative
7
- // to this point so the panel resolves against the document rather than the viewport.
3
+ $tooltip-alignments: ('-centre', '-start', '-end');
8
4
 
9
5
  :host {
10
6
  --tooltip-anchor-top: 0;
@@ -13,12 +9,7 @@ $tooltip-alignments: ('', '-start', '-end');
13
9
  --tooltip-anchor-height: 0;
14
10
  --tooltip-container-inline-size: 0;
15
11
  --tooltip-offset: var(--dt-spacing-a);
16
-
17
- // Arrow protrusion depth; base is size × 2 (16px wide × 8px tall).
18
- // Declared on the host so the placement layer can account for it.
19
12
  --tooltip-arrow-size: 8px;
20
-
21
- // Layer must clear both the offset and the arrow so the arrow tip doesn't touch the trigger.
22
13
  --tooltip-layer-offset: calc(var(--tooltip-offset) + var(--tooltip-arrow-size));
23
14
 
24
15
  position: absolute;
@@ -31,7 +22,7 @@ $tooltip-alignments: ('', '-start', '-end');
31
22
  inline-size: 0;
32
23
  block-size: 0;
33
24
 
34
- // Inherits to the anchor; the panel opts back in with `pointer-events: auto`.
25
+ // The host overlays the trigger; only the panel opts back into pointer events.
35
26
  pointer-events: none;
36
27
  }
37
28
 
@@ -44,7 +35,6 @@ $tooltip-alignments: ('', '-start', '-end');
44
35
  block-size: 0;
45
36
  }
46
37
 
47
- // Sized and positioned to match the trigger; the placement layer resolves against this box.
48
38
  .c-tooltip-anchor {
49
39
  position: absolute;
50
40
  top: var(--tooltip-anchor-top);
@@ -54,8 +44,6 @@ $tooltip-alignments: ('', '-start', '-end');
54
44
  block-size: var(--tooltip-anchor-height);
55
45
  }
56
46
 
57
- // Grid so panel alignment uses logical start/center/end. A panel wider than its track overflows
58
- // evenly from center, keeping it centred on the trigger.
59
47
  .c-tooltip-layer {
60
48
  position: absolute;
61
49
 
@@ -65,8 +53,6 @@ $tooltip-alignments: ('', '-start', '-end');
65
53
  visibility: hidden;
66
54
  transition: visibility 0s linear 150ms;
67
55
 
68
- // `is-anchorVisible` is false when the trigger has been scrolled entirely out of a clipping
69
- // ancestor: there is nothing left to point at, so the panel hides rather than float free.
70
56
  &.is-open.is-positioned.is-anchorVisible {
71
57
  visibility: visible;
72
58
  transition-delay: 0s;
@@ -100,8 +86,7 @@ $tooltip-alignments: ('', '-start', '-end');
100
86
  inset-block-start: calc(100% + var(--tooltip-layer-offset));
101
87
  }
102
88
 
103
- // `left`/`right` resolve on the inline axis and swap in RTL. Their -start/-end alignments
104
- // are on the block axis, which has no direction to mirror.
89
+ // left/right resolve on the inline axis and swap in RTL; their -start/-end alignments do not.
105
90
  .c-tooltip-layer--left#{$alignment} {
106
91
  inset-inline-end: calc(100% + var(--tooltip-layer-offset));
107
92
  }
@@ -112,7 +97,7 @@ $tooltip-alignments: ('', '-start', '-end');
112
97
  }
113
98
 
114
99
  @each $side in (top, bottom, left, right) {
115
- .c-tooltip-layer--#{$side} {
100
+ .c-tooltip-layer--#{$side}-centre {
116
101
  place-items: center;
117
102
  }
118
103
 
@@ -134,15 +119,12 @@ $tooltip-alignments: ('', '-start', '-end');
134
119
  --tooltip-padding-block: var(--dt-spacing-b);
135
120
  --tooltip-padding-inline: var(--dt-spacing-c);
136
121
  --tooltip-close-size: 32px;
122
+ --tooltip-close-block-inset: var(--dt-spacing-a-small);
137
123
  --tooltip-motion-duration: 150ms;
138
124
  --tooltip-motion-easing: cubic-bezier(0.45, 0, 0.55, 1);
139
125
  --tooltip-motion-translate-x: 0;
140
126
  --tooltip-motion-translate-y: 0;
141
-
142
- // Arrow centre offset from the aligned edge for -start/-end. Clear of the corner radius.
143
127
  --tooltip-arrow-inset: calc(var(--tooltip-border-radius) + var(--tooltip-arrow-size));
144
-
145
- // Centre position → box inset. Percentages resolve per axis, so one declaration covers both.
146
128
  --tooltip-arrow-cross-inset: calc(var(--tooltip-arrow-cross-position, 50%) - var(--tooltip-arrow-size));
147
129
 
148
130
  // 2px overlap into the panel prevents subpixel gaps between arrow and edge.
@@ -153,9 +135,7 @@ $tooltip-alignments: ('', '-start', '-end');
153
135
 
154
136
  position: relative;
155
137
 
156
- // Reset the inherited text layout: a host page that sets `white-space` or `text-wrap` to
157
- // `nowrap` (typical of header action bars) would otherwise reach the slotted content and
158
- // inflate the panel's min-content size past its own inline-size.
138
+ // Resets an inherited `nowrap` from the host page that would inflate the panel's min-content size.
159
139
  white-space: normal;
160
140
  text-wrap: wrap;
161
141
 
@@ -235,7 +215,7 @@ $tooltip-alignments: ('', '-start', '-end');
235
215
  --tooltip-max-inline-size: none;
236
216
  }
237
217
 
238
- // Also declares inline-size to prevent `--tooltip-width` from overriding it.
218
+ // Also declares inline-size so `--tooltip-width` cannot override it.
239
219
  .c-tooltip--type-icon {
240
220
  --tooltip-border-radius: var(--dt-radius-rounded-e);
241
221
  --tooltip-padding-block: var(--dt-spacing-a);
@@ -245,19 +225,12 @@ $tooltip-alignments: ('', '-start', '-end');
245
225
  }
246
226
 
247
227
  .c-tooltip.is-dismissible {
248
- // The close button is taller than one content line; reserve its block size.
249
- min-block-size: calc(var(--tooltip-close-size) + var(--dt-spacing-b) * 2);
250
- padding-inline-end: calc(var(--tooltip-padding-inline) + var(--tooltip-close-size));
251
- }
228
+ // Reserves the close button's box so an empty or one-line panel still fits it.
229
+ min-block-size: calc(var(--tooltip-close-size) + var(--tooltip-close-block-inset) * 2);
252
230
 
253
- .c-tooltip.is-dismissible:not(.has-heading) {
254
- display: flex;
255
- flex-direction: column;
256
- justify-content: center;
231
+ padding-inline-end: calc(var(--tooltip-close-size) + var(--dt-spacing-b));
257
232
  }
258
233
 
259
- // Arrow: clip-path triangle, 16px wide × 8px tall. Both insets are logical so the arrow
260
- // follows the panel when left/right swap in RTL.
261
234
  @mixin tooltip-arrow-clip($direction) {
262
235
  @if $direction == up {
263
236
  inline-size: calc(var(--tooltip-arrow-size) * 2);
@@ -279,7 +252,7 @@ $tooltip-alignments: ('', '-start', '-end');
279
252
  }
280
253
 
281
254
  @each $side in (top, bottom, left, right) {
282
- .c-tooltip--#{$side} {
255
+ .c-tooltip--#{$side}-centre {
283
256
  --tooltip-arrow-cross-position: 50%;
284
257
  }
285
258
 
@@ -349,9 +322,7 @@ $tooltip-alignments: ('', '-start', '-end');
349
322
  overflow-wrap: break-word;
350
323
  }
351
324
 
352
- // The content is the programmatic focus target in dialog mode (see `focusPanel`). It is never
353
- // reached by Tab, which moves between the panel's own controls, so it draws no focus ring of
354
- // its own: the controls inside the panel carry their own focus indicators.
325
+ // Programmatic focus target in dialog mode, never reached by Tab, so it draws no focus ring.
355
326
  .c-tooltip-content:focus {
356
327
  outline: none;
357
328
  }
@@ -359,34 +330,21 @@ $tooltip-alignments: ('', '-start', '-end');
359
330
  .c-tooltip.has-action .c-tooltip-action {
360
331
  display: flex;
361
332
  justify-content: flex-end;
362
- margin-block-start: var(--dt-spacing-a);
333
+ margin-block-start: var(--dt-spacing-b);
334
+ margin-inline-end: calc((var(--tooltip-padding-inline) - var(--dt-spacing-b)) * -1);
363
335
  }
364
336
 
365
337
  .c-tooltip.is-dismissible.has-action .c-tooltip-action {
366
338
  margin-inline-end: calc(var(--tooltip-close-size) * -1);
367
339
  }
368
340
 
369
- // Close button is out of flow; reserving its block size on the body aligns the action row with it.
370
- .c-tooltip.is-dismissible.has-action .c-tooltip-body {
371
- min-block-size: var(--tooltip-close-size);
372
- }
373
-
374
- // No heading means one content line; keep it centred against the close button.
375
- .c-tooltip.is-dismissible.has-action:not(.has-heading) .c-tooltip-body {
376
- display: grid;
377
- align-content: center;
378
- }
379
-
380
341
  .c-tooltip-close {
381
342
  position: absolute;
382
- inset-block-start: var(--dt-spacing-b);
383
- inset-inline-end: var(--dt-spacing-b);
343
+ inset-block-start: var(--tooltip-close-block-inset);
344
+ inset-inline-end: 0;
384
345
  }
385
346
 
386
- // Hover bridge: a transparent pseudo-element extending from each panel edge toward its
387
- // trigger, covering the --tooltip-layer-offset gap. Keeps the hover region intact so
388
- // the pointer can travel from the trigger into the panel without the close timer firing.
389
- // Sized from --tooltip-layer-offset so widening --tooltip-offset cannot open a hole.
347
+ // Hover bridge over the --tooltip-layer-offset gap so the pointer can reach the panel without the close timer firing.
390
348
  @each $alignment in $tooltip-alignments {
391
349
  .c-tooltip--top#{$alignment}::before {
392
350
  content: '';