Skip to content

[Bug][FocusTrap]: конфликтует логика при использовании нескольких FocusTrap #8244

@inomdzhon

Description

Описание

При использовать нескольких FocusTrap на странице возникают следующие проблемы:

  1. ломается логика работы Tab (старая проблема);

  2. может переключаться фокус с одного 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)

if (!autoFocus || arraysEquals(oldFocusableNodes, focusableNodesRef.current)) {

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

Type

Projects

  • Status

    👀 In Review

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions