UNPKG

@telekom/scale-components

Version:

Scale is the digital design system for Telekom products and experiences.

635 lines (625 loc) 34.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const index = require('./index-a0ea3d79.js'); const index$1 = require('./index-53f5a5fc.js'); const utils = require('./utils-e9c3b953.js'); const buttonCss = ":host{--width:auto;--spacing-x-right:var(--telekom-spacing-composition-space-07);--spacing-x-left:var(--telekom-spacing-composition-space-07);--spacing-x-icon-only:var(--telekom-spacing-composition-space-05);--min-height:var(--telekom-spacing-composition-space-13);--min-width:var(--telekom-spacing-composition-space-13);--radius:var(--telekom-radius-standard);--transition:all var(--telekom-motion-duration-transition)\n var(--telekom-motion-easing-standard);--color-focus:var(--telekom-color-functional-focus-standard);--font-weight:var(--telekom-typography-font-weight-bold);--font-size:var(--telekom-typography-font-size-body);--line-height:var(--telekom-typography-line-spacing-tight);--spacing-icon-x:var(--telekom-spacing-composition-space-04);--vertical-align:middle;--font-size-small:var(--telekom-typography-font-size-caption);--line-height-small:1.125rem;--min-height-small:var(--telekom-spacing-composition-space-10);--spacing-x-right-small:var(--telekom-spacing-composition-space-06);--spacing-x-left-small:var(--telekom-spacing-composition-space-06);--spacing-x-icon-only-small:var(--telekom-spacing-composition-space-00);--spacing-icon-x-small:var(--telekom-spacing-composition-space-03);--radius-primary:var(--radius);--background-primary:var(--telekom-color-primary-standard);--background-primary-hover:var(--telekom-color-primary-hovered);--background-primary-active:var(--telekom-color-primary-pressed);--background-primary-disabled:var(--telekom-color-ui-disabled);--color-primary:var(--telekom-color-text-and-icon-white-standard);--color-primary-disabled:var(--telekom-color-text-and-icon-disabled);--radius-secondary:var(--radius);--border-width-secondary:var(--telekom-spacing-composition-space-01);--background-secondary:transparent;--color-secondary:var(--telekom-color-text-and-icon-standard);--color-secondary-hover:var(--telekom-color-text-and-icon-standard);--color-secondary-active:var(--telekom-color-text-and-icon-standard);--color-secondary-disabled:var(--telekom-color-text-and-icon-disabled);--background-secondary:var(--telekom-color-ui-state-fill-standard);--background-secondary-hover:var(--telekom-color-ui-state-fill-hovered);--background-secondary-active:var(--telekom-color-ui-state-fill-pressed);--background-secondary-disabled:none;--border-secondary:var(--telekom-color-ui-border-standard);--border-secondary-hover:var(--telekom-color-ui-border-hovered);--border-secondary-active:var(--telekom-color-ui-border-pressed);--border-secondary-focus:var(--telekom-color-functional-focus-standard);--border-secondary-white:var(--telekom-color-ui-white);--color-secondary-white:var(--telekom-color-ui-white);--background-secondary-white-hover:var(\n --telekom-color-ui-state-fill-hovered-inverted\n );--background-secondary-white-active:var(\n --telekom-color-ui-state-fill-pressed-inverted\n );--secondary-white-opacity:0.5;--radius-ghost:var(--radius);--border-width-ghost:var(--telekom-spacing-composition-space-01);--spacing-x-ghost:var(--telekom-spacing-composition-space-04);--color-ghost:var(--telekom-color-text-and-icon-primary-standard);--color-ghost-hover:var(--telekom-color-text-and-icon-primary-hovered);--color-ghost-active:var(--telekom-color-text-and-icon-primary-pressed);--color-ghost-disabled:var(--telekom-color-text-and-icon-disabled);--background-ghost-hover:var(--telekom-color-ui-state-fill-hovered);--background-ghost-active:var(--telekom-color-ui-state-fill-pressed);display:inline-block}.button{box-sizing:border-box;display:inline-flex;align-items:center;position:relative;border:0;outline:none;cursor:pointer;user-select:none;font-family:inherit;word-spacing:inherit;letter-spacing:inherit;justify-content:center;text-decoration:none;font-weight:var(--font-weight);font-size:var(--font-size);line-height:var(--line-height);min-height:var(--min-height);min-width:var(--min-width);width:var(--width);padding-left:var(--spacing-x-left);padding-right:var(--spacing-x-right);vertical-align:var(--vertical-align);transition:var(--transition)}.button.button--size-small{font-size:var(--font-size-small);line-height:var(--line-height-small);min-height:var(--min-height-small);padding-left:var(--spacing-x-left-small);padding-right:var(--spacing-x-right-small)}.button:not(.button--disabled):focus{outline:var(--telekom-line-weight-highlight) solid var(--color-focus);outline-offset:var(--telekom-spacing-composition-space-01)}.button.button--icon-before:not(.button--icon-only) ::slotted(*){margin-right:var(--spacing-icon-x);margin-left:calc(var(--spacing-icon-x-small) * -1);margin-top:var(--spacing-icon-x);margin-bottom:var(--spacing-icon-x)}.button.button--icon-before:not(.button--icon-only).button--size-small ::slotted(*){margin-right:var(--spacing-icon-x-small);margin-left:calc(var(--spacing-icon-x) * -0.5)}.button.button--icon-after:not(.button--icon-only) ::slotted(*){margin-left:var(--spacing-icon-x);margin-right:calc(var(--spacing-icon-x-small) * -1);margin-top:var(--spacing-icon-x);margin-bottom:var(--spacing-icon-x)}.button.button--icon-after:not(.button--icon-only).button--size-small ::slotted(*){margin-left:var(--spacing-icon-x-small);margin-right:calc(var(--spacing-icon-x) * -0.5)}.button:after{top:0;left:0;width:100%;border:var(--telekom-spacing-composition-space-01) solid transparent;height:100%;content:'';display:block;position:absolute;box-sizing:border-box;pointer-events:none;border-radius:var(--radius)}.button--icon-only{padding-left:var(--spacing-x-icon-only);padding-right:var(--spacing-x-icon-only)}.button--icon-only.button--variant-secondary{padding-left:calc(var(--spacing-x-icon-only) - 1px);padding-right:calc(var(--spacing-x-icon-only) - 1px)}.button--icon-only.button--size-small{padding-left:var(--spacing-x-icon-only-small);padding-right:var(--spacing-x-icon-only-small);min-width:32px}.button--icon-only.button--size-small.button--variant-secondary{padding-left:calc(var(--spacing-x-icon-only-small) - 1px);padding-right:calc(var(--spacing-x-icon-only-small) - 1px)}.button--disabled{cursor:not-allowed}.button--variant-primary{text-align:center;border-radius:var(--radius);background:var(--background-primary);color:var(--color-primary)}.button--variant-primary:not(.button--disabled):hover{background:var(--background-primary-hover)}.button--variant-primary:not(.button--disabled):active{background:var(--background-primary-active)}.button--disabled.button--variant-primary{background:var(--background-primary-disabled);color:var(--color-primary-disabled)}.button--variant-secondary{background:var(--background-secondary);text-align:center;border-radius:var(--radius-secondary);border:var(--border-width-secondary) solid currentColor;color:var(--color-secondary);background-color:var(--background-secondary);border-color:var(--border-secondary)}.button--variant-secondary:not(.button--disabled):hover{color:var(--color-secondary-hover);background-color:var(--background-secondary-hover);border-color:var(--border-secondary-hover)}.button--variant-secondary:not(.button--disabled):active{color:var(--color-secondary-active);background-color:var(--background-secondary-active);border-color:var(--border-secondary-active)}.button--disabled.button--variant-secondary{color:var(--color-secondary-disabled);background-color:var(--background-secondary-disabled)}.button--variant-ghost{background:transparent;text-align:center;border-radius:var(--radius-ghost);border:var(--border-width-ghost) solid transparent;color:var(--color-ghost);padding-left:var(--spacing-x-ghost);padding-right:var(--spacing-x-ghost)}.button--variant-ghost:not(.button--disabled):hover{color:var(--color-ghost-hover);background-color:var(--background-ghost-hover)}.button--variant-ghost:not(.button--disabled):active{color:var(--color-ghost-active);background-color:var(--background-ghost-active)}.button--disabled.button--variant-ghost{color:var(--color-ghost-disabled)}.button--variant-secondary-white{background:var(--background-secondary);text-align:center;border-radius:var(--radius-secondary);border:var(--border-width-secondary) solid currentColor;color:var(--color-secondary-white);background-color:var(--background-secondary);border-color:var(--border-secondary-white)}.button--variant-secondary-white:not(.button--disabled):hover{background-color:var(--background-secondary-white-hover)}.button--variant-secondary-white:not(.button--disabled):active{background-color:var(--background-secondary-white-active)}.button--disabled.button--variant-secondary-white{opacity:var(--secondary-white-opacity)}"; const DEFAULT_ICON_SIZE = 24; const buttonIconSizeMap = { small: 16, large: 20, }; const Button = class { constructor(hostRef) { index.registerInstance(this, hostRef); /** (optional) The size of the button */ this.size = 'large'; /** (optional) Button variant */ this.variant = 'primary'; /** (optional) If `true`, the button is disabled */ this.disabled = false; /** (optional) Set to `true` when the button contains only an icon */ this.iconOnly = false; /** (optional) Icon position related to the label */ this.iconPosition = 'before'; /** (optional) The target attribute for the <a> tag */ this.target = '_self'; /** * Hack to make the button behave has expected when inside forms. * @see https://github.com/ionic-team/ionic-framework/blob/master/core/src/components/button/button.tsx#L155-L175 */ this.handleClick = (ev) => { // No need to check for `disabled` because disabled buttons won't emit clicks if (utils.hasShadowDom(this.hostElement)) { const parentForm = this.hostElement.closest('form'); if (parentForm) { ev.preventDefault(); const fakeButton = document.createElement('button'); if (this.type) { fakeButton.type = this.type; } fakeButton.style.display = 'none'; parentForm.appendChild(fakeButton); fakeButton.click(); fakeButton.remove(); } } }; } /** * Prevent clicks from being emitted from the host * when the component is `disabled`. */ handleHostClick(event) { if (this.disabled === true) { event.stopImmediatePropagation(); } } async setFocus() { this.focusableElement.focus(); } componentDidLoad() { this.setChildrenIconSize(); } connectedCallback() { this.setIconPositionProp(); this.appendEnterKeySubmitFallback(); } disconnectedCallback() { this.cleanUpEnterKeySubmitFallback(); } /** * In order for forms to be submitted with the Enter key * there has to be a `button` or an `input[type="submit"]` in the form. * Browsers do not take the <button> inside the Shadow DOM into account for this matter. * So we carefully append an `input[type="submit"]` to overcome this. * * @see https://stackoverflow.com/a/35235768 * @see https://github.com/telekom/scale/issues/859 */ appendEnterKeySubmitFallback() { if (utils.hasShadowDom(this.hostElement)) { const parentForm = this.hostElement.closest('form'); if (parentForm == null) { return; } const hasSubmitInputAlready = parentForm.querySelector('input[type="submit"]') != null; if (hasSubmitInputAlready) { return; } this.fallbackSubmitInputElement = document.createElement('input'); this.fallbackSubmitInputElement.type = 'submit'; this.fallbackSubmitInputElement.hidden = true; parentForm.appendChild(this.fallbackSubmitInputElement); } } cleanUpEnterKeySubmitFallback() { if (this.fallbackSubmitInputElement != null) { try { this.fallbackSubmitInputElement.remove(); this.fallbackSubmitInputElement = null; } catch (err) { } } } /** * Detect whether the last node is an element (not text). * If so, it's probably an icon, so we set `iconPosition` to `after`. */ setIconPositionProp() { const nodes = Array.from(this.hostElement.childNodes).filter((node) => { // ignore empty text nodes, which are probably due to formatting return !(node.nodeType === 3 && node.nodeValue.trim() === ''); }); const lastNode = nodes.length > 1 ? nodes[nodes.length - 1] : null; if (!this.iconOnly && lastNode && utils.isScaleIcon(lastNode)) { this.iconPosition = 'after'; } } /** * Set any children icon's size according the button size. */ setChildrenIconSize() { if (this.size != null && buttonIconSizeMap[this.size] != null) { const icons = Array.from(this.hostElement.childNodes).filter(utils.isScaleIcon); icons.forEach((icon) => { if (icon.size === DEFAULT_ICON_SIZE) { icon.size = buttonIconSizeMap[this.size]; } }); } } render() { const basePart = index$1.classnames('base', this.variant && `variant-${this.variant}`, this.iconOnly && 'icon-only', !this.iconOnly && this.iconPosition, this.disabled && 'disabled'); return (index.h(index.Host, null, this.styles && index.h("style", null, this.styles), this.href ? (index.h("a", { ref: (el) => (this.focusableElement = el), class: this.getCssClassMap(), href: this.disabled ? null : this.href, download: this.download, target: this.target, rel: this.target === '_blank' ? 'noopener noreferrer' : undefined, part: basePart, tabIndex: this.innerTabindex, role: "link", "aria-disabled": this.disabled ? 'true' : null, "aria-label": this.innerAriaLabel }, index.h("slot", null))) : (index.h("button", { ref: (el) => (this.focusableElement = el), class: this.getCssClassMap(), onClick: this.handleClick, disabled: this.disabled, type: this.type, part: basePart, tabIndex: this.innerTabindex, name: this.name, value: this.value, "aria-label": this.innerAriaLabel }, index.h("slot", null))))); } getCssClassMap() { return index$1.classnames('button', this.size && `button--size-${this.size}`, this.variant && `button--variant-${this.variant}`, this.iconOnly && `button--icon-only`, !this.iconOnly && this.iconPosition && `button--icon-${this.iconPosition}`, this.disabled && `button--disabled`); } get hostElement() { return index.getElement(this); } }; Button.style = buttonCss; const menuFlyoutCss = ":host{--spacing-y-list:0;--spacing-x-list:0}"; const MENU_SELECTOR = '[role="menu"]'; const isButtonOrLink = (el) => { if (el.tagName.toUpperCase() === 'BUTTON' || el.tagName.toUpperCase() === 'A' || el.getAttribute('role') === 'button') { return el; } }; const MenuFlyout = class { constructor(hostRef) { index.registerInstance(this, hostRef); /** (optional) Determines whether the flyout should close when a menu item is selected */ this.closeOnSelect = true; /** (optional) Set preference for where the menu appears, space permitting */ this.direction = 'bottom-right'; this.lists = new Set(); this.closeAll = () => { this.lists.forEach(async (list) => { await list.close(); // Wait for `scale-close` event to fire list.active = false; // Make sure focus control is right while reopening }); }; this.toggle = () => { const list = this.getListElement(); if (list.opened) { this.closeAll(); return; } if (this.direction != null) { // Overwrite `direction` in list list.direction = this.direction; } list.trigger = () => this.trigger; list.open(); }; } async handleScaleOpen({ detail }) { // Close the previous active list and its parents if // - it's not the root and // - it's not the one being opened // (useful only with "click" interactions) const rootList = this.getListElement(); if (this.activeList && this.activeList.active && this.activeList !== rootList && this.activeList !== detail.list) { let list = this.activeList; while (list != null && list !== rootList) { await list.close(true); list = list.parentElement.closest(MENU_SELECTOR); } } this.activeList = detail.list; } handleScaleSelect({ detail }) { if (detail.closeOnSelect === false) { return; } if (this.closeOnSelect) { window.requestAnimationFrame(() => { this.closeAll(); }); } } handleScaleClose({ detail }) { const parent = detail.list != null ? detail.list.parentNode.closest(MENU_SELECTOR) : null; if (parent) { window.requestAnimationFrame(() => { parent.active = true; parent.setFocus(); }); } } handleWindowScroll() { this.closeAll(); } handleOutsideClick(event) { if (utils.isClickOutside(event, this.hostElement)) { this.closeAll(); } } handleKeydown(event) { if ('Tab' === event.key && !this.hostElement.querySelector('app-navigation-user-menu')) { if (this.trigger.tagName === 'SCALE-TELEKOM-NAV-ITEM') { this.trigger.firstElementChild.focus(); } this.closeAll(); return; } } componentDidLoad() { const triggerSlot = this.hostElement.querySelector('[slot="trigger"]'); const tagName = triggerSlot ? triggerSlot.tagName.toUpperCase() : ''; // TODO a different, more global, solution less dependent on tag names // would be great… if (triggerSlot && tagName === 'SCALE-BUTTON') { this.trigger = triggerSlot.shadowRoot.querySelector('button'); } else if (triggerSlot && tagName === 'SCALE-NAV-ICON') { this.trigger = triggerSlot.querySelector('a'); } else { this.trigger = triggerSlot; } this.lists = new Set(Array.from(this.hostElement.querySelectorAll(MENU_SELECTOR))); this.setTriggerAttributes(); } setTriggerAttributes() { const triggers = Array.from(this.hostElement.querySelectorAll('[role="menuitem"]')) .filter((el) => el.querySelector('[slot="sublist"]') != null) .concat([isButtonOrLink(this.trigger)]) .filter((x) => x != null); triggers.forEach((el) => { el.setAttribute('aria-haspopup', 'true'); el.setAttribute('aria-expanded', 'false'); }); } getListElement() { // TODO use [role="menu"]? return Array.from(this.hostElement.children).find((el) => el.tagName.toUpperCase().startsWith('SCALE-MENU-FLYOUT')); } render() { return (index.h(index.Host, null, this.styles && index.h("style", null, this.styles), index.h("div", { part: "trigger", onClick: this.toggle }, index.h("slot", { name: "trigger" })), index.h("slot", null))); } get hostElement() { return index.getElement(this); } }; MenuFlyout.style = menuFlyoutCss; const menuFlyoutListCss = ":host{box-sizing:content-box;position:fixed;z-index:100;pointer-events:none}.menu-flyout-list{display:none;position:absolute;pointer-events:initial;z-index:var(--scl-z-index-20);background:var(--telekom-color-background-surface);border-radius:var(--telekom-radius-standard);box-shadow:var(--telekom-shadow-overlay);overflow-y:hidden;margin-top:var(--spacing-y-list, 0);margin-bottom:var(--spacing-y-list, 0);margin-left:var(--spacing-x-list, 0);margin-right:var(--spacing-x-list, 0)}.menu-flyout-list::after{content:'';display:block;position:absolute;width:calc(100% - 2px);height:calc(100% - 2px);inset:0;border-radius:var(--telekom-radius-standard);border:1px solid transparent;pointer-events:none}.menu-flyout-list--opened{display:flex}.menu-flyout-list__list{padding:20px 0;overflow-y:auto;overflow-y:overlay;overscroll-behavior:contain;width:100%}.menu-flyout-list--flip-horizontal.menu-flyout-list--direction-bottom-left,.menu-flyout-list--flip-vertical.menu-flyout-list--direction-top-right,.menu-flyout-list--flip-horizontal.menu-flyout-list--flip-vertical.menu-flyout-list--direction-top-left,.menu-flyout-list--direction-bottom-right{top:calc(100% + var(--telekom-spacing-composition-space-03));left:0;right:auto;bottom:auto}.menu-flyout-list--flip-horizontal.menu-flyout-list--direction-bottom-right,.menu-flyout-list--flip-vertical.menu-flyout-list--direction-top-left,.menu-flyout-list--flip-horizontal.menu-flyout-list--flip-vertical.menu-flyout-list--direction-top-right,.menu-flyout-list--direction-bottom-left{top:calc(100% + var(--telekom-spacing-composition-space-03));right:0;left:auto;bottom:auto}.menu-flyout-list--flip-horizontal.menu-flyout-list--direction-top-left,.menu-flyout-list--flip-vertical.menu-flyout-list--direction-bottom-right,.menu-flyout-list--flip-horizontal.menu-flyout-list--flip-vertical.menu-flyout-list--direction-bottom-left,.menu-flyout-list--direction-top-right{bottom:calc(100% + var(--telekom-spacing-composition-space-03));left:0;right:auto;top:auto}.menu-flyout-list--flip-horizontal.menu-flyout-list--direction-top-right,.menu-flyout-list--flip-vertical.menu-flyout-list--direction-bottom-left,.menu-flyout-list--flip-horizontal.menu-flyout-list--flip-vertical.menu-flyout-list--direction-bottom-right,.menu-flyout-list--direction-top-left{bottom:calc(100% + var(--telekom-spacing-composition-space-03));right:0;left:auto;top:auto}.menu-flyout-list--flip-horizontal.menu-flyout-list--direction-left,.menu-flyout-list--direction-right{left:calc(100% - var(--telekom-spacing-composition-space-03));top:-20px;right:auto;bottom:auto}.menu-flyout-list--flip-horizontal.menu-flyout-list--direction-right,.menu-flyout-list--direction-left{right:calc(100% - var(--telekom-spacing-composition-space-03));top:-20px;left:auto;bottom:auto}.menu-flyout-list__scroll-up-indicator,.menu-flyout-list__scroll-down-indicator{position:absolute;width:0;border:5px solid transparent;pointer-events:none;opacity:0;left:50%}.menu-flyout-list__scroll-up-indicator{top:var(--telekom-spacing-composition-space-04);border-bottom:5px solid var(--telekom-color-ui-faint);border-top:0}.menu-flyout-list__scroll-down-indicator{bottom:var(--telekom-spacing-composition-space-04);border-top:5px solid var(--telekom-color-ui-faint);border-bottom:0}.menu-flyout-list--can-scroll-up .menu-flyout-list__scroll-up-indicator{opacity:1}.menu-flyout-list--can-scroll-down .menu-flyout-list__scroll-down-indicator{opacity:1}.menu-flyout-list--brand-header-dropdown ::slotted(scale-menu-flyout-item){--_min-width-moz:0;--_min-width:0}"; const PAD = 10; const ITEM_ROLES = ['menuitem', 'menuitemcheckbox', 'menuitemradio']; const MenuFlyoutList = class { constructor(hostRef) { index.registerInstance(this, hostRef); this.scaleOpen = index.createEvent(this, "scale-open", 7); this.scaleOpenLegacy = index.createEvent(this, "scaleOpen", 7); this.scaleClose = index.createEvent(this, "scale-close", 7); this.scaleCloseLegacy = index.createEvent(this, "scaleClose", 7); /** Used to force a re-render */ this.forceRender = 0; /** */ this.opened = false; /** (optional) Set preference for where the menu appears, space permitting */ this.direction = 'bottom-right'; /** */ this.active = false; /** (optional) Determines whether the flyout should close when a menu item is selected */ this.closeOnSelect = true; /** (optional) set to true when using in telekom-brand-header */ this.brandHeaderDropdown = false; /** Flags to know if content scrollable */ this.canScrollUp = false; this.canScrollDown = false; /** When menu off the screen horizontally */ this.flipHorizontal = false; /** When menu off the screen vertically */ this.flipVertical = false; /** Set true when resize or when opened */ this.needsCheckPlacement = true; this.handleScroll = () => { this.updateScrollIndicators(); }; this.handleWheel = (event) => { // TODO not sure this is doing anything atm this.stopWheelPropagation(event); }; } get triggerRect() { return this.trigger().getBoundingClientRect(); } componentDidRender() { if (this.opened && this.needsCheckPlacement) { this.setSize(); this.checkPlacement(); } } async open() { this.opened = true; utils.emitEvent(this, 'scaleOpen', { list: this.hostElement }); } async close(silent = false) { if (this.active && silent !== true) { utils.emitEvent(this, 'scaleClose', { list: this.hostElement }); } this.opened = false; } async setFocus() { if (this.focusedItemIndex != null) { this.focusItem(); } else { this.setInitialItemsFocus(); } } handleResize() { this.close(); } handleKeydown(event) { if (!this.active) { return; } if (!this.hostElement.querySelector('app-navigation-user-menu')) { event.preventDefault(); } if ('ArrowDown' === event.key) { this.shiftItemsFocus(); return; } if ('ArrowUp' === event.key) { this.shiftItemsFocus(-1); return; } if ('ArrowLeft' === event.key || 'Escape' === event.key) { this.close(); return; } if (' ' === event.key || 'Enter' === event.key || 'ArrowRight' === event.key) { const item = this.items[this.focusedItemIndex]; if (item != null) { item.triggerEvent(event, this.closeOnSelect); } } } /** * We handle item clicks here, to avoid setting up * listeners on every item */ handleClick(event) { const roleSelector = ITEM_ROLES.map((role) => `[role="${role}"]`).join(','); const item = event.target.closest(roleSelector); if (item != null) { event.stopImmediatePropagation(); item.triggerEvent(event, this.closeOnSelect); } } /** * Focus newly selected item */ handleScaleSelect({ detail }) { if (this.active && this.opened) { const index = this.items.findIndex((x) => x === detail.item); if (index != null) { this.focusedItemIndex = index; this.focusItem(); } } } /** * Set `active` to false when a descendant opens */ handleScaleOpen({ detail }) { if (detail.list !== this.hostElement) { this.active = false; } } openedChanged() { if (!this.opened) { this.active = false; this.focusedItemIndex = null; // Reset checks for boundary-aware placement this.needsCheckPlacement = true; this.flipHorizontal = false; this.flipVertical = false; this.hostElement.style.marginLeft = ''; this.hostElement.style.marginTop = ''; this.hostElement.style.marginRight = ''; this.hostElement.style.marginBottom = ''; if (this.trigger().tagName === 'SCALE-TELEKOM-NAV-ITEM') { this.trigger().style.color = 'var(--telekom-color-text-and-icon-standard)'; } } if (this.opened) { this.active = true; this.setFocus(); this.setWindowSize(); this.setPosition(); this.padForNonOverlayScrollbars(); this.updateScrollIndicators(); } this.updateTriggerAttributes(); } setInitialItemsFocus() { this.items = this.getListItems(); this.focusedItemIndex = -1; if (this.items.length > 0) { this.shiftItemsFocus(); } } shiftItemsFocus(direction = 1) { let nextIndex = this.focusedItemIndex + direction; if (nextIndex === this.items.length) { nextIndex = 0; } else if (nextIndex < 0) { nextIndex = this.items.length - 1; } this.focusedItemIndex = nextIndex; this.focusItem(); } focusItem() { window.requestAnimationFrame(() => { try { this.items[this.focusedItemIndex].focus(); } catch (err) { } }); } updateTriggerAttributes() { const trigger = this.trigger(); if (trigger && trigger.getAttribute('aria-haspopup') === 'true') { trigger.setAttribute('aria-expanded', String(this.opened)); } } setWindowSize() { this.windowWidth = window.innerWidth; this.windowHeight = window.innerHeight; } setPosition() { const { top, left } = this.triggerRect; this.hostElement.style.left = !this.brandHeaderDropdown ? `${left}px` : `${left - 4}px`; if (this.trigger().tagName === 'SCALE-TELEKOM-NAV-ITEM') { this.hostElement.style.top = `${top - 12}px`; this.hostElement.style.left = `${left - 24}px`; this.trigger().style.color = 'var(--telekom-color-text-and-icon-primary-standard)'; } else { this.hostElement.style.top = `${top}px`; } } setSize() { const { width, height } = this.triggerRect; this.hostElement.style.height = `${height}px`; this.hostElement.style.width = `${width}px`; if (this.brandHeaderDropdown) { this.base.style.width = `240px`; } } checkPlacement() { this.needsCheckPlacement = false; let isOutOfBounds = false; const rect = this.base.getBoundingClientRect(); // Check horizontal flips if (rect.left < PAD) { // console.log('off left edge'); isOutOfBounds = true; if (this.direction.includes('left')) { this.flipHorizontal = true; } } if (rect.right > this.windowWidth - PAD) { // console.log('off right edge'); isOutOfBounds = true; if (this.direction.includes('right')) { this.flipHorizontal = true; } } // Check vertical flips if (rect.top < PAD) { // console.log('off top edge'); isOutOfBounds = true; if (this.direction.includes('top')) { this.flipVertical = true; } } if (rect.bottom > this.windowHeight - PAD) { // console.log('off bottom edge'); isOutOfBounds = true; if (this.direction.includes('bottom')) { this.flipVertical = true; } } if (isOutOfBounds) { this.furtherAdjustPlacement(); } } furtherAdjustPlacement() { // Apply flip class changes immediately to avoid frame flash this.base.className = this.getCssClassMap(); // Force layout and style recalculation window.getComputedStyle(this.base); const rect = this.base.getBoundingClientRect(); // TODO: add more functionality for order of priority of which edge to snap to // Shift to be snapped to a padded edge // Note can't use transform as it creates // a relative parent for nested position fixed elements let left = 0; let top = 0; if (rect.left < PAD) { // console.log('still off left edge'); left = PAD - rect.left; } else if (rect.right > this.windowWidth - PAD) { // console.log('still off right edge'); left = this.windowWidth - PAD - rect.right; } if (rect.top < PAD) { // console.log('still off top edge'); top = PAD - rect.top; } else if (rect.bottom > this.windowHeight - PAD) { // console.log('still off bottom edge'); top = this.windowHeight - PAD - rect.bottom; } this.hostElement.style.marginLeft = `${left}px`; this.hostElement.style.marginTop = `${top}px`; this.hostElement.style.marginRight = `${-left}px`; this.hostElement.style.marginBottom = `${-top}px`; // Re-render visibly next frame with correct placement to update vdom setTimeout(() => this.forceRender++); } /** * Add scrollbar width to menu, to avoid horizontal scrollbars * or scrollbar forcing text-overflow. * (This affects Firefox and Safari, where non-overlay scrollbars * eat into content width rather than add) */ padForNonOverlayScrollbars() { this.base.style.paddingRight = `0px`; const scrollbarWidth = this.base.offsetWidth - this.base.clientWidth; this.base.style.paddingRight = `${scrollbarWidth}px`; } updateScrollIndicators() { // Reset this.canScrollDown = false; this.canScrollUp = false; const diff = this.list.scrollHeight - this.list.clientHeight; // Not scrollable if (diff) { if (this.list.scrollTop > 0) { this.canScrollUp = true; } if (this.list.scrollTop < diff) { this.canScrollDown = true; } } this.forceRender++; } /** * Check if going in a direction with content to reach, otherwise stop */ stopWheelPropagation(event) { // This is enough for Chrome event.stopPropagation(); // Needed for Safari and Firefox to prevent scrolling on non-scrollable lists if (!this.canScrollDown && !this.canScrollUp) { event.preventDefault(); } // Needed for Safari to prevent scrolling past the end of a scrollable list if (event.deltaY > 0 && !this.canScrollDown) { event.preventDefault(); } if (event.deltaY < 0 && !this.canScrollUp) { event.preventDefault(); } } getListItems() { return Array.from(this.hostElement.children).filter((el) => ITEM_ROLES.includes(el.getAttribute('role'))); } getCssClassMap() { return index$1.classnames('menu-flyout-list', `menu-flyout-list--direction-${this.direction}`, this.opened && 'menu-flyout-list--opened', this.canScrollUp && 'menu-flyout-list--can-scroll-up', this.canScrollDown && 'menu-flyout-list--can-scroll-down', this.flipHorizontal && `menu-flyout-list--flip-horizontal`, this.flipVertical && `menu-flyout-list--flip-vertical`, this.brandHeaderDropdown && `menu-flyout-list--brand-header-dropdown`); } render() { return (index.h(index.Host, { role: "menu" }, this.styles && index.h("style", null, this.styles), index.h("div", { class: this.getCssClassMap(), ref: (el) => (this.base = el), part: "base", style: { maxHeight: `calc(${this.windowHeight}px - 20px)` }, onWheelCapture: this.handleWheel }, index.h("div", { class: "menu-flyout-list__list", ref: (el) => (this.list = el), onScroll: this.handleScroll }, index.h("slot", null)), index.h("div", { "aria-hidden": "true", class: "menu-flyout-list__scroll-up-indicator" }), index.h("div", { "aria-hidden": "true", class: "menu-flyout-list__scroll-down-indicator" })))); } get hostElement() { return index.getElement(this); } static get watchers() { return { "opened": ["openedChanged"] }; } }; MenuFlyoutList.style = menuFlyoutListCss; exports.scale_button = Button; exports.scale_menu_flyout = MenuFlyout; exports.scale_menu_flyout_list = MenuFlyoutList;