coral-spectrum/coral-component-shell/src/scripts/ShellMenuBarItem.js
- /**
- * 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 {BaseComponent} from '../../../coral-base-component';
- import {commons, transform, validate} from '../../../coral-utils';
- import '../../../coral-component-icon';
- import '../../../coral-component-button';
- import '../../../coral-component-anchorbutton';
- import menuBarItem from '../templates/menuBarItem';
-
- /**
- Enumeration for {@link ShellMenuBarItem} icon variants.
-
- @typedef {Object} ShellMenuBarItemIconVariantEnum
-
- @property {String} DEFAULT
- A default menubar item.
- @property {String} CIRCLE
- A round image as menubar item.
- */
- const iconVariant = {
- DEFAULT: 'default',
- CIRCLE: 'circle'
- };
-
- /**
- Enumeration for valid aria-haspopup values.
-
- @typedef {Object} ShellMenuBarItemHasPopupRoleEnum
- @property {String} MENU
- ShellMenuBarItem opens a menu.
- @property {String} LISTBOX
- ShellMenuBarItem opens a list box.
- @property {String} TREE
- ShellMenuBarItem opens a tree.
- @property {String} GRID
- ShellMenuBarItem opens a grid.
- @property {String} DIALOG
- ShellMenuBarItem opens a dialog.
- @property {Null} DEFAULT
- Defaults to null.
-
- */
- const hasPopupRole = {
- MENU: 'menu',
- LISTBOX: 'listbox',
- TREE: 'tree',
- GRID: 'grid',
- DIALOG: 'dialog',
- DEFAULT: null
- };
-
- // the Menubar Item's base classname
- const CLASSNAME = '_coral-Shell-menubar-item';
-
- // Builds a string containing all possible iconVariant classnames. This will be used to remove classnames when the variant
- // changes
- const ALL_ICON_VARIANT_CLASSES = [];
- for (const variantValue in iconVariant) {
- ALL_ICON_VARIANT_CLASSES.push(`${CLASSNAME}--${iconVariant[variantValue]}`);
- }
-
- /**
- @class Coral.Shell.MenuBar.Item
- @classdesc A Shell MenuBar Item component
- @htmltag coral-shell-menubar-item
- @extends {HTMLElement}
- @extends {BaseComponent}
- */
- class ShellMenuBarItem extends BaseComponent(HTMLElement) {
- /** @ignore */
- constructor() {
- super();
-
- // Templates
- this._elements = {};
- menuBarItem.call(this._elements);
-
- // Events
- this._delegateEvents({
- 'click [handle="shellMenuButton"]': '_handleButtonClick',
-
- // it has to be global because the menus are not direct children
- 'global:coral-overlay:close': '_handleOverlayEvent',
- 'global:coral-overlay:beforeclose': '_handleOverlayBeforeEvent',
- 'global:coral-overlay:open': '_handleOverlayEvent',
- 'global:coral-overlay:beforeopen': '_handleOverlayBeforeEvent'
- });
- }
-
- /**
- Specifies the icon name used inside the menu item.
- See {@link Icon} for valid icon names.
-
- @type {String}
- @default ""
- @htmlattribute icon
- */
- get icon() {
- return this._elements.shellMenuButton.icon;
- }
-
- set icon(value) {
- this._elements.shellMenuButton.icon = value;
- }
-
- /**
- Size of the icon. It accepts both lower and upper case sizes. See {@link ButtonIconSizeEnum}.
-
- @type {String}
- @default ButtonIconSizeEnum.SMALL
- @htmlattribute iconsize
- @htmlattributereflected
- */
- get iconSize() {
- return this._elements.shellMenuButton.iconSize;
- }
-
- set iconSize(value) {
- this._elements.shellMenuButton.iconSize = value;
- // Required for styling
- this._reflectAttribute('iconsize', this.iconSize);
- }
-
- /**
- The menubar item's iconVariant. See {@link ShellMenuBarItemIconVariantEnum}.
-
- @type {String}
- @default ShellMenuBarItemIconVariantEnum.DEFAULT
- @htmlattribute iconvariant
- */
- get iconVariant() {
- return this._iconVariant || iconVariant.DEFAULT;
- }
-
- set iconVariant(value) {
- value = transform.string(value).toLowerCase();
- this._iconVariant = validate.enumeration(iconVariant)(value) && value || iconVariant.DEFAULT;
-
- // removes all the existing variants
- this.classList.remove(...ALL_ICON_VARIANT_CLASSES);
- // adds the new variant
- if (this.variant !== iconVariant.DEFAULT) {
- this.classList.add(`${CLASSNAME}--${this._iconVariant}`);
- }
- }
-
- /**
- The notification badge content.
-
- @type {String}
- @default ""
- @htmlattribute badge
- */
- get badge() {
- return this._elements.shellMenuButton.getAttribute('badge') || '';
- }
-
- set badge(value) {
- // Non-truthy values shouldn't show
- // null, empty string, 0, etc
- this._elements.shellMenuButton[!value || value === '0' ? 'removeAttribute' : 'setAttribute']('badge', value);
- }
-
- /**
- Whether the menu is open or not.
-
- @type {Boolean}
- @default false
- @htmlattribute open
- @htmlattributereflected
-
- @emits {coral-shell-menubar-item:open}
- @emits {coral-shell-menubar-item:close}
- */
- get open() {
- return this._open || false;
- }
-
- set open(value) {
- const menu = this._getMenu();
-
- // if we want to open the dialog we need to make sure there is a valid menu or hasPopup
- if (menu === null && this.hasPopup === hasPopupRole.DEFAULT) {
- return;
- }
-
- this._open = transform.booleanAttr(value);
- this._reflectAttribute('open', this._open);
-
- // if the menu is valid, toggle the menu and trigger the appropriate event
- if (menu !== null) {
- // Toggle the target menu
- if (menu.open !== this._open) {
- menu.open = this._open;
- }
-
- this.trigger(`coral-shell-menubar-item:${this._open ? 'open' : 'close'}`);
- }
-
- this._elements.shellMenuButton.setAttribute('aria-expanded', this._open);
- }
-
- /**
- The menubar item's label content zone.
-
- @type {ButtonLabel}
- @contentzone
- */
- get label() {
- return this._getContentZone(this._elements.shellMenuButtonLabel);
- }
-
- set label(value) {
- this._setContentZone('label', value, {
- handle: 'shellMenuButtonLabel',
- tagName: 'coral-button-label',
- insert: function (label) {
- this._elements.shellMenuButton.label = label;
- }
- });
- }
-
- /**
- The menu that this menu item should show. If a CSS selector is provided, the first matching element will be
- used.
-
- @type {?HTMLElement|String}
- @default null
- @htmlattribute menu
- */
- get menu() {
- return this._menu || null;
- }
-
- set menu(value) {
- let menu;
- if (value instanceof HTMLElement) {
- this._menu = value;
- menu = this._menu;
- } else {
- this._menu = String(value);
- menu = document.querySelector(this._menu);
- }
-
- // Link menu with item
- if (menu !== null) {
- this.id = this.id || commons.getUID();
- menu.setAttribute('target', `#${this.id}`);
- if (this.hasPopup === hasPopupRole.DEFAULT) {
- this.hasPopup = menu.getAttribute('role') || hasPopupRole.DIALOG;
- }
- } else if (this._menu && this.hasPopup !== hasPopupRole.DEFAULT) {
- this.hasPopup = hasPopupRole.DEFAULT;
- }
- }
-
- /**
- Whether the item opens a popup dialog or menu. Accepts either "menu", "listbox", "tree", "grid", or "dialog".
- @type {?String}
- @default ShellMenuBarItemHasPopupRoleEnum.DEFAULT
- @htmlattribute haspopup
- */
- get hasPopup() {
- return this._hasPopup || null;
- }
-
- set hasPopup(value) {
- value = transform.string(value).toLowerCase();
- this._hasPopup = validate.enumeration(hasPopupRole)(value) && value || hasPopupRole.DEFAULT;
-
- const shellMenuButton = this._elements.shellMenuButton;
- let ariaHaspopup = this._hasPopup;
-
- if (ariaHaspopup) {
- shellMenuButton.setAttribute('aria-haspopup', ariaHaspopup);
- shellMenuButton.setAttribute('aria-expanded', this.open);
- } else {
- shellMenuButton.removeAttribute('aria-haspopup');
- shellMenuButton.removeAttribute('aria-expanded');
- }
- }
-
- _handleOverlayBeforeEvent(event) {
- const target = event.target;
-
- if (target === this._getMenu()) {
- // Mark button as selected
- this._elements.shellMenuButton.classList.toggle('is-selected', !target.open);
- }
- }
-
- /** @private */
- _handleOverlayEvent(event) {
- const target = event.target;
-
- // matches the open state of the target in case it was open separately
- if (target === this._getMenu()) {
- const shellMenuButton = this._elements.shellMenuButton;
- if (this.open !== target.open) {
- this.open = target.open;
- } else if (shellMenuButton.getAttribute('aria-expanded') !== target.open) {
- shellMenuButton.setAttribute('aria-expanded', target.open);
- }
- }
- }
-
- /** @ignore */
- _handleButtonClick() {
- this.open = !this.open;
- }
-
- /** @ignore */
- _getMenu(targetValue) {
- // Use passed target
- targetValue = targetValue || this.menu;
-
- if (targetValue instanceof Node) {
- // Just return the provided Node
- return targetValue;
- }
-
- // Dynamically get the target node based on target
- let newTarget = null;
- if (typeof targetValue === 'string') {
- newTarget = document.querySelector(targetValue);
- }
-
- return newTarget;
- }
-
- get _contentZones() {
- return {'coral-button-label': 'label'};
- }
-
- /** @ignore */
- focus() {
- this._elements.shellMenuButton.focus();
- }
-
- /**
- Returns {@link ShellMenuBarItem} icon variants.
-
- @return {ShellMenuBarItemIconVariantEnum}
- */
- static get iconVariant() {
- return iconVariant;
- }
-
- static get _attributePropertyMap() {
- return commons.extend(super._attributePropertyMap, {
- haspopup: 'hasPopup',
- iconsize: 'iconSize',
- iconvariant: 'iconVariant'
- });
- }
-
- /** @ignore */
- static get observedAttributes() {
- return super.observedAttributes.concat([
- 'haspopup',
- 'icon',
- 'iconsize',
- 'iconvariant',
- 'badge',
- 'open',
- 'menu',
- 'aria-label'
- ]);
- }
-
- /** @ignore */
- attributeChangedCallback(name, oldValue, value) {
- // a11y When user doesn't supply a button label (for an icon-only button),
- // providing aria-label will correctly pass it on to the shell menu button child element.
- if (name === 'aria-label') {
- if (value && this._elements.shellMenuButton.textContent.trim() === '') {
- this._elements.shellMenuButton.setAttribute('aria-label', value);
- }
- } else {
- super.attributeChangedCallback(name, oldValue, value);
- }
- }
-
- /** @ignore */
- render() {
- super.render();
-
- this.setAttribute('role', 'listitem');
-
- this.classList.add(CLASSNAME);
-
- const button = this.querySelector('._coral-Shell-menu-button');
-
- if (button) {
- this._elements.shellMenuButton = button;
- this._elements.shellMenuButtonLabel = this.querySelector('coral-button-label');
- } else {
- while (this.firstChild) {
- this._elements.shellMenuButtonLabel.appendChild(this.firstChild);
- }
-
- this.appendChild(this._elements.shellMenuButton);
- }
-
- this.label = this._elements.shellMenuButtonLabel;
-
- // Sync menu
- if (this.menu !== null) {
- this.menu = this.menu;
- }
- }
-
- /**
- Triggered after the {@link ShellMenuBarItem} is opened with <code>show()</code> or <code>instance.open = true</code>
-
- @typedef {CustomEvent} coral-shell-menubar-item:open
- */
-
- /**
- Triggered after the {@link ShellMenuBarItem} is closed with <code>hide()</code> or <code>instance.open = false</code>
-
- @typedef {CustomEvent} coral-shell-menubar-item:close
- */
- }
-
- export default ShellMenuBarItem;