diff --git a/src/js/term/index.ts b/src/js/term/index.ts index a0ea4860..b7ac45d0 100644 --- a/src/js/term/index.ts +++ b/src/js/term/index.ts @@ -1,81 +1,50 @@ +import {getEventTarget, isCustom} from '../utils'; import { - Selector, closeDefinition, - createDefinitionElement, openClass, + openDefinition, openDefinitionClass, - setDefinitionId, setDefinitionPosition, - setDefinitonAriaLive, } from './utils'; -import {getEventTarget, isCustom} from '../utils'; if (typeof document !== 'undefined') { document.addEventListener('click', (event) => { - const openDefinition = document.getElementsByClassName( - openDefinitionClass, - )[0] as HTMLElement; - const target = getEventTarget(event) as HTMLElement; - - const termId = target.getAttribute('id'); - const termKey = target.getAttribute('term-key'); - let definitionElement = document.getElementById(termKey + '_element'); - - if (termKey && !definitionElement) { - definitionElement = createDefinitionElement(target); - } - - const isSameTerm = openDefinition && termId === openDefinition.getAttribute('term-id'); - if (isSameTerm) { - closeDefinition(openDefinition); - return; - } - - const isTargetDefinitionContent = target.closest( - [Selector.CONTENT.replace(' ', ''), openClass].join('.'), - ); - - if (openDefinition && !isTargetDefinitionContent) { - closeDefinition(openDefinition); - } - - if (isCustom(event) || !target.matches(Selector.TITLE) || !definitionElement) { - return; + if (getEventTarget(event) || !isCustom(event)) { + openDefinition(getEventTarget(event) as HTMLElement); } - - setDefinitionId(definitionElement, target); - setDefinitonAriaLive(definitionElement, target); - setDefinitionPosition(definitionElement, target); - - definitionElement.classList.toggle(openClass); }); document.addEventListener('keydown', (event) => { - const openDefinition = document.getElementsByClassName( + const openedDefinition = document.getElementsByClassName( openDefinitionClass, )[0] as HTMLElement; - if (event.key === 'Escape' && openDefinition) { - closeDefinition(openDefinition); + + if (event.key === 'Enter' && document.activeElement) { + openDefinition(document.activeElement as HTMLElement); + } + + if (event.key === 'Escape' && openedDefinition) { + closeDefinition(openedDefinition); } }); window.addEventListener('resize', () => { - const openDefinition = document.getElementsByClassName( + const openedDefinition = document.getElementsByClassName( openDefinitionClass, )[0] as HTMLElement; - if (!openDefinition) { + if (!openedDefinition) { return; } - const termId = openDefinition.getAttribute('term-id') || ''; + const termId = openedDefinition.getAttribute('term-id') || ''; const termElement = document.getElementById(termId); if (!termElement) { - openDefinition.classList.toggle(openClass); + openedDefinition.classList.toggle(openClass); return; } - setDefinitionPosition(openDefinition, termElement); + setDefinitionPosition(openedDefinition, termElement); }); } diff --git a/src/js/term/utils.ts b/src/js/term/utils.ts index db95e6c7..302f971d 100644 --- a/src/js/term/utils.ts +++ b/src/js/term/utils.ts @@ -137,16 +137,95 @@ function termParentElement(term: HTMLElement | null) { return closestScrollableParent || term.parentElement; } +export function openDefinition(target: HTMLElement) { + const openDefinition = document.getElementsByClassName(openDefinitionClass)[0] as HTMLElement; + + const termId = target.getAttribute('id'); + const termKey = target.getAttribute('term-key'); + let definitionElement = document.getElementById(termKey + '_element'); + + if (termKey && !definitionElement) { + definitionElement = createDefinitionElement(target); + } + + const isSameTerm = openDefinition && termId === openDefinition.getAttribute('term-id'); + if (isSameTerm) { + closeDefinition(openDefinition); + return; + } + + const isTargetDefinitionContent = target.closest( + [Selector.CONTENT.replace(' ', ''), openClass].join('.'), + ); + + if (openDefinition && !isTargetDefinitionContent) { + closeDefinition(openDefinition); + } + + if (!target.matches(Selector.TITLE) || !definitionElement) { + return; + } + + setDefinitionId(definitionElement, target); + setDefinitonAriaLive(definitionElement, target); + setDefinitionPosition(definitionElement, target); + + definitionElement.classList.toggle(openClass); + + trapFocus(definitionElement); +} + +export function openDefinition2(target: HTMLElement) { + const openDefinition = document.getElementsByClassName(openDefinitionClass)[0] as HTMLElement; + + const termId = target.getAttribute('id'); + const termKey = target.getAttribute('term-key'); + let definitionElement = document.getElementById(termKey + '_element'); + + if (termKey && !definitionElement) { + definitionElement = createDefinitionElement(target); + } + + const isSameTerm = openDefinition && termId === openDefinition.getAttribute('term-id'); + if (isSameTerm) { + closeDefinition(openDefinition); + return; + } + + const isTargetDefinitionContent = target.closest( + [Selector.CONTENT.replace(' ', ''), openClass].join('.'), + ); + + if (openDefinition && !isTargetDefinitionContent) { + closeDefinition(openDefinition); + } + + if (!target.matches(Selector.TITLE) || !definitionElement) { + return; + } + + setDefinitionId(definitionElement, target); + setDefinitonAriaLive(definitionElement, target); + setDefinitionPosition(definitionElement, target); + + definitionElement.classList.toggle(openClass); + + trapFocus(definitionElement); +} + export function closeDefinition(definition: HTMLElement) { definition.classList.remove(openClass); const termId = definition.getAttribute('term-id') || ''; - const termParent = termParentElement(document.getElementById(termId)); + const term = document.getElementById(termId); + const termParent = termParentElement(term); if (!termParent) { return; } termParent.removeEventListener('scroll', termOnResize); + term?.focus(); // Set focus back to open button after closing popup + isListenerNeeded = true; } @@ -167,3 +246,35 @@ function getCoords(elem: HTMLElement) { return {top: Math.round(top), left: Math.round(left)}; } + +export function trapFocus(element: HTMLElement) { + const focusableElements = element.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ); + const firstFocusableElement = focusableElements[0]; + const lastFocusableElement = focusableElements[focusableElements.length - 1]; + + firstFocusableElement?.focus(); + + element.addEventListener('keydown', function (e) { + const isTabPressed = e.key === 'Tab' || e.keyCode === 9; + if (!isTabPressed) { + return; + } + + if (e.shiftKey) { + // if shift key pressed for shift + tab combination + if (document.activeElement === firstFocusableElement) { + lastFocusableElement.focus(); // add focus for the last focusable element + e.preventDefault(); + } + } else { + // if tab key is pressed + if (document.activeElement === lastFocusableElement) { + // if focused has reached to last focusable element then focus first focusable element after pressing tab + firstFocusableElement.focus(); // add focus for the first focusable element + e.preventDefault(); + } + } + }); +} diff --git a/src/transform/plugins/term/index.ts b/src/transform/plugins/term/index.ts index d4ab68e4..0eab9ad7 100644 --- a/src/transform/plugins/term/index.ts +++ b/src/transform/plugins/term/index.ts @@ -100,6 +100,7 @@ const term: MarkdownItPluginCb = (md, options) => { token.attrSet('class', 'yfm yfm-term_title'); token.attrSet('term-key', ':' + termKey); token.attrSet('aria-describedby', ':' + termKey + '_element'); + token.attrSet('tabindex', '0'); token.attrSet('id', generateID()); nodes.push(token);