@justeattakeaway/pie-tooltip 0.5.1 → 0.6.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
@@ -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;
@@ -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.1",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
package/src/defs.ts CHANGED
@@ -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`. */
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
@@ -153,6 +153,12 @@ $tooltip-alignments: ('', '-start', '-end');
153
153
 
154
154
  position: relative;
155
155
 
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.
159
+ white-space: normal;
160
+ text-wrap: wrap;
161
+
156
162
  inline-size: var(--tooltip-width, var(--tooltip-inline-size));
157
163
  max-inline-size: var(--tooltip-max-inline-size);
158
164
  padding-block-start: var(--tooltip-padding-block);