UNPKG

@rhds/elements

Version:
356 lines 19.9 kB
var _RhDialog_instances, _RhDialog_screenSize, _RhDialog_headerId, _RhDialog_triggerElement, _RhDialog_header, _RhDialog_body, _RhDialog_headings, _RhDialog_cancelling, _RhDialog_slots, _RhDialog_onClick, _RhDialog_onNativeDialogCancel, _RhDialog_onKeyDown; import { __classPrivateFieldGet, __classPrivateFieldSet, __decorate } from "tslib"; import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import { classMap } from 'lit/directives/class-map.js'; import { bound, initializer, observes } from '@patternfly/pfe-core/decorators.js'; import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; import { ScreenSizeController } from '../../lib/ScreenSizeController.js'; import { themable } from '@rhds/elements/lib/themable.js'; import { css } from "lit"; const styles = css `:host{--_dialog-backdrop-bg-color:oklch(from var(--rh-color-surface-darker,#1f1f1f) l c h/0.68);display:block;position:relative}[hidden]{display:none!important}.visually-hidden{block-size:1px;border:0;clip:rect(0,0,0,0);inline-size:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap}#rhds-wrapper{--_offset:var(--rh-space-xl,24px);--_offset-top:var(--_offset);--_offset-right:var(--_offset);display:contents;font-family:var(--rh-font-family-body-text,RedHatText,"Red Hat Text",Helvetica,Arial,sans-serif)}::backdrop{background-color:var(--_dialog-backdrop-bg-color)}#dialog{background-color:light-dark(var(--rh-color-surface-lightest,#fff),var(--rh-color-surface-dark,#383838));border-radius:var(--rh-border-radius-default,3px);border:0;box-shadow:var(--rh-box-shadow-xl,0 8px 24px 3px #15151559);box-sizing:border-box;color:var(--rh-color-text-primary);font-family:inherit;inline-size:100%;margin-inline:auto;max-block-size:var(--_box-max-block-size,calc(100vh - var(--rh-space-3xl, 48px)));max-inline-size:var(--_box-width,min(90%,1140px));overflow-y:auto;overscroll-behavior:contain;padding:var(--rh-space-xl,24px);position:fixed}:host([variant=small]) #dialog,:host([width=small]) #dialog{--_box-width:35rem}:host([variant=medium]) #dialog,:host([width=medium]) #dialog{--_box-width:52.5rem}:host([variant=large]) #dialog,:host([width=large]) #dialog{--_box-width:70rem}#content{margin-block-start:calc(var(--_offset-top)*-1)}#header{background-color:light-dark(var(--rh-color-surface-lightest,#fff),var(--rh-color-surface-dark,#383838));inset-block-start:0;margin-block-end:var(--rh-space-lg,16px);position:relative}#header:before{block-size:100%;inset-block-start:calc(var(--_offset-top)*-1)}#header:after,#header:before{background-color:light-dark(var(--rh-color-surface-lightest,#fff),var(--rh-color-surface-dark,#383838));content:"";inline-size:100%;position:absolute;z-index:-1}#header:after{block-size:var(--rh-space-lg,16px)}@media (min-height:768px){#header{position:sticky}}@media (min-width:768px) and (min-height:576px){#header{position:sticky}}@media (min-width:1200px){#header{margin-block-end:var(--rh-space-xl,24px)}}#header[hidden]+#body{max-inline-size:calc(100% - var(--rh-space-xl, 24px))}#header,#header[hidden]+#body{padding-inline-end:var(--rh-space-3xl,48px)}@media (min-width:1200px){#rhds-wrapper{--_offset:var(--rh-space-2xl,32px)}#dialog{padding:var(--rh-space-2xl,32px)}#content:has(#header[hidden]){--_offset-top:var(--rh-space-xl,24px)}#header[hidden]+#body{max-inline-size:calc(100% - var(--rh-space-2xl, 32px))}}#content ::slotted([slot=header]){margin-block:0!important}#header ::slotted(:is(h1,h2,h3,h4,h5,h6)[slot=header]){font-family:var(--rh-font-family-heading,RedHatDisplay,"Red Hat Display",Helvetica,Arial,sans-serif);font-size:var(--rh-font-size-heading-sm,1.5rem);font-weight:var(--rh-font-weight-body-text-regular,400)}#body ::slotted(p){margin-block:0 var(--rh-space-lg,16px)!important}#body ::slotted(p:last-of-type){margin-block-end:var(--rh-space-xl,24px)!important}#close-button{color:var(--rh-dialog-close-button-color,var(--rh-color-icon-secondary));cursor:pointer;display:block;inset-block-start:0;margin-inline-start:auto;max-inline-size:var(--rh-length-xl,24px);position:sticky;z-index:500}#close-button::part(button){background-color:light-dark(var(--rh-color-surface-lightest,#fff),var(--rh-color-surface-dark,#383838));block-size:var(--rh-length-xl,24px);border-radius:var(--rh-border-radius-default,3px);inline-size:var(--rh-length-xl,24px)}:host([position=top]) #dialog{inline-size:100%;margin-block-start:var(--rh-space-2xl,32px);max-inline-size:var(--_box-width,calc(100% - var(--rh-space-2xl, 32px)))}#footer{align-items:center;display:flex;gap:var(--rh-space-md,8px)}:host([type=video]){--_dialog-backdrop-bg-color:rgb(from var(--rh-color-gray-90,#1f1f1f) r g b/var(--rh-opacity-60,60%));background-color:var(--_dialog-backdrop-bg-color)}:host([type=video]) #rhds-wrapper{--_offset:var(--rh-space-md,8px)}:host([type=video][open]) #dialog{--_aspect-ratio:var(--rh-dialog-video-aspect-ratio,16/9);aspect-ratio:var(--_aspect-ratio);background-color:var(--_dialog-backdrop-bg-color);max-inline-size:var(--_box-width,min(90%,calc(90vh*var(--_aspect-ratio) + var(--_offset-top))));padding:0}:host([type=video]) #close-button{inset-inline-end:var(--_offset-right);inset-block-start:var(--_offset-top);margin-inline-end:0;position:absolute}:host([type=video]) #rhds-wrapper.mobile #close-button{--_offset-right:var(--rh-space-sm,6px)}:host([type=video]) #close-button::part(button){background-color:initial}:host([type=video]) #close-button::part(icon){color:var(--rh-dialog-close-button-color,var(--rh-color-surface-lightest,#fff));filter:drop-shadow(1px 0 1px var(--_dialog-backdrop-bg-color))}:host([type=video]) #content{margin-block-start:0;overflow:hidden;padding:0}:host([type=video]) #content,:host([type=video]) ::slotted(:not([slot])){aspect-ratio:var(--rh-dialog-video-aspect-ratio,16/9);max-inline-size:none;inline-size:100%}:host([type=video]) ::slotted(:not([slot])){inset:0;position:absolute}`; import { query } from 'lit/decorators/query.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import '@rhds/elements/rh-surface/rh-surface.js'; import '@rhds/elements/rh-button/rh-button.js'; export class DialogCancelEvent extends Event { constructor() { super('cancel', { bubbles: true, cancelable: true }); } } export class DialogCloseEvent extends Event { constructor() { super('close', { bubbles: true, cancelable: true }); } } export class DialogOpenEvent extends Event { constructor( /** The element that opened the dialog, or null if opened programmatically. */ trigger) { super('open', { bubbles: true, cancelable: true }); this.trigger = trigger; } } async function pauseYoutube(iframe) { const { pauseVideo } = await import('./yt-api.js'); await pauseVideo(iframe); } /** * Modal overlay for confirming decisions or collecting input. Traps focus and * blocks page interaction. Must have a heading or `accessible-label` for screen * readers. Uses native `<dialog>` with `aria-labelledby`. Escape closes the * dialog; Tab cycles focus within it. Use sparingly to avoid disrupting workflow. * * @summary Modal dialog for confirmations, errors, or required input * * @alias dialog * * @fires {DialogOpenEvent} open - Fires when the dialog opens. The event's `trigger` * property (HTMLElement | null) holds the element that opened it. * @fires {DialogCloseEvent} close - Fires when the dialog closes via close button * or programmatic `close()`. No detail properties. * @fires {DialogCancelEvent} cancel - Fires when the user dismisses via backdrop * click or Escape. No detail properties. */ let RhDialog = class RhDialog extends LitElement { constructor() { super(...arguments); _RhDialog_instances.add(this); /** * Whether the dialog is currently open. */ this.open = false; /** @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue */ this.returnValue = ''; _RhDialog_screenSize.set(this, new ScreenSizeController(this)); _RhDialog_headerId.set(this, getRandomId()); _RhDialog_triggerElement.set(this, null); _RhDialog_header.set(this, null); _RhDialog_body.set(this, []); _RhDialog_headings.set(this, []); _RhDialog_cancelling.set(this, false); _RhDialog_slots.set(this, new SlotController(this, null, 'header', 'description', 'footer')); } connectedCallback() { super.connectedCallback(); this.addEventListener('keydown', __classPrivateFieldGet(this, _RhDialog_instances, "m", _RhDialog_onKeyDown)); this.addEventListener('click', __classPrivateFieldGet(this, _RhDialog_instances, "m", _RhDialog_onClick)); } disconnectedCallback() { super.disconnectedCallback(); __classPrivateFieldGet(this, _RhDialog_triggerElement, "f")?.removeEventListener('click', this.onTriggerClick); } render() { const headerId = (__classPrivateFieldGet(this, _RhDialog_header, "f") || __classPrivateFieldGet(this, _RhDialog_headings, "f").length) ? __classPrivateFieldGet(this, _RhDialog_headerId, "f") : undefined; const triggerLabel = __classPrivateFieldGet(this, _RhDialog_triggerElement, "f") ? __classPrivateFieldGet(this, _RhDialog_triggerElement, "f").innerText : undefined; const hasHeader = __classPrivateFieldGet(this, _RhDialog_slots, "f").hasSlotted('header'); const hasDescription = __classPrivateFieldGet(this, _RhDialog_slots, "f").hasSlotted('description'); const hasFooter = __classPrivateFieldGet(this, _RhDialog_slots, "f").hasSlotted('footer'); const { mobile } = __classPrivateFieldGet(this, _RhDialog_screenSize, "f"); return html ` <div id="rhds-wrapper" class="${classMap({ mobile })}"> <rh-surface class="${classMap({ hasHeader, hasDescription, hasFooter })}" ?hidden="${!this.open}"> <!-- The dialog element --> <dialog id="dialog" part="dialog" aria-labelledby=${ifDefined(this.accessibleLabel ? undefined : headerId)} aria-label=${ifDefined(this.accessibleLabel ? this.accessibleLabel : (!headerId ? triggerLabel : undefined))} @cancel=${__classPrivateFieldGet(this, _RhDialog_instances, "m", _RhDialog_onNativeDialogCancel)}> <!-- The dialog's close button --> <rh-button variant="close" id="close-button" part="close-button" type="button" @click=${this.close}> <span class="visually-hidden">Close Dialog</span> </rh-button> <!-- The container for the dialog content --> <div id="content" part="content"> <!-- The container for the optional dialog header --> <div id="header" part="header" ?hidden=${!hasHeader}> <!-- summary: Dialog heading description: | Should contain an h2-h6 describing the dialog's purpose. The heading becomes the accessible name via aria-labelledby. Sticks to the top when content overflows. --> <slot name="header"></slot> <!-- The container for the optional dialog description in the header --> <div part="description" ?hidden=${!hasDescription}> <!-- summary: Supplementary text below the heading description: | Brief context supporting the header. Hidden when empty. --> <slot name="description"></slot> </div> </div> <!-- The container for the dialog body content --> <div id="body" part="body"> <!-- summary: Primary dialog content description: | Accepts text, forms, images, or interactive elements. Scrolls vertically on overflow. For video dialogs, slot a video or YouTube iframe here. --> <slot></slot> </div> <!-- Actions footer container --> <div id="footer" part="footer" ?hidden=${!hasFooter}> <!-- summary: Action buttons at the bottom of the dialog description: | Primary and secondary action buttons (e.g. confirm, cancel). Hidden when empty. Focusable elements here are part of the dialog's Tab focus cycle. --> <slot name="footer"></slot> </div> </div> </dialog> </div> </div> `; } async _init() { await this.updateComplete; __classPrivateFieldSet(this, _RhDialog_header, this.querySelector(`[slot$="header"]`), "f"); __classPrivateFieldSet(this, _RhDialog_body, [...this.querySelectorAll(`*:not([slot])`)], "f"); __classPrivateFieldSet(this, _RhDialog_headings, __classPrivateFieldGet(this, _RhDialog_body, "f").filter(el => el.tagName.slice(0, 1) === 'H'), "f"); if (__classPrivateFieldGet(this, _RhDialog_triggerElement, "f")) { __classPrivateFieldGet(this, _RhDialog_triggerElement, "f").addEventListener('click', this.onTriggerClick); this.removeAttribute('hidden'); } if (__classPrivateFieldGet(this, _RhDialog_header, "f")) { __classPrivateFieldGet(this, _RhDialog_header, "f").id = __classPrivateFieldGet(this, _RhDialog_headerId, "f"); } else if (__classPrivateFieldGet(this, _RhDialog_headings, "f").length > 0) { // Get the first heading in the dialog if it exists __classPrivateFieldGet(this, _RhDialog_headings, "f")[0].id = __classPrivateFieldGet(this, _RhDialog_headerId, "f"); } } async _openChanged(oldValue, open) { if (this.type === 'video') { if (oldValue === true && this.open === false) { this.querySelector('video')?.pause?.(); const iframe = this.querySelector('iframe'); if (iframe?.src.match(/youtube/)) { pauseYoutube(iframe); } } } else if (oldValue == null || open == null // loosening types to prevent running these effects in unexpected circumstances // eslint-disable-next-line eqeqeq || oldValue == open) { return; } else if (open) { // This prevents background scroll document.body.style.overflow = 'hidden'; await this.updateComplete; this.dispatchEvent(new DialogOpenEvent(__classPrivateFieldGet(this, _RhDialog_triggerElement, "f"))); } else { // Return scrollability document.body.style.overflow = 'auto'; const event = __classPrivateFieldGet(this, _RhDialog_cancelling, "f") ? new DialogCancelEvent() : new DialogCloseEvent(); await this.updateComplete; this.dispatchEvent(event); } } _triggerChanged() { if (this.trigger) { __classPrivateFieldSet(this, _RhDialog_triggerElement, this.getRootNode().getElementById(this.trigger), "f"); __classPrivateFieldGet(this, _RhDialog_triggerElement, "f")?.addEventListener('click', this.onTriggerClick); } } async onTriggerClick(event) { event.preventDefault(); this.showModal(); await this.updateComplete; this.closeButton?.focus(); } /** * Cancels and closes the dialog, dispatching a cancel event. * @param [returnValue] dialog return value */ async cancel(returnValue) { __classPrivateFieldSet(this, _RhDialog_cancelling, true, "f"); this.close(returnValue); this.open = false; await this.updateComplete; __classPrivateFieldSet(this, _RhDialog_cancelling, false, "f"); } /** * Sets the trigger element programmatically. * @param element the element that should open the dialog on click */ setTrigger(element) { __classPrivateFieldSet(this, _RhDialog_triggerElement, element, "f"); __classPrivateFieldGet(this, _RhDialog_triggerElement, "f").addEventListener('click', this.onTriggerClick); } /** Toggles the dialog open or closed. */ toggle() { if (!this.open) { this.showModal(); this.open = true; } else { this.close(); } } /** Opens the dialog as a modal. */ show() { this.dialog?.showModal(); this.open = true; } /** Opens the dialog as a modal. */ showModal() { // TODO: non-modal mode this.show(); } /** * Closes the dialog. * @param [returnValue] dialog return value */ close(returnValue) { if (typeof returnValue === 'string') { this.returnValue = returnValue; } else { this.returnValue = ''; } this.dialog?.close(); this.open = false; } }; _RhDialog_screenSize = new WeakMap(); _RhDialog_headerId = new WeakMap(); _RhDialog_triggerElement = new WeakMap(); _RhDialog_header = new WeakMap(); _RhDialog_body = new WeakMap(); _RhDialog_headings = new WeakMap(); _RhDialog_cancelling = new WeakMap(); _RhDialog_slots = new WeakMap(); _RhDialog_instances = new WeakSet(); _RhDialog_onClick = function _RhDialog_onClick(event) { const { open, content } = this; if (open) { const path = event.composedPath(); if (!path.includes(content)) { event.preventDefault(); this.cancel(); } } }; _RhDialog_onNativeDialogCancel = function _RhDialog_onNativeDialogCancel() { this.cancel(); }; _RhDialog_onKeyDown = function _RhDialog_onKeyDown(event) { switch (event.key) { case 'Escape': case 'Esc': this.cancel(); return; case 'Enter': if (event.target === __classPrivateFieldGet(this, _RhDialog_triggerElement, "f")) { event.preventDefault(); this.showModal(); } return; } }; RhDialog.styles = [styles]; __decorate([ property({ reflect: true }) ], RhDialog.prototype, "variant", void 0); __decorate([ property({ reflect: true }) ], RhDialog.prototype, "position", void 0); __decorate([ property({ attribute: 'accessible-label' }) ], RhDialog.prototype, "accessibleLabel", void 0); __decorate([ property({ type: Boolean, reflect: true }) ], RhDialog.prototype, "open", void 0); __decorate([ property() ], RhDialog.prototype, "trigger", void 0); __decorate([ property({ reflect: true }) ], RhDialog.prototype, "type", void 0); __decorate([ query('#dialog') ], RhDialog.prototype, "dialog", void 0); __decorate([ query('#content') ], RhDialog.prototype, "content", void 0); __decorate([ query('#close-button') ], RhDialog.prototype, "closeButton", void 0); __decorate([ initializer() ], RhDialog.prototype, "_init", null); __decorate([ observes('open') ], RhDialog.prototype, "_openChanged", null); __decorate([ observes('trigger') ], RhDialog.prototype, "_triggerChanged", null); __decorate([ bound ], RhDialog.prototype, "onTriggerClick", null); RhDialog = __decorate([ customElement('rh-dialog'), themable ], RhDialog); export { RhDialog }; //# sourceMappingURL=rh-dialog.js.map