Skip to content

Commit

Permalink
feat: improve focus behavior on terms, anchors, clipboard button (#486)
Browse files Browse the repository at this point in the history
* feat: improve focus behavior on terms

* refactor

* feat: change anchor opacity on focus

* feat: use button tag for clipboard button

* ref

* update snapshots

* snapshots for terms

* update snap

* fix ts erorr
  • Loading branch information
martyanovandrey authored Aug 20, 2024
1 parent 0e7a080 commit 4323b50
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 146 deletions.
2 changes: 1 addition & 1 deletion src/js/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ if (typeof document !== 'undefined') {
}

copyToClipboard(code.innerText).then(() => {
notifySuccess(parent.querySelector('.yfm-clipboard-button'));
notifySuccess(parent.querySelector('.yfm-clipboard-icon'));
});
});
}
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);
});
}
80 changes: 75 additions & 5 deletions src/js/term/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}

Expand All @@ -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();
}
});
}
14 changes: 10 additions & 4 deletions src/scss/_anchor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 23 additions & 10 deletions src/scss/_code.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
68 changes: 35 additions & 33 deletions src/transform/plugins/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,41 @@ import {generateID} from './utils';
const wrapInClipboard = (element: string | undefined, id: number) => {
return `
<div class="yfm-clipboard">
${element}
<svg width="16" height="16" viewBox="0 0 24 24" class="yfm-clipboard-button" data-animation="${id}">
<path
fill="currentColor"
d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"
/>
<path
stroke="currentColor"
fill="transparent"
stroke-width="1.5"
d="M9.5 13l3 3l5 -5"
visibility="hidden"
>
<animate
id="visibileAnimation-${id}"
attributeName="visibility"
from="hidden"
to="visible"
dur="0.2s"
fill="freeze"
begin=""
/>
<animate
id="hideAnimation-${id}"
attributeName="visibility"
from="visible"
to="hidden"
dur="1s"
begin="visibileAnimation-${id}.end+1"
fill="freeze"
/>
</path>
</svg>
${element}
<button class="yfm-clipboard-button">
<svg width="16" height="16" viewBox="0 0 24 24" class="yfm-clipboard-icon" data-animation="${id}">
<path
fill="currentColor"
d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"
/>
<path
stroke="currentColor"
fill="transparent"
stroke-width="1.5"
d="M9.5 13l3 3l5 -5"
visibility="hidden"
>
<animate
id="visibileAnimation-${id}"
attributeName="visibility"
from="hidden"
to="visible"
dur="0.2s"
fill="freeze"
begin=""
/>
<animate
id="hideAnimation-${id}"
attributeName="visibility"
from="visible"
to="hidden"
dur="1s"
begin="visibileAnimation-${id}.end+1"
fill="freeze"
/>
</path>
</svg>
</button>
</div>
`;
};
Expand Down
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
Loading

0 comments on commit 4323b50

Please sign in to comment.