diff --git a/.size-limit.json b/.size-limit.json index 9a06a8d143e..eeffea9ae9e 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,7 +2,7 @@ { "path": "dist/packages/ec/scripts/ecl-ec.js", "webpack": false, - "limit": "40 KB" + "limit": "41 KB" }, { "path": "dist/packages/ec/styles/ecl-ec.css", @@ -17,7 +17,7 @@ { "path": "dist/packages/eu/scripts/ecl-eu.js", "webpack": false, - "limit": "40 KB" + "limit": "41 KB" }, { "path": "dist/packages/eu/styles/ecl-eu.css", diff --git a/src/implementations/vanilla/components/popover/_popover.scss b/src/implementations/vanilla/components/popover/_popover.scss index 8606d59611d..6cf9c392c91 100644 --- a/src/implementations/vanilla/components/popover/_popover.scss +++ b/src/implementations/vanilla/components/popover/_popover.scss @@ -106,7 +106,7 @@ $_item-height: map.get(theme.$line-height-prolonged, 'm') + transform: none; &::before { - left: 25%; + left: var(--ecl-popover-position); } } @@ -116,6 +116,7 @@ $_item-height: map.get(theme.$line-height-prolonged, 'm') + transform: none; &::before { - left: 75%; + left: auto; + right: var(--ecl-popover-position); } } diff --git a/src/implementations/vanilla/components/popover/popover.js b/src/implementations/vanilla/components/popover/popover.js index badb1f53765..d262877f06a 100644 --- a/src/implementations/vanilla/components/popover/popover.js +++ b/src/implementations/vanilla/components/popover/popover.js @@ -5,6 +5,7 @@ import { queryOne } from '@ecl/dom-utils'; * @param {Object} options * @param {String} options.toggleSelector Selector for toggling element * @param {Boolean} options.attachClickListener Whether or not to bind click events on toggle + * @param {Boolean} options.attachKeyListener Whether or not to bind keyboard events */ export class Popover { /** @@ -27,6 +28,7 @@ export class Popover { { toggleSelector = '[data-ecl-popover-toggle]', attachClickListener = true, + attachKeyListener = true, } = {} ) { // Check element @@ -41,13 +43,19 @@ export class Popover { // Options this.toggleSelector = toggleSelector; this.attachClickListener = attachClickListener; + this.attachKeyListener = attachKeyListener; // Private variables this.toggle = null; this.target = null; // Bind `this` for use in callbacks + this.openPopover = this.openPopover.bind(this); + this.closePopover = this.closePopover.bind(this); + this.positionPopover = this.positionPopover.bind(this); this.handleClickOnToggle = this.handleClickOnToggle.bind(this); + this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this); + this.handleClickGlobal = this.handleClickGlobal.bind(this); } /** @@ -56,6 +64,14 @@ export class Popover { init() { this.toggle = queryOne(this.toggleSelector, this.element); + // Bind global events + if (this.attachKeyListener) { + document.addEventListener('keyup', this.handleKeyboardGlobal); + } + if (this.attachClickListener) { + document.addEventListener('click', this.handleClickGlobal); + } + // Get target element this.target = document.querySelector( `#${this.toggle.getAttribute('aria-controls')}` @@ -84,6 +100,14 @@ export class Popover { if (this.attachClickListener && this.toggle) { this.toggle.removeEventListener('click', this.handleClickOnToggle); } + + if (this.attachKeyListener) { + document.removeEventListener('keyup', this.handleKeyboardGlobal); + } + if (this.attachClickListener) { + document.removeEventListener('click', this.handleClickGlobal); + } + if (this.element) { this.element.removeAttribute('data-ecl-auto-initialized'); } @@ -101,13 +125,35 @@ export class Popover { const isExpanded = this.toggle.getAttribute('aria-expanded') === 'true'; // Toggle the popover - this.toggle.setAttribute('aria-expanded', isExpanded ? 'false' : 'true'); if (isExpanded) { - this.target.hidden = true; - } else { - this.target.hidden = false; + this.closePopover(); + return; } + this.openPopover(); + this.positionPopover(); + } + + /** + * Open the popover. + */ + openPopover() { + this.toggle.setAttribute('aria-expanded', 'true'); + this.target.hidden = false; + } + + /** + * Close the popover. + */ + closePopover() { + this.toggle.setAttribute('aria-expanded', 'false'); + this.target.hidden = true; + } + + /** + * Manage popover position. + */ + positionPopover() { // Check available space this.element.classList.remove('ecl-popover--top'); this.element.classList.remove('ecl-popover--push-left'); @@ -125,13 +171,54 @@ export class Popover { if (popoverRect.left < 0) { this.element.classList.add('ecl-popover--push-left'); + + // Adapt arrow position + this.target.style.setProperty( + '--ecl-popover-position', + `${toggleRect.width / 2}px` + ); } if (popoverRect.right > screenWidth) { this.element.classList.add('ecl-popover--push-right'); + + // Adapt arrow position + this.target.style.setProperty( + '--ecl-popover-position', + `calc(${toggleRect.width / 2}px - 0.5rem)` + ); + } + } + + /** + * Handles global keyboard events, triggered outside of the popover. + * + * @param {Event} e + */ + handleKeyboardGlobal(e) { + if (!this.target) return; + + // Detect press on Escape + if (e.key === 'Escape' || e.key === 'Esc') { + this.closePopover(); } + } - return this; + /** + * Handles global click events, triggered outside of the popover. + * + * @param {Event} e + */ + handleClickGlobal(e) { + if (!this.target) return; + + // Check if the popover is open + if (this.toggle.getAttribute('aria-expanded') === 'true') { + // Check if the click occured on the popover + if (!this.target.contains(e.target) && !this.toggle.contains(e.target)) { + this.closePopover(); + } + } } }