Skip to content

Commit

Permalink
feat: improve focus behavior on terms
Browse files Browse the repository at this point in the history
  • Loading branch information
martyanovandrey committed Aug 19, 2024
1 parent c81ad73 commit a64f1ec
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 49 deletions.
65 changes: 17 additions & 48 deletions src/js/term/index.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
113 changes: 112 additions & 1 deletion src/js/term/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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) {

Check warning on line 273 in src/js/term/utils.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected if as the only statement in an else block
// 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();
}
}
});
}
1 change: 1 addition & 0 deletions src/transform/plugins/term/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit a64f1ec

Please sign in to comment.