@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
JavaScript
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 }.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);