UNPKG

@furo/ui5

Version:
364 lines (323 loc) 9 kB
import { LitElement, html, css } from 'lit'; import { FBP } from '@furo/fbp'; // import '@furo/fbp/src/flow-repeat'; // disabled for tests :-( // import '@furo/util/src/furo-navigation-pad'; import './furo-ui5-context-menu-item.js'; /** * `furo-ui5-context-submenu` is a helper component for `furo-ui5-context-menu`. * * Use [`furo-ui5-context-menu`](?t=FuroUi5ContextMenu) to show a context menu. * * @summary helper * @element furo-ui5-context-submenu * @appliesMixin FBP */ export class FuroUi5ContextSubmenu extends FBP(LitElement) { constructor() { super(); this.borderDistance = 48; } /** * @private * @return {Object} */ static get properties() { return { /** * Depth of the submenu * * @type Number */ depth: { type: Number }, }; } /** * flow is ready lifecycle method * @private */ _FBPReady() { super._FBPReady(); /** * Register hook on wire --nav to * listen to the navigation pad */ this._FBPAddWireHook('--nav', e => { this.triggerNavigation(e); }); /** * Listen to item-selected to pass it back to furo-context-menu (callback) * */ this.addEventListener('item-selected', () => { this.hideMenu(); }); } init(e, display, byKeyboard) { this.menuObject = e.detail; this._repeater = this.shadowRoot.getElementById('repeater'); const menucontainer = this.shadowRoot.getElementById('menu'); /** * add flag _noicon if none of the group (between dividers) has no icons */ let noicon = true; let stage = []; this.menuObject.menu.children.repeats.forEach((item, i, items) => { /** * if next item has a leading separator push before, otherwise push after loop */ let pushed = false; if ( item.leading_divider._value && items[i + 1] && items[i + 1].leading_divider._value ) { stage.push(item); pushed = true; } if (item.icon._value) { noicon = false; } if (item.leading_divider._value) { stage.forEach(stg => { // eslint-disable-next-line no-param-reassign stg._noicon = noicon; }); stage = []; noicon = true; } if (!pushed) { stage.push(item); } }); noicon = true; // do the last stage stage.forEach(item => { if (item.icon._value) { noicon = false; } stage.forEach(i => { // eslint-disable-next-line no-param-reassign i._noicon = noicon; }); }); this._FBPTriggerWire('--menuObject', this.menuObject.menu.children.repeats); this.initiator = e.detail.initiator; const thisCR = display.getBoundingClientRect(); const initiatorCR = this.initiator.getBoundingClientRect(); menucontainer.style.removeProperty('maxHeight'); menucontainer.style.removeProperty('bottom'); menucontainer.style.removeProperty('height'); menucontainer.style.removeProperty('left'); menucontainer.style.removeProperty('right'); // find the ideal position and direction const initiatorCoordinates = { left: { x: initiatorCR.left, y: (initiatorCR.top + initiatorCR.bottom) / 2, }, right: { x: initiatorCR.right, y: (initiatorCR.top + initiatorCR.bottom) / 2, }, }; if ( initiatorCoordinates.left.x > thisCR.width - initiatorCoordinates.right.x ) { menucontainer.style.right = `${ thisCR.width - initiatorCoordinates.left.x }px`; menucontainer.style.top = `${initiatorCR.top}px`; } else { menucontainer.style.removeProperty('right'); menucontainer.style.left = `${initiatorCoordinates.right.x}px`; menucontainer.style.top = `${initiatorCR.top}px`; } // calculate container positions from bottom when the initator is in the under third of the screen let onUpperSide = true; if (initiatorCoordinates.right.y * 2.5 > thisCR.height) { // we are in the under half of the screen menucontainer.style.removeProperty('top'); menucontainer.style.bottom = `${ thisCR.height - initiatorCoordinates.right.y }px`; onUpperSide = false; } this._start = true; this.requestUpdate(); // delay fade in effect set height setTimeout(() => { const menucontainerCr = menucontainer.getBoundingClientRect(); let maxHeight = thisCR.height - initiatorCoordinates.left.y - this.borderDistance; if (!onUpperSide) { maxHeight = initiatorCoordinates.left.y - this.borderDistance; } menucontainer.style.maxHeight = `${maxHeight}px`; // max height of ctxmenu should not go outside the screen if (menucontainerCr.height > maxHeight) { menucontainer.style.height = `${maxHeight}px`; } this._show = true; this.requestUpdate(); setTimeout(() => { menucontainer.focus(); // mark first element if opened by keyboard if (byKeyboard) { this._repeater.selectNextIndex(); } }, 100); }, 10); } hideMenu() { this.removeAttribute('backdrop'); this._start = false; this._show = false; if ( this.initiator && this.initiator.parentNode && this.initiator.parentNode.parentNode ) { this.initiator.parentNode.parentNode.focus(); } this.requestUpdate(); // destroy the instance this.remove(); } /** * Interface for the furo navigation pad * @param key */ triggerNavigation(key) { switch (key) { case 'Enter': this._repeater.triggerSelected(key); break; case 'ArrowDown': this._repeater.selectNextIndex(); break; case 'ArrowUp': this._repeater.selectPreviousIndex(); break; case 'PageDown': break; case 'PageUp': this._repeater.select(0); break; case 'End': this.focusLast(); break; case 'Home': this._repeater.select(0); break; case 'ArrowLeft': // closes subnav this.hideMenu(); break; case 'ArrowRight': // opens subnav this._repeater.triggerSelected(key); break; case 'Escape': // closes the menu this.hideMenu(); break; default: } } /** * Themable Styles * @private * @return {CSSResult} */ static get styles() { // language=CSS return css` :host { z-index: 10; } :host([backdrop]) { display: block; } #menu { outline: none; position: absolute; display: none; transition: opacity 0.03s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1), -webkit-transform 0.12s cubic-bezier(0, 0, 0.2, 1); opacity: 0; background-color: var(--ui5-listitem-background-color); overflow-y: auto; box-sizing: border-box; box-shadow: var(--sapContent_Shadow2); border-radius: var(--_ui5-popup-border-radius); } #menu[start] { display: block; opacity: 0; } #menu[show] { opacity: 1; display: block; } .clickcatcher { position: absolute; top: 0; left: 0; bottom: 0; right: 0; } /* only focus is needed, because the menu closes on select */ furo-ui5-context-menu-item:hover, furo-ui5-context-menu-item[focused] { background: var(--sapList_Hover_Background); } .separator { height: 8px; box-sizing: border-box; border-bottom: var(--ui5-listitem-border-bottom); margin-bottom: 8px; } `; } /** * The host is the backdrop * * @returns {TemplateResult} * @private */ render() { // language=HTML return html` <div id="menu" tabindex="0" ?start="${this._start}" ?show="${this._show}" at-mousefocus="--mousefocus" > <!-- the wires --itemSelected and --itemDeSelected means focus, they come from flow-repeat --> <flow-repeat id="repeater" fn-inject-items="--menuObject" fn-select="--mousefocus" > <template> <div> <furo-ui5-context-menu-item fn-index="--index" fn-select="--trigger" fn-set-focused="--itemSelected" fn-unset-focused="--itemDeSelected" fn-bind-data="--itemInjected(*.item)" ></furo-ui5-context-menu-item> </div> </template> </flow-repeat> <furo-navigation-pad at-navigated="--nav"></furo-navigation-pad> </div> `; } } window.customElements.define('furo-ui5-context-submenu', FuroUi5ContextSubmenu);