-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathTooltip.tsx
More file actions
104 lines (95 loc) · 3.86 KB
/
Copy pathTooltip.tsx
File metadata and controls
104 lines (95 loc) · 3.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { Placement } from "@floating-ui/react";
import classNames from "classnames";
import { CSSProperties, ReactElement, ReactNode, useCallback, useEffect, useId, useRef, useState } from "react";
import { OpenOnEnum, RenderMethodEnum } from "../../typings/TooltipProps";
import { useFloatingUI } from "../utils/useFloatingUI";
export interface TooltipProps {
name?: string;
class?: string;
style?: CSSProperties;
tabIndex?: number;
trigger: ReactNode;
renderMethod: RenderMethodEnum;
htmlMessage: ReactNode;
textMessage?: string;
position: Placement;
openOn: OpenOnEnum;
preview?: boolean;
}
export const Tooltip = (props: TooltipProps): ReactElement => {
const { trigger, htmlMessage, textMessage, openOn, position, preview, renderMethod } = props;
const [showTooltip, setShowTooltip] = useState(preview ?? false);
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null);
const contentId = useId();
const hasTooltipContent = !!(textMessage || htmlMessage);
const triggerWrapperRef = useRef<HTMLDivElement>(null);
const { arrowStyles, blurFocusEvents, floatingStyles, getFloatingProps, getReferenceProps, refs, staticSide } =
useFloatingUI({
position,
showTooltip,
setShowTooltip,
arrowElement,
openOn
});
// Apply aria-describedby to all focusable elements inside the trigger wrapper
useEffect(() => {
if (!hasTooltipContent || !triggerWrapperRef.current) {
return;
}
// Find all focusable elements (button, a, input, select, textarea, etc.)
const focusableElements = triggerWrapperRef.current.querySelectorAll<HTMLElement>(
'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
focusableElements.forEach(element => {
element.setAttribute("aria-describedby", contentId);
});
return () => {
focusableElements.forEach(element => {
element.removeAttribute("aria-describedby");
});
};
}
}, [hasTooltipContent, contentId]);
// Merge our ref with floating-ui's ref
const setTriggerRef = useCallback(
(node: HTMLDivElement | null) => {
triggerWrapperRef.current = node;
if (refs?.setReference) {
refs.setReference(node);
}
},
[refs]
);
return (
<div className={classNames(props.class, "widget-tooltip", `widget-tooltip-${position}`)}>
<div
className="widget-tooltip-trigger"
ref={setTriggerRef}
{...(preview
? undefined
: getReferenceProps?.({ ...(openOn === "hoverFocus" && !preview ? blurFocusEvents : undefined) }))}
>
{trigger}
</div>
{/* Hidden content for screen readers - always in DOM, only accessible via aria-describedby */}
{hasTooltipContent && (
<div id={contentId} className="sr-only" aria-hidden="true">
{renderMethod === "text" ? textMessage : htmlMessage}
</div>
)}
{/* Visible content for sighted users */}
{showTooltip && hasTooltipContent ? (
<div
className="widget-tooltip-content"
ref={refs?.setFloating}
style={floatingStyles}
{...getFloatingProps?.()}
>
{renderMethod === "text" ? textMessage : htmlMessage}
<div className={`widget-tooltip-arrow-${staticSide}`} ref={setArrowElement} style={arrowStyles} />
</div>
) : null}
</div>
);
};