From da59e031ae91802a2162ab0b16558f462da953ed Mon Sep 17 00:00:00 2001 From: "e.muhamethanov" Date: Mon, 21 Oct 2024 14:21:39 +0300 Subject: [PATCH 1/2] fix(useFloatingWithInteractions): improve restoreFocus, add restoreFocus to anchor element --- .../src/components/FocusTrap/FocusTrap.tsx | 121 +++++++++++------- .../vkui/src/components/Popover/Popover.tsx | 2 +- .../vkui/src/components/Popover/Readme.md | 1 + .../vkui/src/lib/floating/types/component.ts | 3 +- .../useFloatingWithInteractions/types.ts | 4 +- .../useFloatingWithInteractions.ts | 18 ++- 6 files changed, 97 insertions(+), 52 deletions(-) diff --git a/packages/vkui/src/components/FocusTrap/FocusTrap.tsx b/packages/vkui/src/components/FocusTrap/FocusTrap.tsx index dad2e5be8c..1c25b9eb18 100644 --- a/packages/vkui/src/components/FocusTrap/FocusTrap.tsx +++ b/packages/vkui/src/components/FocusTrap/FocusTrap.tsx @@ -1,9 +1,10 @@ 'use client'; -import { type AllHTMLAttributes, useCallback, useRef, useState } from 'react'; +import { type AllHTMLAttributes, type MutableRefObject, useRef, useState } from 'react'; import { arraysEquals } from '../../helpers/array'; import { useExternRef } from '../../hooks/useExternRef'; import { useMutationObserver } from '../../hooks/useMutationObserver'; +import { useStableCallback } from '../../hooks/useStableCallback'; import { FOCUSABLE_ELEMENTS_LIST, Keys, pressedKey } from '../../lib/accessibility'; import { contains, @@ -21,7 +22,7 @@ export interface FocusTrapProps HasRootRef, HasComponent { autoFocus?: boolean | 'root'; - restoreFocus?: boolean | (() => boolean); + restoreFocus?: boolean | (() => boolean | HTMLElement); mount?: boolean; timeout?: number; onClose?: () => void; @@ -31,6 +32,71 @@ export interface FocusTrapProps disabled?: boolean; } +const useRestoreFocus = ({ + restoreFocus, + timeout, + mount, + ref, +}: Pick & { + ref: MutableRefObject; +}) => { + const restoreFocusRef = useRef(restoreFocus); + restoreFocusRef.current = restoreFocus; + const [restoreFocusTo, setRestoreFocusTo] = useState(null); + + const restoreFocusImpl = useStableCallback(() => { + const shouldRestoreFocus = + typeof restoreFocusRef.current === 'function' + ? restoreFocusRef.current() + : restoreFocusRef.current; + + if (!shouldRestoreFocus) { + return; + } + + setTimeout(() => { + const restoreFocusElement = + (isHTMLElement(shouldRestoreFocus) && shouldRestoreFocus) || + (isHTMLElement(restoreFocusTo) && restoreFocusTo) || + null; + + if (restoreFocusElement) { + restoreFocusElement.focus(); + setRestoreFocusTo(null); + } + }, timeout); + }); + + useIsomorphicLayoutEffect( + function calculateRestoreFocusTo() { + if (!ref.current || !restoreFocusRef.current || !mount) { + setRestoreFocusTo(null); + return; + } + setRestoreFocusTo(getActiveElementByAnotherElement(ref.current)); + }, + [ref, mount], + ); + + useIsomorphicLayoutEffect( + function tryToRestoreFocusOnUnmount() { + return () => { + restoreFocusImpl(); + }; + }, + [restoreFocusImpl], + ); + + useIsomorphicLayoutEffect( + function tryToRestoreFocusWhenFakeUnmount() { + if (!mount) { + restoreFocusImpl(); + } + }, + [mount, restoreFocusImpl], + ); +}; + /** * @see https://vkcom.github.io/VKUI/#/FocusTrap */ @@ -51,8 +117,6 @@ export const FocusTrap = ({ const focusableNodesRef = useRef([]); - const [restoreFocusTo, setRestoreFocusTo] = useState(null); - const focusNodeByIndex = (nodeIndex: number) => { const element = focusableNodesRef.current[nodeIndex]; @@ -63,6 +127,13 @@ export const FocusTrap = ({ } }; + useRestoreFocus({ + restoreFocus, + mount, + timeout, + ref, + }); + const recalculateFocusableNodesRef = (parentNode: HTMLElement) => { // eslint-disable-next-line no-restricted-properties const newFocusableElements = parentNode.querySelectorAll(FOCUSABLE_ELEMENTS); @@ -133,48 +204,6 @@ export const FocusTrap = ({ [autoFocus, timeout, disabled], ); - const restoreFocusImpl = useCallback(() => { - const shouldRestoreFocus = typeof restoreFocus === 'function' ? restoreFocus() : restoreFocus; - - if (!restoreFocusTo || !isHTMLElement(restoreFocusTo) || !shouldRestoreFocus) { - return; - } - - setTimeout(() => { - if (restoreFocusTo) { - restoreFocusTo.focus(); - setRestoreFocusTo(null); - } - }, timeout); - }, [restoreFocus, restoreFocusTo, timeout]); - - useIsomorphicLayoutEffect( - function calculateRestoreFocusTo() { - if (!ref.current || !restoreFocus || !mount) { - setRestoreFocusTo(null); - return; - } - setRestoreFocusTo(getActiveElementByAnotherElement(ref.current)); - }, - [ref, mount, restoreFocus], - ); - - useIsomorphicLayoutEffect( - function tryToRestoreFocusOnUnmount() { - return () => restoreFocusImpl(); - }, - [restoreFocusImpl], - ); - - useIsomorphicLayoutEffect( - function tryToRestoreFocusWhenFakeUnmount() { - if (!mount) { - restoreFocusImpl(); - } - }, - [mount, restoreFocusImpl], - ); - useIsomorphicLayoutEffect(() => { if (!ref.current) { return; diff --git a/packages/vkui/src/components/Popover/Popover.tsx b/packages/vkui/src/components/Popover/Popover.tsx index 0d163461df..6a59534fff 100644 --- a/packages/vkui/src/components/Popover/Popover.tsx +++ b/packages/vkui/src/components/Popover/Popover.tsx @@ -269,7 +269,7 @@ export const Popover = ({ mount={!hidden} disabled={hidden} autoFocus={disableInteractive ? false : autoFocus} - restoreFocus={restoreFocus ? onRestoreFocus : false} + restoreFocus={restoreFocus ? () => onRestoreFocus(restoreFocus) : false} onClose={onEscapeKeyDown} > {arrow} diff --git a/packages/vkui/src/components/Popover/Readme.md b/packages/vkui/src/components/Popover/Readme.md index d8aa7ee32d..1668c3b32b 100644 --- a/packages/vkui/src/components/Popover/Readme.md +++ b/packages/vkui/src/components/Popover/Readme.md @@ -39,6 +39,7 @@ const PopoverWithTriggerHover = () => { Привет } + restoreFocus="anchor-element" >