mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Fix <wa-viewport-demo> zooming in Safari & Firefox
Also some refactoring to use `updated()` rather than `handleXXX()` functions
This commit is contained in:
@@ -11,22 +11,31 @@ export default css`
|
||||
--viewport-initial-aspect-ratio: 16 / 9;
|
||||
--viewport-bezel-width: 0.25em;
|
||||
|
||||
display: contents;
|
||||
display: block;
|
||||
/* Needed for measuring the available space */
|
||||
contain: inline-size;
|
||||
container-type: inline-size;
|
||||
container-name: host;
|
||||
}
|
||||
|
||||
[part~='frame'] {
|
||||
--zoom: 1;
|
||||
--zoom: 1; /* overridden by JS */
|
||||
--available-width: calc((100cqw - var(--offset-inline, 0px)));
|
||||
--iframe-manual-aspect-ratio: calc(var(--iframe-manual-width-px) / var(--iframe-manual-height-px));
|
||||
--iframe-manual-width: calc(var(--iframe-manual-width-px) * 1px * var(--zoom));
|
||||
--iframe-manual-height: calc(var(--iframe-manual-height-px) * 1px * var(--zoom));
|
||||
--width: var(--iframe-manual-width, var(--available-width));
|
||||
--height-auto: calc(var(--width) / (var(--aspect-ratio)));
|
||||
|
||||
--_aspect-ratio: calc(var(--viewport-width-px) / var(--viewport-height-px));
|
||||
--aspect-ratio: var(--_aspect-ratio, var(--viewport-initial-aspect-ratio));
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: end;
|
||||
width: 100%;
|
||||
align-items: start;
|
||||
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
min-width: var(--viewport-min-width, 2em);
|
||||
max-width: min(var(--viewport-max-width), 100%);
|
||||
min-height: var(--viewport-min-height);
|
||||
resize: var(--viewport-resize);
|
||||
overflow: auto;
|
||||
|
||||
/* Style frame like a window */
|
||||
border: var(--viewport-bezel-width) solid transparent;
|
||||
@@ -41,39 +50,56 @@ export default css`
|
||||
1.1em var(--button-params),
|
||||
radial-gradient(circle closest-side, var(--wa-color-green-70) 80%, var(--wa-color-green-60) 98%, transparent)
|
||||
1.8em var(--button-params),
|
||||
linear-gradient(to top, var(--viewport-background-color) 60%, transparent 70%) bottom padding-box,
|
||||
var(--wa-color-gray-95);
|
||||
background-repeat: no-repeat;
|
||||
box-shadow:
|
||||
0 0 0 1px var(--wa-color-gray-90),
|
||||
var(--wa-shadow-m);
|
||||
|
||||
&.resized {
|
||||
aspect-ratio: var(--iframe-manual-aspect-ratio);
|
||||
}
|
||||
|
||||
/* User has not yet resized the viewport */
|
||||
&:not([style*='height:']) ::slotted(iframe) {
|
||||
--_aspect-ratio: calc(var(--viewport-width-px) / var(--viewport-height-px));
|
||||
--aspect-ratio: var(--_aspect-ratio, var(--viewport-initial-aspect-ratio));
|
||||
&:not(.resized) ::slotted(iframe),
|
||||
&:not(.resized) slot {
|
||||
/* Will only be set if we have BOTH width and height */
|
||||
aspect-ratio: var(--aspect-ratio);
|
||||
}
|
||||
}
|
||||
|
||||
slot {
|
||||
display: block;
|
||||
overflow: clip;
|
||||
width: var(--width);
|
||||
max-width: var(--available-width);
|
||||
height: var(--iframe-manual-height, var(--height-auto));
|
||||
}
|
||||
|
||||
::slotted(iframe) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
zoom: var(--zoom);
|
||||
flex: auto;
|
||||
scale: var(--zoom);
|
||||
transform-origin: top left;
|
||||
resize: var(--viewport-resize);
|
||||
overflow: auto;
|
||||
|
||||
/* The width and height specified here are only applied if the iframe is not manually resized */
|
||||
width: calc(var(--available-width) / var(--zoom));
|
||||
height: calc(var(--height-auto) / var(--zoom));
|
||||
|
||||
min-width: calc(var(--viewport-min-width, 10em) / var(--zoom));
|
||||
max-width: calc(var(--available-width) / var(--zoom)) !important;
|
||||
min-height: calc(var(--viewport-min-height) / var(--zoom));
|
||||
|
||||
/* Divide with var(--zoom) to get lengths that stay constant regardless of zoom level */
|
||||
border: calc(1px / var(--zoom)) solid var(--wa-color-gray-90);
|
||||
|
||||
/* If we just set a background-color, Safari will not show the resizer because the iframe is over it.
|
||||
So instead, we make sure that the bottom of the iframe is transparent, and is covered by a gradient on the parent.
|
||||
Why not ONLY specify the gradient on the parent? Because there is no flexible way to know how tall it should be.
|
||||
*/
|
||||
background: linear-gradient(to bottom, var(--viewport-background-color) 60%, transparent 70%);
|
||||
background: var(--viewport-background-color);
|
||||
}
|
||||
|
||||
[part~='controls'] {
|
||||
display: flex;
|
||||
align-self: end;
|
||||
margin-top: -0.2em;
|
||||
font-size: var(--wa-font-size-xs);
|
||||
padding-block-end: 0.25em;
|
||||
|
||||
@@ -135,7 +135,7 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
private innerHeight: number = 0;
|
||||
|
||||
@state()
|
||||
private iframeHOffset: number = 0;
|
||||
private offsetInline: number = 0;
|
||||
|
||||
@state()
|
||||
private availableWidth = 0;
|
||||
@@ -144,7 +144,10 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
private contentWindow: Window | null;
|
||||
|
||||
@state()
|
||||
private needsInternalZoom: boolean | undefined;
|
||||
private iframeManualWidth: number | undefined;
|
||||
|
||||
@state()
|
||||
private iframeManualHeight: number | undefined;
|
||||
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@@ -159,73 +162,76 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private observeResize() {
|
||||
if (this.viewportElement) {
|
||||
this.resizeObserver ??= new ResizeObserver(() => this.handleResize());
|
||||
this.updateComplete.then(() => this.resizeObserver.observe(this));
|
||||
}
|
||||
this.resizeObserver ??= new ResizeObserver(records => this.handleResize(records));
|
||||
this.resizeObserver.observe(this);
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
if (this.iframe) {
|
||||
this.resizeObserver.observe(this.iframe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private unobserveResize() {
|
||||
this.resizeObserver?.unobserve(this);
|
||||
this.resizeObserver?.unobserve(this.iframe);
|
||||
}
|
||||
|
||||
// Called when this.iframe.contentWindow changes
|
||||
private handleIframeLoad() {
|
||||
if (this.iframe.contentWindow) {
|
||||
this.contentWindow = this.iframe.contentWindow;
|
||||
this.updateCS();
|
||||
this.updateZoom();
|
||||
|
||||
this.handleViewportResize();
|
||||
this.contentWindow.addEventListener('resize', () => this.handleViewportResize());
|
||||
|
||||
if (this.needsInternalZoom === undefined) {
|
||||
this.updateComplete.then(() => {
|
||||
const innerWidth = this.contentWindow?.innerWidth || 0;
|
||||
const availableWidth = Math.round(this.availableWidth);
|
||||
const ratio = availableWidth / innerWidth;
|
||||
|
||||
if (Math.abs(ratio - this.computedZoom) > 0.01) {
|
||||
// The actual iframe content is not zoomed. This is a known Safari bug.
|
||||
// We need to zoom the iframe content manually.
|
||||
this.needsInternalZoom = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateCS() {
|
||||
private updateAvailableWidth() {
|
||||
// This is only needed for isolated demos
|
||||
if (this.viewport && globalThis.window) {
|
||||
if (this.iframe) {
|
||||
this.iframeHOffset = getHorizontalOffsets(getComputedStyle(this.iframe));
|
||||
}
|
||||
if (this.viewport && globalThis.window && this.iframe) {
|
||||
const offsets = {
|
||||
host: getHorizontalOffsets(getComputedStyle(this)),
|
||||
frame: getHorizontalOffsets(getComputedStyle(this.viewportElement)),
|
||||
iframe: getHorizontalOffsets(getComputedStyle(this.iframe))
|
||||
};
|
||||
|
||||
const width = this.viewportElement.clientWidth;
|
||||
this.availableWidth = width - this.iframeHOffset;
|
||||
this.offsetInline = offsets.host.inner + offsets.frame.all + offsets.iframe.all;
|
||||
this.availableWidth = this.clientWidth - this.offsetInline;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the user resizes the iframe */
|
||||
private handleIframeResize() {
|
||||
const { width, height } = this.iframe.style;
|
||||
|
||||
this.iframeManualWidth = (width && getNumber(width)) || undefined;
|
||||
this.iframeManualHeight = (height && getNumber(height)) || undefined;
|
||||
}
|
||||
|
||||
/** Gets called when the host gets resized */
|
||||
private handleResize() {
|
||||
private handleResize(records: ResizeObserverEntry[]) {
|
||||
// This is only needed for isolated demos
|
||||
if (this.viewport && globalThis.window) {
|
||||
this.updateCS();
|
||||
this.updateZoom();
|
||||
for (const record of records) {
|
||||
if (record.target === this) {
|
||||
if (this.viewport && globalThis.window) {
|
||||
this.updateAvailableWidth();
|
||||
}
|
||||
} else if (record.target === this.iframe) {
|
||||
this.handleIframeResize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Zoom in by one step */
|
||||
public zoomIn() {
|
||||
this.zoomLevel++;
|
||||
this.updateZoom();
|
||||
}
|
||||
|
||||
/** Zoom out by one step */
|
||||
public zoomOut() {
|
||||
this.zoomLevel--;
|
||||
this.updateZoom();
|
||||
}
|
||||
|
||||
private updateZoom() {
|
||||
@@ -233,7 +239,7 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
|
||||
if (isViewportDimensions(this.viewport)) {
|
||||
if (!this.availableWidth) {
|
||||
this.updateCS();
|
||||
this.updateAvailableWidth();
|
||||
}
|
||||
|
||||
// Zoom level = available width / virtual width
|
||||
@@ -280,24 +286,35 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (
|
||||
this.contentWindow &&
|
||||
['computedZoom', 'needsInternalZoom', 'contentWindow'].some(p => changedProperties.has(p))
|
||||
) {
|
||||
if (changedProperties.has('computedZoom')) {
|
||||
this.viewportElement.style.setProperty('--zoom', this.computedZoom + '');
|
||||
}
|
||||
if (changedProperties.has('iframe')) {
|
||||
this.observeResize();
|
||||
}
|
||||
|
||||
if (this.needsInternalZoom) {
|
||||
const innerWidth = this.contentWindow?.innerWidth || 0;
|
||||
const availableWidth = Math.round(this.availableWidth);
|
||||
const ratio = availableWidth / innerWidth;
|
||||
if (['zoomLevel', 'availableWidth', 'viewport'].some(p => changedProperties.has(p))) {
|
||||
this.updateZoom();
|
||||
}
|
||||
|
||||
if (Math.abs(ratio - this.computedZoom) > 0.01) {
|
||||
// The actual iframe content is not zoomed. This is a known Safari bug.
|
||||
// We need to zoom the iframe content manually.
|
||||
this.iframe.contentDocument!.documentElement.style.setProperty('zoom', this.computedZoom + '');
|
||||
if (changedProperties.has('computedZoom')) {
|
||||
if (this.iframeManualWidth !== undefined || this.iframeManualHeight !== undefined) {
|
||||
// These inline styles have been created based on the previous zoom level
|
||||
// We need to convert them manually and reapply them
|
||||
this.unobserveResize(); // pause the observer
|
||||
|
||||
const previousZoom = changedProperties.get('computedZoom') as number;
|
||||
|
||||
if (this.iframeManualWidth !== undefined) {
|
||||
const width = (this.iframeManualWidth * previousZoom) / this.computedZoom;
|
||||
this.iframe.style.width = width + 'px';
|
||||
this.iframeManualWidth = width;
|
||||
}
|
||||
|
||||
if (this.iframeManualHeight !== undefined) {
|
||||
const height = (this.iframeManualHeight * previousZoom) / this.computedZoom;
|
||||
this.iframe.style.height = height + 'px';
|
||||
this.iframeManualHeight = height;
|
||||
}
|
||||
|
||||
this.observeResize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,8 +325,22 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
const dimensions = width && height ? html`<span class="dimensions">${width} × ${height}</span>` : '';
|
||||
|
||||
const viewportStyle: Record<string, string | number> = {
|
||||
'--zoom': this.computedZoom
|
||||
'--zoom': this.computedZoom,
|
||||
'--offset-inline': this.offsetInline + 'px'
|
||||
};
|
||||
const viewportClasses: Record<string, any> = {
|
||||
'resized-width': this.iframeManualWidth,
|
||||
'resized-height': this.iframeManualHeight,
|
||||
resized: this.iframeManualWidth || this.iframeManualHeight
|
||||
};
|
||||
|
||||
if (this.iframeManualWidth) {
|
||||
viewportStyle['--iframe-manual-width-px'] = this.iframeManualWidth;
|
||||
}
|
||||
if (this.iframeManualHeight) {
|
||||
viewportStyle['--iframe-manual-height-px'] = this.iframeManualHeight;
|
||||
}
|
||||
|
||||
if (isViewportDimensions(this.viewport)) {
|
||||
viewportStyle['--viewport-width-px'] = this.viewport.width;
|
||||
|
||||
@@ -319,12 +350,7 @@ export default class WaViewportDemo extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
id="viewport"
|
||||
part="frame"
|
||||
style=${styleMap(viewportStyle)}
|
||||
class=${classMap({ 'needs-internal-zoom': this.needsInternalZoom! })}
|
||||
>
|
||||
<div id="viewport" part="frame" style=${styleMap(viewportStyle)} class=${classMap(viewportClasses)}>
|
||||
<span part="controls">
|
||||
${dimensions}
|
||||
<span class="zoom">
|
||||
@@ -382,18 +408,29 @@ function getNumber(value: string | number): number {
|
||||
return (typeof value === 'string' ? parseFloat(value) : value) || 0;
|
||||
}
|
||||
|
||||
interface HorizontalOffsets {
|
||||
padding: number;
|
||||
border: number;
|
||||
margin: number;
|
||||
inner: number;
|
||||
all: number;
|
||||
}
|
||||
|
||||
const noOffsets: HorizontalOffsets = { padding: 0, border: 0, margin: 0, inner: 0, all: 0 };
|
||||
|
||||
/**
|
||||
* Get the horizontal padding and border widths of an element
|
||||
*/
|
||||
function getHorizontalOffsets(cs: CSSStyleDeclaration | null): number {
|
||||
function getHorizontalOffsets(cs: CSSStyleDeclaration | null): HorizontalOffsets {
|
||||
if (!cs) {
|
||||
return 0;
|
||||
return noOffsets;
|
||||
}
|
||||
|
||||
return (
|
||||
getNumber(cs.paddingLeft) +
|
||||
getNumber(cs.paddingRight) +
|
||||
getNumber(cs.borderLeftWidth) +
|
||||
getNumber(cs.borderRightWidth)
|
||||
);
|
||||
const padding = getNumber(cs.paddingLeft) + getNumber(cs.paddingRight);
|
||||
const border = getNumber(cs.borderLeftWidth) + getNumber(cs.borderRightWidth);
|
||||
const margin = getNumber(cs.marginLeft) + getNumber(cs.marginRight);
|
||||
const inner = padding + border;
|
||||
const all = inner + margin;
|
||||
|
||||
return { padding, border, margin, inner, all };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user