@operato/utils 10.0.0 → 10.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.
@@ -2,55 +2,96 @@ import { hasOverflow } from '../has-overflow.js';
2
2
  export class TooltipReactiveController {
3
3
  constructor(host, options) {
4
4
  this.handleMouseOver = (e) => {
5
- var _a;
6
- const element = e.target;
7
- if (element.hasAttribute(this.attributeName) && hasOverflow(element)) {
8
- const tooltipContent = typeof this.content === 'function'
9
- ? this.content.call(this, element)
10
- : this.content || ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || '';
11
- const rect = element.getBoundingClientRect();
12
- // Calculate the fixed position
13
- const fixedTop = rect.top + rect.height;
14
- const fixedLeft = rect.left;
15
- // Set tooltip container position and content
16
- TooltipReactiveController.tooltipContainer.style.top = `${fixedTop}px`;
17
- TooltipReactiveController.tooltipContainer.style.left = `${fixedLeft}px`;
18
- TooltipReactiveController.tooltipContainer.style.display = 'block';
19
- TooltipReactiveController.tooltipContainer.textContent = tooltipContent;
5
+ var _a, _b;
6
+ /*
7
+ * 표식이 달린 **조상**을 찾는다 — 포인터가 닿는 것은 대개 그 안의 조각이다.
8
+ *
9
+ * `e.target` 만 보고 있었다. 그런데 툴팁을 붙이고 싶은 줄은 거의 언제나 안에 조각을 가진다 —
10
+ * 공장 달력의 한 줄은 [표시 6px][줄머리][제목] 셋이고, data-grist 의 머리말도 안에 아이콘이
11
+ * 들어간다. 포인터가 그 조각 위에 있으면 `e.target` 이 조각이라 표식이 없고, 그대로 돌아갔다.
12
+ *
13
+ * [확인함] 2026-09-11 공장 달력 한 줄 114px 중 제목 조각이 104px.
14
+ * 줄에 직접 쏘면 뜨고, 제목 조각에 쏘면 안 뜬다.
15
+ * **91% 의 자리에서 안 뜬다** — 사실상 안 뜨는 것이다.
16
+ *
17
+ * `closest` 는 자기 자신부터 본다. 표식이 잎에 달린 경우는 전과 똑같이 동작한다.
18
+ */
19
+ const element = (_b = (_a = e.target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, `[${this.attributeName}]`);
20
+ if (!element || !hasOverflow(element)) {
21
+ return;
20
22
  }
23
+ TooltipReactiveController.cancelPending();
24
+ /* 이미 떠 있으면 기다리지 않는다(위 머리말). */
25
+ if (TooltipReactiveController.shown || this.enterDelay <= 0) {
26
+ this.show(element);
27
+ return;
28
+ }
29
+ TooltipReactiveController.pending = setTimeout(() => {
30
+ TooltipReactiveController.pending = null;
31
+ this.show(element);
32
+ }, this.enterDelay);
21
33
  };
22
34
  this.handleMouseOut = (e) => {
35
+ /* 기다리던 것도 취소한다 — 안 그러면 떠난 뒤에 뜬다. */
36
+ TooltipReactiveController.cancelPending();
23
37
  if (TooltipReactiveController.tooltipContainer) {
24
38
  TooltipReactiveController.tooltipContainer.style.display = 'none';
25
39
  }
40
+ TooltipReactiveController.shown = false;
26
41
  };
27
- const { attributeName, content } = options || {};
42
+ const { attributeName, content, enterDelay } = options || {};
28
43
  this.host = host;
29
44
  this.attributeName = attributeName || 'data-reactive-tooltip';
30
45
  this.content = content;
46
+ this.enterDelay = enterDelay !== null && enterDelay !== void 0 ? enterDelay : TooltipReactiveController.DEFAULT_ENTER_DELAY;
31
47
  host.addController(this);
32
- // Create a tooltip container element if it doesn't exist
33
- if (!TooltipReactiveController.tooltipContainer) {
34
- TooltipReactiveController.tooltipContainer = document.createElement('div');
35
- TooltipReactiveController.tooltipContainer.className = 'ox-tooltip-container';
36
- TooltipReactiveController.tooltipContainer.style.position = 'fixed';
37
- TooltipReactiveController.tooltipContainer.style.pointerEvents = 'none';
38
- TooltipReactiveController.tooltipContainer.style.zIndex = '1000';
39
- // Set default styles using CSS variables
40
- TooltipReactiveController.tooltipContainer.style.fontSize = 'var(--tooltip-font-size, 1.0em)';
41
- TooltipReactiveController.tooltipContainer.style.color = 'var(--tooltip-color, #fff)';
42
- TooltipReactiveController.tooltipContainer.style.backgroundColor =
43
- 'var(--tooltip-background-color, rgba(0, 0, 0, 0.7))';
44
- TooltipReactiveController.tooltipContainer.style.padding = 'var(--tooltip-padding, 5px)';
45
- TooltipReactiveController.tooltipContainer.style.borderRadius = 'var(--tooltip-border-radius, 3px)';
46
- TooltipReactiveController.tooltipContainer.style.boxShadow =
47
- 'var(--tooltip-box-shadow, 0 2px 10px rgba(0, 0, 0, 0.2))';
48
- TooltipReactiveController.tooltipContainer.style.maxWidth = 'var(--tooltip-max-width, 300px)';
49
- TooltipReactiveController.tooltipContainer.style.wordWrap = 'break-word';
50
- TooltipReactiveController.tooltipContainer.style.whiteSpace = 'pre-wrap';
51
- TooltipReactiveController.tooltipContainer.style.overflow = 'hidden';
52
- document.body.appendChild(TooltipReactiveController.tooltipContainer);
48
+ TooltipReactiveController.ensureContainer();
49
+ }
50
+ /**
51
+ * 툴팁 상자를 만든다 — 문서에 하나만 둔다.
52
+ *
53
+ * ── 모양 ────────────────────────────────────────────────────────────────
54
+ * 전에는 검정 70% 바탕 · 3px 둥글기 · 물려받은 글자 크기였다. 앱의 토큰을 하나도 쓰지 않아서, 화면이
55
+ * 어떤 색과 크기를 쓰든 툴팁만 브라우저 기본처럼 보였다.
56
+ *
57
+ * 이제 --ui-* 를 본다. 툴팁은 **화면 위에 잠깐 떠 있는 것**이라 화면과 같은 면이 아니고, 그래서
58
+ * 어두운 면에 흰 글자다 — 밝은 면으로 두면 표와 섞여 어디까지가 툴팁인지 안 보인다.
59
+ *
60
+ * 글자는 본문보다 한 단 작다(12px). 툴팁은 값이 아니라 값을 거드는 말이다.
61
+ *
62
+ * 테두리는 두지 않는다 — 어두운 면이 스스로 경계다. 그림자는 떠 있음을 말할 만큼만 옅게 둔다.
63
+ *
64
+ * 값은 모두 --tooltip-* 으로 덮을 수 있다. 어두운 테마는 이 값들을 뒤집어야 한다.
65
+ */
66
+ static ensureContainer() {
67
+ if (TooltipReactiveController.tooltipContainer) {
68
+ return;
53
69
  }
70
+ const box = document.createElement('div');
71
+ box.className = 'ox-tooltip-container';
72
+ box.style.position = 'fixed';
73
+ box.style.pointerEvents = 'none';
74
+ box.style.zIndex = '1000';
75
+ box.style.display = 'none';
76
+ /*
77
+ * 낱개 속성으로 적는다. font 축약형은 글꼴 이름을 반드시 요구해서 inherit 을 넣으면 선언이 통째로
78
+ * 버려진다. 글꼴은 적지 않아도 body 에서 물려받는다 — 이 상자가 body 의 자식이다.
79
+ */
80
+ box.style.fontFamily = 'var(--tooltip-font-family, inherit)';
81
+ box.style.fontSize = 'var(--tooltip-font-size, 12px)';
82
+ box.style.lineHeight = 'var(--tooltip-line-height, 1.45)';
83
+ box.style.fontWeight = 'var(--tooltip-font-weight, 400)';
84
+ box.style.color = 'var(--tooltip-color, #ffffff)';
85
+ box.style.backgroundColor = 'var(--tooltip-background-color, rgba(23, 36, 44, 0.94))';
86
+ box.style.padding = 'var(--tooltip-padding, 6px 8px)';
87
+ box.style.borderRadius = 'var(--tooltip-border-radius, 6px)';
88
+ box.style.boxShadow = 'var(--tooltip-box-shadow, 0 4px 12px rgba(23, 36, 44, 0.18))';
89
+ box.style.maxWidth = 'var(--tooltip-max-width, 280px)';
90
+ box.style.wordWrap = 'break-word';
91
+ box.style.whiteSpace = 'pre-wrap';
92
+ box.style.overflow = 'hidden';
93
+ TooltipReactiveController.tooltipContainer = box;
94
+ document.body.appendChild(box);
54
95
  }
55
96
  hostConnected() {
56
97
  var _a;
@@ -64,6 +105,69 @@ export class TooltipReactiveController {
64
105
  target.removeEventListener('mouseover', this.handleMouseOver);
65
106
  target.removeEventListener('mouseout', this.handleMouseOut);
66
107
  }
108
+ static cancelPending() {
109
+ if (TooltipReactiveController.pending) {
110
+ clearTimeout(TooltipReactiveController.pending);
111
+ TooltipReactiveController.pending = null;
112
+ }
113
+ }
114
+ show(element) {
115
+ var _a;
116
+ {
117
+ const tooltipContent = typeof this.content === 'function'
118
+ ? this.content.call(this, element)
119
+ : this.content || ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || '';
120
+ const rect = element.getBoundingClientRect();
121
+ const box = TooltipReactiveController.tooltipContainer;
122
+ /* 내용을 먼저 넣어야 크기를 잴 수 있다. */
123
+ box.textContent = tooltipContent;
124
+ box.style.display = 'block';
125
+ box.style.left = '0px';
126
+ box.style.top = '0px';
127
+ /*
128
+ * 화면 안으로 밀어 넣는다.
129
+ *
130
+ * 전에는 겨눈 요소의 왼쪽 · 아래에 그냥 놓았다. 그래서 오른쪽 끝 칸이나 마지막 줄에서는 툴팁이
131
+ * 화면 밖으로 나가 **보이지 않았다.** 잘린 글자를 보여 주려고 있는 것인데 그때 안 보인다.
132
+ *
133
+ * 넘치면 반대쪽으로 접는다 — 오른쪽이 모자라면 오른쪽 끝에 맞추고, 아래가 모자라면 요소 위로
134
+ * 올린다.
135
+ */
136
+ const GAP = 4;
137
+ const size = box.getBoundingClientRect();
138
+ let left = rect.left;
139
+ let top = rect.bottom + GAP;
140
+ if (left + size.width > window.innerWidth - GAP) {
141
+ left = Math.max(GAP, window.innerWidth - size.width - GAP);
142
+ }
143
+ if (top + size.height > window.innerHeight - GAP) {
144
+ top = Math.max(GAP, rect.top - size.height - GAP);
145
+ }
146
+ box.style.left = `${Math.round(left)}px`;
147
+ box.style.top = `${Math.round(top)}px`;
148
+ TooltipReactiveController.shown = true;
149
+ }
150
+ }
67
151
  }
152
+ /**
153
+ * 뜨기까지 기다리는 시간(밀리초).
154
+ *
155
+ * ── 왜 기다리나 ─────────────────────────────────────────────────────────
156
+ * 표를 훑을 때 마우스는 여러 칸을 스쳐 지난다. 지연이 없으면 그 칸마다 툴팁이 떴다 사라져서, 읽으려는
157
+ * 것을 가리고 눈이 따라가야 한다. 지연을 두면 **머문 곳에만** 뜬다 — 스쳐 지난 것과 보려고 멈춘 것을
158
+ * 가른다(hover intent).
159
+ *
160
+ * 450ms 는 관례에 가까운 값이다. 더 짧으면 스치는 것과 구별되지 않고, 더 길면 「안 뜨는 것」처럼
161
+ * 느껴진다.
162
+ *
163
+ * ── 한 번 뜬 뒤에는 기다리지 않는다 ──────────────────────────────────────
164
+ * 이미 떠 있으면 다음 요소로 옮길 때 바로 바꾼다. 한 번 「보고 싶다」를 표현했으니 옆 칸을 볼 때마다
165
+ * 다시 기다리게 하지 않는다.
166
+ */
167
+ TooltipReactiveController.DEFAULT_ENTER_DELAY = 450;
68
168
  TooltipReactiveController.tooltipContainer = null;
169
+ /** 뜨기를 기다리는 중인 타이머. */
170
+ TooltipReactiveController.pending = null;
171
+ /** 지금 툴팁이 떠 있는가. 떠 있으면 다음 요소로 옮길 때 기다리지 않는다. */
172
+ TooltipReactiveController.shown = false;
69
173
  //# sourceMappingURL=tooltip-reactive-controller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip-reactive-controller.js","sourceRoot":"","sources":["../../../src/reactive-controllers/tooltip-reactive-controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,MAAM,OAAO,yBAAyB;IAMpC,YACE,IAA0C,EAC1C,OAGC;QA8CK,oBAAe,GAAG,CAAC,CAAQ,EAAE,EAAE;;YACrC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAqB,CAAA;YACvC,IAAI,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;gBACrE,MAAM,cAAc,GAClB,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU;oBAChC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;oBAClC,CAAC,CAAC,IAAI,CAAC,OAAO,KAAI,MAAA,OAAO,CAAC,WAAW,0CAAE,IAAI,EAAE,CAAA,IAAI,EAAE,CAAA;gBAEvD,MAAM,IAAI,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAA;gBAE5C,+BAA+B;gBAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,CAAA;gBACvC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAA;gBAE3B,6CAA6C;gBAC7C,yBAAyB,CAAC,gBAAiB,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,QAAQ,IAAI,CAAA;gBACvE,yBAAyB,CAAC,gBAAiB,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,SAAS,IAAI,CAAA;gBACzE,yBAAyB,CAAC,gBAAiB,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAA;gBACnE,yBAAyB,CAAC,gBAAiB,CAAC,WAAW,GAAG,cAAc,CAAA;YAC1E,CAAC;QACH,CAAC,CAAA;QAEO,mBAAc,GAAG,CAAC,CAAQ,EAAE,EAAE;YACpC,IAAI,yBAAyB,CAAC,gBAAgB,EAAE,CAAC;gBAC/C,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAA;YACnE,CAAC;QACH,CAAC,CAAA;QAtEC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,OAAO,IAAI,EAAE,CAAA;QAEhD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,aAAa,GAAG,aAAa,IAAI,uBAAuB,CAAA;QAC7D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QAEtB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;QAExB,yDAAyD;QACzD,IAAI,CAAC,yBAAyB,CAAC,gBAAgB,EAAE,CAAC;YAChD,yBAAyB,CAAC,gBAAgB,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;YAC1E,yBAAyB,CAAC,gBAAgB,CAAC,SAAS,GAAG,sBAAsB,CAAA;YAC7E,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,GAAG,OAAO,CAAA;YACnE,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAA;YACvE,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAA;YAChE,yCAAyC;YACzC,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,GAAG,iCAAiC,CAAA;YAC7F,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,KAAK,GAAG,4BAA4B,CAAA;YACrF,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,eAAe;gBAC9D,qDAAqD,CAAA;YACvD,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,GAAG,6BAA6B,CAAA;YACxF,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,YAAY,GAAG,mCAAmC,CAAA;YACnG,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,SAAS;gBACxD,0DAA0D,CAAA;YAC5D,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,GAAG,iCAAiC,CAAA;YAC7F,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,GAAG,YAAY,CAAA;YACxE,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,UAAU,GAAG,UAAU,CAAA;YACxE,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAA;YACpE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAAC,gBAAgB,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAED,aAAa;;QACX,MAAM,MAAM,GAAG,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,mCAAI,IAAI,CAAC,IAAI,CAAA;QAChD,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;QAC1D,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;IAC1D,CAAC;IAED,gBAAgB;;QACd,MAAM,MAAM,GAAG,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,mCAAI,IAAI,CAAC,IAAI,CAAA;QAChD,MAAM,CAAC,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;QAC7D,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;IAC7D,CAAC;;AAnDc,0CAAgB,GAAuB,IAAI,AAA3B,CAA2B","sourcesContent":["import { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { hasOverflow } from '../has-overflow.js'\n\nexport class TooltipReactiveController implements ReactiveController {\n private host: ReactiveControllerHost & HTMLElement\n private attributeName: string\n private content?: string | ((element: HTMLElement) => string)\n private static tooltipContainer: HTMLElement | null = null\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n options?: {\n content?: string | ((element: HTMLElement) => string)\n attributeName?: string\n }\n ) {\n const { attributeName, content } = options || {}\n\n this.host = host\n this.attributeName = attributeName || 'data-reactive-tooltip'\n this.content = content\n\n host.addController(this)\n\n // Create a tooltip container element if it doesn't exist\n if (!TooltipReactiveController.tooltipContainer) {\n TooltipReactiveController.tooltipContainer = document.createElement('div')\n TooltipReactiveController.tooltipContainer.className = 'ox-tooltip-container'\n TooltipReactiveController.tooltipContainer.style.position = 'fixed'\n TooltipReactiveController.tooltipContainer.style.pointerEvents = 'none'\n TooltipReactiveController.tooltipContainer.style.zIndex = '1000'\n // Set default styles using CSS variables\n TooltipReactiveController.tooltipContainer.style.fontSize = 'var(--tooltip-font-size, 1.0em)'\n TooltipReactiveController.tooltipContainer.style.color = 'var(--tooltip-color, #fff)'\n TooltipReactiveController.tooltipContainer.style.backgroundColor =\n 'var(--tooltip-background-color, rgba(0, 0, 0, 0.7))'\n TooltipReactiveController.tooltipContainer.style.padding = 'var(--tooltip-padding, 5px)'\n TooltipReactiveController.tooltipContainer.style.borderRadius = 'var(--tooltip-border-radius, 3px)'\n TooltipReactiveController.tooltipContainer.style.boxShadow =\n 'var(--tooltip-box-shadow, 0 2px 10px rgba(0, 0, 0, 0.2))'\n TooltipReactiveController.tooltipContainer.style.maxWidth = 'var(--tooltip-max-width, 300px)'\n TooltipReactiveController.tooltipContainer.style.wordWrap = 'break-word'\n TooltipReactiveController.tooltipContainer.style.whiteSpace = 'pre-wrap'\n TooltipReactiveController.tooltipContainer.style.overflow = 'hidden'\n document.body.appendChild(TooltipReactiveController.tooltipContainer)\n }\n }\n\n hostConnected() {\n const target = this.host.shadowRoot ?? this.host\n target.addEventListener('mouseover', this.handleMouseOver)\n target.addEventListener('mouseout', this.handleMouseOut)\n }\n\n hostDisconnected() {\n const target = this.host.shadowRoot ?? this.host\n target.removeEventListener('mouseover', this.handleMouseOver)\n target.removeEventListener('mouseout', this.handleMouseOut)\n }\n\n private handleMouseOver = (e: Event) => {\n const element = e.target as HTMLElement\n if (element.hasAttribute(this.attributeName) && hasOverflow(element)) {\n const tooltipContent =\n typeof this.content === 'function'\n ? this.content.call(this, element)\n : this.content || element.textContent?.trim() || ''\n\n const rect = element.getBoundingClientRect()\n\n // Calculate the fixed position\n const fixedTop = rect.top + rect.height\n const fixedLeft = rect.left\n\n // Set tooltip container position and content\n TooltipReactiveController.tooltipContainer!.style.top = `${fixedTop}px`\n TooltipReactiveController.tooltipContainer!.style.left = `${fixedLeft}px`\n TooltipReactiveController.tooltipContainer!.style.display = 'block'\n TooltipReactiveController.tooltipContainer!.textContent = tooltipContent\n }\n }\n\n private handleMouseOut = (e: Event) => {\n if (TooltipReactiveController.tooltipContainer) {\n TooltipReactiveController.tooltipContainer.style.display = 'none'\n }\n }\n}\n"]}
1
+ {"version":3,"file":"tooltip-reactive-controller.js","sourceRoot":"","sources":["../../../src/reactive-controllers/tooltip-reactive-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { hasOverflow } from '../has-overflow.js'\n\nexport class TooltipReactiveController implements ReactiveController {\n /**\n * 뜨기까지 기다리는 시간(밀리초).\n *\n * ── 왜 기다리나 ─────────────────────────────────────────────────────────\n * 표를 훑을 때 마우스는 여러 칸을 스쳐 지난다. 지연이 없으면 그 칸마다 툴팁이 떴다 사라져서, 읽으려는\n * 것을 가리고 눈이 따라가야 한다. 지연을 두면 **머문 곳에만** 뜬다 — 스쳐 지난 것과 보려고 멈춘 것을\n * 가른다(hover intent).\n *\n * 450ms 는 관례에 가까운 값이다. 더 짧으면 스치는 것과 구별되지 않고, 더 길면 「안 뜨는 것」처럼\n * 느껴진다.\n *\n * ── 한 번 뜬 뒤에는 기다리지 않는다 ──────────────────────────────────────\n * 이미 떠 있으면 다음 요소로 옮길 때 바로 바꾼다. 한 번 「보고 싶다」를 표현했으니 옆 칸을 볼 때마다\n * 다시 기다리게 하지 않는다.\n */\n private static readonly DEFAULT_ENTER_DELAY = 450\n\n private host: ReactiveControllerHost & HTMLElement\n private attributeName: string\n private content?: string | ((element: HTMLElement) => string)\n private enterDelay: number\n private static tooltipContainer: HTMLElement | null = null\n /** 뜨기를 기다리는 중인 타이머. */\n private static pending: ReturnType<typeof setTimeout> | null = null\n /** 지금 툴팁이 떠 있는가. 떠 있으면 다음 요소로 옮길 때 기다리지 않는다. */\n private static shown = false\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n options?: {\n content?: string | ((element: HTMLElement) => string)\n attributeName?: string\n /** 머무른 시간이 이만큼을 넘으면 뜬다(밀리초). 0 이면 바로 뜬다. */\n enterDelay?: number\n }\n ) {\n const { attributeName, content, enterDelay } = options || {}\n\n this.host = host\n this.attributeName = attributeName || 'data-reactive-tooltip'\n this.content = content\n this.enterDelay = enterDelay ?? TooltipReactiveController.DEFAULT_ENTER_DELAY\n\n host.addController(this)\n\n TooltipReactiveController.ensureContainer()\n }\n\n /**\n * 툴팁 상자를 만든다 — 문서에 하나만 둔다.\n *\n * ── 모양 ────────────────────────────────────────────────────────────────\n * 전에는 검정 70% 바탕 · 3px 둥글기 · 물려받은 글자 크기였다. 앱의 토큰을 하나도 쓰지 않아서, 화면이\n * 어떤 색과 크기를 쓰든 툴팁만 브라우저 기본처럼 보였다.\n *\n * 이제 --ui-* 를 본다. 툴팁은 **화면 위에 잠깐 떠 있는 것**이라 화면과 같은 면이 아니고, 그래서\n * 어두운 면에 흰 글자다 — 밝은 면으로 두면 표와 섞여 어디까지가 툴팁인지 안 보인다.\n *\n * 글자는 본문보다 한 단 작다(12px). 툴팁은 값이 아니라 값을 거드는 말이다.\n *\n * 테두리는 두지 않는다 — 어두운 면이 스스로 경계다. 그림자는 떠 있음을 말할 만큼만 옅게 둔다.\n *\n * 값은 모두 --tooltip-* 으로 덮을 수 있다. 어두운 테마는 이 값들을 뒤집어야 한다.\n */\n private static ensureContainer() {\n if (TooltipReactiveController.tooltipContainer) {\n return\n }\n\n const box = document.createElement('div')\n\n box.className = 'ox-tooltip-container'\n box.style.position = 'fixed'\n box.style.pointerEvents = 'none'\n box.style.zIndex = '1000'\n box.style.display = 'none'\n\n /*\n * 낱개 속성으로 적는다. font 축약형은 글꼴 이름을 반드시 요구해서 inherit 을 넣으면 선언이 통째로\n * 버려진다. 글꼴은 적지 않아도 body 에서 물려받는다 — 이 상자가 body 의 자식이다.\n */\n box.style.fontFamily = 'var(--tooltip-font-family, inherit)'\n box.style.fontSize = 'var(--tooltip-font-size, 12px)'\n box.style.lineHeight = 'var(--tooltip-line-height, 1.45)'\n box.style.fontWeight = 'var(--tooltip-font-weight, 400)'\n box.style.color = 'var(--tooltip-color, #ffffff)'\n box.style.backgroundColor = 'var(--tooltip-background-color, rgba(23, 36, 44, 0.94))'\n box.style.padding = 'var(--tooltip-padding, 6px 8px)'\n box.style.borderRadius = 'var(--tooltip-border-radius, 6px)'\n box.style.boxShadow = 'var(--tooltip-box-shadow, 0 4px 12px rgba(23, 36, 44, 0.18))'\n box.style.maxWidth = 'var(--tooltip-max-width, 280px)'\n box.style.wordWrap = 'break-word'\n box.style.whiteSpace = 'pre-wrap'\n box.style.overflow = 'hidden'\n\n TooltipReactiveController.tooltipContainer = box\n document.body.appendChild(box)\n }\n\n hostConnected() {\n const target = this.host.shadowRoot ?? this.host\n target.addEventListener('mouseover', this.handleMouseOver)\n target.addEventListener('mouseout', this.handleMouseOut)\n }\n\n hostDisconnected() {\n const target = this.host.shadowRoot ?? this.host\n target.removeEventListener('mouseover', this.handleMouseOver)\n target.removeEventListener('mouseout', this.handleMouseOut)\n }\n\n private handleMouseOver = (e: Event) => {\n /*\n * 표식이 달린 **조상**을 찾는다 — 포인터가 닿는 것은 대개 그 안의 조각이다.\n *\n * `e.target` 만 보고 있었다. 그런데 툴팁을 붙이고 싶은 줄은 거의 언제나 안에 조각을 가진다 —\n * 공장 달력의 한 줄은 [표시 6px][줄머리][제목] 셋이고, data-grist 의 머리말도 안에 아이콘이\n * 들어간다. 포인터가 그 조각 위에 있으면 `e.target` 이 조각이라 표식이 없고, 그대로 돌아갔다.\n *\n * [확인함] 2026-09-11 공장 달력 한 줄 114px 중 제목 조각이 104px.\n * 줄에 직접 쏘면 뜨고, 제목 조각에 쏘면 안 뜬다.\n * **91% 의 자리에서 안 뜬다** — 사실상 안 뜨는 것이다.\n *\n * `closest` 는 자기 자신부터 본다. 표식이 잎에 달린 경우는 전과 똑같이 동작한다.\n */\n const element = (e.target as HTMLElement).closest?.(`[${this.attributeName}]`) as HTMLElement\n\n if (!element || !hasOverflow(element)) {\n return\n }\n\n TooltipReactiveController.cancelPending()\n\n /* 이미 떠 있으면 기다리지 않는다(위 머리말). */\n if (TooltipReactiveController.shown || this.enterDelay <= 0) {\n this.show(element)\n return\n }\n\n TooltipReactiveController.pending = setTimeout(() => {\n TooltipReactiveController.pending = null\n this.show(element)\n }, this.enterDelay)\n }\n\n private static cancelPending() {\n if (TooltipReactiveController.pending) {\n clearTimeout(TooltipReactiveController.pending)\n TooltipReactiveController.pending = null\n }\n }\n\n private show(element: HTMLElement) {\n {\n const tooltipContent =\n typeof this.content === 'function'\n ? this.content.call(this, element)\n : this.content || element.textContent?.trim() || ''\n\n const rect = element.getBoundingClientRect()\n const box = TooltipReactiveController.tooltipContainer!\n\n /* 내용을 먼저 넣어야 크기를 잴 수 있다. */\n box.textContent = tooltipContent\n box.style.display = 'block'\n box.style.left = '0px'\n box.style.top = '0px'\n\n /*\n * 화면 안으로 밀어 넣는다.\n *\n * 전에는 겨눈 요소의 왼쪽 · 아래에 그냥 놓았다. 그래서 오른쪽 끝 칸이나 마지막 줄에서는 툴팁이\n * 화면 밖으로 나가 **보이지 않았다.** 잘린 글자를 보여 주려고 있는 것인데 그때 안 보인다.\n *\n * 넘치면 반대쪽으로 접는다 — 오른쪽이 모자라면 오른쪽 끝에 맞추고, 아래가 모자라면 요소 위로\n * 올린다.\n */\n const GAP = 4\n const size = box.getBoundingClientRect()\n\n let left = rect.left\n let top = rect.bottom + GAP\n\n if (left + size.width > window.innerWidth - GAP) {\n left = Math.max(GAP, window.innerWidth - size.width - GAP)\n }\n\n if (top + size.height > window.innerHeight - GAP) {\n top = Math.max(GAP, rect.top - size.height - GAP)\n }\n\n box.style.left = `${Math.round(left)}px`\n box.style.top = `${Math.round(top)}px`\n\n TooltipReactiveController.shown = true\n }\n }\n\n private handleMouseOut = (e: Event) => {\n /* 기다리던 것도 취소한다 — 안 그러면 떠난 뒤에 뜬다. */\n TooltipReactiveController.cancelPending()\n\n if (TooltipReactiveController.tooltipContainer) {\n TooltipReactiveController.tooltipContainer.style.display = 'none'\n }\n\n TooltipReactiveController.shown = false\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { expect, fixture, html } from '@open-wc/testing';
2
+ import { hasOverflow } from '../src/has-overflow.js';
3
+ /**
4
+ * A tooltip that only expands cut text has to know when text is cut.
5
+ *
6
+ * scrollWidth answers that for a block. It does not for a flex row: flex lays its items out to
7
+ * fit the box, so the box reports no overflow even while the title inside it is ellipsised.
8
+ * Measured on the plant calendar - 25 event rows, every title visibly cut, every one reported
9
+ * as not overflowing, and the tooltip never appeared on any of them.
10
+ */
11
+ describe('hasOverflow', () => {
12
+ const LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME';
13
+ it('says no when the text fits', async () => {
14
+ const el = (await fixture(html `
15
+ <div style="width: 400px; overflow: hidden; white-space: nowrap">short</div>
16
+ `));
17
+ expect(hasOverflow(el)).to.be.false;
18
+ });
19
+ it('says yes for a block whose text is cut', async () => {
20
+ const el = (await fixture(html `
21
+ <div style="width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">${LONG}</div>
22
+ `));
23
+ expect(hasOverflow(el), 'a block overflows its own box, which scrollWidth reports').to.be.true;
24
+ });
25
+ it('says yes for a flex row whose title span is cut', async () => {
26
+ /*
27
+ * The shape the plant calendar draws: a flex row holding a mark and the title, with the
28
+ * title in its own span that clips. overflow:hidden resolves that span's min-width to 0, so
29
+ * it shrinks to fit and the row around it never reports overflow of its own.
30
+ */
31
+ const el = (await fixture(html `
32
+ <div style="display: flex; gap: 4px; width: 60px; overflow: hidden; white-space: nowrap">
33
+ <span style="flex: none; width: 6px; height: 6px"></span>
34
+ <span style="overflow: hidden; text-overflow: ellipsis">${LONG}</span>
35
+ </div>
36
+ `));
37
+ expect(el.scrollWidth, 'the row reports no overflow of its own — this is why the first check is not enough').to.equal(el.clientWidth);
38
+ expect(hasOverflow(el), 'the title is cut, so a tooltip is worth showing').to.be.true;
39
+ });
40
+ it('says no for a flex row that fits, so tooltips do not fire on everything', async () => {
41
+ const el = (await fixture(html `
42
+ <div style="display: flex; width: 400px; overflow: hidden; white-space: nowrap">
43
+ <span style="flex: none; width: 6px; height: 6px"></span>short
44
+ </div>
45
+ `));
46
+ expect(hasOverflow(el)).to.be.false;
47
+ });
48
+ });
49
+ //# sourceMappingURL=has-overflow.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"has-overflow.test.js","sourceRoot":"","sources":["../../test/has-overflow.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEpD;;;;;;;GAOG;AACH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,MAAM,IAAI,GAAG,sDAAsD,CAAA;IAEnE,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;;KAE7B,CAAC,CAAgB,CAAA;QAElB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAA;IACrC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;iGAC+D,IAAI;KAChG,CAAC,CAAgB,CAAA;QAElB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,0DAA0D,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAA;IAChG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D;;;;WAIG;QACH,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;;;kEAGgC,IAAI;;KAEjE,CAAC,CAAgB,CAAA;QAElB,MAAM,CACJ,EAAE,CAAC,WAAW,EACd,oFAAoF,CACrF,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,CAAA;QAC1B,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,iDAAiD,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAA;IACvF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;;;;KAI7B,CAAC,CAAgB,CAAA;QAElB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAA;IACrC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { expect, fixture, html } from '@open-wc/testing'\n\nimport { hasOverflow } from '../src/has-overflow.js'\n\n/**\n * A tooltip that only expands cut text has to know when text is cut.\n *\n * scrollWidth answers that for a block. It does not for a flex row: flex lays its items out to\n * fit the box, so the box reports no overflow even while the title inside it is ellipsised.\n * Measured on the plant calendar - 25 event rows, every title visibly cut, every one reported\n * as not overflowing, and the tooltip never appeared on any of them.\n */\ndescribe('hasOverflow', () => {\n const LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME'\n\n it('says no when the text fits', async () => {\n const el = (await fixture(html`\n <div style=\"width: 400px; overflow: hidden; white-space: nowrap\">short</div>\n `)) as HTMLElement\n\n expect(hasOverflow(el)).to.be.false\n })\n\n it('says yes for a block whose text is cut', async () => {\n const el = (await fixture(html`\n <div style=\"width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap\">${LONG}</div>\n `)) as HTMLElement\n\n expect(hasOverflow(el), 'a block overflows its own box, which scrollWidth reports').to.be.true\n })\n\n it('says yes for a flex row whose title span is cut', async () => {\n /*\n * The shape the plant calendar draws: a flex row holding a mark and the title, with the\n * title in its own span that clips. overflow:hidden resolves that span's min-width to 0, so\n * it shrinks to fit and the row around it never reports overflow of its own.\n */\n const el = (await fixture(html`\n <div style=\"display: flex; gap: 4px; width: 60px; overflow: hidden; white-space: nowrap\">\n <span style=\"flex: none; width: 6px; height: 6px\"></span>\n <span style=\"overflow: hidden; text-overflow: ellipsis\">${LONG}</span>\n </div>\n `)) as HTMLElement\n\n expect(\n el.scrollWidth,\n 'the row reports no overflow of its own — this is why the first check is not enough'\n ).to.equal(el.clientWidth)\n expect(hasOverflow(el), 'the title is cut, so a tooltip is worth showing').to.be.true\n })\n\n it('says no for a flex row that fits, so tooltips do not fire on everything', async () => {\n const el = (await fixture(html`\n <div style=\"display: flex; width: 400px; overflow: hidden; white-space: nowrap\">\n <span style=\"flex: none; width: 6px; height: 6px\"></span>short\n </div>\n `)) as HTMLElement\n\n expect(hasOverflow(el)).to.be.false\n })\n})\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { __decorate } from "tslib";
2
+ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
+ import { LitElement, html as litHtml } from 'lit';
4
+ import { customElement } from 'lit/decorators.js';
5
+ import { TooltipReactiveController } from '../src/reactive-controllers/tooltip-reactive-controller.js';
6
+ const LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME';
7
+ /**
8
+ * A row of the shape hosts actually draw: a marker, a prefix and the title, each in its own
9
+ * span. The pointer lands on one of those children, never on the row itself - on the plant
10
+ * calendar the title span alone covers 104 of the row's 114px.
11
+ */
12
+ let TooltipHostFixture = class TooltipHostFixture extends LitElement {
13
+ constructor() {
14
+ super(...arguments);
15
+ /*
16
+ * The content function the plant calendar passes: the host writes the sentence it wants read
17
+ * into the attribute, which is more than the row's own cut text.
18
+ */
19
+ this.tooltip = new TooltipReactiveController(this, {
20
+ enterDelay: 0,
21
+ content: element => { var _a; return element.getAttribute('data-reactive-tooltip') || ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || ''; }
22
+ });
23
+ }
24
+ render() {
25
+ return litHtml `
26
+ <div
27
+ data-reactive-tooltip="the whole sentence"
28
+ style="display:flex; gap:4px; width:60px; overflow:hidden; white-space:nowrap"
29
+ >
30
+ <span mark style="flex:none; width:6px; height:6px"></span>
31
+ <span title-part style="overflow:hidden; text-overflow:ellipsis">${LONG}</span>
32
+ </div>
33
+ `;
34
+ }
35
+ };
36
+ TooltipHostFixture = __decorate([
37
+ customElement('tooltip-host-fixture')
38
+ ], TooltipHostFixture);
39
+ function box() {
40
+ return document.querySelector('.ox-tooltip-container');
41
+ }
42
+ function hover(el) {
43
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, composed: true }));
44
+ }
45
+ describe('TooltipReactiveController', () => {
46
+ let host;
47
+ beforeEach(async () => {
48
+ host = (await fixture(html `<tooltip-host-fixture></tooltip-host-fixture>`));
49
+ box().style.display = 'none';
50
+ });
51
+ it('shows when the pointer is over a child of the marked row', async () => {
52
+ /*
53
+ * The regression this file exists for. The controller read the attribute off e.target, and
54
+ * the pointer is over a child for all but a few pixels of the row - so the tooltip never
55
+ * appeared on the plant calendar, on any row.
56
+ */
57
+ hover(host.shadowRoot.querySelector('[title-part]'));
58
+ await waitUntil(() => box().style.display === 'block');
59
+ expect(box().textContent).to.equal('the whole sentence');
60
+ });
61
+ it('shows when the pointer is over the marked row itself', async () => {
62
+ hover(host.shadowRoot.querySelector('[data-reactive-tooltip]'));
63
+ await waitUntil(() => box().style.display === 'block');
64
+ });
65
+ it('reads the attribute of the marked ancestor, not the child under the pointer', async () => {
66
+ hover(host.shadowRoot.querySelector('[mark]'));
67
+ await waitUntil(() => box().style.display === 'block');
68
+ expect(box().textContent, 'the marker span holds no text of its own').to.equal('the whole sentence');
69
+ });
70
+ it('stays hidden when nothing in the ancestry is marked', async () => {
71
+ const plain = (await fixture(html `<span>${LONG}</span>`));
72
+ host.shadowRoot.appendChild(plain);
73
+ hover(plain);
74
+ await new Promise(resolve => setTimeout(resolve, 50));
75
+ expect(box().style.display).to.equal('none');
76
+ });
77
+ });
78
+ //# sourceMappingURL=tooltip-reactive-controller.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tooltip-reactive-controller.test.js","sourceRoot":"","sources":["../../test/tooltip-reactive-controller.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture, html, waitUntil } from '@open-wc/testing'\nimport { LitElement, html as litHtml } from 'lit'\nimport { customElement } from 'lit/decorators.js'\n\nimport { TooltipReactiveController } from '../src/reactive-controllers/tooltip-reactive-controller.js'\n\nconst LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME'\n\n/**\n * A row of the shape hosts actually draw: a marker, a prefix and the title, each in its own\n * span. The pointer lands on one of those children, never on the row itself - on the plant\n * calendar the title span alone covers 104 of the row's 114px.\n */\n@customElement('tooltip-host-fixture')\nclass TooltipHostFixture extends LitElement {\n /*\n * The content function the plant calendar passes: the host writes the sentence it wants read\n * into the attribute, which is more than the row's own cut text.\n */\n tooltip = new TooltipReactiveController(this, {\n enterDelay: 0,\n content: element => element.getAttribute('data-reactive-tooltip') || element.textContent?.trim() || ''\n })\n\n render() {\n return litHtml`\n <div\n data-reactive-tooltip=\"the whole sentence\"\n style=\"display:flex; gap:4px; width:60px; overflow:hidden; white-space:nowrap\"\n >\n <span mark style=\"flex:none; width:6px; height:6px\"></span>\n <span title-part style=\"overflow:hidden; text-overflow:ellipsis\">${LONG}</span>\n </div>\n `\n }\n}\n\nfunction box(): HTMLElement {\n return document.querySelector('.ox-tooltip-container') as HTMLElement\n}\n\nfunction hover(el: Element) {\n el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, composed: true }))\n}\n\ndescribe('TooltipReactiveController', () => {\n let host: TooltipHostFixture\n\n beforeEach(async () => {\n host = (await fixture(html`<tooltip-host-fixture></tooltip-host-fixture>`)) as TooltipHostFixture\n box().style.display = 'none'\n })\n\n it('shows when the pointer is over a child of the marked row', async () => {\n /*\n * The regression this file exists for. The controller read the attribute off e.target, and\n * the pointer is over a child for all but a few pixels of the row - so the tooltip never\n * appeared on the plant calendar, on any row.\n */\n hover(host.shadowRoot!.querySelector('[title-part]')!)\n\n await waitUntil(() => box().style.display === 'block')\n expect(box().textContent).to.equal('the whole sentence')\n })\n\n it('shows when the pointer is over the marked row itself', async () => {\n hover(host.shadowRoot!.querySelector('[data-reactive-tooltip]')!)\n\n await waitUntil(() => box().style.display === 'block')\n })\n\n it('reads the attribute of the marked ancestor, not the child under the pointer', async () => {\n hover(host.shadowRoot!.querySelector('[mark]')!)\n\n await waitUntil(() => box().style.display === 'block')\n expect(box().textContent, 'the marker span holds no text of its own').to.equal('the whole sentence')\n })\n\n it('stays hidden when nothing in the ancestry is marked', async () => {\n const plain = (await fixture(html`<span>${LONG}</span>`)) as HTMLElement\n host.shadowRoot!.appendChild(plain)\n\n hover(plain)\n\n await new Promise(resolve => setTimeout(resolve, 50))\n expect(box().style.display).to.equal('none')\n })\n})\n"]}