Skip to content

Commit

Permalink
Refactor: tweaked focus handling
Browse files Browse the repository at this point in the history
  • Loading branch information
orestbida committed Oct 21, 2023
1 parent 0a449c0 commit f318393
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 24 deletions.
18 changes: 14 additions & 4 deletions src/core/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
toggleDisableInteraction,
fireEvent,
getKeys,
focusAfterTransition,
deepCopy
} from '../utils/general';

Expand Down Expand Up @@ -197,13 +198,17 @@ export const show = (createModal) => {
if(_state._disablePageInteraction)
toggleDisableInteraction(true);

focusAfterTransition(_dom._cm, 1);

addClass(_dom._htmlDom, TOGGLE_CONSENT_MODAL_CLASS);
setAttribute(_dom._cm, ARIA_HIDDEN, 'false');

/**
* Set focus to consentModal
*/
focus(_dom._cmContainer, 1);
setTimeout(() => {
focus(globalObj._dom._cmDivTabindex, 1);
}, 100);

_log('CookieConsent [TOGGLE]: show consentModal');

Expand Down Expand Up @@ -257,8 +262,6 @@ export const showPreferences = () => {
createPreferencesModal(miniAPI, createMainContainer);

state._preferencesModalVisible = true;
addClass(globalObj._dom._htmlDom, TOGGLE_PREFERENCES_MODAL_CLASS);
setAttribute(globalObj._dom._pm, ARIA_HIDDEN, 'false');

// If there is no consent-modal, keep track of the last focused elem.
if(!state._consentModalVisible){
Expand All @@ -267,10 +270,17 @@ export const showPreferences = () => {
state._lastFocusedModalElement = getActiveElement();
}

focusAfterTransition(globalObj._dom._pm, 2);

addClass(globalObj._dom._htmlDom, TOGGLE_PREFERENCES_MODAL_CLASS);
setAttribute(globalObj._dom._pm, ARIA_HIDDEN, 'false');

/**
* Set focus to preferencesModal
*/
focus(globalObj._dom._pmContainer, 2);
setTimeout(() => {
focus(globalObj._dom._pmDivTabindex, 2);
}, 100);

_log('CookieConsent [TOGGLE]: show preferencesModal');

Expand Down
16 changes: 6 additions & 10 deletions src/core/modals/consentModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export const createConsentModal = (api, createMainContainer) => {
addClassCm(dom._cmTexts, 'texts');
addClassCm(dom._cmBtns, 'btns');

dom._cmContainer.tabIndex = -1;
setAttribute(dom._cm, 'role', 'dialog');
setAttribute(dom._cm, 'aria-modal', 'true');
setAttribute(dom._cm, ARIA_HIDDEN, 'false');
Expand All @@ -104,11 +103,6 @@ export const createConsentModal = (api, createMainContainer) => {
else if(consentModalTitleValue)
setAttribute(dom._cm, 'aria-labelledby', 'cm__title');

/**
* Make modal by default hidden to prevent weird page jumps/flashes (shown only once css is loaded)
*/
dom._cm.style.visibility = 'hidden';

const
boxLayout = 'box',
guiOptions = state._userConfig.guiOptions,
Expand Down Expand Up @@ -140,17 +134,19 @@ export const createConsentModal = (api, createMainContainer) => {
if(acceptAllBtnData || acceptNecessaryBtnData || showPreferencesBtnData)
appendChild(dom._cmBody, dom._cmBtns);

dom._cmDivTabindex = createNode(DIV_TAG);
setAttribute(dom._cmDivTabindex, 'tabIndex', -1);
appendChild(dom._cm, dom._cmDivTabindex);

appendChild(dom._cm, dom._cmBody);
appendChild(dom._cmContainer, dom._cm);
}

if(consentModalTitleValue){

if(!dom._cmTitle){
dom._cmTitle = createNode(DIV_TAG);
dom._cmTitle = createNode('h2');
dom._cmTitle.className = dom._cmTitle.id = 'cm__title';
setAttribute(dom._cmTitle, 'role', 'heading');
setAttribute(dom._cmTitle, 'aria-level', '2');
appendChild(dom._cmTexts, dom._cmTitle);
}

Expand All @@ -170,7 +166,7 @@ export const createConsentModal = (api, createMainContainer) => {
}

if(!dom._cmDescription){
dom._cmDescription = createNode(DIV_TAG);
dom._cmDescription = createNode('p');
dom._cmDescription.className = dom._cmDescription.id = 'cm__desc';
appendChild(dom._cmTexts, dom._cmDescription);
}
Expand Down
12 changes: 6 additions & 6 deletions src/core/modals/preferencesModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export const createPreferencesModal = (api, createMainContainer) => {
// modal container
dom._pmContainer = createNode(DIV_TAG);
addClass(dom._pmContainer, 'pm-wrapper');
dom._pmContainer.tabIndex = -1;

// modal overlay
const pmOverlay = createNode('div');
Expand All @@ -93,7 +92,6 @@ export const createPreferencesModal = (api, createMainContainer) => {

// preferences modal
dom._pm = createNode(DIV_TAG);
dom._pm.style.visibility = 'hidden';

addClass(dom._pm, 'pm');
setAttribute(dom._pm, 'role', 'dialog');
Expand All @@ -110,11 +108,9 @@ export const createPreferencesModal = (api, createMainContainer) => {
dom._pmHeader = createNode(DIV_TAG);
addClassPm(dom._pmHeader, 'header');

dom._pmTitle = createNode(DIV_TAG);
dom._pmTitle = createNode('h2');
addClassPm(dom._pmTitle, 'title');
dom._pmTitle.id = 'pm__title';
setAttribute(dom._pmTitle, 'role', 'heading');
setAttribute(dom._pmTitle, 'aria-level', '2');

dom._pmCloseBtn = createNode(BUTTON_TAG);
addClassPm(dom._pmCloseBtn, 'close-btn');
Expand Down Expand Up @@ -145,6 +141,10 @@ export const createPreferencesModal = (api, createMainContainer) => {
appendChild(dom._pmHeader, dom._pmTitle);
appendChild(dom._pmHeader, dom._pmCloseBtn);

dom._pmDivTabindex = createNode(DIV_TAG);
setAttribute(dom._pmDivTabindex, 'tabIndex', -1);
appendChild(dom._pm, dom._pmDivTabindex);

appendChild(dom._pm, dom._pmHeader);
appendChild(dom._pm, dom._pmBody);

Expand Down Expand Up @@ -305,7 +305,7 @@ export const createPreferencesModal = (api, createMainContainer) => {
}

if(sDescriptionData){
var sDesc = createNode(DIV_TAG);
var sDesc = createNode('p');
addClassPm(sDesc, 'section-desc');

sDesc.innerHTML = sDescriptionData;
Expand Down
2 changes: 2 additions & 0 deletions src/scss/core/_reset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
div,
span,
a,
h2,
p,
button,
input,
::before,
Expand Down
31 changes: 27 additions & 4 deletions src/utils/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ export const uuidv4 = () => {
* Add event listener to dom object (cross browser function)
* @param {Element} elem
* @param {keyof WindowEventMap} event
* @param {EventListenerOrEventListenerObject} fn
* @param {EventListener} fn
* @param {boolean} [saveListener]
*/
export const addEvent = (elem, event, fn, saveListener) => {
Expand Down Expand Up @@ -741,8 +741,8 @@ export const focus = (el, modalId, toggleTabIndex) => {

if(modalId) {
globalObj._state._currentFocusedModal = modalId === 1
? globalObj._dom._cmContainer
: globalObj._dom._pmContainer;
? globalObj._dom._cm
: globalObj._dom._pm;

globalObj._state._currentFocusEdges = modalId === 1
? globalObj._state._cmFocusableElements
Expand All @@ -756,6 +756,26 @@ export const focus = (el, modalId, toggleTabIndex) => {
toggleTabIndex && (el && el.removeAttribute('tabindex'));
};

/**
* @param {HTMLDivElement} element
* @param {1 | 2} modalId
*/
export const focusAfterTransition = (element, modalId) => {

const getVisibleDiv = (modalId) => modalId === 1
? globalObj._dom._cmDivTabindex
: globalObj._dom._pmDivTabindex;

const setFocus = (event) => {
event.target.removeEventListener('transitionend', setFocus);
if (event.propertyName === 'opacity' && getComputedStyle(element).opacity === '1') {
focus(getVisibleDiv(modalId), modalId);
}
};

addEvent(element, 'transitionend', setFocus);
};

/**
* Obtain accepted and rejected categories
* @returns {{accepted: string[], rejected: string[]}}
Expand Down Expand Up @@ -818,8 +838,11 @@ export const handleFocusTrap = () => {

const dom = globalObj._dom;
const state = globalObj._state;
const trapFocusScope = globalObj._state._userConfig.disablePageInteraction
? dom._htmlDom
: dom._ccMain;

addEvent(dom._htmlDom, 'keydown', (e) => {
addEvent(trapFocusScope, 'keydown', (e) => {

if(e.key !== 'Tab')
return;
Expand Down
21 changes: 21 additions & 0 deletions tests/gui.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { globalObj } from "../src/core/global";
import { getActiveElement } from "../src/utils/general";
import * as CookieConsent from "../src/index"
import testConfig from "./config/full-config";

Expand Down Expand Up @@ -131,6 +132,26 @@ describe("Test UI options", () =>{
expect(classList.contains('cm--box')).toBe(true);
expect(classList2.contains('pm--box')).toBe(true);
});

it('consentModal should receive focus when it is shown', async () => {
await api.run(testConfig);
const prevActiveElement = getActiveElement();
api.show();
await new Promise(r => setTimeout(r, 300));
const currActiveElement = getActiveElement();
expect(currActiveElement).not.toBe(prevActiveElement);
expect(document.querySelector('.cm > div[tabIndex="-1"]')).toBe(currActiveElement);
});

it('preferencesModal should receive focus when it is shown', async () => {
await api.run(testConfig);
const prevActiveElement = getActiveElement();
api.showPreferences();
await new Promise(r => setTimeout(r, 300));
const currActiveElement = getActiveElement();
expect(currActiveElement).not.toBe(prevActiveElement);
expect(document.querySelector('.pm > div[tabIndex="-1"]')).toBe(currActiveElement);
});
})

function getModalClassList(selector){
Expand Down

0 comments on commit f318393

Please sign in to comment.