Показать сообщение отдельно
  #10 (permalink)  
Старый 11.10.2023, 11:59
Аватар для ruslan_mart
Профессор
Отправить личное сообщение для ruslan_mart Посмотреть профиль Найти все сообщения от ruslan_mart
 
Регистрация: 30.04.2012
Сообщений: 3,018

Aetae, да, упустил, спасибо)

Насчет jquery, глянул, у них там подход немного другой. В jQuery пытаются получить может ли быть элемент фокусируемый, но суть примерно та же самая, да. Вроде как там аналогично проверяют href через hasAttribute.

Я тут просто делаю Popover, который можно вешать на любые элементы и настраивать триггер, по которому он будет вызван (click, focus, hover, contextmenu). Сам Popover вставляю порталом в конец body, хотя популярные UI либы вставляют Popover в место вызова, но мне кажется, что это не очень классно, так как может нарушиться семантика и валидность элемента. Поэтому я решил, что Popover будет рисоваться в конце body. Но возник нюанс с фокусом, что когда мы фокусируемся на элемент, к которому привязан Popover, то при табуляции мы не проваливаемся в сам Popover, а идем дальше по дереву фокусируемых элементов DOM, так как наш Popover находится в конце.

Что пришло в голову, так это знать о всех фокусируемых элементах на странице, знать текущий фокусируемый элемент, знать следующий фокусируемый элемент, первый и последний фокусируемый элемент в Popover. И тут уже совершать магию При потере фокуса табом на текущем элементе, принудительно фокусироваться на первом элементе в Popover, ждать пока мы сфокусируемся на последнем элементе Popover и далее продолжать уже фокусировку по элементу, который был после целевого элемента.

Но столкнулся с множеством нюансов. Это то, что у нас могут быть разные tabIndex, contentEditable элементы, элементы форм с disabled состоянием (причем это состояние может быть вызвано родительским fieldset, поэтому это тоже нужно учитывать).

Вот такое решение у меня в итоге получилось.

const FOCUSABLE_ELEMENTS_SELECTOR =
  'a, button, input, select, textarea, details summary, [contenteditable], [tabindex]';

function filterFocusableElements(element: HTMLElement) {
  return (
    getRealTabIndex(element) !== -1 &&
    (!(
      element instanceof HTMLButtonElement ||
      element instanceof HTMLInputElement ||
      element instanceof HTMLSelectElement ||
      element instanceof HTMLTextAreaElement
    ) ||
      !element.matches(':disabled'))
  );
}

function getRealTabIndex(element: HTMLElement) {
  if (!element.hasAttribute('tabindex')) {
    // ContentEditable elements have a default tabindex of -1, although they behave like 0.
    // If this is the case, then treat the tabindex as 0
    if (element.isContentEditable) {
      return 0;
    }

    // Anchors without the "href" attribute have a default tab index of 0, although they behave like -1.
    // If this is the case, then treat the tabindex as -1
    if (element instanceof HTMLAnchorElement && !element.hasAttribute('href')) {
      return -1;
    }
  }

  return element.tabIndex;
}

function sortElementsCallback(elementA: HTMLElement, elementB: HTMLElement) {
  return getRealTabIndex(elementA) - getRealTabIndex(elementB);
}

export function getAllFocusableElements(parentElement: Element) {
  const allElements = parentElement.querySelectorAll(
    FOCUSABLE_ELEMENTS_SELECTOR
  ) as NodeListOf<HTMLElement>;
  return Array.from(allElements).filter(filterFocusableElements).sort(sortElementsCallback);
}



export function isDisabledElement(element: Element) {
  if (
    element instanceof HTMLButtonElement ||
    element instanceof HTMLInputElement ||
    element instanceof HTMLSelectElement ||
    element instanceof HTMLTextAreaElement
  ) {
    // The element may be in a disabled fieldset, so we check through the selector
    return element.matches(':disabled');
  }

  return false;
}


import { useContext, useEffect, useRef } from 'react';

import { PrivateContext } from '../context';
import { getAllFocusableElements } from '../utils/getAllFocusableElements';

interface ContentFocusOptions {
  enabled: boolean;
}

export function useContentFocus(options: ContentFocusOptions) {
  const { enabled } = options;

  const { contentWrapperRef, targetContainerRef } = useContext(PrivateContext)!;

  const contentFocusingRef = useRef(false);

  useEffect(() => {
    if (!enabled) {
      return undefined;
    }

    const handleWindowKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Tab') {
        const contentWrapperElement = contentWrapperRef.current;
        const targetContainerElement = targetContainerRef.current;

        if (contentWrapperElement !== null && targetContainerElement !== null) {
          if (!contentFocusingRef.current) {
            const targetFocusableElements = getAllFocusableElements(targetContainerElement);

            if (
              targetFocusableElements.length === 0 ||
              targetFocusableElements.pop() === document.activeElement
            ) {
              const contentFocusableElements = getAllFocusableElements(contentWrapperElement);

              if (contentFocusableElements.length > 0) {
                contentFocusableElements[0]!.focus();
                contentFocusingRef.current = true;

                event.preventDefault();
              }
            }
          } else {
            const contentFocusableElements = getAllFocusableElements(contentWrapperElement);

            if (contentFocusableElements.pop() === document.activeElement) {
              const targetFocusableElements = getAllFocusableElements(targetContainerElement);

              if (targetFocusableElements.length > 0) {
                const documentFocusableElements = getAllFocusableElements(document.body);
                const targetLastFocusableElement = targetFocusableElements.pop()!;
                const targetLastFocusableIndex = documentFocusableElements.indexOf(
                  targetLastFocusableElement
                );

                const nextFocusableIndex =
                  (targetLastFocusableIndex + 1) % documentFocusableElements.length;
                const nextFocusableElement = documentFocusableElements[nextFocusableIndex];

                nextFocusableElement!.focus();
                event.preventDefault();
              }
            }
          }
        }
      }
    };

    window.addEventListener('keydown', handleWindowKeyDown);

    return () => {
      contentFocusingRef.current = false;
      window.removeEventListener('keydown', handleWindowKeyDown);
    };
  }, [enabled]);
}


Это все работает, но пока все-равно подумываю над улучшениями.

Последний раз редактировалось ruslan_mart, 11.10.2023 в 12:08.
Ответить с цитированием