@clayui/tooltip 3.75.2 → 3.78.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/lib/TooltipProvider.js +124 -229
- package/lib/useAlign.d.ts +18 -0
- package/lib/useAlign.js +121 -0
- package/lib/useClosestTitle.d.ts +23 -0
- package/lib/useClosestTitle.js +138 -0
- package/lib/useTooltipState.d.ts +13 -0
- package/lib/useTooltipState.js +51 -0
- package/package.json +3 -3
- package/src/TooltipProvider.tsx +128 -307
- package/src/__tests__/index.tsx +28 -0
- package/src/useAlign.ts +137 -0
- package/src/useClosestTitle.ts +170 -0
- package/src/useTooltipState.ts +43 -0
package/src/TooltipProvider.tsx
CHANGED
|
@@ -8,83 +8,28 @@ import {
|
|
|
8
8
|
IPortalBaseProps,
|
|
9
9
|
Keys,
|
|
10
10
|
delegate,
|
|
11
|
-
|
|
12
|
-
useMousePosition,
|
|
11
|
+
useInteractionFocus,
|
|
13
12
|
} from '@clayui/shared';
|
|
14
|
-
import {alignPoint} from 'dom-align';
|
|
15
13
|
import React, {useCallback, useEffect, useReducer, useRef} from 'react';
|
|
16
14
|
import warning from 'warning';
|
|
17
15
|
|
|
18
16
|
import ClayTooltip from './Tooltip';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'top-right',
|
|
23
|
-
'right',
|
|
24
|
-
'bottom-right',
|
|
25
|
-
'bottom',
|
|
26
|
-
'bottom-left',
|
|
27
|
-
'left',
|
|
28
|
-
'top-left',
|
|
29
|
-
] as const;
|
|
30
|
-
|
|
31
|
-
const ALIGNMENTS_MAP = {
|
|
32
|
-
bottom: ['tc', 'bc'],
|
|
33
|
-
'bottom-left': ['tl', 'bl'],
|
|
34
|
-
'bottom-right': ['tr', 'br'],
|
|
35
|
-
left: ['cr', 'cl'],
|
|
36
|
-
right: ['cl', 'cr'],
|
|
37
|
-
top: ['bc', 'tc'],
|
|
38
|
-
'top-left': ['bl', 'tl'],
|
|
39
|
-
'top-right': ['br', 'tr'],
|
|
40
|
-
} as const;
|
|
41
|
-
|
|
42
|
-
const ALIGNMENTS_INVERSE_MAP = {
|
|
43
|
-
bctc: 'top',
|
|
44
|
-
bltl: 'top-left',
|
|
45
|
-
brtr: 'top-right',
|
|
46
|
-
clcr: 'right',
|
|
47
|
-
crcl: 'left',
|
|
48
|
-
tcbc: 'bottom',
|
|
49
|
-
tlbl: 'bottom-left',
|
|
50
|
-
trbr: 'bottom-right',
|
|
51
|
-
} as const;
|
|
52
|
-
|
|
53
|
-
const BOTTOM_OFFSET = [0, 7] as const;
|
|
54
|
-
const LEFT_OFFSET = [-7, 0] as const;
|
|
55
|
-
const RIGHT_OFFSET = [7, 0] as const;
|
|
56
|
-
const TOP_OFFSET = [0, -7] as const;
|
|
57
|
-
|
|
58
|
-
const OFFSET_MAP = {
|
|
59
|
-
bctc: TOP_OFFSET,
|
|
60
|
-
bltl: TOP_OFFSET,
|
|
61
|
-
brtr: TOP_OFFSET,
|
|
62
|
-
clcr: RIGHT_OFFSET,
|
|
63
|
-
crcl: LEFT_OFFSET,
|
|
64
|
-
tcbc: BOTTOM_OFFSET,
|
|
65
|
-
tlbl: BOTTOM_OFFSET,
|
|
66
|
-
trbr: BOTTOM_OFFSET,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const ALIGNMENTS_FORCE_MAP = {
|
|
70
|
-
...ALIGNMENTS_INVERSE_MAP,
|
|
71
|
-
bctc: 'top-left',
|
|
72
|
-
tcbc: 'bottom-left',
|
|
73
|
-
} as const;
|
|
17
|
+
import {Align, useAlign} from './useAlign';
|
|
18
|
+
import {useClosestTitle} from './useClosestTitle';
|
|
19
|
+
import {useTooltipState} from './useTooltipState';
|
|
74
20
|
|
|
75
21
|
interface IState {
|
|
76
|
-
align
|
|
77
|
-
floating
|
|
22
|
+
align: Align;
|
|
23
|
+
floating: boolean;
|
|
78
24
|
message?: string;
|
|
79
|
-
show?: boolean;
|
|
80
25
|
setAsHTML?: boolean;
|
|
81
26
|
}
|
|
82
27
|
|
|
83
28
|
const initialState: IState = {
|
|
84
29
|
align: 'top',
|
|
30
|
+
floating: false,
|
|
85
31
|
message: '',
|
|
86
32
|
setAsHTML: false,
|
|
87
|
-
show: false,
|
|
88
33
|
};
|
|
89
34
|
|
|
90
35
|
const TRIGGER_HIDE_EVENTS = [
|
|
@@ -102,64 +47,25 @@ const TRIGGER_SHOW_EVENTS = [
|
|
|
102
47
|
'touchstart',
|
|
103
48
|
] as const;
|
|
104
49
|
|
|
105
|
-
interface IAction extends IState {
|
|
106
|
-
type: '
|
|
50
|
+
interface IAction extends Partial<IState> {
|
|
51
|
+
type: 'reset' | 'update';
|
|
107
52
|
}
|
|
108
53
|
|
|
109
54
|
const reducer = (state: IState, {type, ...payload}: IAction): IState => {
|
|
110
55
|
switch (type) {
|
|
111
|
-
case '
|
|
56
|
+
case 'update':
|
|
112
57
|
return {...state, ...payload};
|
|
113
|
-
case '
|
|
114
|
-
return {...state, ...payload, show: true};
|
|
115
|
-
case 'hide':
|
|
58
|
+
case 'reset':
|
|
116
59
|
return {
|
|
117
60
|
...state,
|
|
118
61
|
align: initialState.align,
|
|
119
|
-
floating:
|
|
120
|
-
show: false,
|
|
62
|
+
floating: false,
|
|
121
63
|
};
|
|
122
64
|
default:
|
|
123
65
|
throw new TypeError();
|
|
124
66
|
}
|
|
125
67
|
};
|
|
126
68
|
|
|
127
|
-
function matches(
|
|
128
|
-
element: HTMLElement & {
|
|
129
|
-
msMatchesSelector?: HTMLElement['matches'];
|
|
130
|
-
},
|
|
131
|
-
selectorString: string
|
|
132
|
-
) {
|
|
133
|
-
if (element.matches) {
|
|
134
|
-
return element.matches(selectorString);
|
|
135
|
-
} else if (element.msMatchesSelector) {
|
|
136
|
-
return element.msMatchesSelector(selectorString);
|
|
137
|
-
} else if (element.webkitMatchesSelector) {
|
|
138
|
-
return element.webkitMatchesSelector(selectorString);
|
|
139
|
-
} else {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function closestAncestor(node: HTMLElement, s: string) {
|
|
145
|
-
const element = node;
|
|
146
|
-
let ancestor: HTMLElement | null = node;
|
|
147
|
-
|
|
148
|
-
if (!document.documentElement.contains(element)) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
do {
|
|
153
|
-
if (matches(ancestor, s)) {
|
|
154
|
-
return ancestor;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
ancestor = ancestor.parentElement;
|
|
158
|
-
} while (ancestor !== null);
|
|
159
|
-
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
69
|
type TContentRenderer = (props: {
|
|
164
70
|
targetNode?: HTMLElement | null;
|
|
165
71
|
title: string;
|
|
@@ -211,238 +117,151 @@ const TooltipProvider = ({
|
|
|
211
117
|
delay = 600,
|
|
212
118
|
scope,
|
|
213
119
|
}: IPropsWithChildren | IPropsWithScope) => {
|
|
214
|
-
const [{align, floating, message = '', setAsHTML
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Using `any` type since TS incorrectly infers setTimeout to be from NodeJS
|
|
220
|
-
const timeoutIdRef = useRef<any>();
|
|
221
|
-
const targetRef = useRef<HTMLElement | null>(null);
|
|
222
|
-
const titleNodeRef = useRef<HTMLElement | null>(null);
|
|
223
|
-
const tooltipRef = useRef<HTMLElement | null>(null);
|
|
224
|
-
|
|
225
|
-
const saveTitle = useCallback((element: HTMLElement) => {
|
|
226
|
-
titleNodeRef.current = element;
|
|
227
|
-
|
|
228
|
-
const title = element.getAttribute('title');
|
|
229
|
-
|
|
230
|
-
if (title) {
|
|
231
|
-
element.setAttribute('data-restore-title', title);
|
|
232
|
-
element.removeAttribute('title');
|
|
233
|
-
} else if (element.tagName === 'svg') {
|
|
234
|
-
const titleTag = element.querySelector('title');
|
|
235
|
-
|
|
236
|
-
if (titleTag) {
|
|
237
|
-
element.setAttribute('data-restore-title', titleTag.innerHTML);
|
|
238
|
-
|
|
239
|
-
titleTag.remove();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}, []);
|
|
243
|
-
|
|
244
|
-
const restoreTitle = useCallback(() => {
|
|
245
|
-
const element = titleNodeRef.current;
|
|
120
|
+
const [{align, floating, message = '', setAsHTML}, dispatch] = useReducer(
|
|
121
|
+
reducer,
|
|
122
|
+
initialState
|
|
123
|
+
);
|
|
246
124
|
|
|
247
|
-
|
|
248
|
-
const title = element.getAttribute('data-restore-title');
|
|
125
|
+
const tooltipRef = useRef<HTMLElement>(null);
|
|
249
126
|
|
|
250
|
-
|
|
251
|
-
if (element.tagName === 'svg') {
|
|
252
|
-
const titleTag = document.createElement('title');
|
|
127
|
+
const {getInteraction, isFocusVisible} = useInteractionFocus();
|
|
253
128
|
|
|
254
|
-
|
|
129
|
+
const isHovered = useRef(false);
|
|
130
|
+
const isFocused = useRef(false);
|
|
255
131
|
|
|
256
|
-
|
|
257
|
-
} else {
|
|
258
|
-
element.setAttribute('title', title);
|
|
259
|
-
}
|
|
132
|
+
const {close, isOpen, open} = useTooltipState({delay});
|
|
260
133
|
|
|
261
|
-
|
|
134
|
+
const {getProps, onHide, target, titleNode} = useClosestTitle({
|
|
135
|
+
onClick: useCallback(() => {
|
|
136
|
+
isFocused.current = false;
|
|
137
|
+
isHovered.current = false;
|
|
138
|
+
}, []),
|
|
139
|
+
onHide: useCallback(() => {
|
|
140
|
+
if (!isHovered.current && !isFocused.current) {
|
|
141
|
+
dispatch({type: 'reset'});
|
|
142
|
+
close();
|
|
262
143
|
}
|
|
144
|
+
}, []),
|
|
145
|
+
tooltipRef,
|
|
146
|
+
});
|
|
263
147
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
dispatch({type: 'hide'});
|
|
278
|
-
|
|
279
|
-
clearTimeout(timeoutIdRef.current);
|
|
280
|
-
|
|
281
|
-
restoreTitle();
|
|
282
|
-
|
|
283
|
-
if (targetRef.current) {
|
|
284
|
-
targetRef.current.removeEventListener('click', handleHide);
|
|
285
|
-
|
|
286
|
-
targetRef.current = null;
|
|
287
|
-
}
|
|
288
|
-
}, []);
|
|
148
|
+
useAlign({
|
|
149
|
+
align,
|
|
150
|
+
autoAlign,
|
|
151
|
+
floating,
|
|
152
|
+
isOpen,
|
|
153
|
+
onAlign: useCallback((align) => dispatch({align, type: 'update'}), []),
|
|
154
|
+
sourceElement: tooltipRef,
|
|
155
|
+
targetElement: titleNode,
|
|
156
|
+
});
|
|
289
157
|
|
|
290
|
-
const
|
|
158
|
+
const onShow = useCallback(
|
|
291
159
|
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const title =
|
|
309
|
-
titleNode.getAttribute('title') ||
|
|
310
|
-
titleNode.getAttribute('data-title') ||
|
|
311
|
-
'';
|
|
312
|
-
|
|
313
|
-
saveTitle(titleNode);
|
|
314
|
-
|
|
315
|
-
const customDelay =
|
|
316
|
-
titleNode.getAttribute('data-tooltip-delay');
|
|
317
|
-
const newAlign = titleNode.getAttribute(
|
|
318
|
-
'data-tooltip-align'
|
|
319
|
-
) as typeof align;
|
|
320
|
-
const setAsHTML = !!titleNode.getAttribute(
|
|
321
|
-
'data-title-set-as-html'
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
const isFloating = titleNode.getAttribute(
|
|
325
|
-
'data-tooltip-floating'
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
clearTimeout(timeoutIdRef.current);
|
|
329
|
-
|
|
330
|
-
timeoutIdRef.current = setTimeout(
|
|
331
|
-
() => {
|
|
332
|
-
dispatch({
|
|
333
|
-
align: newAlign || align,
|
|
334
|
-
floating: Boolean(isFloating),
|
|
335
|
-
message: title,
|
|
336
|
-
setAsHTML,
|
|
337
|
-
type: 'show',
|
|
338
|
-
});
|
|
339
|
-
},
|
|
340
|
-
customDelay ? Number(customDelay) : delay
|
|
341
|
-
);
|
|
160
|
+
if (isHovered.current || isFocused.current) {
|
|
161
|
+
const props = getProps(event);
|
|
162
|
+
|
|
163
|
+
if (props) {
|
|
164
|
+
dispatch({
|
|
165
|
+
align: (props.align as any) ?? align,
|
|
166
|
+
floating: props.floating,
|
|
167
|
+
message: props.title,
|
|
168
|
+
setAsHTML: props.setAsHTML,
|
|
169
|
+
type: 'update',
|
|
170
|
+
});
|
|
171
|
+
open(
|
|
172
|
+
isFocused.current,
|
|
173
|
+
props.delay ? Number(props.delay) : undefined
|
|
174
|
+
);
|
|
175
|
+
}
|
|
342
176
|
}
|
|
343
177
|
},
|
|
344
|
-
[]
|
|
178
|
+
[align]
|
|
345
179
|
);
|
|
346
180
|
|
|
347
181
|
useEffect(() => {
|
|
348
182
|
const handleEsc = (event: KeyboardEvent) => {
|
|
349
|
-
if (
|
|
183
|
+
if (isOpen && event.key === Keys.Esc) {
|
|
350
184
|
event.stopImmediatePropagation();
|
|
351
185
|
|
|
352
|
-
|
|
186
|
+
onHide();
|
|
353
187
|
}
|
|
354
188
|
};
|
|
355
189
|
|
|
356
190
|
document.addEventListener('keyup', handleEsc, true);
|
|
357
191
|
|
|
358
192
|
return () => document.removeEventListener('keyup', handleEsc, true);
|
|
359
|
-
}, [
|
|
193
|
+
}, [isOpen]);
|
|
194
|
+
|
|
195
|
+
const onHoverStart = (event: any) => {
|
|
196
|
+
if (getInteraction() === 'pointer') {
|
|
197
|
+
isHovered.current = true;
|
|
198
|
+
} else {
|
|
199
|
+
isHovered.current = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onShow(event);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const onHoverEnd = (event: any) => {
|
|
206
|
+
isFocused.current = false;
|
|
207
|
+
isHovered.current = false;
|
|
208
|
+
|
|
209
|
+
onHide(event);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const onFocus = (event: any) => {
|
|
213
|
+
if (isFocusVisible()) {
|
|
214
|
+
isFocused.current = true;
|
|
215
|
+
|
|
216
|
+
onShow(event);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const onBlur = (event: any) => {
|
|
221
|
+
isFocused.current = false;
|
|
222
|
+
isHovered.current = false;
|
|
223
|
+
|
|
224
|
+
onHide(event);
|
|
225
|
+
};
|
|
360
226
|
|
|
361
227
|
useEffect(() => {
|
|
362
228
|
if (scope) {
|
|
363
|
-
const disposeShowEvents = TRIGGER_SHOW_EVENTS.map((eventName) =>
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const disposeHideEvents = TRIGGER_HIDE_EVENTS.map((eventName) =>
|
|
367
|
-
|
|
229
|
+
const disposeShowEvents = TRIGGER_SHOW_EVENTS.map((eventName) =>
|
|
230
|
+
delegate(document.body, eventName, scope, onHoverStart)
|
|
231
|
+
);
|
|
232
|
+
const disposeHideEvents = TRIGGER_HIDE_EVENTS.map((eventName) =>
|
|
233
|
+
delegate(
|
|
368
234
|
document.body,
|
|
369
235
|
eventName,
|
|
370
236
|
`${scope}, .tooltip`,
|
|
371
|
-
|
|
372
|
-
)
|
|
373
|
-
|
|
237
|
+
onHoverEnd
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const disposeShowFocus = delegate(
|
|
242
|
+
document.body,
|
|
243
|
+
'focus',
|
|
244
|
+
`${scope}, .tooltip`,
|
|
245
|
+
onFocus,
|
|
246
|
+
true
|
|
247
|
+
);
|
|
248
|
+
const disposeCloseBlur = delegate(
|
|
249
|
+
document.body,
|
|
250
|
+
'blur',
|
|
251
|
+
`${scope}, .tooltip`,
|
|
252
|
+
onBlur,
|
|
253
|
+
true
|
|
254
|
+
);
|
|
374
255
|
|
|
375
256
|
return () => {
|
|
376
257
|
disposeShowEvents.forEach(({dispose}) => dispose());
|
|
377
258
|
disposeHideEvents.forEach(({dispose}) => dispose());
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
}, [handleShow]);
|
|
381
259
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
show &&
|
|
386
|
-
floating
|
|
387
|
-
) {
|
|
388
|
-
const points = ALIGNMENTS_MAP[align || 'top'] as [string, string];
|
|
389
|
-
|
|
390
|
-
const [clientX, clientY] = mousePosition;
|
|
391
|
-
|
|
392
|
-
alignPoint(
|
|
393
|
-
(tooltipRef as React.RefObject<HTMLDivElement>).current!,
|
|
394
|
-
{
|
|
395
|
-
clientX,
|
|
396
|
-
clientY,
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
offset: OFFSET_MAP[
|
|
400
|
-
points.join('') as keyof typeof OFFSET_MAP
|
|
401
|
-
] as [number, number],
|
|
402
|
-
points,
|
|
403
|
-
}
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
}, [show, floating]);
|
|
407
|
-
|
|
408
|
-
useEffect(() => {
|
|
409
|
-
if (
|
|
410
|
-
titleNodeRef.current &&
|
|
411
|
-
(tooltipRef as React.RefObject<HTMLDivElement>).current &&
|
|
412
|
-
!floating
|
|
413
|
-
) {
|
|
414
|
-
const points = ALIGNMENTS_MAP[align || 'top'] as [string, string];
|
|
415
|
-
|
|
416
|
-
const alignment = doAlign({
|
|
417
|
-
overflow: {
|
|
418
|
-
adjustX: autoAlign,
|
|
419
|
-
adjustY: autoAlign,
|
|
420
|
-
},
|
|
421
|
-
points,
|
|
422
|
-
sourceElement: (tooltipRef as React.RefObject<HTMLDivElement>)
|
|
423
|
-
.current!,
|
|
424
|
-
targetElement: titleNodeRef.current,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
const alignmentString = alignment.points.join(
|
|
428
|
-
''
|
|
429
|
-
) as keyof typeof ALIGNMENTS_INVERSE_MAP;
|
|
430
|
-
|
|
431
|
-
const pointsString = points.join('');
|
|
432
|
-
|
|
433
|
-
if (alignment.overflow.adjustX) {
|
|
434
|
-
dispatch({
|
|
435
|
-
align: ALIGNMENTS_FORCE_MAP[alignmentString],
|
|
436
|
-
type: 'align',
|
|
437
|
-
});
|
|
438
|
-
} else if (pointsString !== alignmentString) {
|
|
439
|
-
dispatch({
|
|
440
|
-
align: ALIGNMENTS_INVERSE_MAP[alignmentString],
|
|
441
|
-
type: 'align',
|
|
442
|
-
});
|
|
443
|
-
}
|
|
260
|
+
disposeShowFocus.dispose();
|
|
261
|
+
disposeCloseBlur.dispose();
|
|
262
|
+
};
|
|
444
263
|
}
|
|
445
|
-
}, [
|
|
264
|
+
}, [onShow]);
|
|
446
265
|
|
|
447
266
|
warning(
|
|
448
267
|
(typeof children === 'undefined' && typeof scope !== 'undefined') ||
|
|
@@ -461,11 +280,11 @@ const TooltipProvider = ({
|
|
|
461
280
|
);
|
|
462
281
|
|
|
463
282
|
const titleContent = contentRenderer({
|
|
464
|
-
targetNode:
|
|
283
|
+
targetNode: target.current,
|
|
465
284
|
title: message,
|
|
466
285
|
});
|
|
467
286
|
|
|
468
|
-
const tooltip =
|
|
287
|
+
const tooltip = isOpen && (
|
|
469
288
|
<ClayPortal {...containerProps}>
|
|
470
289
|
<ClayTooltip alignPosition={align} ref={tooltipRef} show>
|
|
471
290
|
{setAsHTML && typeof titleContent === 'string' ? (
|
|
@@ -498,8 +317,10 @@ const TooltipProvider = ({
|
|
|
498
317
|
{tooltip}
|
|
499
318
|
</>
|
|
500
319
|
),
|
|
501
|
-
|
|
502
|
-
|
|
320
|
+
onBlur,
|
|
321
|
+
onFocus,
|
|
322
|
+
onMouseOut: onHoverEnd,
|
|
323
|
+
onMouseOver: onHoverStart,
|
|
503
324
|
})
|
|
504
325
|
)}
|
|
505
326
|
</>
|
package/src/__tests__/index.tsx
CHANGED
|
@@ -33,6 +33,8 @@ describe('ClayTooltip', () => {
|
|
|
33
33
|
|
|
34
34
|
expect(document.querySelector('.tooltip')).toBeFalsy();
|
|
35
35
|
|
|
36
|
+
fireEvent.mouseDown(document.body);
|
|
37
|
+
|
|
36
38
|
fireEvent.mouseOver(getByTestId('button'));
|
|
37
39
|
|
|
38
40
|
act(() => {
|
|
@@ -49,4 +51,30 @@ describe('ClayTooltip', () => {
|
|
|
49
51
|
|
|
50
52
|
expect(document.querySelector('.tooltip')).toBeFalsy();
|
|
51
53
|
});
|
|
54
|
+
|
|
55
|
+
it('show tooltip on element focus and hide on blur', () => {
|
|
56
|
+
const {getByTestId} = render(
|
|
57
|
+
<ClayTooltipProvider>
|
|
58
|
+
<button
|
|
59
|
+
data-testid="button"
|
|
60
|
+
data-tooltip-align="bottom"
|
|
61
|
+
title="Bottom"
|
|
62
|
+
>
|
|
63
|
+
tooltip
|
|
64
|
+
</button>
|
|
65
|
+
</ClayTooltipProvider>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
fireEvent.keyDown(document.body, {key: 'Tab'});
|
|
69
|
+
|
|
70
|
+
expect(document.querySelector('.tooltip')).toBeFalsy();
|
|
71
|
+
|
|
72
|
+
fireEvent.focus(getByTestId('button'));
|
|
73
|
+
|
|
74
|
+
expect(document.querySelector('.tooltip')).toBeTruthy();
|
|
75
|
+
|
|
76
|
+
fireEvent.blur(getByTestId('button'));
|
|
77
|
+
|
|
78
|
+
expect(document.querySelector('.tooltip')).toBeFalsy();
|
|
79
|
+
});
|
|
52
80
|
});
|
package/src/useAlign.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2019 Liferay, Inc. <https://liferay.com>
|
|
3
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {doAlign, useMousePosition} from '@clayui/shared';
|
|
7
|
+
import {alignPoint} from 'dom-align';
|
|
8
|
+
import React, {useEffect} from 'react';
|
|
9
|
+
|
|
10
|
+
const ALIGNMENTS = [
|
|
11
|
+
'top',
|
|
12
|
+
'top-right',
|
|
13
|
+
'right',
|
|
14
|
+
'bottom-right',
|
|
15
|
+
'bottom',
|
|
16
|
+
'bottom-left',
|
|
17
|
+
'left',
|
|
18
|
+
'top-left',
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
const ALIGNMENTS_MAP = {
|
|
22
|
+
bottom: ['tc', 'bc'],
|
|
23
|
+
'bottom-left': ['tl', 'bl'],
|
|
24
|
+
'bottom-right': ['tr', 'br'],
|
|
25
|
+
left: ['cr', 'cl'],
|
|
26
|
+
right: ['cl', 'cr'],
|
|
27
|
+
top: ['bc', 'tc'],
|
|
28
|
+
'top-left': ['bl', 'tl'],
|
|
29
|
+
'top-right': ['br', 'tr'],
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const ALIGNMENTS_INVERSE_MAP = {
|
|
33
|
+
bctc: 'top',
|
|
34
|
+
bltl: 'top-left',
|
|
35
|
+
brtr: 'top-right',
|
|
36
|
+
clcr: 'right',
|
|
37
|
+
crcl: 'left',
|
|
38
|
+
tcbc: 'bottom',
|
|
39
|
+
tlbl: 'bottom-left',
|
|
40
|
+
trbr: 'bottom-right',
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const BOTTOM_OFFSET = [0, 7] as const;
|
|
44
|
+
const LEFT_OFFSET = [-7, 0] as const;
|
|
45
|
+
const RIGHT_OFFSET = [7, 0] as const;
|
|
46
|
+
const TOP_OFFSET = [0, -7] as const;
|
|
47
|
+
|
|
48
|
+
const OFFSET_MAP = {
|
|
49
|
+
bctc: TOP_OFFSET,
|
|
50
|
+
bltl: TOP_OFFSET,
|
|
51
|
+
brtr: TOP_OFFSET,
|
|
52
|
+
clcr: RIGHT_OFFSET,
|
|
53
|
+
crcl: LEFT_OFFSET,
|
|
54
|
+
tcbc: BOTTOM_OFFSET,
|
|
55
|
+
tlbl: BOTTOM_OFFSET,
|
|
56
|
+
trbr: BOTTOM_OFFSET,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ALIGNMENTS_FORCE_MAP = {
|
|
60
|
+
...ALIGNMENTS_INVERSE_MAP,
|
|
61
|
+
bctc: 'top-left',
|
|
62
|
+
tcbc: 'bottom-left',
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export type Align = typeof ALIGNMENTS[number];
|
|
66
|
+
|
|
67
|
+
type Props = {
|
|
68
|
+
align: Align;
|
|
69
|
+
autoAlign: boolean;
|
|
70
|
+
floating: boolean;
|
|
71
|
+
isOpen: boolean;
|
|
72
|
+
onAlign: (align: Align) => void;
|
|
73
|
+
sourceElement: React.MutableRefObject<HTMLElement | null>;
|
|
74
|
+
targetElement: React.MutableRefObject<HTMLElement | null>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function useAlign({
|
|
78
|
+
align,
|
|
79
|
+
autoAlign,
|
|
80
|
+
floating,
|
|
81
|
+
isOpen,
|
|
82
|
+
onAlign,
|
|
83
|
+
sourceElement,
|
|
84
|
+
targetElement,
|
|
85
|
+
}: Props) {
|
|
86
|
+
const mousePosition = useMousePosition(20);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (sourceElement.current && isOpen && floating) {
|
|
90
|
+
const points = ALIGNMENTS_MAP[align || 'top'] as [string, string];
|
|
91
|
+
|
|
92
|
+
const [clientX, clientY] = mousePosition;
|
|
93
|
+
|
|
94
|
+
alignPoint(
|
|
95
|
+
sourceElement.current,
|
|
96
|
+
{
|
|
97
|
+
clientX,
|
|
98
|
+
clientY,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
offset: OFFSET_MAP[
|
|
102
|
+
points.join('') as keyof typeof OFFSET_MAP
|
|
103
|
+
] as [number, number],
|
|
104
|
+
points,
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}, [isOpen, floating]);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (targetElement.current && sourceElement.current && !floating) {
|
|
112
|
+
const points = ALIGNMENTS_MAP[align || 'top'] as [string, string];
|
|
113
|
+
|
|
114
|
+
const alignment = doAlign({
|
|
115
|
+
overflow: {
|
|
116
|
+
adjustX: autoAlign,
|
|
117
|
+
adjustY: autoAlign,
|
|
118
|
+
},
|
|
119
|
+
points,
|
|
120
|
+
sourceElement: sourceElement.current,
|
|
121
|
+
targetElement: targetElement.current,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const alignmentString = alignment.points.join(
|
|
125
|
+
''
|
|
126
|
+
) as keyof typeof ALIGNMENTS_INVERSE_MAP;
|
|
127
|
+
|
|
128
|
+
const pointsString = points.join('');
|
|
129
|
+
|
|
130
|
+
if (alignment.overflow.adjustX) {
|
|
131
|
+
onAlign(ALIGNMENTS_FORCE_MAP[alignmentString]);
|
|
132
|
+
} else if (pointsString !== alignmentString) {
|
|
133
|
+
onAlign(ALIGNMENTS_INVERSE_MAP[alignmentString]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}, [align, isOpen]);
|
|
137
|
+
}
|