fix: fit parameter should apply to entire container when aspect_ratio is not set (#2126)
* Closes #2125
This commit is contained in:
@@ -150,7 +150,7 @@ cameras:
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `fit` | `contain` | If `contain`, the media is contained within the card and letterboxed if necessary. If `cover`, the media is expanded proportionally (i.e. maintaining the media aspect ratio) until the camera/card dimensions are fully covered. If `fill`, the media is stretched to fill the camera/card dimensions (i.e. ignoring the media aspect ratio). See [CSS object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) for technical details and a visualization. |
|
||||
| `fit` | `contain` | If `contain`, the media is contained within the camera container/card and letterboxed if necessary. If `cover`, the media is expanded proportionally (i.e. maintaining the media aspect ratio) until the camera/card dimensions are fully covered. If `fill`, the media is stretched to fill the camera/card dimensions (i.e. ignoring the media aspect ratio). See [CSS object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) for technical details and a visualization. Note that if `aspect_ratio` is also set, this is controlling the behavior "within" that aspect-ratio, otherwise it's within the container for the camera (which is effectively the whole card for single card configurations). |
|
||||
| `pan` | | A dictionary that may contain an `x` and `y` percentage (`0` - `100`) to control the position of the media when "digitally zoomed in" (see `zoom` parameter). This can be effectively used to "pan"/cut the media shown. A value of `0` means maximally to the left or top of the media, a value of `100` means maximally to the right or bottom of the media. See visualizations below. |
|
||||
| `position` | | A dictionary that may contain an `x` and `y` percentage (`0` - `100`) to control the position of the media when the fit is `cover` (for other values of `fit` this option has no effect). This can be effectively used to "pan"/cut the media shown. At any given time, only one of `x` and `y` will have an effect, depending on whether media width is larger than the camera/card dimensions (in which case `x` controls the position) or the media height is larger than the camera/card dimensions (in which case `y` controls the position). A value of `0` means maximally to the left or top of the media, a value of `100` means maximally to the right or bottom of the media. See [CSS object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) for technicals. See visualizations below. |
|
||||
| `view_box` | | A dictionary that may contain a `top`, `bottom`, `left` and `right` percentage (`0` - `100`) to precisely crop what part of the media to show by specifying a % inset value from each side. Browsers apply this cropping after `position` and `fit` have been applied. Unlike `zoom`, the user cannot dynamically zoom back out -- however the builtin media controls will work as normal. See visualizations below. Limited [browser support](https://caniuse.com/mdn-css_properties_object-view-box):    |
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout';
|
||||
|
||||
const SIZE_ATTRIBUTE = 'size';
|
||||
type SizeMode = 'sized' | 'unsized' | 'unsized-portrait' | 'unsized-landscape';
|
||||
type SizeMode = 'custom' | 'max' | 'max-height' | 'max-width';
|
||||
|
||||
export class MediaProviderDimensionsController implements ReactiveController {
|
||||
public resize = throttle(this._resizeHandler.bind(this), 100, {
|
||||
@@ -70,9 +70,9 @@ export class MediaProviderDimensionsController implements ReactiveController {
|
||||
SIZE_ATTRIBUTE,
|
||||
this._cameraConfig?.aspect_ratio
|
||||
? this._cameraConfig?.aspect_ratio[0] >= this._cameraConfig?.aspect_ratio[1]
|
||||
? 'unsized-landscape'
|
||||
: 'unsized-portrait'
|
||||
: 'unsized',
|
||||
? 'max-width'
|
||||
: 'max-height'
|
||||
: 'max',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,12 +90,17 @@ export class MediaProviderDimensionsController implements ReactiveController {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private _resizeHandler(_entries?: ResizeObserverEntry[]): void {
|
||||
// Custom sizing only applies if the aspect ratio is set.
|
||||
if (!this._cameraConfig?.aspect_ratio?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rememberHostSize = (): void => {
|
||||
this._intendedHostSize = this._host.getBoundingClientRect();
|
||||
};
|
||||
|
||||
const setUnsizedAttribute = (): void => {
|
||||
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'unsized');
|
||||
const setMaxAttribute = (): void => {
|
||||
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'max');
|
||||
};
|
||||
|
||||
const setContainerIntrinsicSize = (container: HTMLElement): void => {
|
||||
@@ -123,7 +128,7 @@ export class MediaProviderDimensionsController implements ReactiveController {
|
||||
}
|
||||
|
||||
if (!this._container) {
|
||||
setUnsizedAttribute();
|
||||
setMaxAttribute();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,7 +139,7 @@ export class MediaProviderDimensionsController implements ReactiveController {
|
||||
const containerSize = this._container.getBoundingClientRect();
|
||||
|
||||
if (!containerSize.width || !containerSize.height) {
|
||||
setUnsizedAttribute();
|
||||
setMaxAttribute();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,6 +158,6 @@ export class MediaProviderDimensionsController implements ReactiveController {
|
||||
);
|
||||
}
|
||||
|
||||
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'sized');
|
||||
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'custom');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:host([size='unsized']) > .container {
|
||||
:host([size='max']) > .container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:host([size='unsized-portrait']) > .container {
|
||||
:host([size='max-height']) > .container {
|
||||
height: 100%;
|
||||
}
|
||||
:host([size='unsized-landscape']) > .container {
|
||||
:host([size='max-width']) > .container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@ describe('MediaProviderDimensionsController', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const configWithAspectRatioLandscape: CameraDimensionsConfig = {
|
||||
aspect_ratio: [16, 9],
|
||||
};
|
||||
const configWithAspectRatioPortrait: CameraDimensionsConfig = {
|
||||
aspect_ratio: [9, 16],
|
||||
};
|
||||
|
||||
it('should construct', () => {
|
||||
const host = createLitElement();
|
||||
const eventListener = vi.fn();
|
||||
@@ -95,8 +102,7 @@ describe('MediaProviderDimensionsController', () => {
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
|
||||
const config = { aspect_ratio: [16, 9] };
|
||||
controller.setCameraConfig(config);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||
@@ -104,9 +110,9 @@ describe('MediaProviderDimensionsController', () => {
|
||||
|
||||
describe('should set host attribute', () => {
|
||||
it.each([
|
||||
['unsized', {}],
|
||||
['unsized-landscape', { aspect_ratio: [16, 9] }],
|
||||
['unsized-portrait', { aspect_ratio: [9, 16] }],
|
||||
['max', {}],
|
||||
['max-width', { aspect_ratio: [16, 9] }],
|
||||
['max-height', { aspect_ratio: [9, 16] }],
|
||||
])('%s', async (value: string, config: CameraDimensionsConfig) => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
@@ -166,64 +172,64 @@ describe('MediaProviderDimensionsController', () => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
|
||||
controller.setContainer(container);
|
||||
|
||||
expect(host.getAttribute('size')).toBe('unsized');
|
||||
host.setAttribute('size', 'sized');
|
||||
expect(host.getAttribute('size')).toBe('max-width');
|
||||
host.setAttribute('size', 'custom');
|
||||
|
||||
controller.setContainer(container);
|
||||
expect(host.getAttribute('size')).toBe('sized');
|
||||
expect(host.getAttribute('size')).toBe('custom');
|
||||
});
|
||||
|
||||
it('should reset container', () => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
|
||||
controller.setContainer(container);
|
||||
controller.setContainer();
|
||||
|
||||
expect(host.getAttribute('size')).toBe('unsized');
|
||||
expect(host.getAttribute('size')).toBe('max-width');
|
||||
});
|
||||
|
||||
describe('should set size attribute correctly', () => {
|
||||
it('should set unsized-landscape', () => {
|
||||
it('should set max-width', () => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
const config = { aspect_ratio: [16, 9] };
|
||||
|
||||
controller.setCameraConfig(config);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||
expect(host.getAttribute('size')).toBe('unsized-landscape');
|
||||
expect(host.getAttribute('size')).toBe('max-width');
|
||||
});
|
||||
|
||||
it('should set unsized-portrait', () => {
|
||||
it('should set max-height', () => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
const config = { aspect_ratio: [9, 16] };
|
||||
|
||||
controller.setCameraConfig(config);
|
||||
controller.setCameraConfig(configWithAspectRatioPortrait);
|
||||
controller.setContainer(container);
|
||||
|
||||
expect(container.style.aspectRatio).toBe('9 / 16');
|
||||
expect(host.getAttribute('size')).toBe('unsized-portrait');
|
||||
expect(host.getAttribute('size')).toBe('max-height');
|
||||
});
|
||||
|
||||
it('should set unsized', () => {
|
||||
it('should set max', () => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
|
||||
controller.setContainer(container);
|
||||
expect(host.getAttribute('size')).toBe('unsized');
|
||||
expect(host.getAttribute('size')).toBe('max');
|
||||
});
|
||||
|
||||
it('should set unsized without a config', () => {
|
||||
it('should set max without a config', () => {
|
||||
const host = createLitElement();
|
||||
const container = document.createElement('div');
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
@@ -231,14 +237,14 @@ describe('MediaProviderDimensionsController', () => {
|
||||
controller.setCameraConfig();
|
||||
controller.setContainer(container);
|
||||
|
||||
expect(host.getAttribute('size')).toBe('unsized');
|
||||
expect(host.getAttribute('size')).toBe('max');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should respond to size changes', () => {
|
||||
it('should set host to unsized if no container', () => {
|
||||
it('should ignore without an aspect ratio', () => {
|
||||
const host = createLitElement();
|
||||
host.setAttribute('size', 'sized');
|
||||
host.setAttribute('size', '__RANDOM__');
|
||||
|
||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
height: 600,
|
||||
@@ -249,12 +255,29 @@ describe('MediaProviderDimensionsController', () => {
|
||||
|
||||
callResizeHandler();
|
||||
|
||||
expect(host.getAttribute('size')).toBe('unsized');
|
||||
expect(host.getAttribute('size')).toBe('__RANDOM__');
|
||||
});
|
||||
|
||||
it('should set host to unsized if container has no dimensions', () => {
|
||||
it('should set host to max if no container', () => {
|
||||
const host = createLitElement();
|
||||
host.setAttribute('size', 'sized');
|
||||
host.setAttribute('size', 'custom');
|
||||
|
||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
height: 600,
|
||||
width: 300,
|
||||
});
|
||||
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
|
||||
callResizeHandler();
|
||||
|
||||
expect(host.getAttribute('size')).toBe('max');
|
||||
});
|
||||
|
||||
it('should set host to max if container has no dimensions', () => {
|
||||
const host = createLitElement();
|
||||
host.setAttribute('size', 'custom');
|
||||
|
||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
height: 100,
|
||||
@@ -268,16 +291,17 @@ describe('MediaProviderDimensionsController', () => {
|
||||
});
|
||||
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
callResizeHandler();
|
||||
|
||||
expect(host.getAttribute('size')).toBe('unsized');
|
||||
expect(host.getAttribute('size')).toBe('max');
|
||||
});
|
||||
|
||||
it('should ignore resize calls where actual equals intended size', () => {
|
||||
const host = createLitElement();
|
||||
host.setAttribute('size', 'sized');
|
||||
host.setAttribute('size', 'custom');
|
||||
|
||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
height: 100,
|
||||
@@ -291,6 +315,7 @@ describe('MediaProviderDimensionsController', () => {
|
||||
});
|
||||
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
callResizeHandler();
|
||||
@@ -318,13 +343,14 @@ describe('MediaProviderDimensionsController', () => {
|
||||
});
|
||||
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
callResizeHandler();
|
||||
|
||||
expect(container.style.width).toBe('100%');
|
||||
expect(container.style.height).toBe('auto');
|
||||
expect(host.getAttribute('size')).toBe('sized');
|
||||
expect(host.getAttribute('size')).toBe('custom');
|
||||
});
|
||||
|
||||
it('should resize container to fit height-limited container', () => {
|
||||
@@ -341,13 +367,14 @@ describe('MediaProviderDimensionsController', () => {
|
||||
});
|
||||
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
callResizeHandler();
|
||||
|
||||
expect(container.style.width).toBe(`${100 * (160 / 200)}px`);
|
||||
expect(container.style.height).toBe('100px');
|
||||
expect(host.getAttribute('size')).toBe('sized');
|
||||
expect(host.getAttribute('size')).toBe('custom');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -377,6 +404,7 @@ describe('MediaProviderDimensionsController', () => {
|
||||
});
|
||||
|
||||
const controller = new MediaProviderDimensionsController(host);
|
||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
||||
controller.setContainer(container);
|
||||
|
||||
controller.hostConnected();
|
||||
@@ -385,7 +413,7 @@ describe('MediaProviderDimensionsController', () => {
|
||||
|
||||
expect(container.style.width).toBe(`100%`);
|
||||
expect(container.style.height).toBe('auto');
|
||||
expect(host.getAttribute('size')).toBe('sized');
|
||||
expect(host.getAttribute('size')).toBe('custom');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user