mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(app): support keyboard tooltip controls
This commit is contained in:
@@ -47,6 +47,8 @@ interface TooltipContextValue {
|
||||
cancelClose: () => void;
|
||||
scheduleClose: () => void;
|
||||
triggerRef: React.RefObject<View | null>;
|
||||
contentRef: React.RefObject<View | null>;
|
||||
focusContent: () => boolean;
|
||||
enabled: boolean;
|
||||
openOnPress: boolean;
|
||||
delayDuration: number;
|
||||
@@ -101,6 +103,17 @@ function composeEventHandlers(
|
||||
};
|
||||
}
|
||||
|
||||
function focusFirstContentControl(content: View | null): boolean {
|
||||
if (!isWeb || typeof document === "undefined" || !content) return false;
|
||||
const root = content as unknown as HTMLElement;
|
||||
const control = root.querySelector<HTMLElement>(
|
||||
'[role="button"][tabindex]:not([aria-disabled="true"]), [role="checkbox"][tabindex]:not([aria-disabled="true"]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])',
|
||||
);
|
||||
if (!control) return false;
|
||||
control.focus();
|
||||
return document.activeElement === control;
|
||||
}
|
||||
|
||||
function useControllableOpenState({
|
||||
open,
|
||||
defaultOpen,
|
||||
@@ -247,6 +260,7 @@ export function Tooltip({
|
||||
interactive?: boolean;
|
||||
}>): ReactElement {
|
||||
const triggerRef = useRef<View>(null);
|
||||
const contentRef = useRef<View>(null);
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [isOpen, setIsOpen] = useControllableOpenState({
|
||||
open,
|
||||
@@ -274,6 +288,7 @@ export function Tooltip({
|
||||
closeTimerRef.current = null;
|
||||
}, 100);
|
||||
}, [cancelClose, interactive, setIsOpen]);
|
||||
const focusContent = useCallback(() => focusFirstContentControl(contentRef.current), []);
|
||||
|
||||
useEffect(() => cancelClose, [cancelClose]);
|
||||
|
||||
@@ -284,6 +299,8 @@ export function Tooltip({
|
||||
cancelClose,
|
||||
scheduleClose,
|
||||
triggerRef,
|
||||
contentRef,
|
||||
focusContent,
|
||||
enabled,
|
||||
openOnPress: opensOnPress,
|
||||
delayDuration,
|
||||
@@ -294,6 +311,7 @@ export function Tooltip({
|
||||
setIsOpen,
|
||||
cancelClose,
|
||||
scheduleClose,
|
||||
focusContent,
|
||||
enabled,
|
||||
opensOnPress,
|
||||
delayDuration,
|
||||
@@ -311,6 +329,7 @@ export function TooltipTrigger({
|
||||
onHoverOut,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
onPress,
|
||||
asChild = false,
|
||||
triggerRefProp = "ref",
|
||||
@@ -318,6 +337,7 @@ export function TooltipTrigger({
|
||||
}: PressableProps & {
|
||||
asChild?: boolean;
|
||||
triggerRefProp?: string;
|
||||
onKeyDown?: (event: unknown) => void;
|
||||
}): ReactElement {
|
||||
const ctx = useTooltipContext("TooltipTrigger");
|
||||
const openTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -388,6 +408,21 @@ export function TooltipTrigger({
|
||||
[close, onBlur],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: unknown) => {
|
||||
if (isCallable(onKeyDown)) onKeyDown(event);
|
||||
if (!ctx.interactive || !ctx.open) return;
|
||||
const keyboardEvent = event as {
|
||||
key?: unknown;
|
||||
shiftKey?: unknown;
|
||||
preventDefault?: () => void;
|
||||
};
|
||||
if (keyboardEvent.key !== "Tab" || keyboardEvent.shiftKey) return;
|
||||
if (ctx.focusContent()) keyboardEvent.preventDefault?.();
|
||||
},
|
||||
[ctx, onKeyDown],
|
||||
);
|
||||
|
||||
const handlePress = useCallback(
|
||||
(e: unknown) => {
|
||||
if (isCallable(onPress)) onPress(e);
|
||||
@@ -412,6 +447,7 @@ export function TooltipTrigger({
|
||||
onHoverOut: handleHoverOut,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
onPress: handlePress,
|
||||
...(isWeb
|
||||
? ({
|
||||
@@ -442,6 +478,7 @@ export function TooltipTrigger({
|
||||
onHoverOut: composeEventHandlers(Reflect.get(rawProps, "onHoverOut"), handleHoverOut),
|
||||
onFocus: composeEventHandlers(Reflect.get(rawProps, "onFocus"), handleFocus),
|
||||
onBlur: composeEventHandlers(Reflect.get(rawProps, "onBlur"), handleBlur),
|
||||
onKeyDown: composeEventHandlers(Reflect.get(rawProps, "onKeyDown"), handleKeyDown),
|
||||
onPress: composeEventHandlers(Reflect.get(rawProps, "onPress"), handlePress),
|
||||
onPointerEnter: composeEventHandlers(Reflect.get(rawProps, "onPointerEnter"), handleHoverIn),
|
||||
onPointerLeave: composeEventHandlers(Reflect.get(rawProps, "onPointerLeave"), handleHoverOut),
|
||||
@@ -561,6 +598,7 @@ export function TooltipContent({
|
||||
<View pointerEvents={ctx.interactive ? "box-none" : "none"} style={styles.portalOverlay}>
|
||||
<FloatingSurface
|
||||
pointerEvents={ctx.interactive ? "auto" : "none"}
|
||||
ref={ctx.contentRef}
|
||||
entering={FadeIn.duration(80)}
|
||||
exiting={FadeOut.duration(80)}
|
||||
collapsable={false}
|
||||
@@ -597,6 +635,7 @@ export function TooltipContent({
|
||||
/>
|
||||
<FloatingSurface
|
||||
pointerEvents="auto"
|
||||
ref={ctx.contentRef}
|
||||
entering={FadeIn.duration(80)}
|
||||
exiting={FadeOut.duration(80)}
|
||||
collapsable={false}
|
||||
@@ -614,6 +653,7 @@ export function TooltipContent({
|
||||
<Pressable testID="tooltip-dismiss" style={styles.overlay} onPress={handleDismiss}>
|
||||
<FloatingSurface
|
||||
pointerEvents="none"
|
||||
ref={ctx.contentRef}
|
||||
entering={FadeIn.duration(80)}
|
||||
exiting={FadeOut.duration(80)}
|
||||
collapsable={false}
|
||||
|
||||
Reference in New Issue
Block a user