@harborclient/sdk 1.1.11 → 1.1.12
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/components/VariableInput/index.d.ts.map +1 -1
- package/dist/components/VariableInput/index.js +238 -24
- package/dist/components/VariableTooltip/VariableTooltipValue.d.ts +6 -2
- package/dist/components/VariableTooltip/VariableTooltipValue.d.ts.map +1 -1
- package/dist/components/VariableTooltip/VariableTooltipValue.js +6 -4
- package/dist/components/useDialogFocus.d.ts.map +1 -1
- package/dist/components/useDialogFocus.js +22 -8
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/VariableInput/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,aAAa,EAAc,MAAM,OAAO,CAAC;AACtF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/VariableInput/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,aAAa,EAAc,MAAM,OAAO,CAAC;AACtF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAwBnE,MAAM,WAAW,KAAM,SAAQ,IAAI,CACjC,wBAAwB,CAAC,KAAK,CAAC,EAC/B,UAAU,GAAG,WAAW,GAAG,IAAI,GAAG,YAAY,GAAG,iBAAiB,GAAG,WAAW,GAAG,UAAU,CAC9F;IACC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;OAEG;IACH,SAAS,EAAE,QAAQ,EAAE,CAAC;IAEtB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAEzD;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAEvC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,SAAS,EACT,WAAW,EACX,SAAS,EACT,SAAc,EACd,gBAAgB,EAChB,cAAc,EACd,EAAE,EACF,YAAY,EAAE,SAAS,EACvB,iBAAiB,EAAE,cAAc,EACjC,MAAM,EACN,GAAG,KAAK,EACT,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CA4lBrB"}
|
|
@@ -6,6 +6,7 @@ import { useAutocomplete } from '../Autocomplete/useAutocomplete.js';
|
|
|
6
6
|
import { Button } from '../Button/index.js';
|
|
7
7
|
import { VariableTooltipValue } from '../VariableTooltip/index.js';
|
|
8
8
|
import { Input } from '../forms/index.js';
|
|
9
|
+
import { getFocusableElements } from '../useDialogFocus.js';
|
|
9
10
|
import { cn } from '../utils.js';
|
|
10
11
|
/** Delay after the pointer stops moving before a hover tooltip is shown. */
|
|
11
12
|
const TOOLTIP_SHOW_DELAY_MS = 500;
|
|
@@ -19,12 +20,21 @@ const TOOLTIP_HIDE_DELAY_MS = 400;
|
|
|
19
20
|
*/
|
|
20
21
|
export function VariableInput({ value, onChange, variables, placeholder, onKeyDown, className = '', wrapperClassName, onEditVariable, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, source, ...props }) {
|
|
21
22
|
const safeValue = value ?? '';
|
|
23
|
+
const wrapperRef = useRef(null);
|
|
22
24
|
const inputRef = useRef(null);
|
|
23
25
|
const backdropRef = useRef(null);
|
|
24
|
-
const
|
|
26
|
+
const tokenButtonRefs = useRef(new Map());
|
|
27
|
+
const tooltipRef = useRef(null);
|
|
25
28
|
const hideTimer = useRef(null);
|
|
26
29
|
const showTimer = useRef(null);
|
|
30
|
+
const suppressReopen = useRef(false);
|
|
31
|
+
const pendingEnterFocus = useRef(false);
|
|
32
|
+
const tooltipEntered = useRef(false);
|
|
33
|
+
const tooltipSourceRef = useRef(null);
|
|
34
|
+
const activeTokenIndexRef = useRef(null);
|
|
27
35
|
const [tooltip, setTooltip] = useState(null);
|
|
36
|
+
const [tooltipSource, setTooltipSource] = useState(null);
|
|
37
|
+
const [activeTokenIndex, setActiveTokenIndex] = useState(null);
|
|
28
38
|
const tooltipId = useId();
|
|
29
39
|
const { open: autocompleteOpen, items: autocompleteItems, activeIndex: autocompleteActiveIndex, listboxId, onFocus: openAutocomplete, onBlur: closeAutocomplete, onInputKeyDown, selectItem, setActiveIndex, closeSuggestions } = useAutocomplete({
|
|
30
40
|
source,
|
|
@@ -36,6 +46,13 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
36
46
|
* Splits the input value into plain text and {{variable}} token spans for highlighting.
|
|
37
47
|
*/
|
|
38
48
|
const tokens = useMemo(() => tokenizeVariables(safeValue), [safeValue]);
|
|
49
|
+
/**
|
|
50
|
+
* Keeps tooltip refs aligned with React state for timer and document handlers.
|
|
51
|
+
*/
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
tooltipSourceRef.current = tooltipSource;
|
|
54
|
+
activeTokenIndexRef.current = activeTokenIndex;
|
|
55
|
+
}, [tooltipSource, activeTokenIndex]);
|
|
39
56
|
/**
|
|
40
57
|
* Clears any pending tooltip hide timer.
|
|
41
58
|
*/
|
|
@@ -45,12 +62,55 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
45
62
|
hideTimer.current = null;
|
|
46
63
|
}
|
|
47
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Clears tooltip state regardless of source.
|
|
67
|
+
*/
|
|
68
|
+
const dismissTooltip = () => {
|
|
69
|
+
pendingEnterFocus.current = false;
|
|
70
|
+
tooltipEntered.current = false;
|
|
71
|
+
setTooltip(null);
|
|
72
|
+
setTooltipSource(null);
|
|
73
|
+
setActiveTokenIndex(null);
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Clears tooltip state and optionally refocuses the triggering token.
|
|
77
|
+
*
|
|
78
|
+
* @param refocusToken - When true, moves focus back to the active token without reopening.
|
|
79
|
+
*/
|
|
80
|
+
const dismissFocusTooltip = (refocusToken = false) => {
|
|
81
|
+
const index = activeTokenIndexRef.current;
|
|
82
|
+
dismissTooltip();
|
|
83
|
+
if (refocusToken && index != null) {
|
|
84
|
+
suppressReopen.current = true;
|
|
85
|
+
tokenButtonRefs.current.get(index)?.focus();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Moves focus to the first control inside the open focus tooltip.
|
|
90
|
+
*/
|
|
91
|
+
const focusFirstTooltipControl = () => {
|
|
92
|
+
const tooltipEl = tooltipRef.current;
|
|
93
|
+
if (!tooltipEl) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
getFocusableElements(tooltipEl)[0]?.focus();
|
|
97
|
+
tooltipEntered.current = true;
|
|
98
|
+
};
|
|
48
99
|
/**
|
|
49
100
|
* Hides the tooltip after a short grace period so the pointer can reach it.
|
|
50
101
|
*/
|
|
51
102
|
const scheduleHide = () => {
|
|
103
|
+
if (tooltipSourceRef.current === 'focus') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
52
106
|
cancelHide();
|
|
53
|
-
hideTimer.current = window.setTimeout(() =>
|
|
107
|
+
hideTimer.current = window.setTimeout(() => {
|
|
108
|
+
hideTimer.current = null;
|
|
109
|
+
if (tooltipSourceRef.current === 'focus') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
dismissTooltip();
|
|
113
|
+
}, TOOLTIP_HIDE_DELAY_MS);
|
|
54
114
|
};
|
|
55
115
|
/**
|
|
56
116
|
* Clears any pending tooltip show timer.
|
|
@@ -64,7 +124,7 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
64
124
|
/**
|
|
65
125
|
* Shows a tooltip for a variable token at the given screen position.
|
|
66
126
|
*/
|
|
67
|
-
const showTooltipForKey = (key, top, left) => {
|
|
127
|
+
const showTooltipForKey = (key, top, left, source, tokenIndex = null) => {
|
|
68
128
|
setTooltip({
|
|
69
129
|
key,
|
|
70
130
|
value: resolveVariable(key, variables),
|
|
@@ -72,6 +132,8 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
72
132
|
top,
|
|
73
133
|
left
|
|
74
134
|
});
|
|
135
|
+
setTooltipSource(source);
|
|
136
|
+
setActiveTokenIndex(tokenIndex);
|
|
75
137
|
};
|
|
76
138
|
/**
|
|
77
139
|
* Shows the tooltip once the pointer has stopped moving for {@link TOOLTIP_SHOW_DELAY_MS}.
|
|
@@ -84,7 +146,7 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
84
146
|
cancelShow();
|
|
85
147
|
showTimer.current = window.setTimeout(() => {
|
|
86
148
|
showTimer.current = null;
|
|
87
|
-
showTooltipForKey(key, top, left);
|
|
149
|
+
showTooltipForKey(key, top, left, 'hover', null);
|
|
88
150
|
}, TOOLTIP_SHOW_DELAY_MS);
|
|
89
151
|
};
|
|
90
152
|
/**
|
|
@@ -94,6 +156,65 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
94
156
|
cancelHide();
|
|
95
157
|
cancelShow();
|
|
96
158
|
}, []);
|
|
159
|
+
/**
|
|
160
|
+
* Traps Tab within the tooltip controls once focus has entered the tooltip.
|
|
161
|
+
*
|
|
162
|
+
* While focus is still on the token (tooltip open but not entered), Tab is left
|
|
163
|
+
* alone so it moves naturally between variable tokens; only pressing Enter moves
|
|
164
|
+
* focus into the tooltip and engages this trap.
|
|
165
|
+
*/
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (tooltipSource !== 'focus' || activeTokenIndex == null || !tooltip) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Cycles Tab and Shift+Tab within the tooltip controls while focus is inside it.
|
|
172
|
+
*
|
|
173
|
+
* @param event - Document keydown event.
|
|
174
|
+
*/
|
|
175
|
+
const handleKeyDown = (event) => {
|
|
176
|
+
if (event.key === 'Escape') {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
dismissFocusTooltip(true);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (event.key !== 'Tab' || !tooltipEntered.current) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const tooltipEl = tooltipRef.current;
|
|
185
|
+
if (!tooltipEl) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const controls = getFocusableElements(tooltipEl);
|
|
189
|
+
if (controls.length === 0) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
const active = document.activeElement;
|
|
195
|
+
const index = active ? controls.indexOf(active) : -1;
|
|
196
|
+
if (index === -1) {
|
|
197
|
+
controls[0].focus();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const nextIndex = event.shiftKey
|
|
201
|
+
? (index - 1 + controls.length) % controls.length
|
|
202
|
+
: (index + 1) % controls.length;
|
|
203
|
+
controls[nextIndex]?.focus();
|
|
204
|
+
};
|
|
205
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
206
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
207
|
+
}, [tooltipSource, activeTokenIndex, tooltip]);
|
|
208
|
+
/**
|
|
209
|
+
* Moves focus into the tooltip after Enter opens it.
|
|
210
|
+
*/
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!pendingEnterFocus.current || tooltipSource !== 'focus' || !tooltip) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
pendingEnterFocus.current = false;
|
|
216
|
+
focusFirstTooltipControl();
|
|
217
|
+
}, [tooltip, tooltipSource]);
|
|
97
218
|
/**
|
|
98
219
|
* Keeps the colored backdrop aligned with horizontal scroll in the input.
|
|
99
220
|
*/
|
|
@@ -108,13 +229,22 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
108
229
|
* Updates the tooltip based on the current text caret position.
|
|
109
230
|
*/
|
|
110
231
|
const updateTooltipFromCaret = () => {
|
|
232
|
+
if (tooltipSourceRef.current === 'focus') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
111
235
|
const input = inputRef.current;
|
|
112
236
|
if (!input)
|
|
113
237
|
return;
|
|
114
|
-
const
|
|
238
|
+
const selectionStart = input.selectionStart ?? 0;
|
|
239
|
+
const selectionEnd = input.selectionEnd ?? 0;
|
|
240
|
+
if (selectionStart !== selectionEnd) {
|
|
241
|
+
dismissTooltip();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const offset = selectionStart;
|
|
115
245
|
const match = getVariableTokenAtOffset(safeValue, offset);
|
|
116
246
|
if (!match) {
|
|
117
|
-
|
|
247
|
+
dismissTooltip();
|
|
118
248
|
return;
|
|
119
249
|
}
|
|
120
250
|
let tokenIndex = -1;
|
|
@@ -126,27 +256,77 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
126
256
|
}
|
|
127
257
|
position += token.text.length;
|
|
128
258
|
}
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
const rect =
|
|
132
|
-
showTooltipForKey(match.key, rect.top, rect.left + rect.width / 2);
|
|
259
|
+
const tokenButton = tokenIndex >= 0 ? tokenButtonRefs.current.get(tokenIndex) : undefined;
|
|
260
|
+
if (tokenButton) {
|
|
261
|
+
const rect = tokenButton.getBoundingClientRect();
|
|
262
|
+
showTooltipForKey(match.key, rect.top, rect.left + rect.width / 2, 'hover', null);
|
|
133
263
|
return;
|
|
134
264
|
}
|
|
135
265
|
const rect = input.getBoundingClientRect();
|
|
136
|
-
showTooltipForKey(match.key, rect.top, rect.left + rect.width / 2);
|
|
266
|
+
showTooltipForKey(match.key, rect.top, rect.left + rect.width / 2, 'hover', null);
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Opens a focus-trapped tooltip for a keyboard-focused variable token.
|
|
270
|
+
*
|
|
271
|
+
* @param index - Token index within the rendered token list.
|
|
272
|
+
* @param key - Variable name from the {{key}} token.
|
|
273
|
+
* @param button - Focused token button element.
|
|
274
|
+
*/
|
|
275
|
+
const handleTokenFocus = (index, key, button) => {
|
|
276
|
+
if (suppressReopen.current) {
|
|
277
|
+
suppressReopen.current = false;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
cancelHide();
|
|
281
|
+
cancelShow();
|
|
282
|
+
tooltipEntered.current = false;
|
|
283
|
+
const rect = button.getBoundingClientRect();
|
|
284
|
+
showTooltipForKey(key, rect.top, rect.left + rect.width / 2, 'focus', index);
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Handles keyboard interaction on a variable token button.
|
|
288
|
+
*/
|
|
289
|
+
const handleTokenKeyDown = (event, index, key) => {
|
|
290
|
+
if (event.key === 'Escape' && tooltipSource === 'focus' && activeTokenIndex != null) {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
event.stopPropagation();
|
|
293
|
+
dismissFocusTooltip(false);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (event.key !== 'Enter') {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
if (tooltipSource === 'focus' && activeTokenIndex === index && tooltipRef.current) {
|
|
301
|
+
focusFirstTooltipControl();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
pendingEnterFocus.current = true;
|
|
305
|
+
const button = tokenButtonRefs.current.get(index);
|
|
306
|
+
if (!button) {
|
|
307
|
+
pendingEnterFocus.current = false;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
cancelHide();
|
|
311
|
+
cancelShow();
|
|
312
|
+
const rect = button.getBoundingClientRect();
|
|
313
|
+
showTooltipForKey(key, rect.top, rect.left + rect.width / 2, 'focus', index);
|
|
137
314
|
};
|
|
138
315
|
/**
|
|
139
316
|
* Shows a tooltip when the pointer rests over a variable token span.
|
|
140
317
|
*/
|
|
141
318
|
const handleMouseMove = (e) => {
|
|
319
|
+
if (tooltipSourceRef.current === 'focus') {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
142
322
|
cancelHide();
|
|
143
323
|
for (const [index, token] of tokens.entries()) {
|
|
144
324
|
if (!token.key)
|
|
145
325
|
continue;
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
326
|
+
const tokenButton = tokenButtonRefs.current.get(index);
|
|
327
|
+
if (!tokenButton)
|
|
148
328
|
continue;
|
|
149
|
-
const rect =
|
|
329
|
+
const rect = tokenButton.getBoundingClientRect();
|
|
150
330
|
if (e.clientX >= rect.left &&
|
|
151
331
|
e.clientX <= rect.right &&
|
|
152
332
|
e.clientY >= rect.top &&
|
|
@@ -162,9 +342,28 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
162
342
|
* Cancels a pending show and hides the tooltip when the pointer leaves the input.
|
|
163
343
|
*/
|
|
164
344
|
const handleMouseLeave = () => {
|
|
345
|
+
if (tooltipSourceRef.current === 'focus') {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
165
348
|
cancelShow();
|
|
166
349
|
scheduleHide();
|
|
167
350
|
};
|
|
351
|
+
/**
|
|
352
|
+
* Closes any open tooltip when focus leaves the field.
|
|
353
|
+
*/
|
|
354
|
+
const handleWrapperBlur = () => {
|
|
355
|
+
const wrapper = wrapperRef.current;
|
|
356
|
+
if (!wrapper) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
queueMicrotask(() => {
|
|
360
|
+
if (!wrapper.contains(document.activeElement)) {
|
|
361
|
+
cancelHide();
|
|
362
|
+
cancelShow();
|
|
363
|
+
dismissTooltip();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
};
|
|
168
367
|
/**
|
|
169
368
|
* Handles keyboard events on the input, including tooltip dismissal.
|
|
170
369
|
*/
|
|
@@ -172,9 +371,9 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
172
371
|
if (onInputKeyDown(event)) {
|
|
173
372
|
return;
|
|
174
373
|
}
|
|
175
|
-
if (event.key === 'Escape' && tooltip) {
|
|
374
|
+
if (event.key === 'Escape' && tooltip && tooltipSource !== 'focus') {
|
|
176
375
|
event.preventDefault();
|
|
177
|
-
|
|
376
|
+
dismissTooltip();
|
|
178
377
|
return;
|
|
179
378
|
}
|
|
180
379
|
onKeyDown?.(event);
|
|
@@ -193,21 +392,36 @@ export function VariableInput({ value, onChange, variables, placeholder, onKeyDo
|
|
|
193
392
|
}
|
|
194
393
|
};
|
|
195
394
|
const tooltipContent = tooltip ? getVariableTooltipContent(tooltip.key, variables) : null;
|
|
196
|
-
|
|
395
|
+
const focusTooltipOpen = tooltipSource === 'focus' && tooltip != null;
|
|
396
|
+
return (_jsxs("div", { ...props, ref: wrapperRef, className: cn('hc-variable-input relative min-w-0', wrapperClassName ?? 'flex-1'), onBlurCapture: handleWrapperBlur, children: [_jsx("div", { ref: backdropRef, className: "hc-variable-input-backdrop pointer-events-none absolute inset-0 overflow-hidden px-2.5 py-1.5 whitespace-nowrap text-inherit", children: safeValue ? (tokens.map((token, index) => token.key ? (_jsx("button", { type: "button", ref: (el) => {
|
|
197
397
|
if (el)
|
|
198
|
-
|
|
398
|
+
tokenButtonRefs.current.set(index, el);
|
|
199
399
|
else
|
|
200
|
-
|
|
201
|
-
}, className:
|
|
400
|
+
tokenButtonRefs.current.delete(index);
|
|
401
|
+
}, tabIndex: 0, "aria-label": `Variable ${token.key}`, "aria-expanded": focusTooltipOpen && activeTokenIndex === index ? true : undefined, "aria-describedby": focusTooltipOpen && activeTokenIndex === index ? tooltipId : undefined, className: cn('hc-variable-input-token hc-variable-input-token-variable font-inherit inline border-none bg-transparent p-0 text-[#32D2E2] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-accent'), onFocus: (event) => handleTokenFocus(index, token.key, event.currentTarget), onKeyDown: (event) => handleTokenKeyDown(event, index, token.key), children: token.text }, `${index}-${token.text}`)) : (_jsx("span", { "aria-hidden": true, className: "hc-variable-input-token", children: token.text }, `${index}-${token.text}`)))) : (_jsx("span", { "aria-hidden": true, className: "hc-variable-input-placeholder text-muted", children: placeholder })) }), _jsx(Input, { ref: inputRef, id: id, variant: "plain", role: source ? 'combobox' : undefined, "aria-autocomplete": source ? 'list' : undefined, "aria-expanded": source ? autocompleteOpen : undefined, "aria-controls": source && autocompleteOpen ? listboxId : undefined, "aria-activedescendant": source && autocompleteOpen && autocompleteActiveIndex >= 0
|
|
202
402
|
? `${listboxId}-option-${autocompleteActiveIndex}`
|
|
203
|
-
: undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": tooltip ? tooltipId : undefined, className: cn('hc-variable-input-field relative w-full min-w-0 border-none bg-transparent px-2.5 py-1.5 text-transparent caret-text focus-visible:shadow-none', className), type: "text", placeholder: placeholder, value: safeValue, onChange: (e) => {
|
|
403
|
+
: undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": tooltip && tooltipSource !== 'focus' ? tooltipId : undefined, className: cn('hc-variable-input-field relative w-full min-w-0 border-none bg-transparent px-2.5 py-1.5 text-transparent caret-text focus-visible:shadow-none', className), type: "text", placeholder: placeholder, value: safeValue, onChange: (e) => {
|
|
204
404
|
onChange(e.target.value);
|
|
205
405
|
queueMicrotask(updateTooltipFromCaret);
|
|
206
406
|
}, onFocus: () => {
|
|
407
|
+
if (tooltipSourceRef.current === 'focus') {
|
|
408
|
+
dismissTooltip();
|
|
409
|
+
}
|
|
207
410
|
openAutocomplete();
|
|
208
|
-
|
|
209
|
-
|
|
411
|
+
}, onBlur: closeAutocomplete, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onSelect: updateTooltipFromCaret, onClick: updateTooltipFromCaret, onScroll: syncScroll, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave }), source && (_jsx(SuggestionList, { open: autocompleteOpen, items: autocompleteItems, activeIndex: autocompleteActiveIndex, anchorRef: inputRef, listboxId: listboxId, onSelect: selectItem, onActiveIndexChange: setActiveIndex, onClose: closeSuggestions })), tooltip && tooltipContent && (_jsxs("div", { ref: tooltipRef, id: tooltipId, role: "tooltip", className: "hc-variable-input-tooltip pointer-events-auto fixed z-50 flex max-w-sm -translate-x-1/2 -translate-y-full flex-col gap-2 rounded-lg border border-separator bg-surface px-4 py-3 text-text shadow-md after:pointer-events-auto after:absolute after:right-0 after:-bottom-2 after:left-0 after:h-2 after:content-['']", style: { position: 'fixed', top: tooltip.top - 4, left: tooltip.left }, onMouseEnter: cancelHide, onMouseLeave: scheduleHide, children: [_jsx(VariableTooltipValue, { value: tooltipContent.text, variableKey: tooltip.key, muted: tooltipContent.muted, onClose: () => {
|
|
412
|
+
if (tooltipSource === 'focus') {
|
|
413
|
+
dismissFocusTooltip(true);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
dismissTooltip();
|
|
417
|
+
}
|
|
418
|
+
} }), onEditVariable && (_jsx(Button, { variant: "secondary", className: "hc-variable-input-tooltip-edit self-start", "aria-label": `Edit value for ${tooltip.key}`, onClick: () => {
|
|
210
419
|
onEditVariable(tooltip.key);
|
|
211
|
-
|
|
420
|
+
if (tooltipSource === 'focus') {
|
|
421
|
+
dismissFocusTooltip(true);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
dismissTooltip();
|
|
425
|
+
}
|
|
212
426
|
}, children: "Edit value" }))] }))] }));
|
|
213
427
|
}
|
|
@@ -12,10 +12,14 @@ interface Props {
|
|
|
12
12
|
* When true, styles the value as muted placeholder text.
|
|
13
13
|
*/
|
|
14
14
|
muted?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* When provided, renders a close control to the right of the copy button.
|
|
17
|
+
*/
|
|
18
|
+
onClose?: () => void;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
|
-
* Readonly resolved-value field with
|
|
21
|
+
* Readonly resolved-value field with copy and optional close controls for variable tooltips.
|
|
18
22
|
*/
|
|
19
|
-
export declare function VariableTooltipValue({ value, variableKey, muted }: Props): JSX.Element;
|
|
23
|
+
export declare function VariableTooltipValue({ value, variableKey, muted, onClose }: Props): JSX.Element;
|
|
20
24
|
export {};
|
|
21
25
|
//# sourceMappingURL=VariableTooltipValue.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VariableTooltipValue.d.ts","sourceRoot":"","sources":["../../../src/components/VariableTooltip/VariableTooltipValue.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAMjC,UAAU,KAAK;IACb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VariableTooltipValue.d.ts","sourceRoot":"","sources":["../../../src/components/VariableTooltip/VariableTooltipValue.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAMjC,UAAU,KAAK;IACb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CAgD/F"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@harborclient/sdk/jsx-runtime";
|
|
2
|
-
import { faCircleCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { faCircleCheck, faCopy, faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
3
3
|
import { useState } from '@harborclient/sdk/react';
|
|
4
4
|
import { Button } from '../Button/index.js';
|
|
5
5
|
import { FaIcon } from '../FaIcon/index.js';
|
|
6
6
|
import { Input } from '../forms/index.js';
|
|
7
7
|
import { cn } from '../utils.js';
|
|
8
8
|
/**
|
|
9
|
-
* Readonly resolved-value field with
|
|
9
|
+
* Readonly resolved-value field with copy and optional close controls for variable tooltips.
|
|
10
10
|
*/
|
|
11
|
-
export function VariableTooltipValue({ value, variableKey, muted }) {
|
|
11
|
+
export function VariableTooltipValue({ value, variableKey, muted, onClose }) {
|
|
12
12
|
const [copied, setCopied] = useState(false);
|
|
13
13
|
/**
|
|
14
14
|
* Copies the resolved value to the clipboard and briefly confirms success in the icon.
|
|
@@ -21,5 +21,7 @@ export function VariableTooltipValue({ value, variableKey, muted }) {
|
|
|
21
21
|
};
|
|
22
22
|
return (_jsxs("div", { className: "hc-variable-tooltip-value-row flex items-center gap-1.5", children: [_jsx(Input, { readOnly: true, value: value, "aria-label": `Resolved value for ${variableKey}`, className: cn('min-w-0 flex-1', muted && 'text-muted') }), _jsx(Button, { type: "button", variant: "icon", "aria-label": copied ? `Copied value for ${variableKey}` : `Copy value for ${variableKey}`, onMouseDown: (event) => {
|
|
23
23
|
event.preventDefault();
|
|
24
|
-
}, onClick: handleCopy, children: _jsx(FaIcon, { icon: copied ? faCircleCheck : faCopy, className: "h-4 w-4" }) })
|
|
24
|
+
}, onClick: handleCopy, children: _jsx(FaIcon, { icon: copied ? faCircleCheck : faCopy, className: "h-4 w-4" }) }), onClose && (_jsx(Button, { type: "button", variant: "icon", className: "hc-variable-tooltip-close", "aria-label": `Close tooltip for ${variableKey}`, onMouseDown: (event) => {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
}, onClick: onClose, children: _jsx(FaIcon, { icon: faXmark, className: "h-4 w-4" }) }))] }));
|
|
25
27
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDialogFocus.d.ts","sourceRoot":"","sources":["../../src/components/useDialogFocus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useDialogFocus.d.ts","sourceRoot":"","sources":["../../src/components/useDialogFocus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuFvC;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,WAAW,GAAG,WAAW,EAAE,CAI1E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CA8D5E"}
|
|
@@ -53,6 +53,27 @@ function unlockOverlaySiblings(siblings) {
|
|
|
53
53
|
setInertLocked(sibling, false);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns whether a focusable element is visible enough to receive keyboard focus.
|
|
58
|
+
*
|
|
59
|
+
* @param element - Candidate focusable element.
|
|
60
|
+
*/
|
|
61
|
+
function isFocusableVisible(element) {
|
|
62
|
+
if (element.closest('[aria-hidden="true"]')) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (element.offsetParent !== null || getComputedStyle(element).position === 'fixed') {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
let parent = element.parentElement;
|
|
69
|
+
while (parent) {
|
|
70
|
+
if (getComputedStyle(parent).position === 'fixed') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
parent = parent.parentElement;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
56
77
|
/**
|
|
57
78
|
* Returns visible, enabled focusable descendants of a container, excluding
|
|
58
79
|
* elements inside `aria-hidden` subtrees.
|
|
@@ -62,14 +83,7 @@ function unlockOverlaySiblings(siblings) {
|
|
|
62
83
|
*/
|
|
63
84
|
export function getFocusableElements(container) {
|
|
64
85
|
const candidates = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
65
|
-
return candidates.filter(
|
|
66
|
-
if (element.closest('[aria-hidden="true"]'))
|
|
67
|
-
return false;
|
|
68
|
-
if (element.offsetParent === null && getComputedStyle(element).position !== 'fixed') {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
});
|
|
86
|
+
return candidates.filter(isFocusableVisible);
|
|
73
87
|
}
|
|
74
88
|
/**
|
|
75
89
|
* Traps keyboard focus within a dialog panel, inerts background siblings,
|