chrome-devtools-frontend
Version:
Chrome DevTools UI
740 lines (649 loc) • 28.3 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-lit-render-outside-of-view */
import * as i18n from '../../../core/i18n/i18n.js';
import * as Platform from '../../../core/platform/platform.js';
import * as WindowBoundsService from '../../../services/window_bounds/window_bounds.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
import * as Buttons from '../buttons/buttons.js';
import dialogStyles from './dialog.css.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Title of close button for the shortcuts dialog.
*/
close: 'Close',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/components/dialogs/Dialog.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const IS_DIALOG_SUPPORTED = 'HTMLDialogElement' in globalThis;
// Height in pixels of the dialog's connector. The connector is represented as
// as a diamond and the height corresponds to half the height of the diamond.
// (the visible height is only half of the diamond).
export const CONNECTOR_HEIGHT = 10;
const CONNECTOR_WIDTH = 2 * CONNECTOR_HEIGHT;
// The offset used by the dialog's animation as it slides in when opened.
const DIALOG_ANIMATION_OFFSET = 20;
export const DIALOG_SIDE_PADDING = 5;
export const DIALOG_VERTICAL_PADDING = 3;
// If the content of the dialog cannot be completely shown because otherwise
// the dialog would overflow the window, the dialog's max width and height are
// set such that the dialog remains inside the visible bounds. In this cases
// some extra, determined by this constant, is added so that the dialog's borders
// remain clearly visible. This constant accounts for the padding of the dialog's
// content (20 px) and a 5px gap left on each extreme of the dialog from the viewport.
export const DIALOG_PADDING_FROM_WINDOW = 3 * CONNECTOR_HEIGHT;
interface DialogData {
/**
* Position or point the dialog is shown relative to.
* If the dialog instance will be shown as a modal, set
* this property to MODAL.
*/
origin: DialogOrigin;
position: DialogVerticalPosition;
/**
* Horizontal alignment of the dialog with respect to its origin.
* Center by default.
*/
horizontalAlignment: DialogHorizontalAlignment;
/**
* Optional function used to the determine the x coordinate of the connector's
* end (tip of the triangle), relative to the viewport. If not defined, the x
* coordinate of the origin's center is used instead.
*/
getConnectorCustomXPosition: (() => number)|null;
/**
* Optional function called when the dialog is shown.
*/
dialogShownCallback: (() => unknown)|null;
/**
* Optional. Service that provides the window dimensions used for positioning the Dialog.
*/
windowBoundsService: WindowBoundsService.WindowBoundsService.WindowBoundsService;
/**
* Whether the dialog is closed when the 'Escape' key is pressed. When true, the event is
* propagation is stopped.
*/
closeOnESC: boolean;
/**
* Whether the dialog is closed when a scroll event is detected outside of the dialog's
* content. Defaults to true.
*/
closeOnScroll: boolean;
/**
* Whether render a closed button, when it is clicked, close the dialog. Defaults to false.
*/
closeButton: boolean;
/**
* The string used in the header row of the dialog.
*/
dialogTitle: string;
/**
* Specifies a context for the visual element.
*/
jslogContext: string;
}
type DialogAnchor = HTMLElement|DOMRect|DOMPoint;
export const MODAL = 'MODAL';
export type DialogOrigin = DialogAnchor|null|(() => DialogAnchor)|typeof MODAL;
export class Dialog extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
readonly #forceDialogCloseInDevToolsBound = this.#forceDialogCloseInDevToolsMutation.bind(this);
readonly #handleScrollAttemptBound = this.#handleScrollAttempt.bind(this);
readonly #props: DialogData = {
origin: MODAL,
position: DialogVerticalPosition.BOTTOM,
horizontalAlignment: DialogHorizontalAlignment.CENTER,
getConnectorCustomXPosition: null,
dialogShownCallback: null,
windowBoundsService: WindowBoundsService.WindowBoundsService.WindowBoundsServiceImpl.instance(),
closeOnESC: true,
closeOnScroll: true,
closeButton: false,
dialogTitle: '',
jslogContext: '',
};
#dialog: HTMLDialogElement|null = null;
#isPendingShowDialog = false;
#isPendingCloseDialog = false;
#hitArea = new DOMRect(0, 0, 0, 0);
#dialogClientRect = new DOMRect(0, 0, 0, 0);
#bestVerticalPositionInternal: DialogVerticalPosition|null = null;
#bestHorizontalAlignment: DialogHorizontalAlignment|null = null;
readonly #devtoolsMutationObserver = new MutationObserver(this.#forceDialogCloseInDevToolsBound);
readonly #dialogResizeObserver = new ResizeObserver(this.#updateDialogBounds.bind(this));
#devToolsBoundingElement = this.windowBoundsService.getDevToolsBoundingElement();
// We bind here because we have to listen to keydowns on the entire window,
// not on the Dialog element itself. This is because if the user has the
// dialog open, but their focus is elsewhere, and they hit ESC, we should
// still close the dialog.
#onKeyDownBound = this.#onKeyDown.bind(this);
get origin(): DialogOrigin {
return this.#props.origin;
}
set origin(origin: DialogOrigin) {
this.#props.origin = origin;
this.#onStateChange();
}
get position(): DialogVerticalPosition {
return this.#props.position;
}
set position(position: DialogVerticalPosition) {
this.#props.position = position;
this.#onStateChange();
}
get horizontalAlignment(): DialogHorizontalAlignment {
return this.#props.horizontalAlignment;
}
set horizontalAlignment(alignment: DialogHorizontalAlignment) {
this.#props.horizontalAlignment = alignment;
this.#onStateChange();
}
get windowBoundsService(): WindowBoundsService.WindowBoundsService.WindowBoundsService {
return this.#props.windowBoundsService;
}
set windowBoundsService(windowBoundsService: WindowBoundsService.WindowBoundsService.WindowBoundsService) {
this.#props.windowBoundsService = windowBoundsService;
this.#devToolsBoundingElement = this.windowBoundsService.getDevToolsBoundingElement();
this.#onStateChange();
}
get bestVerticalPosition(): DialogVerticalPosition|null {
return this.#bestVerticalPositionInternal;
}
get bestHorizontalAlignment(): DialogHorizontalAlignment|null {
return this.#bestHorizontalAlignment;
}
get getConnectorCustomXPosition(): (() => number)|null {
return this.#props.getConnectorCustomXPosition;
}
set getConnectorCustomXPosition(connectorXPosition: (() => number)|null) {
this.#props.getConnectorCustomXPosition = connectorXPosition;
this.#onStateChange();
}
get dialogShownCallback(): (() => unknown)|null {
return this.#props.dialogShownCallback;
}
get jslogContext(): string {
return this.#props.jslogContext;
}
set dialogShownCallback(dialogShownCallback: (() => unknown)|null) {
this.#props.dialogShownCallback = dialogShownCallback;
this.#onStateChange();
}
set closeOnESC(closeOnESC: boolean) {
this.#props.closeOnESC = closeOnESC;
this.#onStateChange();
}
set closeOnScroll(closeOnScroll: boolean) {
this.#props.closeOnScroll = closeOnScroll;
this.#onStateChange();
}
set closeButton(closeButton: boolean) {
this.#props.closeButton = closeButton;
this.#onStateChange();
}
set dialogTitle(dialogTitle: string) {
this.#props.dialogTitle = dialogTitle;
this.#onStateChange();
}
set jslogContext(jslogContext: string) {
this.#props.jslogContext = jslogContext;
this.#onStateChange();
}
#updateDialogBounds(): void {
this.#dialogClientRect = this.#getDialog().getBoundingClientRect();
}
#onStateChange(): void {
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
}
connectedCallback(): void {
window.addEventListener('resize', this.#forceDialogCloseInDevToolsBound);
this.#devtoolsMutationObserver.observe(this.#devToolsBoundingElement, {childList: true, subtree: true});
this.#devToolsBoundingElement.addEventListener('wheel', this.#handleScrollAttemptBound);
this.style.setProperty('--dialog-padding', '0');
this.style.setProperty('--dialog-display', IS_DIALOG_SUPPORTED ? 'block' : 'none');
this.style.setProperty('--override-dialog-content-border', `${CONNECTOR_HEIGHT}px solid transparent`);
this.style.setProperty('--dialog-padding', `${DIALOG_VERTICAL_PADDING}px ${DIALOG_SIDE_PADDING}px`);
}
disconnectedCallback(): void {
window.removeEventListener('resize', this.#forceDialogCloseInDevToolsBound);
this.#devToolsBoundingElement.removeEventListener('wheel', this.#handleScrollAttemptBound);
this.#devtoolsMutationObserver.disconnect();
this.#dialogResizeObserver.disconnect();
}
#getDialog(): HTMLDialogElement {
if (!this.#dialog) {
this.#dialog = this.#shadow.querySelector('dialog');
if (!this.#dialog) {
throw new Error('Dialog not found');
}
this.#dialogResizeObserver.observe(this.#dialog);
}
return this.#dialog;
}
getHitArea(): DOMRect {
return this.#hitArea;
}
async setDialogVisible(show: boolean): Promise<void> {
if (show) {
await this.#showDialog();
return;
}
this.#closeDialog();
}
async #handlePointerEvent(evt: MouseEvent|PointerEvent): Promise<void> {
evt.stopPropagation();
// If the user uses the keyboard to interact with an element within the
// dialog, it will trigger a pointer event (for example, the user might use
// their spacebar to "click" on a form input element). In that case the
// pointerType will be an empty string, rather than `mouse`, `pen` or
// `touch`. In this instance, we early return, because we only need to
// worry about clicks outside of the dialog. Once the dialog is open, the
// user can only use the keyboard to navigate within the dialog; so we
// don't have to concern ourselves with keyboard events that occur outside
// the dialog's bounds.
if (evt instanceof PointerEvent && evt.pointerType === '') {
return;
}
const eventWasInDialogContent = this.#mouseEventWasInDialogContent(evt);
const eventWasInHitArea = this.#mouseEventWasInHitArea(evt);
if (eventWasInDialogContent) {
return;
}
if (evt.type === 'pointermove') {
if (eventWasInHitArea) {
return;
}
this.dispatchEvent(new PointerLeftDialogEvent());
return;
}
this.dispatchEvent(new ClickOutsideDialogEvent());
}
#animationEndedEvent(): void {
this.dispatchEvent(new AnimationEndedEvent());
}
#mouseEventWasInDialogContent(evt: MouseEvent): boolean {
const dialogBounds = this.#dialogClientRect;
let animationOffSetValue = this.bestVerticalPosition === DialogVerticalPosition.BOTTOM ?
DIALOG_ANIMATION_OFFSET :
-1 * DIALOG_ANIMATION_OFFSET;
if (this.#props.origin === MODAL) {
// When shown as a modal, the dialog is not animated
animationOffSetValue = 0;
}
const eventWasDialogContentX =
evt.pageX >= dialogBounds.left && evt.pageX <= dialogBounds.left + dialogBounds.width;
const eventWasDialogContentY = evt.pageY >= dialogBounds.top + animationOffSetValue &&
evt.pageY <= dialogBounds.top + dialogBounds.height + animationOffSetValue;
return eventWasDialogContentX && eventWasDialogContentY;
}
#mouseEventWasInHitArea(evt: MouseEvent): boolean {
const hitAreaBounds = this.#hitArea;
const eventWasInHitAreaX = evt.pageX >= hitAreaBounds.left && evt.pageX <= hitAreaBounds.left + hitAreaBounds.width;
const eventWasInHitAreaY = evt.pageY >= hitAreaBounds.top && evt.pageY <= hitAreaBounds.top + hitAreaBounds.height;
return eventWasInHitAreaX && eventWasInHitAreaY;
}
#getCoordinatesFromDialogOrigin(origin: DialogOrigin): AnchorBounds {
if (!origin || origin === MODAL) {
throw new Error('Dialog origin is null');
}
const anchor = origin instanceof Function ? origin() : origin;
if (anchor instanceof DOMPoint) {
return {top: anchor.y, bottom: anchor.y, left: anchor.x, right: anchor.x};
}
if (anchor instanceof HTMLElement) {
return anchor.getBoundingClientRect();
}
return anchor;
}
#getBestHorizontalAlignment(anchorBounds: AnchorBounds, devtoolsBounds: DOMRect): DialogHorizontalAlignment {
if (devtoolsBounds.right - anchorBounds.left > anchorBounds.right - devtoolsBounds.left) {
return DialogHorizontalAlignment.LEFT;
}
return DialogHorizontalAlignment.RIGHT;
}
#getBestVerticalPosition(originBounds: AnchorBounds, dialogHeight: number, devtoolsBounds: DOMRect):
DialogVerticalPosition {
// If the dialog's full height doesn't fit at the bottom attempt to
// position it at the top. If it doesn't fit at the top either
// position it at the bottom and make the overflow scrollable.
if (originBounds.bottom + dialogHeight > devtoolsBounds.height &&
originBounds.top - dialogHeight > devtoolsBounds.top) {
return DialogVerticalPosition.TOP;
}
return DialogVerticalPosition.BOTTOM;
}
#positionDialog(): void {
if (!this.#props.origin) {
return;
}
this.#isPendingShowDialog = true;
void RenderCoordinator.read(() => {
// Fixed elements are positioned relative to the window, regardless if
// DevTools is docked. As such, if DevTools is docked we must account for
// its offset relative to the window when positioning fixed elements.
// DevTools' effective offset can be determined using
// this.#devToolsBoundingElement.
const devtoolsBounds = this.#devToolsBoundingElement.getBoundingClientRect();
const devToolsWidth = devtoolsBounds.width;
const devToolsHeight = devtoolsBounds.height;
const devToolsLeft = devtoolsBounds.left;
const devToolsTop = devtoolsBounds.top;
const devToolsRight = devtoolsBounds.right;
if (this.#props.origin === MODAL) {
void RenderCoordinator.write(() => {
this.style.setProperty('--dialog-top', `${devToolsTop}px`);
this.style.setProperty('--dialog-left', `${devToolsLeft}px`);
this.style.setProperty('--dialog-margin', 'auto');
this.style.setProperty('--dialog-margin-left', 'auto');
this.style.setProperty('--dialog-margin-bottom', 'auto');
this.style.setProperty('--dialog-max-height', `${devToolsHeight - DIALOG_PADDING_FROM_WINDOW}px`);
this.style.setProperty('--dialog-max-width', `${devToolsWidth - DIALOG_PADDING_FROM_WINDOW}px`);
this.style.setProperty('--dialog-right', `${document.body.clientWidth - devToolsRight}px`);
});
return;
}
const anchor = this.#props.origin;
const absoluteAnchorBounds = this.#getCoordinatesFromDialogOrigin(anchor);
const {top: anchorTop, right: anchorRight, bottom: anchorBottom, left: anchorLeft} = absoluteAnchorBounds;
const originCenterX = (anchorLeft + anchorRight) / 2;
const hitAreaWidth = anchorRight - anchorLeft + CONNECTOR_HEIGHT;
const windowWidth = document.body.clientWidth;
const connectorFixedXValue =
this.#props.getConnectorCustomXPosition ? this.#props.getConnectorCustomXPosition() : originCenterX;
void RenderCoordinator.write(() => {
this.style.setProperty('--dialog-top', '0');
// Start by showing the dialog hidden to allow measuring its width.
const dialog = this.#getDialog();
dialog.style.visibility = 'hidden';
if (this.#isPendingShowDialog && !dialog.hasAttribute('open')) {
dialog.showModal();
this.setAttribute('open', '');
this.#isPendingShowDialog = false;
}
const {width: dialogWidth, height: dialogHeight} = dialog.getBoundingClientRect();
this.#bestHorizontalAlignment = this.#props.horizontalAlignment === DialogHorizontalAlignment.AUTO ?
this.#getBestHorizontalAlignment(absoluteAnchorBounds, devtoolsBounds) :
this.#props.horizontalAlignment;
this.#bestVerticalPositionInternal = this.#props.position === DialogVerticalPosition.AUTO ?
this.#getBestVerticalPosition(absoluteAnchorBounds, dialogHeight, devtoolsBounds) :
this.#props.position;
if (this.#bestHorizontalAlignment === DialogHorizontalAlignment.AUTO ||
this.#bestVerticalPositionInternal === DialogVerticalPosition.AUTO) {
return;
}
this.#hitArea.height = anchorBottom - anchorTop + CONNECTOR_HEIGHT;
this.#hitArea.width = hitAreaWidth;
// If the connector is to be shown, the dialog needs a minimum width such that it covers
// the connector's width.
this.style.setProperty(
'--content-min-width',
`${connectorFixedXValue - anchorLeft + CONNECTOR_WIDTH + DIALOG_SIDE_PADDING * 2}px`);
this.style.setProperty('--dialog-left', 'auto');
this.style.setProperty('--dialog-right', 'auto');
this.style.setProperty('--dialog-margin', '0');
switch (this.#bestHorizontalAlignment) {
case DialogHorizontalAlignment.LEFT: {
// Position the dialog such that its left border is in line with that of its anchor.
// If this means the dialog's left border is out of DevTools bounds, move it to the right.
// Cap its width as needed so that the right border doesn't overflow.
const dialogLeft = Math.max(anchorLeft, devToolsLeft);
const devtoolsRightBorderToDialogLeft = devToolsRight - dialogLeft;
const dialogMaxWidth = devtoolsRightBorderToDialogLeft - DIALOG_PADDING_FROM_WINDOW;
this.style.setProperty('--dialog-left', `${dialogLeft}px`);
this.#hitArea.x = anchorLeft;
this.style.setProperty('--dialog-max-width', `${dialogMaxWidth}px`);
break;
}
case DialogHorizontalAlignment.RIGHT: {
// Position the dialog such that its right border is in line with that of its anchor.
// If this means the dialog's right border is out of DevTools bounds, move it to the left.
// Cap its width as needed so that the left border doesn't overflow.
const windowRightBorderToAnchorRight = windowWidth - anchorRight;
const windowRightBorderToDevToolsRight = windowWidth - devToolsRight;
const windowRightBorderToDialogRight =
Math.max(windowRightBorderToAnchorRight, windowRightBorderToDevToolsRight);
const dialogRight = windowWidth - windowRightBorderToDialogRight;
const devtoolsLeftBorderToDialogRight = dialogRight - devToolsLeft;
const dialogMaxWidth = devtoolsLeftBorderToDialogRight - DIALOG_PADDING_FROM_WINDOW;
this.#hitArea.x = windowWidth - windowRightBorderToDialogRight - hitAreaWidth;
this.style.setProperty('--dialog-right', `${windowRightBorderToDialogRight}px`);
this.style.setProperty('--dialog-max-width', `${dialogMaxWidth}px`);
break;
}
case DialogHorizontalAlignment.CENTER: {
// Position the dialog aligned with its anchor's center as long as its borders don't overlap
// with those of DevTools. In case one border overlaps, move the dialog to the opposite side.
// In case both borders overlap, reduce its width to that of DevTools.
const dialogCappedWidth = Math.min(devToolsWidth - DIALOG_PADDING_FROM_WINDOW, dialogWidth);
let dialogLeft = Math.max(originCenterX - dialogCappedWidth * 0.5, devToolsLeft);
dialogLeft = Math.min(dialogLeft, devToolsRight - dialogCappedWidth);
this.style.setProperty('--dialog-left', `${dialogLeft}px`);
this.#hitArea.x = originCenterX - hitAreaWidth * 0.5;
this.style.setProperty('--dialog-max-width', `${devToolsWidth - DIALOG_PADDING_FROM_WINDOW}px`);
break;
}
default:
Platform.assertNever(
this.#bestHorizontalAlignment, `Unknown alignment type: ${this.#bestHorizontalAlignment}`);
}
switch (this.#bestVerticalPositionInternal) {
case DialogVerticalPosition.TOP: {
this.style.setProperty('--dialog-top', '0');
this.style.setProperty('--dialog-margin', 'auto');
this.style.setProperty('--dialog-margin-bottom', `${innerHeight - anchorTop}px`);
this.#hitArea.y = anchorTop - CONNECTOR_HEIGHT;
this.style.setProperty('--dialog-offset-y', `${DIALOG_ANIMATION_OFFSET}px`);
this.style.setProperty(
'--dialog-max-height', `${devToolsHeight - (innerHeight - anchorTop) - DIALOG_PADDING_FROM_WINDOW}px`);
break;
}
case DialogVerticalPosition.BOTTOM: {
this.style.setProperty('--dialog-top', `${anchorBottom}px`);
this.#hitArea.y = anchorTop;
this.style.setProperty('--dialog-offset-y', `-${DIALOG_ANIMATION_OFFSET}px`);
this.style.setProperty(
'--dialog-max-height',
`${devToolsHeight - (anchorBottom - devToolsTop) - DIALOG_PADDING_FROM_WINDOW}px`);
break;
}
default:
Platform.assertNever(
this.#bestVerticalPositionInternal, `Unknown position type: ${this.#bestVerticalPositionInternal}`);
}
dialog.close();
dialog.style.visibility = '';
});
});
}
async #showDialog(): Promise<void> {
if (!IS_DIALOG_SUPPORTED) {
return;
}
if (this.#isPendingShowDialog || this.hasAttribute('open')) {
return;
}
this.#isPendingShowDialog = true;
this.#positionDialog();
// Allow the CSS variables to be set before showing.
await RenderCoordinator.done();
this.#isPendingShowDialog = false;
const dialog = this.#getDialog();
// Make the dialog visible now.
if (!dialog.hasAttribute('open')) {
dialog.showModal();
}
if (this.#props.dialogShownCallback) {
await this.#props.dialogShownCallback();
}
this.#updateDialogBounds();
document.body.addEventListener('keydown', this.#onKeyDownBound);
}
#handleScrollAttempt(event: WheelEvent): void {
if (this.#mouseEventWasInDialogContent(event) || !this.#props.closeOnScroll ||
!this.#getDialog().hasAttribute('open')) {
return;
}
this.#closeDialog();
this.dispatchEvent(new ForcedDialogClose());
}
#onKeyDown(event: KeyboardEvent): void {
if (!this.#getDialog().hasAttribute('open') || !this.#props.closeOnESC) {
return;
}
if (event.key !== Platform.KeyboardUtilities.ESCAPE_KEY) {
return;
}
event.stopPropagation();
event.preventDefault();
this.#closeDialog();
this.dispatchEvent(new ForcedDialogClose());
}
#onCancel(event: Event): void {
event.stopPropagation();
event.preventDefault();
if (!this.#getDialog().hasAttribute('open') || !this.#props.closeOnESC) {
return;
}
this.dispatchEvent(new ForcedDialogClose());
}
#forceDialogCloseInDevToolsMutation(): void {
if (!this.#dialog?.hasAttribute('open')) {
return;
}
if (this.#devToolsBoundingElement === document.body) {
// Do not close if running in test environment.
return;
}
this.#closeDialog();
this.dispatchEvent(new ForcedDialogClose());
}
#closeDialog(): void {
if (this.#isPendingCloseDialog || !this.#getDialog().hasAttribute('open')) {
return;
}
this.#isPendingCloseDialog = true;
void RenderCoordinator.write(() => {
this.#hitArea.width = 0;
this.removeAttribute('open');
this.#getDialog().close();
this.#isPendingCloseDialog = false;
document.body.removeEventListener('keydown', this.#onKeyDownBound);
});
}
getDialogBounds(): DOMRect {
return this.#dialogClientRect;
}
#renderHeaderRow(): Lit.TemplateResult|null {
// If the title is empty and close button is false, let's skip the header row.
if (!this.#props.dialogTitle && !this.#props.closeButton) {
return null;
}
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
<span class="dialog-header-text">${this.#props.dialogTitle}</span>
${this.#props.closeButton ? html`
<devtools-button
=${this.#closeDialog}
.data=${{
variant: Buttons.Button.Variant.TOOLBAR,
iconName: 'cross',
title: i18nString(UIStrings.close),
size: Buttons.Button.Size.SMALL,
} as Buttons.Button.ButtonData}
jslog=${VisualLogging.close().track({click: true})}
></devtools-button>
` : Lit.nothing}
`;
// clang-format on
}
#render(): void {
if (!ComponentHelpers.ScheduledRender.isScheduledRender(this)) {
throw new Error('Dialog render was not scheduled');
}
if (!IS_DIALOG_SUPPORTED) {
// To make sure that light dom content passed into this component doesn't show up,
// we have to explicitly render a slot and hide it with CSS.
Lit.render(
// clang-format off
html`
<slot></slot>
`, this.#shadow, {host: this});
// clang-format on
return;
}
// clang-format off
Lit.render(html`
<style>${dialogStyles}</style>
<dialog =${this.#handlePointerEvent} =${this.#handlePointerEvent} =${this.#onCancel} =${this.#animationEndedEvent}
jslog=${VisualLogging.dialog(this.#props.jslogContext).track({resize: true, keydown: 'Escape'}).parent('mapped')}>
<div id="content">
<div class="dialog-header">${this.#renderHeaderRow()}</div>
<div class='dialog-content'>
<slot></slot>
</div>
</div>
</dialog>
`, this.#shadow, { host: this });
VisualLogging.setMappedParent(this.#getDialog(), this.parentElementOrShadowHost() as HTMLElement);
// clang-format on
}
}
customElements.define('devtools-dialog', Dialog);
declare global {
interface HTMLElementTagNameMap {
'devtools-dialog': Dialog;
}
}
export class PointerLeftDialogEvent extends Event {
static readonly eventName = 'pointerleftdialog';
constructor() {
super(PointerLeftDialogEvent.eventName, {bubbles: true, composed: true});
}
}
export class ClickOutsideDialogEvent extends Event {
static readonly eventName = 'clickoutsidedialog';
constructor() {
super(ClickOutsideDialogEvent.eventName, {bubbles: true, composed: true});
}
}
export class AnimationEndedEvent extends Event {
static readonly eventName = 'animationended';
constructor() {
super(AnimationEndedEvent.eventName, {bubbles: true, composed: true});
}
}
export class ForcedDialogClose extends Event {
static readonly eventName = 'forceddialogclose';
constructor() {
super(ForcedDialogClose.eventName, {bubbles: true, composed: true});
}
}
export const enum DialogVerticalPosition {
TOP = 'top',
BOTTOM = 'bottom',
AUTO = 'auto',
}
export const enum DialogHorizontalAlignment {
// Dialog and anchor are aligned on their left borders.
LEFT = 'left',
// Dialog and anchor are aligned on their right borders.
RIGHT = 'right',
CENTER = 'center',
// This option allows to set the alignment
// automatically to LEFT or RIGHT depending
// on whether the dialog overflows the
// viewport if it's aligned to the left.
AUTO = 'auto',
}
interface AnchorBounds {
top: number;
bottom: number;
left: number;
right: number;
}