/*! DSFR v1.11.2 | SPDX-License-Identifier: MIT | License-Filename: LICENSE.md | restricted use (see terms and conditions) */ const config = { prefix: 'fr', namespace: 'dsfr', organisation: '@gouvfr', version: '1.11.2' }; const api = window[config.namespace]; const ModalSelector = { MODAL: api.internals.ns.selector('modal'), SCROLL_DIVIDER: api.internals.ns.selector('scroll-divider'), BODY: api.internals.ns.selector('modal__body'), TITLE: api.internals.ns.selector('modal__title') }; class ModalButton extends api.core.DisclosureButton { constructor () { super(api.core.DisclosureType.OPENED); } static get instanceClassName () { return 'ModalButton'; } } const ModalAttribute = { CONCEALING_BACKDROP: api.internals.ns.attr('concealing-backdrop') }; class Modal extends api.core.Disclosure { constructor () { super(api.core.DisclosureType.OPENED, ModalSelector.MODAL, ModalButton, 'ModalsGroup'); this._isActive = false; this.scrolling = this.resize.bind(this, false); this.resizing = this.resize.bind(this, true); } static get instanceClassName () { return 'Modal'; } init () { super.init(); this._isDialog = this.node.tagName === 'DIALOG'; this.isScrolling = false; this.listenClick(); this.addEmission(api.core.RootEmission.KEYDOWN, this._keydown.bind(this)); } _keydown (keyCode) { switch (keyCode) { case api.core.KeyCodes.ESCAPE: this._escape(); break; } } // TODO v2 : passer les tagName d'action en constante _escape () { const tagName = document.activeElement ? document.activeElement.tagName : undefined; switch (tagName) { case 'INPUT': case 'LABEL': case 'TEXTAREA': case 'SELECT': case 'AUDIO': case 'VIDEO': break; default: if (this.isDisclosed) { this.conceal(); this.focus(); } } } retrieved () { this._ensureAccessibleName(); } get body () { return this.element.getDescendantInstances('ModalBody', 'Modal')[0]; } handleClick (e) { if (e.target === this.node && this.getAttribute(ModalAttribute.CONCEALING_BACKDROP) !== 'false') this.conceal(); } disclose (withhold) { if (!super.disclose(withhold)) return false; if (this.body) this.body.activate(); this.isScrollLocked = true; this.setAttribute('aria-modal', 'true'); this.setAttribute('open', 'true'); if (!this._isDialog) { this.activateModal(); } return true; } conceal (withhold, preventFocus) { if (!super.conceal(withhold, preventFocus)) return false; this.isScrollLocked = false; this.removeAttribute('aria-modal'); this.removeAttribute('open'); if (this.body) this.body.deactivate(); if (!this._isDialog) { this.deactivateModal(); } return true; } get isDialog () { return this._isDialog; } set isDialog (value) { this._isDialog = value; } activateModal () { if (this._isActive) return; this._isActive = true; this._hasDialogRole = this.getAttribute('role') === 'dialog'; if (!this._hasDialogRole) this.setAttribute('role', 'dialog'); } deactivateModal () { if (!this._isActive) return; this._isActive = false; if (!this._hasDialogRole) this.removeAttribute('role'); } _setAccessibleName (node, append) { const id = this.retrieveNodeId(node, append); this.warn(`add reference to ${append} for accessible name (aria-labelledby)`); this.setAttribute('aria-labelledby', id); } _ensureAccessibleName () { if (this.hasAttribute('aria-labelledby') || this.hasAttribute('aria-label')) return; this.warn('missing accessible name'); const title = this.node.querySelector(ModalSelector.TITLE); const primary = this.primaryButtons[0]; switch (true) { case title !== null: this._setAccessibleName(title, 'title'); break; case primary !== undefined: this.warn('missing required title, fallback to primary button'); this._setAccessibleName(primary, 'primary'); break; } } } const unordereds = [ '[tabindex="0"]', 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', 'details>summary:first-of-type', 'details', 'iframe' ]; const UNORDEREDS = unordereds.join(); const ordereds = [ '[tabindex]:not([tabindex="-1"]):not([tabindex="0"])' ]; const ORDEREDS = ordereds.join(); const isFocusable = (element, container) => { if (!(element instanceof Element)) return false; const style = window.getComputedStyle(element); if (!style) return false; if (style.visibility === 'hidden') return false; if (container === undefined) container = element; while (container.contains(element)) { if (style.display === 'none') return false; element = element.parentElement; } return true; }; class FocusTrap { constructor (onTrap, onUntrap) { this.element = null; this.activeElement = null; this.onTrap = onTrap; this.onUntrap = onUntrap; this.waiting = this.wait.bind(this); this.handling = this.handle.bind(this); this.focusing = this.maintainFocus.bind(this); this.current = null; } get trapped () { return this.element !== null; } trap (element) { if (this.trapped) this.untrap(); this.element = element; this.isTrapping = true; this.wait(); if (this.onTrap) this.onTrap(); } wait () { if (!isFocusable(this.element)) { window.requestAnimationFrame(this.waiting); return; } this.trapping(); } trapping () { if (!this.isTrapping) return; this.isTrapping = false; const focusables = this.focusables; if (focusables.length && focusables.indexOf(document.activeElement) === -1) focusables[0].focus(); this.element.setAttribute('aria-modal', true); window.addEventListener('keydown', this.handling); document.body.addEventListener('focus', this.focusing, true); } stun (node) { for (const child of node.children) { if (child === this.element) continue; if (child.contains(this.element)) { this.stun(child); continue; } this.stunneds.push(new Stunned(child)); } } maintainFocus (event) { if (!this.element.contains(event.target)) { const focusables = this.focusables; if (focusables.length === 0) return; const first = focusables[0]; event.preventDefault(); first.focus(); } } handle (e) { if (e.keyCode !== 9) return; const focusables = this.focusables; if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; const index = focusables.indexOf(document.activeElement); if (e.shiftKey) { if (!this.element.contains(document.activeElement) || index < 1) { e.preventDefault(); last.focus(); } else if (document.activeElement.tabIndex > 0 || focusables[index - 1].tabIndex > 0) { e.preventDefault(); focusables[index - 1].focus(); } } else { if (!this.element.contains(document.activeElement) || index === focusables.length - 1 || index === -1) { e.preventDefault(); first.focus(); } else if (document.activeElement.tabIndex > 0) { e.preventDefault(); focusables[index + 1].focus(); } } } get focusables () { let unordereds = api.internals.dom.querySelectorAllArray(this.element, UNORDEREDS); /** * filtrage des radiobutttons de même name (la navigations d'un groupe de radio se fait à la flèche et non pas au tab **/ const radios = api.internals.dom.querySelectorAllArray(document.documentElement, 'input[type="radio"]'); if (radios.length) { const groups = {}; for (const radio of radios) { const name = radio.getAttribute('name'); if (groups[name] === undefined) groups[name] = new RadioButtonGroup(name); groups[name].push(radio); } unordereds = unordereds.filter((unordered) => { if (unordered.tagName.toLowerCase() !== 'input' || unordered.getAttribute('type').toLowerCase() !== 'radio') return true; const name = unordered.getAttribute('name'); return groups[name].keep(unordered); }); } const ordereds = api.internals.dom.querySelectorAllArray(this.element, ORDEREDS); ordereds.sort((a, b) => a.tabIndex - b.tabIndex); const noDuplicates = unordereds.filter((element) => ordereds.indexOf(element) === -1); const concateneds = ordereds.concat(noDuplicates); return concateneds.filter((element) => element.tabIndex !== '-1' && isFocusable(element, this.element)); } untrap () { if (!this.trapped) return; this.isTrapping = false; this.element.removeAttribute('aria-modal'); window.removeEventListener('keydown', this.handling); document.body.removeEventListener('focus', this.focusing, true); this.element = null; if (this.onUntrap) this.onUntrap(); } dispose () { this.untrap(); } } class Stunned { constructor (element) { this.element = element; // this.hidden = element.getAttribute('aria-hidden'); this.inert = element.getAttribute('inert'); // this.element.setAttribute('aria-hidden', true); this.element.setAttribute('inert', ''); } unstun () { /* if (this.hidden === null) this.element.removeAttribute('aria-hidden'); else this.element.setAttribute('aria-hidden', this.hidden); */ if (this.inert === null) this.element.removeAttribute('inert'); else this.element.setAttribute('inert', this.inert); } } class RadioButtonGroup { constructor (name) { this.name = name; this.buttons = []; } push (button) { this.buttons.push(button); if (button === document.activeElement || button.checked || this.selected === undefined) this.selected = button; } keep (button) { return this.selected === button; } } class ModalsGroup extends api.core.DisclosuresGroup { constructor () { super('Modal', false); this.focusTrap = new FocusTrap(); } static get instanceClassName () { return 'ModalsGroup'; } apply (value, initial) { super.apply(value, initial); if (this.current === null) this.focusTrap.untrap(); else this.focusTrap.trap(this.current.node); } } const OFFSET = 32; // 32px => 8v => 2rem class ModalBody extends api.core.Instance { static get instanceClassName () { return 'ModalBody'; } init () { this.listen('scroll', this.divide.bind(this)); } activate () { this.isResizing = true; this.resize(); } deactivate () { this.isResizing = false; } divide () { if (this.node.scrollHeight > this.node.clientHeight) { if (this.node.offsetHeight + this.node.scrollTop >= this.node.scrollHeight) { this.removeClass(ModalSelector.SCROLL_DIVIDER); } else { this.addClass(ModalSelector.SCROLL_DIVIDER); } } else { this.removeClass(ModalSelector.SCROLL_DIVIDER); } } resize () { this.adjust(); this.request(this.adjust.bind(this)); } adjust () { const offset = OFFSET * (this.isBreakpoint(api.core.Breakpoints.MD) ? 2 : 1); if (this.isLegacy) this.style.maxHeight = `${window.innerHeight - offset}px`; else this.style.setProperty('--modal-max-height', `${window.innerHeight - offset}px`); this.divide(); } } api.modal = { Modal: Modal, ModalButton: ModalButton, ModalBody: ModalBody, ModalsGroup: ModalsGroup, ModalSelector: ModalSelector }; api.internals.register(api.modal.ModalSelector.MODAL, api.modal.Modal); api.internals.register(api.modal.ModalSelector.BODY, api.modal.ModalBody); api.internals.register(api.core.RootSelector.ROOT, api.modal.ModalsGroup); //# sourceMappingURL=modal.module.js.map