diff --git a/src/js/code.ts b/src/js/code.ts index 34af04aa..84002483 100644 --- a/src/js/code.ts +++ b/src/js/code.ts @@ -59,7 +59,7 @@ if (typeof document !== 'undefined') { } copyToClipboard(code.innerText).then(() => { - notifySuccess(parent.querySelector('.yfm-clipboard-button')); + notifySuccess(parent.querySelector('.yfm-clipboard-icon')); }); }); } 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..a30d080f 100644 --- a/src/js/term/utils.ts +++ b/src/js/term/utils.ts @@ -112,19 +112,19 @@ export function setDefinitionPosition( } function termOnResize() { - const openDefinition = document.getElementsByClassName(openDefinitionClass)[0] as HTMLElement; + 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) { return; } - setDefinitionPosition(openDefinition, termElement); + setDefinitionPosition(openedDefinition, termElement); } function termParentElement(term: HTMLElement | null) { @@ -137,16 +137,57 @@ function termParentElement(term: HTMLElement | null) { return closestScrollableParent || term.parentElement; } +export function openDefinition(target: HTMLElement) { + const openedDefinition = 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 = openedDefinition && termId === openedDefinition.getAttribute('term-id'); + if (isSameTerm) { + closeDefinition(openedDefinition); + return; + } + + const isTargetDefinitionContent = target.closest( + [Selector.CONTENT.replace(' ', ''), openClass].join('.'), + ); + + if (openedDefinition && !isTargetDefinitionContent) { + closeDefinition(openedDefinition); + } + + 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 +208,32 @@ 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] as HTMLElement; + const lastFocusableElement = focusableElements[focusableElements.length - 1] as HTMLElement; + + if (firstFocusableElement) { + firstFocusableElement.focus(); + } + + element.addEventListener('keydown', function (e) { + const isTabPressed = e.key === 'Tab' || e.keyCode === 9; + if (!isTabPressed) { + return; + } + + if (e.shiftKey) { + if (document.activeElement === firstFocusableElement) { + lastFocusableElement.focus(); + e.preventDefault(); + } + } else if (document.activeElement === lastFocusableElement) { + firstFocusableElement.focus(); + e.preventDefault(); + } + }); +} diff --git a/src/scss/_anchor.scss b/src/scss/_anchor.scss index 3905cffd..8b9df03c 100644 --- a/src/scss/_anchor.scss +++ b/src/scss/_anchor.scss @@ -9,11 +9,17 @@ text-align: center; font-size: 18px; - } - .yfm-anchor::before { - content: '#'; - opacity: 0; + &:focus { + &::before { + opacity: 1; + } + } + + &::before { + content: '#'; + opacity: 0; + } } &:hover .yfm-anchor::before { diff --git a/src/scss/_code.scss b/src/scss/_code.scss index a1a3b58d..a5b1b89d 100644 --- a/src/scss/_code.scss +++ b/src/scss/_code.scss @@ -2,22 +2,35 @@ &-clipboard { position: relative; - &:hover &-button { - display: block; + &-button { + &:hover, &:focus { + opacity: 1; + } } & > pre { border-radius: 10px; overflow: hidden; } - } - &-clipboard-button { - display: none; - position: absolute; - cursor: pointer; - top: 16px; - right: 16px; - z-index: 1; + &-button { + position: absolute; + top: 16px; + right: 16px; + z-index: 1; + opacity: 0; + + //reset default button style + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + } + + &-icon { + pointer-events: none; + } } } diff --git a/src/transform/plugins/code.ts b/src/transform/plugins/code.ts index 49405f0e..e3b76306 100644 --- a/src/transform/plugins/code.ts +++ b/src/transform/plugins/code.ts @@ -6,39 +6,41 @@ import {generateID} from './utils'; const wrapInClipboard = (element: string | undefined, id: number) => { return `
- ${element} - - - - - - - + ${element} +
`; }; 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); diff --git a/test/__snapshots__/term.test.ts.snap b/test/__snapshots__/term.test.ts.snap new file mode 100644 index 00000000..049041e1 --- /dev/null +++ b/test/__snapshots__/term.test.ts.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Terms Should create term in code with definition template 1`] = ` +"

Web

+ +
+
HTML: Lorem
+
+ + +
+" +`; + +exports[`Terms Should create term in table with definition template 1`] = ` +"

Web

+ + + + + + + + + + + + + +
LanguageInitial release
HTML1993
+" +`; + +exports[`Terms Should create term in text with definition template 1`] = ` +"

Web

+

The HTML specification

+" +`; + +exports[`Terms Term should use content from include 1`] = ` +"

Web

+

The HTML specification

+" +`; diff --git a/test/term.test.ts b/test/term.test.ts index 16685533..301c297d 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -32,11 +32,7 @@ describe('Terms', () => { const input = readFileSync(inputPath, 'utf8'); const result = transformYfm(input, inputPath); - expect(clearRandomId(result)).toEqual(`\ -

Web

-

The HTML specification

-`); + expect(clearRandomId(result)).toMatchSnapshot(); }); test('Should create term in table with definition template', () => { @@ -44,24 +40,7 @@ describe('Terms', () => { const input = readFileSync(inputPath, 'utf8'); const result = transformYfm(input, inputPath); - expect(clearRandomId(result)).toEqual(`\ -

Web

- - - - - - - - - - - - - -
LanguageInitial release
HTML1993
-`); + expect(clearRandomId(result)).toMatchSnapshot(); }); test('Should create term in code with definition template', () => { @@ -69,23 +48,7 @@ describe('Terms', () => { const input = readFileSync(inputPath, 'utf8'); const result = transformYfm(input, inputPath); - expect(clearRandomId(result)).toEqual(`\ -

Web

- -
-
HTML: Lorem
-
- - - - - - - - -
-`); + expect(clearRandomId(result)).toMatchSnapshot(); }); test('Term should use content from include', () => { @@ -93,10 +56,6 @@ describe('Terms', () => { const input = readFileSync(inputPath, 'utf8'); const result = transformYfm(input, inputPath); - expect(clearRandomId(result)).toEqual(`\ -

Web

-

The HTML specification

-`); + expect(clearRandomId(result)).toMatchSnapshot(); }); });