![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
02.10.2023, 19:10
|
![Аватар для ruslan_mart](https://javascript.ru/forum/image.php?u=20699&dateline=1502382178) |
Профессор
|
|
Регистрация: 30.04.2012
Сообщений: 3,018
|
|
contendeditable и tabIndex
Всем привет, давно не виделись
Обнаружил странное поведение во всех браузерах (проверял в хроме, фф и сафари)
<div id="foo" contenteditable="true">123</div>
<script>
console.log(foo.tabIndex); // -1
</script>
Свойство tabIndex по умолчанию -1, хотя сам элемент фокусируется табом в потоке элементов с tabIndex 0
Но если засетить tabIndex вручную, то он станет работать правильно (фокуса по табу в элемент не будет)
console.log(foo.tabIndex); // -1
// работает как будто бы он 0
foo.tabIndex = -1;
// или foo.tabIndex = foo.tabIndex;
console.log(foo.tabIndex); // -1
// теперь работает как -1
Кто-нибудь может объяснить в чём тут прикол? В гугле ничего не нашел по этому поводу
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
03.10.2023, 14:38
|
![Аватар для ksa](https://javascript.ru/forum/image.php?u=8616&dateline=1282216923) |
CacheVar
|
|
Регистрация: 19.08.2010
Сообщений: 14,236
|
|
Сообщение от ruslan_mart
|
Свойство tabIndex по умолчанию -1, хотя сам элемент фокусируется табом в потоке элементов с tabIndex 0
Но если засетить tabIndex вручную, то он станет работать правильно (фокуса по табу в элемент не будет)
|
Такое вот нашел...
Цитата:
|
Переход к элементам, у которых не задан атрибут tabindex или его значение равно 0, происходит после всех «нумерованных» элементов в том порядке, как они указаны в коде.
|
https://htmlbook.ru/html/attr/tabindex
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
04.10.2023, 19:00
|
![Аватар для ruslan_mart](https://javascript.ru/forum/image.php?u=20699&dateline=1502382178) |
Профессор
|
|
Регистрация: 30.04.2012
Сообщений: 3,018
|
|
@ksa
Это да, потому у элементов форм он и так по умолчанию 0:
document.createElement('input').tabIndex // 0
Но если явно указать -1, хоть инпуту, хоть диву, то таб работать не будет.
и свойство будет возвращать -1 (как и должно быть).
Но вот когда div с contenteditable, то он ведет себя как tabindex="0", что, в принципе. логично. Но вот почему-то само свойство возвращает -1. Причем если это свойство явно переопределяешь на тот же самый -1, то начинает работать правильно.
В общем, похоже, что какой-то массовый баг всех браузеров, который унаследовали из-за какой-либо обратной совместимости. Это как с typeof null === 'object'
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
04.10.2023, 21:10
|
![Аватар для voraa](https://javascript.ru/forum/image.php?u=69123&dateline=1640150450) |
Профессор
|
|
Регистрация: 03.02.2020
Сообщений: 2,756
|
|
Сообщение от ruslan_mart
|
В общем, похоже, что какой-то массовый баг всех браузеров,
|
Смотрим спеку на HTML
https://html.spec.whatwg.org/multipa...#attr-tabindex
Особенно зеленые примечания.
Я так понял, что если не задано значение атрибута, то "пользовательский агент" (браузер) сам решает быть ли элементу фокусируемым и участвовать ли в последовательной навигации (с помощью Tab).
Во втором замечании говорится, что даже если tabIndex = -1 то браузер вправе игнорировать требование, что элемент не участвует в последовательной навигации, т.к. это единственный способ добраться до элемента управления, если у пользователя нет мыши.
Короче полная хрень и неопределенность с этим tabindex.
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
05.10.2023, 00:06
|
![Аватар для Aetae](https://javascript.ru/forum/image.php?u=4993&dateline=1299014303) |
Тлен
|
|
Регистрация: 02.01.2010
Сообщений: 6,590
|
|
Ну, почему там -1 понятно: у div по умолачнию именно -1 т.к. div не табается.
Потом появилась надстройка в виде contentEditable и оноую сделали табаемой, но значения по умолчанию видать менять не стали. Подозреваю там пришлось бы много переделывать чисто в плане кода, чтоб оно зависело не только от тега но и от атрибута. ![](https://javascript.ru/forum/images/smilies/smile.gif)
__________________
29375, 35
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
05.10.2023, 16:56
|
![Аватар для ruslan_mart](https://javascript.ru/forum/image.php?u=20699&dateline=1502382178) |
Профессор
|
|
Регистрация: 30.04.2012
Сообщений: 3,018
|
|
Если кому-то нужно, то вот мой костыль:
function getRealTabIndex(element: HTMLElement) {
if (element.isContentEditable && element.tabIndex === -1 && !element.hasAttribute('tabindex')) {
return 0;
}
return element.tabIndex;
}
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
08.10.2023, 16:40
|
![Аватар для ruslan_mart](https://javascript.ru/forum/image.php?u=20699&dateline=1502382178) |
Профессор
|
|
Регистрация: 30.04.2012
Сообщений: 3,018
|
|
С ссылками (якорями) похожая история, кстати.
Ссылка по умолчанию имеет tabindex 0, но при этом, если отсутствует `href`, то она не табается, не смотря на то, что tabindex все еще 0. Но если явно его переопределить, то все начинает работать правильно.
<a id="link" href="">Табается</a>
<script>
console.log(link.tabIndex); // 0
</script>
<a id="link">Не табается</a>
<script>
console.log(link.tabIndex); // 0
</script>
<a id="link">табается</a>
<script>
console.log(link.tabIndex); // 0
link.tabIndex = link.tabIndex;
</script>
------
Угадайте какой tabIndex будет здесь
<a contenteditable="true">123</a>
------
Финальный костыль:
function getRealTabIndex(element: HTMLElement) {
// 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') &&
!element.hasAttribute('tabindex')
) {
return -1;
}
// 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 && element.tabIndex === -1 && !element.hasAttribute('tabindex')) {
return 0;
}
return element.tabIndex;
}
Последний раз редактировалось ruslan_mart, 08.10.2023 в 16:50.
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
09.10.2023, 00:20
|
![Аватар для Aetae](https://javascript.ru/forum/image.php?u=4993&dateline=1299014303) |
Тлен
|
|
Регистрация: 02.01.2010
Сообщений: 6,590
|
|
ruslan_mart, твой костыль не учитывает тобой же приведённый вариант <a contentEditable>. ![](https://javascript.ru/forum/images/smilies/smile.gif)
Как-то так оно должно выглядеть:
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;
}
Но чё-то мне кажется что как-то проще оно должно быть...
__________________
29375, 35
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
09.10.2023, 00:27
|
![Аватар для Aetae](https://javascript.ru/forum/image.php?u=4993&dateline=1299014303) |
Тлен
|
|
Регистрация: 02.01.2010
Сообщений: 6,590
|
|
В jquery ui есть псевдоселектор :tabbable, т.к. либа древняя там должны были бы учесть все нюансы, можно посмотреть на их код и адапртировакт к себе: tabbable < focusable < visible. ![](https://javascript.ru/forum/images/smilies/smile.gif)
__________________
29375, 35
Последний раз редактировалось Aetae, 09.10.2023 в 00:37.
|
|
![Старый](/forum/images/ca_serenity/statusicon/post_old.gif)
11.10.2023, 11:59
|
![Аватар для ruslan_mart](https://javascript.ru/forum/image.php?u=20699&dateline=1502382178) |
Профессор
|
|
Регистрация: 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. И тут уже совершать магию ![](https://javascript.ru/forum/images/smilies/smile.gif) При потере фокуса табом на текущем элементе, принудительно фокусироваться на первом элементе в 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.
|
|
|
|