UNPKG

@adobe/coral-spectrum

Version:

Coral Spectrum is a JavaScript library of Web Components following Spectrum design patterns.

352 lines (287 loc) 11.3 kB
/** * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import ActionBarContainerCollection from './ActionBarContainerCollection'; import {Button} from '../../../coral-component-button'; import '../../../coral-component-anchorbutton'; import moreOverlay from '../templates/moreOverlay'; import moreButton from '../templates/moreButton'; import overlayContent from '../templates/overlayContent'; import {commons, transform, i18n} from '../../../coral-utils'; // Matches private Coral classes in class attribute const REG_EXP = /_coral([^\s]+)/g; const copyAttributes = (from, to) => { const excludedAttributes = ['is', 'id', 'variant', 'size']; for (let i = 0 ; i < from.attributes.length ; i++) { const attr = from.attributes[i]; if (excludedAttributes.indexOf(attr.nodeName) === -1) { if (attr.nodeName === 'class') { // Filter out private Coral classes to.setAttribute(attr.nodeName, `${to.className} ${attr.nodeValue.replace(REG_EXP, '')}`); } else { to.setAttribute(attr.nodeName, attr.nodeValue); } } } }; /** @base BaseActionBarContainer @classdesc The base element for action bar containers */ const BaseActionBarContainer = (superClass) => class extends superClass { /** @ignore */ constructor() { super(); // Templates this._elements = {}; this._itemsInPopover = []; moreButton.call(this._elements, {i18n}); moreOverlay.call(this._elements, {commons}); overlayContent.call(this._elements, { items: this._itemsInPopover, copyAttributes }); // Return focus to overlay by default this._elements.overlay.focusOnShow = this._elements.overlay; const overlayId = this._elements.overlay.id; const events = {}; events[`global:capture:coral-overlay:beforeopen #${overlayId}`] = '_onOverlayBeforeOpen'; events[`global:capture:coral-overlay:beforeclose #${overlayId}`] = '_onOverlayBeforeClose'; // Keyboard interaction events[`global:key:down #${overlayId}`] = '_onOverlayKeyDown'; events[`global:key:up #${overlayId}`] = '_onOverlayKeyUp'; // Events this._delegateEvents(events); // Init the collection mutation observer this.items._startHandlingItems(true); } /** Returns the inner overlay to allow customization. @type {Popover} @readonly */ get overlay() { return this._elements.overlay; } /** The Collection Interface that allows interacting with the items that the component contains. @type {ActionBarContainerCollection} @readonly */ get items() { // Construct the collection on first request: if (!this._items) { this._items = new ActionBarContainerCollection({ host: this, itemTagName: 'coral-actionbar-item', onItemAdded: this._styleItem }); } return this._items; } /** The amount of items that are maximally visible inside the container. Using a value <= 0 will disable this feature and show as many items as possible. @type {Number} @default -1 @htmlattribute threshold @htmlattributereflected */ get threshold() { return typeof this._threshold === 'number' ? this._threshold : -1; } set threshold(value) { this._threshold = transform.number(value); this._reflectAttribute('threshold', this._threshold); } /** If there are more ActionBarItems inside the ActionBar than currently can be shown, then a "more" Button with the following text will be rendered (and some ActionBarItems will be hidden inside of a Popover). @type {String} @default "" @htmlattribute morebuttontext */ get moreButtonText() { return this._moreButtonText || ''; } set moreButtonText(value) { this._moreButtonText = transform.string(value); if (this._elements.moreButton) { // moreButton might not have been created so far this._elements.moreButtonLabel.innerHTML = this._moreButtonText; this._elements.moreButton[this._moreButtonText.trim() === '' ? 'setAttribute' : 'removeAttribute']('title', i18n.get('More')); } } /** Style item content */ _styleItem(item) { const button = item.querySelector('button[is="coral-button"]') || item.querySelector('a[is="coral-anchorbutton"]'); if (button) { button.classList.add('_coral-ActionBar-button'); const oldVariant = button.getAttribute('variant'); if (oldVariant === Button.variant.ACTION || oldVariant === Button.variant.QUIET_ACTION) { return; } button.setAttribute('variant', oldVariant === Button.variant.QUIET ? Button.variant.QUIET_ACTION : Button.variant.ACTION); } } /** Called after popover.open is set to true, but before the transition of the popover is done. Show elements inside the actionbar, that are hidden due to space problems. @ignore */ _onOverlayBeforeOpen(event) { // there might be popovers in popover => ignore them if (event.target !== this._elements.overlay) { return; } this._itemsInPopover = this.items._getAllOffScreen(); if (this._itemsInPopover.length < 1) { return; } // Set focus to first focusable descendant of the overlay by default this._elements.overlay.focusOnShow = 'on'; this._itemsInPopover.forEach((item) => { item.style.visibility = ''; // Store the button and popover on the item item._button = item.querySelector(':scope > button[is="coral-button"]') || item.querySelector(':scope > a[is="coral-anchorbutton"]'); item._popover = item.querySelector('coral-popover'); if (item._popover) { item._popoverId = item._popover.id; } }); // Whether a ButtonList or AnchorList should be rendered this._itemsInPopover.isButtonList = this._itemsInPopover.every(item => item._button && item._button.tagName === 'BUTTON'); this._itemsInPopover.isAnchorList = this._itemsInPopover.every(item => item._button && item._button.tagName === 'A'); // show the current popover (hidden needed to disable fade time of popover) this._elements.overlay.hidden = false; // render popover content const popover = this._elements.overlay; popover.content.innerHTML = ''; popover.content.appendChild(overlayContent.call(this._elements, { items: this._itemsInPopover, copyAttributes })); } /** Called after popover.open is set to false, but before the transition of the popover is done. Make items visible again, that now do fit into the actionbar. @ignore */ _onOverlayBeforeClose(event) { // there might be popovers in popover => ignore them if (event.target !== this._elements.overlay) { return; } const focusedItem = document.activeElement.parentNode; // we need to check if item has 'hasAttribute' because it is not present on the document const isFocusedItemInsideActionBar = this.parentNode.contains(focusedItem); const isFocusedItemOffscreen = focusedItem.hasAttribute && focusedItem.hasAttribute('coral-actionbar-offscreen'); if (isFocusedItemInsideActionBar && isFocusedItemOffscreen) { // if currently an element is focused, that should not be visible (or is no actionbar-item) => select 'more' // button this._elements.moreButton.focus(); } // hide the popover(needed to disable fade time of popover) this._elements.overlay.hidden = true; this._elements.overlay.focusOnShow = this._elements.overlay; // close any popovers, that might be inside the 'more' popover const childPopovers = this._elements.overlay.getElementsByTagName('coral-popover'); for (let i = 0 ; i < childPopovers.length ; i++) { childPopovers[i].open = false; } // return all elements from popover this._returnElementsFromPopover(); // clear cached items from popover this._itemsInPopover = []; // clear overlay this._elements.overlay.content.innerHTML = ''; } _onOverlayKeyDown(event) { event.preventDefault(); // Focus first item this._elements.anchorList && this._elements.anchorList._focusFirstItem(event); this._elements.buttonList && this._elements.buttonList._focusFirstItem(event); } _onOverlayKeyUp(event) { event.preventDefault(); // Focus last item this._elements.anchorList && this._elements.anchorList._focusLastItem(event); this._elements.buttonList && this._elements.buttonList._focusLastItem(event); } static get _attributePropertyMap() { return commons.extend(super._attributePropertyMap, { morebuttontext: 'moreButtonText' }); } /** @ignore */ static get observedAttributes() { return super.observedAttributes.concat(['morebuttontext', 'threshold']); } /** @ignore */ connectedCallback() { super.connectedCallback(); const overlay = this._elements.overlay; // Cannot be open by default when rendered overlay.removeAttribute('open'); // Restore in DOM if (overlay._parent) { overlay._parent.appendChild(overlay); } } /** @ignore */ render() { super.render(); // Cleanup resize helpers object (cloneNode support) const resizeHelpers = this.getElementsByTagName('object'); for (let i = 0 ; i < resizeHelpers.length ; ++i) { const resizeElement = resizeHelpers[i]; if (resizeElement.parentNode === this) { this.removeChild(resizeElement); } } // Cleanup 'More' button const more = this.querySelector('[coral-actionbar-more]'); if (more) { this.removeChild(more); } // Cleanup 'More' popover const popover = this.querySelector('[coral-actionbar-popover]'); if (popover) { this.removeChild(popover); } // Copy more text this._elements.moreButton.label.textContent = this.moreButtonText; // Init 'More' popover this._elements.overlay.target = this._elements.moreButton; // Create empty frag const frag = document.createDocumentFragment(); // 'More' button might be moved later in dom when Container is attached to parent frag.appendChild(this._elements.moreButton); frag.appendChild(this._elements.overlay); // Render template this.appendChild(frag); // Style the items to match action items this.items.getAll().forEach(item => this._styleItem(item)); } /** @ignore */ disconnectedCallback() { super.disconnectedCallback(); const overlay = this._elements.overlay; // In case it was moved out don't forget to remove it if (!this.contains(overlay)) { overlay._parent = overlay._repositioned ? document.body : this; overlay.remove(); } } }; export default BaseActionBarContainer;