UNPKG

@zywave/zui-bundle

Version:

ZUI, out of the box, provides ES modules with bare path modifiers (e.g. `import '@zywave/zui-foo-bar'`). This is great as that's the way browsers are _going_, but they aren't there quite yet. Tooling exists to help solve this problem like webpack or rollu

448 lines (444 loc) 18.1 kB
import { c as css, p as property, Z as ZuiBaseElement, h as html, a as classMap, n as nothing } from '../internals/_ab875545.js'; import { q as queryAssignedNodes, s as screenBreakpoints } from '../internals/_1b5b557a.js'; /** * A class that mimics DOMTokenList for similar API and behavior. * DOMTokenList cannot be extended or instantiated directly. */ class ZuiDOMTokenList extends EventTarget { #tokens = new Set(); keys() { const arr = [...this.#tokens]; return arr.keys(); } values() { const arr = [...this.#tokens]; return arr.values(); } add(...tokens) { for (const token of tokens) { this.toggle(token, true); } } remove(...tokens) { for (const token of tokens) { this.toggle(token, false); } } toggle(token, force) { const originalSize = this.#tokens.size; // the below conditional checks if force argument was supplied (if force is a boolean, then !!force is the same value that was supplied) // if it wasn't supplied, then we toggle the token based on it existing in the set // if it was supplied, then we ensure that it is in or is not in the set, depending on the value of force if (force !== !!force) { this.#tokens.has(token) ? this.#tokens.delete(token) : this.#tokens.add(token); } else { force ? this.#tokens.add(token) : this.#tokens.delete(token); } if (this.#tokens.size !== originalSize) { this.#onTokenChange(); } return this.#tokens.has(token); } contains(token) { return this.#tokens.has(token); } item(index) { return [...this.#tokens][index] ?? null; } get length() { return this.#tokens.size; } [Symbol.iterator]() { const arr = [...this.#tokens]; return arr.values(); } #onTokenChange() { this.dispatchEvent(new Event('tokenchange')); } } const style = css`@supports(scrollbar-width: thin){section{scrollbar-color:var(--zui-gray-400) rgba(0,0,0,0);scrollbar-width:thin}}section::-webkit-scrollbar{width:7px;height:7px}section::-webkit-scrollbar-track{background-color:rgba(0,0,0,0);border-radius:7px}section::-webkit-scrollbar-thumb{background-color:var(--zui-gray-400);border-radius:7px}section::-webkit-scrollbar-thumb:hover{background-color:var(--zui-gray-300)}section::-webkit-scrollbar-thumb:active{background-color:var(--zui-gray-600)}:host([opened]) .content-root{right:0;visibility:visible}:host([mode=overlay]) article{height:100dvh}:host([mode=inline]) .content-root{top:var(--zui-flyout-top, 0);z-index:1001;background-color:#fff;box-shadow:0 .25rem .25rem rgba(0,0,0,.25)}:host([mode=inline]) .content-root article{height:100vh}:host([mode=inline]) .content-root.using-shell{top:var(--zui-flyout-top, calc(var(--zui-shell-topbar-global-height) + var(--zui-shell-topbar-app-height-collapsed) + var(--zui-shell-banner-height)))}:host([mode=inline]) .content-root.using-shell article{height:calc(100vh - (var(--zui-shell-topbar-global-height) + var(--zui-shell-topbar-app-height-collapsed) + var(--zui-shell-banner-height)))}:host([mode=inline]) .content-root.using-shell.shell-has-app-name{top:var(--zui-flyout-top, calc(var(--zui-shell-topbar-global-height) + var(--zui-shell-topbar-app-height) + var(--zui-shell-banner-height)))}:host([mode=inline]) .content-root.using-shell.shell-has-app-name article{height:calc(100vh - (var(--zui-flyout-top, calc(var(--zui-shell-topbar-global-height) + var(--zui-shell-topbar-app-height) + var(--zui-shell-banner-height)))))}:host([mode=inline][expanded]) .content-root{width:100vw}@media(min-width: 45em){:host([mode=inline][expanded]) .content-root{width:calc(100vw - var(--zui-shell-nav-width))}:host([mode=inline][expanded]) .content-root.shell-nav-collapsed{width:calc(100vw - var(--zui-shell-nav-width-collapsed))}}:host([mode=overlay][expanded]) .content-root{width:100vw}::slotted(h1[slot=header]){margin:0 !important}.content-root{position:fixed;right:-50vw;display:block;width:50vw;visibility:hidden;transition:right 250ms ease,width 250ms ease}@media(min-width: 45em){.content-root{right:calc(var(--zui-flyout-width, 50ch)*-1);width:var(--zui-flyout-width, 50ch)}}dialog{display:flex;max-width:none;max-height:none;margin-top:0;margin-right:0;margin-bottom:0;padding:0;border:0;box-shadow:0 .25rem .25rem rgba(0,0,0,.25);color:var(--zui-gray-800)}dialog::backdrop{background:rgba(0,0,0,.25)}article{display:flex;flex-direction:column}header{display:flex;justify-content:space-between;align-items:center;padding:.9375rem 1.25rem;border-bottom:.0625rem solid var(--zui-gray-100);gap:1ch}header button{display:block;padding:0;background:none;border:0;line-height:0;cursor:pointer}header zui-icon{color:var(--zui-gray-500)}header zui-icon[icon=zui-remove]{--zui-icon-size: 1.25rem}section,footer{padding:.625rem}section{height:100%;overflow-y:auto}footer{margin-top:auto;background-color:var(--zui-flyout-footer-background-color);border-top:.0625rem solid var(--zui-gray-100)}footer.hide{display:none}`; var __decorate = undefined && undefined.__decorate || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var _a; /** * `zui-flyout` is a panel that slides out from the right edge of the screen to display additional content. * * @element zui-flyout * * @slot - Default, unnamed slot; for inserting content into `<zui-flyout>`. * @slot header - Slot for inserting content into the header of `<zui-flyout>`. * @slot footer - Slot for inserting content into the footer of `<zui-flyout>`. * * @cssprop [--zui-flyout-top] - (optional) Not set; Override top position for `<zui-flyout>` in inline mode. Unnecessary if using `<zui-flyout>` inside of `<zui-shell>` or `<zywave-shell>`. * * @attr {boolean} [no-light-dismiss=false] - When set, prevents the flyout from closing when the user clicks on the backdrop * * ### Invoker Commands * `<zui-flyout>` supports the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API). * Use `commandfor` and `command` attributes on a `<button>` to control the flyout without JavaScript: * - `command="--zui-open"` - opens the flyout * - `command="--zui-close"` - closes the flyout * - `command="--zui-toggle"` - toggles the flyout open/closed */ class ZuiFlyoutElement extends ZuiBaseElement { static #flyouts = new Set(); #originalBodyOverflow; #mode; #opened; #expanded; #hasFooterContent; #controlsList; #boundNavCollapseChange; #internals; #identifier; get #contentElement() { if (this.contentSelector) { return document.querySelector(this.contentSelector); } const zuiShellElement = this._getElements('ZUI-SHELL')?.[0]; if (zuiShellElement) { return zuiShellElement; } return document.querySelector('body'); } static get styles() { return [super.styles, style]; } /** * The mode of the flyout. * @type { 'overlay' | 'inline' } */ get mode() { return this.#mode; } set mode(value) { const oldVal = this.#mode; if (oldVal !== value) { this.#mode = value; this.#onModeChanged(value, oldVal); } } /** * Returns a `ZuiDOMTokenList` containing the tokens of the `controlslist` attribute. * * When setting this property, provide a space-separated string of tokens. The tokens will be parsed * and added to the internal token list. Any tokens not present in the new value will be removed. * * **Supported tokens:** * - `noclose` - Hides the close button from the flyout header * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/controlsList | HTMLMediaElement.controlsList} */ get controlsList() { return this.#controlsList; } set controlsList(value) { if (!value) { return; } const tokens = value.split(/\s+/).filter(t => t); for (const token of tokens) { this.#controlsList.add(token); } const tokensToRemove = [...this.#controlsList].filter(token => !tokens.includes(token)); for (const token of tokensToRemove) { this.#controlsList.remove(token); } } /** * Represents if the dialog is currently open or not. Can apply to automatically open the dialog when attached to the DOM. */ get opened() { return this.#opened; } set opened(val) { const oldVal = this.#opened; if (oldVal !== val) { this.#opened = val; this.#onOpenedChanged(val, oldVal); } } /** * Represents if the flyout is currently expanded or not. */ get expanded() { return this.#expanded; } set expanded(val) { const oldVal = this.#expanded; if (oldVal !== val) { this.#expanded = val; } } get #dialog() { if (this.mode === 'overlay') { return this.shadowRoot?.querySelector('dialog') ?? null; } return null; } get #usingShell() { return !!this._getElements('ZUI-SHELL')?.length; } constructor() { super(); this.#originalBodyOverflow = null; this.#mode = 'overlay'; this.#opened = false; this.#expanded = false; this.#hasFooterContent = false; this.#controlsList = new ZuiDOMTokenList(); this.#boundNavCollapseChange = this.#onNavCollapseChange.bind(this); this.#identifier = Symbol(); /** * Specifies a CSS selector that targets the main content element of the application, allowing `<zui-flyout mode="inline">` to push the content over when opened. * Used when {@link mode} is set to `inline` in order to properly push content over. * This element will receive a class of `zui-flyout-inline-opened` when the flyout is opened. * @see mode */ this.contentSelector = null; /** * When set, prevents the flyout from closing when the user clicks on the backdrop (light dismiss). * Only applicable when {@link mode} is `overlay`. */ this.noLightDismiss = false; this.#onDialogClick = e => { if (!this.noLightDismiss && e.target === e.currentTarget) { this.close(); } }; this.#handleInvokerCommand = e => { const command = e.command; switch (command) { case '--zui-open': this.open(); break; case '--zui-close': this.close(); break; case '--zui-toggle': this.opened ? this.close() : this.open(); break; } }; this.#controlsList.addEventListener('tokenchange', () => this.requestUpdate()); this.#internals = this.attachInternals(); // initial a11y setup this.#internals.role = 'dialog'; this.#internals.ariaModal = 'true'; this.#internals.ariaHidden = 'true'; } connectedCallback() { super.connectedCallback(); window.addEventListener('navcollapsechange', this.#boundNavCollapseChange); _a.#flyouts.add(this); this.addEventListener('command', this.#handleInvokerCommand); this.#ensureExternalDOMState(); } disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener('navcollapsechange', this.#boundNavCollapseChange); _a.#flyouts.delete(this); this.removeEventListener('command', this.#handleInvokerCommand); this.#ensureExternalDOMState(); } /** * Opens the flyout. * @returns {Promise<void>} A promise that resolves when the flyout is opened. */ async open() { this.opened = true; await this.updateComplete; } /** * Closes the flyout. * @returns {Promise<void>} A promise that resolves when the flyout is closed. */ async close() { this.opened = false; this.expanded = false; await this.updateComplete; } /** * Expands the flyout */ expand() { this.expanded = true; } /** * Collapses the flyout */ collapse() { this.expanded = false; } render() { const contentRootStyles = { 'using-shell': this.#usingShell }; return this.mode === 'inline' ? this.#renderInlineFlyout(contentRootStyles) : this.#renderOverlayFlyout(contentRootStyles); } #renderOverlayFlyout(contentRootClasses) { return html`<dialog class="content-root ${classMap(contentRootClasses)}" @close="${() => this.close()}" @click="${this.#onDialogClick}" > ${this.#renderInternals()} </dialog>`; } #renderInlineFlyout(contentRootClasses) { const zuiShell = this._getElements('ZUI-SHELL')?.[0]; const zuiShellNav = this._getElements('ZUI-SHELL-NAV')?.[0]; const classes = { ...contentRootClasses, 'shell-has-app-name': !!zuiShell?.getAttribute('app-name'), 'shell-nav-collapsed': !!zuiShellNav?.hasAttribute('collapsed') }; return html`<div class="content-root ${classMap(classes)}"> ${this.#renderInternals()} </div>`; } #renderInternals() { return html` <article> <header> ${this.#renderExpandCollapseButton()} <slot name="header"></slot> ${this.#renderCloseButton()} </header> <section> <slot></slot> </section> <footer class="${classMap({ hide: !this.#hasFooterContent })}"> <slot name="footer" @slotchange="${this.#toggleFooter}"></slot> </footer> </article> `; } //todo: render if flyout is allowed to be expandable #renderExpandCollapseButton() { return this.expanded ? html`<button class="collapse" @click="${() => this.collapse()}"> <zui-icon icon="zui-collapse" class="small"></zui-icon> </button>` : html`<button class="expand" @click="${() => this.expand()}"> <zui-icon icon="zui-expand" class="small"></zui-icon> </button>`; } #renderCloseButton() { if (this.#controlsList.contains('noclose')) { return nothing; } return html`<button class="close" @click="${() => this.close()}"> <zui-icon icon="zui-remove"></zui-icon> </button>`; } #onOpenedChanged(newVal, oldVal) { this.requestUpdate('opened', oldVal); if (newVal) { for (const flyout of _a.#flyouts) { if (flyout.#identifier !== this.#identifier && flyout.opened) { flyout.close(); } } if (this.mode === 'overlay' && window.matchMedia(`(max-width: ${screenBreakpoints.xsmall})`).matches) { this.expanded = true; } } if (this.mode !== 'inline') { this.#ensureDialogState(newVal); } this.#ensureExternalDOMState(); this.#internals.ariaHidden = newVal ? 'false' : 'true'; const eventName = newVal ? 'open' : 'close'; this.dispatchEvent(new CustomEvent(eventName, { bubbles: true, composed: true })); } #onModeChanged(newVal, oldVal) { if (newVal === 'inline') { this.#internals.role = 'complementary'; this.#internals.ariaModal = 'false'; } else { this.#internals.role = 'dialog'; this.#internals.ariaModal = 'true'; } this.requestUpdate('mode', oldVal); } #onNavCollapseChange() { this.requestUpdate(); } #toggleFooter() { this.#hasFooterContent = this._footerSlottedNodes.length > 0; this.requestUpdate(); } /** * This method will ensure that the impacted layout changes are correct based on whether or not * any flyouts are open */ #ensureExternalDOMState() { const flyouts = Array.from(_a.#flyouts); const openedFlyouts = flyouts.filter(flyout => flyout.opened && flyout.isConnected); const anyOpenInlineFlyouts = openedFlyouts.some(flyout => flyout.mode === 'inline'); const anyOpenedFlyouts = openedFlyouts.length > 0; // there is a potential bug where the body can change its overflow later, but we have an old reference // this could maybe be improved by some property on ZuiFlyoutElement for developers to provider and/or a MutationObserver if (this.#originalBodyOverflow === null) { this.#originalBodyOverflow = document.body.style.overflow; } document.body.style.overflow = anyOpenedFlyouts ? 'hidden' : this.#originalBodyOverflow; document.body.classList.toggle('zui-flyout-inline-opened', anyOpenInlineFlyouts); if (this.#contentElement) { this.#contentElement.classList.toggle('zui-flyout-inline-opened', anyOpenInlineFlyouts); } const zuiShellContentActionbar = this._getElements('ZUI-SHELL-CONTENT-ACTIONBAR')?.[0]; zuiShellContentActionbar?.classList.toggle('zui-flyout-inline-opened', anyOpenInlineFlyouts); } async #ensureDialogState(opened) { if (!this.#dialog) { await this.updateComplete; } opened ??= this.opened; opened ? this.#dialog?.showModal() : this.#dialog?.close(); } #onDialogClick; #handleInvokerCommand; } _a = ZuiFlyoutElement; __decorate([property({ type: String, reflect: true })], ZuiFlyoutElement.prototype, "mode", null); __decorate([property({ attribute: 'controlslist', converter: { fromAttribute: value => value, toAttribute: value => { const tokens = [...value]; return tokens.length > 0 ? tokens.join(' ') : null; } } })], ZuiFlyoutElement.prototype, "controlsList", null); __decorate([property({ type: String, attribute: 'content-selector' })], ZuiFlyoutElement.prototype, "contentSelector", void 0); __decorate([property({ type: Boolean, reflect: true })], ZuiFlyoutElement.prototype, "opened", null); __decorate([property({ type: Boolean, reflect: true })], ZuiFlyoutElement.prototype, "expanded", null); __decorate([property({ type: Boolean, attribute: 'no-light-dismiss' })], ZuiFlyoutElement.prototype, "noLightDismiss", void 0); __decorate([queryAssignedNodes({ slot: 'footer' })], ZuiFlyoutElement.prototype, "_footerSlottedNodes", void 0); window.customElements.define('zui-flyout', ZuiFlyoutElement);