Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(useFloatingWithInteractions): improve restoreFocus, add restoreFocus to anchor element #7806

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vkui/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions packages/vkui/src/components/Popover/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const PopoverWithTriggerHover = () => {
<Text>Привет</Text>
</Div>
}
restoreFocus="anchor-element"
>
<Button id="tooltip-1" mode="outline">
Наведи на меня
Expand Down
121 changes: 75 additions & 46 deletions packages/vkui/src/hooks/useFocusTrap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type RefObject, useCallback, useRef, useState } from 'react';
import { type RefObject, useRef, useState } from 'react';
import { arraysEquals } from '../helpers/array';
import { FOCUSABLE_ELEMENTS_LIST, Keys, pressedKey } from '../lib/accessibility';
import {
Expand All @@ -10,6 +10,72 @@ import {
} from '../lib/dom';
import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect';
import { useMutationObserver } from './useMutationObserver';
import { useStableCallback } from './useStableCallback';

const useRestoreFocus = ({
restoreFocus,
timeout,
mount,
ref,
}: Pick<UseFocusTrapProps, 'restoreFocus' | 'timeout' | 'mount'> & {
ref: RefObject<HTMLElement | null>;
}) => {
const restoreFocusRef = useRef(restoreFocus);
restoreFocusRef.current = restoreFocus;
const [restoreFocusTo, setRestoreFocusTo] = useState<Element | null>(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],
);
};

const FOCUSABLE_ELEMENTS: string = FOCUSABLE_ELEMENTS_LIST.join();

Expand All @@ -31,7 +97,7 @@ export type UseFocusTrapProps = {
/**
* @default true
*/
restoreFocus?: boolean | (() => boolean);
restoreFocus?: boolean | (() => boolean | HTMLElement);
/**
* @default 0
*/
Expand Down Expand Up @@ -60,8 +126,6 @@ export const useFocusTrap = (

const focusableNodesRef = useRef<HTMLElement[]>([]);

const [restoreFocusTo, setRestoreFocusTo] = useState<Element | null>(null);

const focusNodeByIndex = (nodeIndex: number) => {
const element = focusableNodesRef.current[nodeIndex];

Expand All @@ -72,6 +136,13 @@ export const useFocusTrap = (
}
};

useRestoreFocus({
restoreFocus,
mount,
timeout,
ref,
});

const recalculateFocusableNodesRef = (parentNode: HTMLElement) => {
// eslint-disable-next-line no-restricted-properties
const newFocusableElements = parentNode.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS);
Expand Down Expand Up @@ -142,48 +213,6 @@ export const useFocusTrap = (
[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(
function initializeFocusTrap() {
if (!ref.current) {
Expand Down
3 changes: 2 additions & 1 deletion packages/vkui/src/lib/floating/types/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
UseFloatingWithInteractionsProps,
UseFloatingWithInteractionsReturn,
} from '../useFloatingWithInteractions';
import { type RestoreFocusType } from '../useFloatingWithInteractions/types';
import type { OnPlacementChange } from './common';

/**
Expand Down Expand Up @@ -44,7 +45,7 @@ export interface FloatingComponentProps
/**
* Нужно ли после закрытия всплывающего элемента возвращать фокус на предыдущий активный элемент.
*/
restoreFocus?: boolean;
restoreFocus?: RestoreFocusType;
/**
* Перебивает zIndex заданный по умолчанию.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type ManualTriggerType = 'manual';

export type TriggerType = ManualTriggerType | InteractiveTriggerType | InteractiveTriggerType[];

export type RestoreFocusType = boolean | 'latest-active' | 'anchor-element' | HTMLElement;

export type ShownChangeReason =
| 'click-outside'
| 'escape-key'
Expand Down Expand Up @@ -110,5 +112,5 @@ export interface UseFloatingWithInteractionsReturn<T extends HTMLElement = HTMLE
middlewareData: UseFloatingData['middlewareData'];
onClose: (this: void) => void;
onEscapeKeyDown?: (this: void) => void;
onRestoreFocus: (this: void) => boolean;
onRestoreFocus: (restoreFocus?: RestoreFocusType) => boolean | HTMLElement;
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,21 @@
commitShownLocalState(false, 'callback');
}, [commitShownLocalState]);

const handleRestoreFocus = React.useCallback(
() => (triggerOnFocus ? blockFocusRef.current : true),
[triggerOnFocus],
const handleRestoreFocus: UseFloatingWithInteractionsReturn['onRestoreFocus'] = React.useCallback(
(restoreFocus = true) => {
if (!restoreFocus) {
return false;

Check warning on line 235 in packages/vkui/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts#L235

Added line #L235 was not covered by tests
}
if (restoreFocus === true || restoreFocus === 'latest-active') {
return triggerOnFocus ? blockFocusRef.current : true;
} else if (restoreFocus === 'anchor-element') {
return refs.reference.current as HTMLElement;
} else if (restoreFocus instanceof HTMLElement) {
return restoreFocus;

Check warning on line 242 in packages/vkui/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts#L239-L242

Added lines #L239 - L242 were not covered by tests
}
return false;

Check warning on line 244 in packages/vkui/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts#L244

Added line #L244 was not covered by tests
},
[refs.reference, triggerOnFocus],
);

const handleEscapeKeyDown = React.useCallback(() => {
Expand Down
Loading