diff options
10 files changed, 386 insertions, 293 deletions
diff --git a/.changeset/healthy-hornets-kiss.md b/.changeset/healthy-hornets-kiss.md new file mode 100644 index 000000000..df69c840c --- /dev/null +++ b/.changeset/healthy-hornets-kiss.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix Dev Overlay not working properly when view transitions are enabled diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 7b08808a5..4ae5e7138 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -21,7 +21,11 @@ import type { TSConfig } from '../core/config/tsconfig.js'; import type { AstroCookies } from '../core/cookies/index.js'; import type { ResponseWithEncoding } from '../core/endpoint/index.js'; import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js'; +import type { AstroDevOverlay, DevOverlayCanvas } from '../runtime/client/dev-overlay/overlay.js'; +import type { DevOverlayHighlight } from '../runtime/client/dev-overlay/ui-library/highlight.js'; import type { Icon } from '../runtime/client/dev-overlay/ui-library/icons.js'; +import type { DevOverlayTooltip } from '../runtime/client/dev-overlay/ui-library/tooltip.js'; +import type { DevOverlayWindow } from '../runtime/client/dev-overlay/ui-library/window.js'; import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js'; import type { OmitIndexSignature, Simplify } from '../type-utils.js'; import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'; @@ -2322,3 +2326,13 @@ export type DevOverlayMetadata = Window & root: string; }; }; + +declare global { + interface HTMLElementTagNameMap { + 'astro-dev-overlay': AstroDevOverlay; + 'astro-dev-overlay-window': DevOverlayWindow; + 'astro-dev-overlay-plugin-canvas': DevOverlayCanvas; + 'astro-dev-overlay-tooltip': DevOverlayTooltip; + 'astro-dev-overlay-highlight': DevOverlayHighlight; + } +} diff --git a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts new file mode 100644 index 000000000..fe7efcccc --- /dev/null +++ b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts @@ -0,0 +1,84 @@ +import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js'; +import { type AstroDevOverlay, type DevOverlayPlugin } from './overlay.js'; + +let overlay: AstroDevOverlay; + +document.addEventListener('DOMContentLoaded', async () => { + const [ + { loadDevOverlayPlugins }, + { default: astroDevToolPlugin }, + { default: astroAuditPlugin }, + { default: astroXrayPlugin }, + { AstroDevOverlay, DevOverlayCanvas }, + { DevOverlayCard }, + { DevOverlayHighlight }, + { DevOverlayTooltip }, + { DevOverlayWindow }, + ] = await Promise.all([ + // @ts-expect-error + import('astro:dev-overlay'), + import('./plugins/astro.js'), + import('./plugins/audit.js'), + import('./plugins/xray.js'), + import('./overlay.js'), + import('./ui-library/card.js'), + import('./ui-library/highlight.js'), + import('./ui-library/tooltip.js'), + import('./ui-library/window.js'), + ]); + + // Register custom elements + customElements.define('astro-dev-overlay', AstroDevOverlay); + customElements.define('astro-dev-overlay-window', DevOverlayWindow); + customElements.define('astro-dev-overlay-plugin-canvas', DevOverlayCanvas); + customElements.define('astro-dev-overlay-tooltip', DevOverlayTooltip); + customElements.define('astro-dev-overlay-highlight', DevOverlayHighlight); + customElements.define('astro-dev-overlay-card', DevOverlayCard); + + overlay = document.createElement('astro-dev-overlay'); + + const preparePlugin = ( + pluginDefinition: DevOverlayPluginDefinition, + builtIn: boolean + ): DevOverlayPlugin => { + const eventTarget = new EventTarget(); + const plugin = { + ...pluginDefinition, + builtIn: builtIn, + active: false, + status: 'loading' as const, + eventTarget: eventTarget, + }; + + // Events plugins can send to the overlay to update their status + eventTarget.addEventListener('plugin-notification', (evt) => { + const target = overlay.shadowRoot?.querySelector(`[data-plugin-id="${plugin.id}"]`); + if (!target) return; + + let newState = true; + if (evt instanceof CustomEvent) { + newState = evt.detail.state ?? true; + } + + target.querySelector('.notification')?.toggleAttribute('data-active', newState); + }); + + return plugin; + }; + + const customPluginsDefinitions = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[]; + const plugins: DevOverlayPlugin[] = [ + ...[astroDevToolPlugin, astroXrayPlugin, astroAuditPlugin].map((pluginDef) => + preparePlugin(pluginDef, true) + ), + ...customPluginsDefinitions.map((pluginDef) => preparePlugin(pluginDef, false)), + ]; + + overlay.plugins = plugins; + + document.body.append(overlay); + + document.addEventListener('astro:after-swap', () => { + document.body.append(overlay); + }); +}); diff --git a/packages/astro/src/runtime/client/dev-overlay/overlay.ts b/packages/astro/src/runtime/client/dev-overlay/overlay.ts index e93f3bcac..00ca35569 100644 --- a/packages/astro/src/runtime/client/dev-overlay/overlay.ts +++ b/packages/astro/src/runtime/client/dev-overlay/overlay.ts @@ -1,79 +1,47 @@ /* eslint-disable no-console */ -// @ts-expect-error -import { loadDevOverlayPlugins } from 'astro:dev-overlay'; import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js'; -import astroDevToolPlugin from './plugins/astro.js'; -import astroAuditPlugin from './plugins/audit.js'; -import astroXrayPlugin from './plugins/xray.js'; -import { DevOverlayCard } from './ui-library/card.js'; -import { DevOverlayHighlight } from './ui-library/highlight.js'; import { getIconElement, isDefinedIcon, type Icon } from './ui-library/icons.js'; -import { DevOverlayTooltip } from './ui-library/tooltip.js'; -import { DevOverlayWindow } from './ui-library/window.js'; -type DevOverlayPlugin = DevOverlayPluginDefinition & { +export type DevOverlayPlugin = DevOverlayPluginDefinition & { + builtIn: boolean; active: boolean; status: 'ready' | 'loading' | 'error'; eventTarget: EventTarget; }; -document.addEventListener('DOMContentLoaded', async () => { - const WS_EVENT_NAME = 'astro-dev-overlay'; - const HOVER_DELAY = 750; - - const builtinPlugins: DevOverlayPlugin[] = [ - astroDevToolPlugin, - astroXrayPlugin, - astroAuditPlugin, - ].map((plugin) => ({ - ...plugin, - active: false, - status: 'loading', - eventTarget: new EventTarget(), - })); - - const customPluginsImports = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[]; - const customPlugins: DevOverlayPlugin[] = []; - customPlugins.push( - ...customPluginsImports.map((plugin) => ({ - ...plugin, - active: false, - status: 'loading' as const, - eventTarget: new EventTarget(), - })) - ); - - const plugins: DevOverlayPlugin[] = [...builtinPlugins, ...customPlugins]; - - for (const plugin of plugins) { - plugin.eventTarget.addEventListener('plugin-notification', (evt) => { - const target = overlay.shadowRoot?.querySelector(`[data-plugin-id="${plugin.id}"]`); - if (!target) return; - - let newState = true; - if (evt instanceof CustomEvent) { - newState = evt.detail.state ?? true; - } +const WS_EVENT_NAME = 'astro-dev-overlay'; - target.querySelector('.notification')?.toggleAttribute('data-active', newState); - }); - } +export class AstroDevOverlay extends HTMLElement { + shadowRoot: ShadowRoot; + hoverTimeout: number | undefined; + isHidden: () => boolean = () => this.devOverlay?.hasAttribute('data-hidden') ?? true; + devOverlay: HTMLDivElement | undefined; + plugins: DevOverlayPlugin[] = []; + HOVER_DELAY = 750; + hasBeenInitialized = false; - class AstroDevOverlay extends HTMLElement { - shadowRoot: ShadowRoot; - hoverTimeout: number | undefined; - isHidden: () => boolean = () => this.devOverlay?.hasAttribute('data-hidden') ?? true; - devOverlay: HTMLDivElement | undefined; - - constructor() { - super(); - this.shadowRoot = this.attachShadow({ mode: 'open' }); - } + constructor() { + super(); + this.shadowRoot = this.attachShadow({ mode: 'open' }); + } - // connect component - async connectedCallback() { + // Happens whenever the component is connected to the DOM + // When view transitions are enabled, this happens every time the view changes + async connectedCallback() { + if (!this.hasBeenInitialized) { this.shadowRoot.innerHTML = ` <style> + :host { + z-index: 999999; + view-transition-name: astro-dev-overlay; + display: contents; + } + + ::view-transition-old(astro-dev-overlay), + ::view-transition-new(astro-dev-overlay) { + animation: none; + } + #dev-overlay { position: fixed; bottom: 7.5%; @@ -250,258 +218,252 @@ document.addEventListener('DOMContentLoaded', async () => { <div id="dev-overlay"> <div id="dev-bar"> <div id="bar-container"> - ${builtinPlugins.map((plugin) => this.getPluginTemplate(plugin)).join('')} + ${this.plugins + .filter((plugin) => plugin.builtIn) + .map((plugin) => this.getPluginTemplate(plugin)) + .join('')} <div class="separator"></div> - ${customPlugins.map((plugin) => this.getPluginTemplate(plugin)).join('')} + ${this.plugins + .filter((plugin) => !plugin.builtIn) + .map((plugin) => this.getPluginTemplate(plugin)) + .join('')} </div> </div> <button id="minimize-button">${getIconElement('arrow-down')?.outerHTML}</button> </div>`; - this.devOverlay = this.shadowRoot.querySelector<HTMLDivElement>('#dev-overlay')!; this.attachEvents(); + } - // Init plugin lazily - if ('requestIdleCallback' in window) { - window.requestIdleCallback(async () => { - await this.initAllPlugins(); - }); - } else { - // Fallback to setTimeout for.. Safari... - setTimeout(async () => { - await this.initAllPlugins(); - }, 200); + // Create plugin canvases + this.plugins.forEach((plugin) => { + if (!this.hasBeenInitialized) { + console.log(`Creating plugin canvas for ${plugin.id}`); + const pluginCanvas = document.createElement('astro-dev-overlay-plugin-canvas'); + pluginCanvas.dataset.pluginId = plugin.id; + this.shadowRoot?.append(pluginCanvas); } + + this.togglePluginStatus(plugin, plugin.active); + }); + + // Init plugin lazily - This is safe to do here because only plugins that are not initialized yet will be affected + if ('requestIdleCallback' in window) { + window.requestIdleCallback(async () => { + await this.initAllPlugins(); + }); + } else { + // Fallback to setTimeout for.. Safari... + setTimeout(async () => { + await this.initAllPlugins(); + }, 200); } - attachEvents() { - const items = this.shadowRoot.querySelectorAll<HTMLDivElement>('.item'); - items.forEach((item) => { - item.addEventListener('click', async (e) => { - const target = e.currentTarget; - if (!target || !(target instanceof HTMLElement)) return; + this.hasBeenInitialized = true; + } + + attachEvents() { + const items = this.shadowRoot.querySelectorAll<HTMLDivElement>('.item'); + items.forEach((item) => { + item.addEventListener('click', async (e) => { + const target = e.currentTarget; + if (!target || !(target instanceof HTMLElement)) return; - const id = target.dataset.pluginId; - if (!id) return; + const id = target.dataset.pluginId; + if (!id) return; - const plugin = this.getPluginById(id); - if (!plugin) return; + const plugin = this.getPluginById(id); + if (!plugin) return; - if (plugin.status === 'loading') { - await this.initPlugin(plugin); - } + if (plugin.status === 'loading') { + await this.initPlugin(plugin); + } - this.togglePluginStatus(plugin); - }); + this.togglePluginStatus(plugin); }); + }); - const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button'); - if (minimizeButton && this.devOverlay) { - minimizeButton.addEventListener('click', () => { - this.toggleOverlay(false); - this.toggleMinimizeButton(false); - }); - } - - const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar'); - if (devBar) { - // On hover: - // - If the overlay is hidden, show it after the hover delay - // - If the overlay is visible, show the minimize button after the hover delay - (['mouseenter', 'focusin'] as const).forEach((event) => { - devBar.addEventListener(event, () => { - if (this.hoverTimeout) { - window.clearTimeout(this.hoverTimeout); - } - - if (this.isHidden()) { - this.hoverTimeout = window.setTimeout(() => { - this.toggleOverlay(true); - }, HOVER_DELAY); - } else { - this.hoverTimeout = window.setTimeout(() => { - this.toggleMinimizeButton(true); - }, HOVER_DELAY); - } - }); - }); + const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button'); + if (minimizeButton && this.devOverlay) { + minimizeButton.addEventListener('click', () => { + this.toggleOverlay(false); + this.toggleMinimizeButton(false); + }); + } - // On unhover: - // - Reset every timeout, as to avoid showing the overlay/minimize button when the user didn't really want to hover - // - If the overlay is visible, hide the minimize button after the hover delay - devBar.addEventListener('mouseleave', () => { + const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar'); + if (devBar) { + // On hover: + // - If the overlay is hidden, show it after the hover delay + // - If the overlay is visible, show the minimize button after the hover delay + (['mouseenter', 'focusin'] as const).forEach((event) => { + devBar.addEventListener(event, () => { if (this.hoverTimeout) { window.clearTimeout(this.hoverTimeout); } - if (!this.isHidden()) { + if (this.isHidden()) { + this.hoverTimeout = window.setTimeout(() => { + this.toggleOverlay(true); + }, this.HOVER_DELAY); + } else { this.hoverTimeout = window.setTimeout(() => { - this.toggleMinimizeButton(false); - }, HOVER_DELAY); + this.toggleMinimizeButton(true); + }, this.HOVER_DELAY); } }); + }); + + // On unhover: + // - Reset every timeout, as to avoid showing the overlay/minimize button when the user didn't really want to hover + // - If the overlay is visible, hide the minimize button after the hover delay + devBar.addEventListener('mouseleave', () => { + if (this.hoverTimeout) { + window.clearTimeout(this.hoverTimeout); + } - // On click, show the overlay if it's hidden, it's likely the user wants to interact with it - devBar.addEventListener('click', () => { + if (!this.isHidden()) { + this.hoverTimeout = window.setTimeout(() => { + this.toggleMinimizeButton(false); + }, this.HOVER_DELAY); + } + }); + + // On click, show the overlay if it's hidden, it's likely the user wants to interact with it + devBar.addEventListener('click', () => { + if (!this.isHidden()) return; + this.toggleOverlay(true); + }); + + devBar.addEventListener('keyup', (event) => { + if (event.code === 'Space' || event.code === 'Enter') { if (!this.isHidden()) return; this.toggleOverlay(true); - }); - - devBar.addEventListener('keyup', (event) => { - if (event.code === 'Space' || event.code === 'Enter') { - if (!this.isHidden()) return; - this.toggleOverlay(true); - } - }); - } + } + }); } + } - async initAllPlugins() { - await Promise.all( - plugins - .filter((plugin) => plugin.status === 'loading') - .map((plugin) => this.initPlugin(plugin)) - ); - } + async initAllPlugins() { + await Promise.all( + this.plugins + .filter((plugin) => plugin.status === 'loading') + .map((plugin) => this.initPlugin(plugin)) + ); + } - async initPlugin(plugin: DevOverlayPlugin) { - if (plugin.status === 'ready') return; + async initPlugin(plugin: DevOverlayPlugin) { + if (plugin.status === 'ready') return; - const shadowRoot = this.getPluginCanvasById(plugin.id)!.shadowRoot!; + const shadowRoot = this.getPluginCanvasById(plugin.id)!.shadowRoot!; - try { - console.info(`Initing plugin ${plugin.id}`); - await plugin.init?.(shadowRoot, plugin.eventTarget); - plugin.status = 'ready'; + try { + console.info(`Initializing plugin ${plugin.id}`); + await plugin.init?.(shadowRoot, plugin.eventTarget); + plugin.status = 'ready'; - if (import.meta.hot) { - import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:init`); - } - } catch (e) { - console.error(`Failed to init plugin ${plugin.id}, error: ${e}`); - plugin.status = 'error'; + if (import.meta.hot) { + import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:init`); } + } catch (e) { + console.error(`Failed to init plugin ${plugin.id}, error: ${e}`); + plugin.status = 'error'; } + } - getPluginTemplate(plugin: DevOverlayPlugin) { - return `<button class="item" data-plugin-id="${plugin.id}"> + getPluginTemplate(plugin: DevOverlayPlugin) { + return `<button class="item" data-plugin-id="${plugin.id}"> <div class="icon">${this.getPluginIcon(plugin.icon)}<div class="notification"></div></div> <span class="sr-only">${plugin.name}</span> </button>`; - } - - getPluginIcon(icon: Icon) { - if (isDefinedIcon(icon)) { - return getIconElement(icon)?.outerHTML; - } + } - return icon; + getPluginIcon(icon: Icon) { + if (isDefinedIcon(icon)) { + return getIconElement(icon)?.outerHTML; } - getPluginById(id: string) { - return plugins.find((plugin) => plugin.id === id); - } + return icon; + } - getPluginCanvasById(id: string) { - return this.shadowRoot.querySelector( - `astro-dev-overlay-plugin-canvas[data-plugin-id="${id}"]` - ); - } + getPluginById(id: string) { + return this.plugins.find((plugin) => plugin.id === id); + } - togglePluginStatus(plugin: DevOverlayPlugin, status?: boolean) { - plugin.active = status ?? !plugin.active; - const target = this.shadowRoot.querySelector(`[data-plugin-id="${plugin.id}"]`); - if (!target) return; - target.classList.toggle('active', plugin.active); - this.getPluginCanvasById(plugin.id)?.toggleAttribute('data-active', plugin.active); - - plugin.eventTarget.dispatchEvent( - new CustomEvent('plugin-toggle', { - detail: { - state: plugin.active, - plugin, - }, - }) - ); + getPluginCanvasById(id: string) { + return this.shadowRoot.querySelector(`astro-dev-overlay-plugin-canvas[data-plugin-id="${id}"]`); + } - if (import.meta.hot) { - import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:toggle`, { state: plugin.active }); - } + togglePluginStatus(plugin: DevOverlayPlugin, status?: boolean) { + plugin.active = status ?? !plugin.active; + const target = this.shadowRoot.querySelector(`[data-plugin-id="${plugin.id}"]`); + if (!target) return; + target.classList.toggle('active', plugin.active); + this.getPluginCanvasById(plugin.id)?.toggleAttribute('data-active', plugin.active); + + plugin.eventTarget.dispatchEvent( + new CustomEvent('plugin-toggle', { + detail: { + state: plugin.active, + plugin, + }, + }) + ); + + if (import.meta.hot) { + import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:toggle`, { state: plugin.active }); } + } - toggleMinimizeButton(newStatus?: boolean) { - const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button'); - if (!minimizeButton) return; - - if (newStatus !== undefined) { - if (newStatus === true) { - minimizeButton.removeAttribute('inert'); - minimizeButton.style.opacity = '1'; - } else { - minimizeButton.setAttribute('inert', ''); - minimizeButton.style.opacity = '0'; - } + toggleMinimizeButton(newStatus?: boolean) { + const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button'); + if (!minimizeButton) return; + + if (newStatus !== undefined) { + if (newStatus === true) { + minimizeButton.removeAttribute('inert'); + minimizeButton.style.opacity = '1'; } else { - minimizeButton.toggleAttribute('inert'); - minimizeButton.style.opacity = minimizeButton.hasAttribute('inert') ? '0' : '1'; + minimizeButton.setAttribute('inert', ''); + minimizeButton.style.opacity = '0'; } + } else { + minimizeButton.toggleAttribute('inert'); + minimizeButton.style.opacity = minimizeButton.hasAttribute('inert') ? '0' : '1'; } + } - toggleOverlay(newStatus?: boolean) { - const barContainer = this.shadowRoot.querySelector<HTMLDivElement>('#bar-container'); - const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar'); - - if (newStatus !== undefined) { - if (newStatus === true) { - this.devOverlay?.removeAttribute('data-hidden'); - barContainer?.removeAttribute('inert'); - devBar?.removeAttribute('tabindex'); - } else { - this.devOverlay?.setAttribute('data-hidden', ''); - barContainer?.setAttribute('inert', ''); - devBar?.setAttribute('tabindex', '0'); - } + toggleOverlay(newStatus?: boolean) { + const barContainer = this.shadowRoot.querySelector<HTMLDivElement>('#bar-container'); + const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar'); + + if (newStatus !== undefined) { + if (newStatus === true) { + this.devOverlay?.removeAttribute('data-hidden'); + barContainer?.removeAttribute('inert'); + devBar?.removeAttribute('tabindex'); } else { - this.devOverlay?.toggleAttribute('data-hidden'); - barContainer?.toggleAttribute('inert'); - if (this.isHidden()) { - devBar?.setAttribute('tabindex', '0'); - } else { - devBar?.removeAttribute('tabindex'); - } + this.devOverlay?.setAttribute('data-hidden', ''); + barContainer?.setAttribute('inert', ''); + devBar?.setAttribute('tabindex', '0'); + } + } else { + this.devOverlay?.toggleAttribute('data-hidden'); + barContainer?.toggleAttribute('inert'); + if (this.isHidden()) { + devBar?.setAttribute('tabindex', '0'); + } else { + devBar?.removeAttribute('tabindex'); } } } +} - class DevOverlayCanvas extends HTMLElement { - shadowRoot: ShadowRoot; +export class DevOverlayCanvas extends HTMLElement { + shadowRoot: ShadowRoot; - constructor() { - super(); - this.shadowRoot = this.attachShadow({ mode: 'open' }); - } - - // connect component - async connectedCallback() { - this.shadowRoot.innerHTML = ``; - } + constructor() { + super(); + this.shadowRoot = this.attachShadow({ mode: 'open' }); } - - customElements.define('astro-dev-overlay', AstroDevOverlay); - customElements.define('astro-dev-overlay-window', DevOverlayWindow); - customElements.define('astro-dev-overlay-plugin-canvas', DevOverlayCanvas); - customElements.define('astro-dev-overlay-tooltip', DevOverlayTooltip); - customElements.define('astro-dev-overlay-highlight', DevOverlayHighlight); - customElements.define('astro-dev-overlay-card', DevOverlayCard); - - const overlay = document.createElement('astro-dev-overlay'); - overlay.style.zIndex = '999999'; - document.body.append(overlay); - - // Create plugin canvases - plugins.forEach((plugin) => { - const pluginCanvas = document.createElement('astro-dev-overlay-plugin-canvas'); - pluginCanvas.dataset.pluginId = plugin.id; - overlay.shadowRoot?.append(pluginCanvas); - }); -}); +} diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts index 11e7bb7e0..cc83cbe83 100644 --- a/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts +++ b/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts @@ -1,12 +1,11 @@ import type { DevOverlayPlugin } from '../../../../@types/astro.js'; -import type { DevOverlayWindow } from '../ui-library/window.js'; export default { id: 'astro', name: 'Astro', icon: 'astro:logo', init(canvas) { - const astroWindow = document.createElement('astro-dev-overlay-window') as DevOverlayWindow; + const astroWindow = document.createElement('astro-dev-overlay-window'); astroWindow.windowTitle = 'Astro'; astroWindow.windowIcon = 'astro:logo'; diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts index f8bda831f..46ed88da0 100644 --- a/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts +++ b/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts @@ -1,6 +1,5 @@ import type { DevOverlayPlugin } from '../../../../@types/astro.js'; import type { DevOverlayHighlight } from '../ui-library/highlight.js'; -import type { DevOverlayTooltip } from '../ui-library/tooltip.js'; import { attachTooltipToHighlight, createHighlight, positionHighlight } from './utils/highlight.js'; const icon = @@ -26,20 +25,39 @@ export default { init(canvas, eventTarget) { let audits: { highlightElement: DevOverlayHighlight; auditedElement: HTMLElement }[] = []; - selectorBasedRules.forEach((rule) => { - document.querySelectorAll(rule.selector).forEach((el) => { - createAuditProblem(rule, el); + lint(); + + document.addEventListener('astro:after-swap', lint); + document.addEventListener('astro:page-load', refreshLintPositions); + + function lint() { + audits.forEach(({ highlightElement }) => { + highlightElement.remove(); + }); + audits = []; + + selectorBasedRules.forEach((rule) => { + document.querySelectorAll(rule.selector).forEach((el) => { + createAuditProblem(rule, el); + }); + }); + + if (audits.length > 0) { + eventTarget.dispatchEvent( + new CustomEvent('plugin-notification', { + detail: { + state: true, + }, + }) + ); + } + } + + function refreshLintPositions() { + audits.forEach(({ highlightElement, auditedElement }) => { + const rect = auditedElement.getBoundingClientRect(); + positionHighlight(highlightElement, rect); }); - }); - - if (audits.length > 0) { - eventTarget.dispatchEvent( - new CustomEvent('plugin-notification', { - detail: { - state: true, - }, - }) - ); } function createAuditProblem(rule: AuditRule, originalElement: Element) { @@ -60,17 +78,12 @@ export default { audits.push({ highlightElement: highlight, auditedElement: originalElement as HTMLElement }); (['scroll', 'resize'] as const).forEach((event) => { - window.addEventListener(event, () => { - audits.forEach(({ highlightElement, auditedElement }) => { - const newRect = auditedElement.getBoundingClientRect(); - positionHighlight(highlightElement, newRect); - }); - }); + window.addEventListener(event, refreshLintPositions); }); } function buildAuditTooltip(rule: AuditRule) { - const tooltip = document.createElement('astro-dev-overlay-tooltip') as DevOverlayTooltip; + const tooltip = document.createElement('astro-dev-overlay-tooltip'); tooltip.sections = [ { icon: 'warning', diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts index 79948dcd7..3af467ecd 100644 --- a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts +++ b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts @@ -2,16 +2,21 @@ import type { DevOverlayHighlight } from '../../ui-library/highlight.js'; import type { Icon } from '../../ui-library/icons.js'; export function createHighlight(rect: DOMRect, icon?: Icon) { - const highlight = document.createElement('astro-dev-overlay-highlight') as DevOverlayHighlight; + const highlight = document.createElement('astro-dev-overlay-highlight'); if (icon) highlight.icon = icon; highlight.tabIndex = 0; - positionHighlight(highlight, rect); + if (rect.width === 0 || rect.height === 0) { + highlight.style.display = 'none'; + } else { + positionHighlight(highlight, rect); + } return highlight; } export function positionHighlight(highlight: DevOverlayHighlight, rect: DOMRect) { + highlight.style.display = 'block'; // Make an highlight that is 10px bigger than the element on all sides highlight.style.top = `${Math.max(rect.top + window.scrollY - 10, 0)}px`; highlight.style.left = `${Math.max(rect.left + window.scrollX - 10, 0)}px`; diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts index fe92604f4..7b5f3539c 100644 --- a/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts +++ b/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts @@ -1,6 +1,5 @@ import type { DevOverlayMetadata, DevOverlayPlugin } from '../../../../@types/astro.js'; import type { DevOverlayHighlight } from '../ui-library/highlight.js'; -import type { DevOverlayTooltip } from '../ui-library/tooltip.js'; import { attachTooltipToHighlight, createHighlight, positionHighlight } from './utils/highlight.js'; const icon = @@ -12,9 +11,18 @@ export default { icon: icon, init(canvas) { let islandsOverlays: { highlightElement: DevOverlayHighlight; island: HTMLElement }[] = []; + addIslandsOverlay(); + document.addEventListener('astro:after-swap', addIslandsOverlay); + document.addEventListener('astro:page-load', refreshIslandsOverlayPositions); + function addIslandsOverlay() { + islandsOverlays.forEach(({ highlightElement }) => { + highlightElement.remove(); + }); + islandsOverlays = []; + const islands = document.querySelectorAll<HTMLElement>('astro-island'); islands.forEach((island) => { @@ -22,6 +30,7 @@ export default { const islandElement = (island.children[0] as HTMLElement) || island; // If the island is hidden, don't show an overlay on it + // TODO: For `client:only` islands, it might not have finished loading yet, so we should wait for that if (islandElement.offsetParent === null || computedStyle.display === 'none') { return; } @@ -36,17 +45,19 @@ export default { }); (['scroll', 'resize'] as const).forEach((event) => { - window.addEventListener(event, () => { - islandsOverlays.forEach(({ highlightElement, island: islandElement }) => { - const newRect = islandElement.getBoundingClientRect(); - positionHighlight(highlightElement, newRect); - }); - }); + window.addEventListener(event, refreshIslandsOverlayPositions); + }); + } + + function refreshIslandsOverlayPositions() { + islandsOverlays.forEach(({ highlightElement, island: islandElement }) => { + const rect = islandElement.getBoundingClientRect(); + positionHighlight(highlightElement, rect); }); } function buildIslandTooltip(island: HTMLElement) { - const tooltip = document.createElement('astro-dev-overlay-tooltip') as DevOverlayTooltip; + const tooltip = document.createElement('astro-dev-overlay-tooltip'); tooltip.sections = []; const islandProps = island.getAttribute('props') diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts index 0671fff0c..0ff67750f 100644 --- a/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts +++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts @@ -59,7 +59,7 @@ export class DevOverlayTooltip extends HTMLElement { .section-content { max-height: 250px; - overflow-y: scroll; + overflow-y: auto; } .modal-title { diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 163fcdd04..c9eb06bf6 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -281,7 +281,7 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa scripts.add({ props: { type: 'module', - src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/dev-overlay/overlay.js'), + src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/dev-overlay/entrypoint.js'), }, children: '', }); |