Skip to content

Commit 69ae80d

Browse files
authored
fix: fit parameter should apply to entire container when aspect_ratio is not set (#2126)
* Closes #2125
1 parent c3e7cef commit 69ae80d

File tree

4 files changed

+77
-44
lines changed

4 files changed

+77
-44
lines changed

docs/configuration/cameras/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ cameras:
150150

151151
| Option | Default | Description |
152152
| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
153-
| `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. |
153+
| `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). |
154154
| `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. |
155155
| `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. |
156156
| `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): ![](../../images/browsers/chrome_16x16.png 'Google Chrome :no-zoom') ![](../../images/browsers/chromium_16x16.png 'Chromium :no-zoom') ![](../../images/browsers/edge_16x16.png 'Microsoft Edge :no-zoom') |

src/components-lib/media-provider-dimensions-controller.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info';
77
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout';
88

99
const SIZE_ATTRIBUTE = 'size';
10-
type SizeMode = 'sized' | 'unsized' | 'unsized-portrait' | 'unsized-landscape';
10+
type SizeMode = 'custom' | 'max' | 'max-height' | 'max-width';
1111

1212
export class MediaProviderDimensionsController implements ReactiveController {
1313
public resize = throttle(this._resizeHandler.bind(this), 100, {
@@ -70,9 +70,9 @@ export class MediaProviderDimensionsController implements ReactiveController {
7070
SIZE_ATTRIBUTE,
7171
this._cameraConfig?.aspect_ratio
7272
? this._cameraConfig?.aspect_ratio[0] >= this._cameraConfig?.aspect_ratio[1]
73-
? 'unsized-landscape'
74-
: 'unsized-portrait'
75-
: 'unsized',
73+
? 'max-width'
74+
: 'max-height'
75+
: 'max',
7676
);
7777
}
7878

@@ -90,12 +90,17 @@ export class MediaProviderDimensionsController implements ReactiveController {
9090

9191
// eslint-disable-next-line @typescript-eslint/no-unused-vars
9292
private _resizeHandler(_entries?: ResizeObserverEntry[]): void {
93+
// Custom sizing only applies if the aspect ratio is set.
94+
if (!this._cameraConfig?.aspect_ratio?.length) {
95+
return;
96+
}
97+
9398
const rememberHostSize = (): void => {
9499
this._intendedHostSize = this._host.getBoundingClientRect();
95100
};
96101

97-
const setUnsizedAttribute = (): void => {
98-
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'unsized');
102+
const setMaxAttribute = (): void => {
103+
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'max');
99104
};
100105

101106
const setContainerIntrinsicSize = (container: HTMLElement): void => {
@@ -123,7 +128,7 @@ export class MediaProviderDimensionsController implements ReactiveController {
123128
}
124129

125130
if (!this._container) {
126-
setUnsizedAttribute();
131+
setMaxAttribute();
127132
return;
128133
}
129134

@@ -134,7 +139,7 @@ export class MediaProviderDimensionsController implements ReactiveController {
134139
const containerSize = this._container.getBoundingClientRect();
135140

136141
if (!containerSize.width || !containerSize.height) {
137-
setUnsizedAttribute();
142+
setMaxAttribute();
138143
return;
139144
}
140145

@@ -153,6 +158,6 @@ export class MediaProviderDimensionsController implements ReactiveController {
153158
);
154159
}
155160

156-
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'sized');
161+
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'custom');
157162
}
158163
}

src/scss/provider.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
max-width: 100%;
2121
}
2222

23-
:host([size='unsized']) > .container {
23+
:host([size='max']) > .container {
2424
width: 100%;
2525
height: 100%;
2626
}
27-
:host([size='unsized-portrait']) > .container {
27+
:host([size='max-height']) > .container {
2828
height: 100%;
2929
}
30-
:host([size='unsized-landscape']) > .container {
30+
:host([size='max-width']) > .container {
3131
width: 100%;
3232
}

tests/components-lib/media-provider-dimensions-controller.test.ts

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ describe('MediaProviderDimensionsController', () => {
3636
vi.clearAllMocks();
3737
});
3838

39+
const configWithAspectRatioLandscape: CameraDimensionsConfig = {
40+
aspect_ratio: [16, 9],
41+
};
42+
const configWithAspectRatioPortrait: CameraDimensionsConfig = {
43+
aspect_ratio: [9, 16],
44+
};
45+
3946
it('should construct', () => {
4047
const host = createLitElement();
4148
const eventListener = vi.fn();
@@ -95,18 +102,17 @@ describe('MediaProviderDimensionsController', () => {
95102
const container = document.createElement('div');
96103
const controller = new MediaProviderDimensionsController(host);
97104

98-
const config = { aspect_ratio: [16, 9] };
99-
controller.setCameraConfig(config);
105+
controller.setCameraConfig(configWithAspectRatioLandscape);
100106
controller.setContainer(container);
101107

102108
expect(container.style.aspectRatio).toBe('16 / 9');
103109
});
104110

105111
describe('should set host attribute', () => {
106112
it.each([
107-
['unsized', {}],
108-
['unsized-landscape', { aspect_ratio: [16, 9] }],
109-
['unsized-portrait', { aspect_ratio: [9, 16] }],
113+
['max', {}],
114+
['max-width', { aspect_ratio: [16, 9] }],
115+
['max-height', { aspect_ratio: [9, 16] }],
110116
])('%s', async (value: string, config: CameraDimensionsConfig) => {
111117
const host = createLitElement();
112118
const container = document.createElement('div');
@@ -166,79 +172,79 @@ describe('MediaProviderDimensionsController', () => {
166172
const host = createLitElement();
167173
const container = document.createElement('div');
168174
const controller = new MediaProviderDimensionsController(host);
175+
controller.setCameraConfig(configWithAspectRatioLandscape);
169176

170177
controller.setContainer(container);
171178

172-
expect(host.getAttribute('size')).toBe('unsized');
173-
host.setAttribute('size', 'sized');
179+
expect(host.getAttribute('size')).toBe('max-width');
180+
host.setAttribute('size', 'custom');
174181

175182
controller.setContainer(container);
176-
expect(host.getAttribute('size')).toBe('sized');
183+
expect(host.getAttribute('size')).toBe('custom');
177184
});
178185

179186
it('should reset container', () => {
180187
const host = createLitElement();
181188
const container = document.createElement('div');
182189
const controller = new MediaProviderDimensionsController(host);
190+
controller.setCameraConfig(configWithAspectRatioLandscape);
183191

184192
controller.setContainer(container);
185193
controller.setContainer();
186194

187-
expect(host.getAttribute('size')).toBe('unsized');
195+
expect(host.getAttribute('size')).toBe('max-width');
188196
});
189197

190198
describe('should set size attribute correctly', () => {
191-
it('should set unsized-landscape', () => {
199+
it('should set max-width', () => {
192200
const host = createLitElement();
193201
const container = document.createElement('div');
194202
const controller = new MediaProviderDimensionsController(host);
195-
const config = { aspect_ratio: [16, 9] };
196203

197-
controller.setCameraConfig(config);
204+
controller.setCameraConfig(configWithAspectRatioLandscape);
198205
controller.setContainer(container);
199206

200207
expect(container.style.aspectRatio).toBe('16 / 9');
201-
expect(host.getAttribute('size')).toBe('unsized-landscape');
208+
expect(host.getAttribute('size')).toBe('max-width');
202209
});
203210

204-
it('should set unsized-portrait', () => {
211+
it('should set max-height', () => {
205212
const host = createLitElement();
206213
const container = document.createElement('div');
207214
const controller = new MediaProviderDimensionsController(host);
208-
const config = { aspect_ratio: [9, 16] };
209215

210-
controller.setCameraConfig(config);
216+
controller.setCameraConfig(configWithAspectRatioPortrait);
211217
controller.setContainer(container);
212218

213219
expect(container.style.aspectRatio).toBe('9 / 16');
214-
expect(host.getAttribute('size')).toBe('unsized-portrait');
220+
expect(host.getAttribute('size')).toBe('max-height');
215221
});
216222

217-
it('should set unsized', () => {
223+
it('should set max', () => {
218224
const host = createLitElement();
219225
const container = document.createElement('div');
220226
const controller = new MediaProviderDimensionsController(host);
221227

222228
controller.setContainer(container);
223-
expect(host.getAttribute('size')).toBe('unsized');
229+
expect(host.getAttribute('size')).toBe('max');
224230
});
225231

226-
it('should set unsized without a config', () => {
232+
it('should set max without a config', () => {
227233
const host = createLitElement();
228234
const container = document.createElement('div');
229235
const controller = new MediaProviderDimensionsController(host);
230236

231237
controller.setCameraConfig();
232238
controller.setContainer(container);
233239

234-
expect(host.getAttribute('size')).toBe('unsized');
240+
expect(host.getAttribute('size')).toBe('max');
235241
});
236242
});
237243

238244
describe('should respond to size changes', () => {
239-
it('should set host to unsized if no container', () => {
245+
it('should ignore without an aspect ratio', () => {
240246
const host = createLitElement();
241-
host.setAttribute('size', 'sized');
247+
host.setAttribute('size', '__RANDOM__');
242248

243249
host.getBoundingClientRect = vi.fn().mockReturnValue({
244250
height: 600,
@@ -249,12 +255,29 @@ describe('MediaProviderDimensionsController', () => {
249255

250256
callResizeHandler();
251257

252-
expect(host.getAttribute('size')).toBe('unsized');
258+
expect(host.getAttribute('size')).toBe('__RANDOM__');
259+
});
260+
261+
it('should set host to max if no container', () => {
262+
const host = createLitElement();
263+
host.setAttribute('size', 'custom');
264+
265+
host.getBoundingClientRect = vi.fn().mockReturnValue({
266+
height: 600,
267+
width: 300,
268+
});
269+
270+
const controller = new MediaProviderDimensionsController(host);
271+
controller.setCameraConfig(configWithAspectRatioLandscape);
272+
273+
callResizeHandler();
274+
275+
expect(host.getAttribute('size')).toBe('max');
253276
});
254277

255-
it('should set host to unsized if container has no dimensions', () => {
278+
it('should set host to max if container has no dimensions', () => {
256279
const host = createLitElement();
257-
host.setAttribute('size', 'sized');
280+
host.setAttribute('size', 'custom');
258281

259282
host.getBoundingClientRect = vi.fn().mockReturnValue({
260283
height: 100,
@@ -268,16 +291,17 @@ describe('MediaProviderDimensionsController', () => {
268291
});
269292

270293
const controller = new MediaProviderDimensionsController(host);
294+
controller.setCameraConfig(configWithAspectRatioLandscape);
271295
controller.setContainer(container);
272296

273297
callResizeHandler();
274298

275-
expect(host.getAttribute('size')).toBe('unsized');
299+
expect(host.getAttribute('size')).toBe('max');
276300
});
277301

278302
it('should ignore resize calls where actual equals intended size', () => {
279303
const host = createLitElement();
280-
host.setAttribute('size', 'sized');
304+
host.setAttribute('size', 'custom');
281305

282306
host.getBoundingClientRect = vi.fn().mockReturnValue({
283307
height: 100,
@@ -291,6 +315,7 @@ describe('MediaProviderDimensionsController', () => {
291315
});
292316

293317
const controller = new MediaProviderDimensionsController(host);
318+
controller.setCameraConfig(configWithAspectRatioLandscape);
294319
controller.setContainer(container);
295320

296321
callResizeHandler();
@@ -318,13 +343,14 @@ describe('MediaProviderDimensionsController', () => {
318343
});
319344

320345
const controller = new MediaProviderDimensionsController(host);
346+
controller.setCameraConfig(configWithAspectRatioLandscape);
321347
controller.setContainer(container);
322348

323349
callResizeHandler();
324350

325351
expect(container.style.width).toBe('100%');
326352
expect(container.style.height).toBe('auto');
327-
expect(host.getAttribute('size')).toBe('sized');
353+
expect(host.getAttribute('size')).toBe('custom');
328354
});
329355

330356
it('should resize container to fit height-limited container', () => {
@@ -341,13 +367,14 @@ describe('MediaProviderDimensionsController', () => {
341367
});
342368

343369
const controller = new MediaProviderDimensionsController(host);
370+
controller.setCameraConfig(configWithAspectRatioLandscape);
344371
controller.setContainer(container);
345372

346373
callResizeHandler();
347374

348375
expect(container.style.width).toBe(`${100 * (160 / 200)}px`);
349376
expect(container.style.height).toBe('100px');
350-
expect(host.getAttribute('size')).toBe('sized');
377+
expect(host.getAttribute('size')).toBe('custom');
351378
});
352379
});
353380
});
@@ -377,6 +404,7 @@ describe('MediaProviderDimensionsController', () => {
377404
});
378405

379406
const controller = new MediaProviderDimensionsController(host);
407+
controller.setCameraConfig(configWithAspectRatioLandscape);
380408
controller.setContainer(container);
381409

382410
controller.hostConnected();
@@ -385,7 +413,7 @@ describe('MediaProviderDimensionsController', () => {
385413

386414
expect(container.style.width).toBe(`100%`);
387415
expect(container.style.height).toBe('auto');
388-
expect(host.getAttribute('size')).toBe('sized');
416+
expect(host.getAttribute('size')).toBe('custom');
389417
});
390418
});
391419
});

0 commit comments

Comments
 (0)