Description
Описание
При использовать нескольких FocusTrap
на странице возникают следующие проблемы:
-
ломается логика работы Tab (старая проблема);
-
может переключаться фокус с одного
FocusTrap
на другойFocusTrap
– условием является изменения количества фокусируемых элементов.Такое может возникнуть при использовании одновременно двух
ModalPage
. После refactor: ModalRoot/ModalPage/ModalCard #6759 уModalPage
параметрautoFocus
зависит отnoFocusToDialog
. Вv6.6.0
, где появилось изменение fix: fix rerender component when slide controlled changes #7485, не будет воспроизводится, т.к. в v6 модальное окно оборачивает вFocusTrap
с зашитымautoFocus={false}
.В v7 можно передать
noFocusToDialog
, чтобы решить проблему, но из-за этого будет терятьсяautoFocus
, да и сам баг отстаётся, т.к. кейс сModalPage
лишь пример.Подробнее в секции Видео → Проблема 2. Переключается фокус.
Версия
>=7.0.0
Видео
На видео используется Пример с воспроизведением 1 из секции ниже.
Проблема 1. Ломается логика работы Tab
2025-02-05.17.08.03.mov
Проблема 2. Переключается фокус
В втором модальном окно теряется фокус с поля ввода на кнопку в первом модальном окне – это связано с неравенством oldFocusableNodes
в onMutateParentHandler()
из-за появления нового фокусируемого элемента (примере, появление второй кнопки с цифрой 2)
VKUI/packages/vkui/src/hooks/useFocusTrap.ts
Line 169 in cec3927
2025-02-05.16.32.46.mov
Пример с воспроизведением
Примеры сделаны под Storybook
Пример с воспроизведением 1
Рендерим вторую модалку внутри первой, но выносим в портал.
import { useState } from '@storybook/preview-api';
import { ModalPage } from '../ModalPage/ModalPage';
import { ModalPageHeader } from '../ModalPageHeader/ModalPageHeader';
import { AppRootPortal } from '../AppRoot/AppRootPortal';
export const DoubleFocusTrap: Story = {
render: function Render() {
const [openInnerModal, setOpenInnerModal] = useState(false);
const [searchText, onChangeSearch] = useState('');
return (
<ModalPage
open
id="root"
header={<ModalPageHeader>Фильтр</ModalPageHeader>}
settlingHeight={100}
>
<div style={{ padding: 16 }}>
<button onClick={() => setOpenInnerModal(true)}>Inner modal</button>
</div>
<div style={{ padding: 16 }}>
Updates here:{' '}
{searchText.split('').map((i) => (
<button key={i}>{i}</button>
))}
</div>
<AppRootPortal usePortal>
<ModalPage
id="inner"
open={openInnerModal}
settlingHeight={50}
header={<ModalPageHeader>Местонахождение</ModalPageHeader>}
onClose={() => setOpenInnerModal(false)}
>
<input
style={{ padding: 16 }}
placeholder="Поиск"
value={searchText}
onChange={({ target }) => onChangeSearch(String(target.value))}
/>
</ModalPage>
</AppRootPortal>
</ModalPage>
);
},
};
Пример с воспроизведением 2
Рендерим вторую модалку рядом с первой.
import { useState } from '@storybook/preview-api';
import { ModalPage } from '../ModalPage/ModalPage';
import { ModalPageHeader } from '../ModalPageHeader/ModalPageHeader';
export const Playground: Story = {
render: function Render() {
const [openInnerModal, setOpenInnerModal] = useState(false);
const [searchText, onChangeSearch] = useState('');
return (
<>
<ModalPage
open
id="root"
header={<ModalPageHeader>Фильтр</ModalPageHeader>}
settlingHeight={100}
>
<div style={{ padding: 16 }}>
<button onClick={() => setOpenInnerModal(true)}>Inner modal</button>
</div>
<div style={{ padding: 16 }}>
Updates here:{' '}
{searchText.split('').map((i) => (
<button key={i}>{i}</button>
))}
</div>
</ModalPage>
<ModalPage
id="inner"
open={openInnerModal}
settlingHeight={50}
header={<ModalPageHeader>Местонахождение</ModalPageHeader>}
onClose={() => setOpenInnerModal(false)}
>
<input
style={{ padding: 16 }}
placeholder="Поиск"
value={searchText}
onChange={({ target }) => onChangeSearch(String(target.value))}
/>
</ModalPage>
</>
);
},
};
Пример как не воспроизводится, но при этом ломает модалки в текущем виде
Рендерим вторую модалку прямо в разметку первой.
import { useState } from '@storybook/preview-api';
import { ModalPage } from '../ModalPage/ModalPage';
import { ModalPageHeader } from '../ModalPageHeader/ModalPageHeader';
export const DoubleFocusTrap: Story = {
render: function Render() {
const [openInnerModal, setOpenInnerModal] = useState(false);
const [searchText, onChangeSearch] = useState('');
return (
<ModalPage
open
id="root"
header={<ModalPageHeader>Фильтр</ModalPageHeader>}
settlingHeight={100}
>
<div style={{ padding: 16 }}>
<button onClick={() => setOpenInnerModal(true)}>Inner modal</button>
</div>
<div style={{ padding: 16 }}>
Updates here:{' '}
{searchText.split('').map((i) => (
<button key={i}>{i}</button>
))}
</div>
<ModalPage
id="inner"
open={openInnerModal}
settlingHeight={50}
header={<ModalPageHeader>Местонахождение</ModalPageHeader>}
onClose={() => setOpenInnerModal(false)}
>
<input
style={{ padding: 16 }}
placeholder="Поиск"
value={searchText}
onChange={({ target }) => onChangeSearch(String(target.value))}
/>
</ModalPage>
</ModalPage>
);
},
};
Metadata
Assignees
Type
Projects
Status
👀 In Review