From 2c46d513363806576975df006ccabac1a1d1dccc Mon Sep 17 00:00:00 2001 From: rmch91 Date: Tue, 23 Jul 2024 12:36:04 +0200 Subject: [PATCH 01/40] changes --- .../content/banner/banner.component.html | 15 ++- .../recipes/config/default-media.config.ts | 25 ++++ .../components/media/media.component.html | 36 ++++++ .../components/media/media.component.ts | 10 +- .../shared/components/media/media.config.ts | 36 +++++- .../shared/components/media/media.model.ts | 45 +++++++ .../shared/components/media/media.service.ts | 121 +++++++++++++++++- 7 files changed, 275 insertions(+), 13 deletions(-) diff --git a/projects/storefrontlib/cms-components/content/banner/banner.component.html b/projects/storefrontlib/cms-components/content/banner/banner.component.html index f8bb4414150..3d012014a14 100644 --- a/projects/storefrontlib/cms-components/content/banner/banner.component.html +++ b/projects/storefrontlib/cms-components/content/banner/banner.component.html @@ -10,7 +10,10 @@ >

- +

@@ -21,14 +24,20 @@ [target]="getTarget(data)" >

- +

diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index 0df19d9ea29..eca3b953c64 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -18,4 +18,29 @@ export const mediaConfig: MediaConfig = { product: { width: 284 }, zoom: { width: 515 }, }, + useLegacyMediaComponent: false, + pictureElementFormats: { + mobile: { + maxWidth: '767px', + }, + tablet: { + minWidth: '768px', + maxWidth: '1024px', + }, + desktop: { + minWidth: '1025px', + maxWidth: '1439px', + }, + widescreen: { + minWidth: '1440px', + }, + retina_mobile: { + maxWidth: '786px', + minDevicePixelRatio: 3, + }, + retina_desktop: { + minWidth: '1440px', + minDevicePixelRatio: 2, + }, + }, }; diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index 50d303c2214..51494a5cc01 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,4 +1,39 @@ + + + + + + + + + + + + + diff --git a/projects/storefrontlib/shared/components/media/media.component.ts b/projects/storefrontlib/shared/components/media/media.component.ts index 1df9612fc95..6e78b72e4f7 100644 --- a/projects/storefrontlib/shared/components/media/media.component.ts +++ b/projects/storefrontlib/shared/components/media/media.component.ts @@ -61,6 +61,8 @@ export class MediaComponent implements OnChanges { */ @Input() loading: ImageLoadingStrategy | null = this.loadingStrategy; + @Input() usePictureElement: boolean = false; + /** * Once the media is loaded, we emit an event. */ @@ -96,7 +98,7 @@ export class MediaComponent implements OnChanges { item.media; protected isLegacy = - inject(USE_LEGACY_MEDIA_COMPONENT, { optional: true }) || + inject(USE_LEGACY_MEDIA_COMPONENT) || (inject(Config) as any)['useLegacyMediaComponent'] || false; @@ -106,6 +108,9 @@ export class MediaComponent implements OnChanges { this.create(); } + log(log) { + console.log(log); + } /** * Creates the `Media` object */ @@ -114,7 +119,8 @@ export class MediaComponent implements OnChanges { this.container instanceof Array ? this.container[0] : this.container, this.format, this.alt, - this.role + this.role, + this.usePictureElement ); if (!this.media?.src) { this.handleMissing(); diff --git a/projects/storefrontlib/shared/components/media/media.config.ts b/projects/storefrontlib/shared/components/media/media.config.ts index db5a428d457..63062eb3d89 100644 --- a/projects/storefrontlib/shared/components/media/media.config.ts +++ b/projects/storefrontlib/shared/components/media/media.config.ts @@ -6,7 +6,11 @@ import { Injectable } from '@angular/core'; import { Config } from '@spartacus/core'; -import { ImageLoadingStrategy, MediaFormatSize } from './media.model'; +import { + ImageLoadingStrategy, + MediaFormatSize, + PictureElementQueries, +} from './media.model'; /** * Provides configuration specific to Media, such as images. This is used to optimize @@ -29,6 +33,35 @@ export abstract class MediaConfig { [format: string]: MediaFormatSize; }; + /** + * Picture element configuration holds the media queries assigned to + * a format. + * The order of formats matters. + * elements in will be sorted based on this order. + * This is necessary because the browser evaluates each + * element in order and uses the first one that matches. + */ + pictureElementFormats?: { + [format: string]: PictureElementQueries; + }; + + /** + * Used to specify the order of formats. + * elements in will be sorted based on this order. + * This is necessary because the browser evaluates each + * element in order and uses the first one that matches. + * + * @example + * ['mobile', 'tablet', 'desktop'] + */ + pictureFormatsOrder?: string[]; + + /** + * Specify when need to use element + * over the element + */ + usePictureElement?: boolean; + /** * Indicates how the browser should load the image. There's only one * global strategy for all media cross media in Spartacus. @@ -42,6 +75,7 @@ export abstract class MediaConfig { imageLoadingStrategy?: ImageLoadingStrategy; /** + * @deprecated * As of v7.0, Spartacus started using the element by default when a srcset is available. * * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture for more diff --git a/projects/storefrontlib/shared/components/media/media.model.ts b/projects/storefrontlib/shared/components/media/media.model.ts index 5bfbef423dd..aba18de7298 100644 --- a/projects/storefrontlib/shared/components/media/media.model.ts +++ b/projects/storefrontlib/shared/components/media/media.model.ts @@ -29,6 +29,8 @@ export interface Media { * readers or magnifiers */ role?: string; + + sources?: any[]; } /** @@ -51,6 +53,49 @@ export interface MediaFormatSize { width?: number; } +/** + * Specifies media queries that can be used to generate information for the + * browser to resolve the right media for the right layout or device. + */ +export interface PictureElementQueries { + /** + * The minimum viewport width. + */ + minWidth?: string; + /** + * The maximum viewport width. + */ + maxWidth?: string; + /** + * The minimum viewport height. + */ + minHeight?: string; + /** + * The maximum viewport height. + */ + maxHeight?: string; + /** + * The minimum device pixel ratio + */ + minDevicePixelRatio?: number; + /** + * The maximum device pixel ratio + */ + maxDevicePixelRatio?: number; + /** + * The orientation of the viewport (portrait or landscape) + */ + orientation?: string; + /** + * The minimum aspect ratio of the viewport + */ + minAspectRatio?: string; + /** + * The maximum aspect ratio of the viewport + */ + maxAspectRatio?: string; +} + /** * Indicates how the browser should load the image. * diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index ac8412fe178..980651460ff 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -12,6 +12,7 @@ import { Media, MediaContainer, MediaFormatSize, + PictureElementQueries, } from './media.model'; /** @@ -47,7 +48,8 @@ export class MediaService { mediaContainer?: MediaContainer | Image, format?: string, alt?: string, - role?: string + role?: string, + usePictureElement?: boolean ): Media | undefined { if (!mediaContainer) { return; @@ -56,13 +58,21 @@ export class MediaService { const mainMedia: Image = mediaContainer.url ? mediaContainer : this.resolveMedia(mediaContainer as MediaContainer, format); + // console.log('*********', format); - return { - src: this.resolveAbsoluteUrl(mainMedia?.url ?? ''), - alt: alt ?? mainMedia?.altText, - role: role ?? mainMedia?.role, - srcset: this.resolveSrcSet(mediaContainer, format), - }; + return usePictureElement + ? { + src: this.resolveAbsoluteUrl(mainMedia?.url ?? ''), + alt: alt ?? mainMedia?.altText, + role: role ?? mainMedia?.role, + sources: this.resolveSources(mediaContainer, format), + } + : { + src: this.resolveAbsoluteUrl(mainMedia?.url ?? ''), + alt: alt ?? mainMedia?.altText, + role: role ?? mainMedia?.role, + srcset: this.resolveSrcSet(mediaContainer, format), + }; } /** @@ -84,6 +94,7 @@ export class MediaService { */ protected get sortedFormats(): { code: string; size: MediaFormatSize }[] { const mediaFormats = this.config?.mediaFormats; + if (!this._sortedFormats && mediaFormats) { this._sortedFormats = Object.keys(mediaFormats) .map((key) => ({ @@ -94,9 +105,67 @@ export class MediaService { a.size.width && b.size.width && a.size.width > b.size.width ? 1 : -1 ); } + return this._sortedFormats ?? []; } + protected get sortedPictureFormats(): { + code: string; + mediaQuery: string; + }[] { + const pictureElementMediaFormats = this.config?.pictureElementFormats; + const pictureFormatsOrder = this.config?.pictureFormatsOrder; + + if (!pictureElementMediaFormats) { + return []; + } + + const mediaFormatsArray = Object.keys(pictureElementMediaFormats).map( + (key) => ({ + code: key, + mediaQuery: this.generateMediaQuery(pictureElementMediaFormats[key]), + }) + ); + + if (pictureFormatsOrder) { + mediaFormatsArray.sort((a, b) => { + const orderA = pictureFormatsOrder.indexOf(a.code); + const orderB = pictureFormatsOrder.indexOf(b.code); + + return ( + (orderA !== -1 ? orderA : Infinity) - + (orderB !== -1 ? orderB : Infinity) + ); + }); + } + + return mediaFormatsArray; + } + + private generateMediaQuery(queries: PictureElementQueries): string { + const queryMap: { [key in keyof PictureElementQueries]: string } = { + minWidth: 'min-width', + maxWidth: 'max-width', + minHeight: 'min-height', + maxHeight: 'max-height', + minDevicePixelRatio: 'min-device-pixel-ratio', + maxDevicePixelRatio: 'max-device-pixel-ratio', + orientation: 'orientation', + minAspectRatio: 'min-aspect-ratio', + maxAspectRatio: 'max-aspect-ratio', + }; + + const queryParts = Object.keys(queries) + .map((key) => { + const cssProperty = queryMap[key as keyof PictureElementQueries]; + const value = (queries as any)[key]; + return value !== undefined ? `(${cssProperty}: ${value})` : null; + }) + .filter((part) => part !== null); + + return queryParts.join(' and '); + } + /** * Creates the media formats in a reversed sorted order. */ @@ -180,6 +249,44 @@ export class MediaService { return srcset === '' ? undefined : srcset; } + protected resolveSources( + media: MediaContainer | Image, + maxFormat?: string + ): any[] | undefined { + if (!media) { + return undefined; + } + + // maxFormat = 'widescreen'; + // Only create srcset images that are smaller than the given `maxFormat` (if any) + + let pictureFormats = this.sortedPictureFormats; + const max: number = pictureFormats.findIndex((f) => f.code === maxFormat); + if (max > -1) { + pictureFormats = pictureFormats.slice(0, max + 1); + } + + // console.log(pictureFormats, maxFormat); + + const sourceArray = pictureFormats.reduce((sources: any[], format) => { + const image = (media as MediaContainer)[format.code]; + + console.log(format); + + if (!!image) { + sources.push({ + srcset: this.resolveAbsoluteUrl(image.url ?? ''), + media: format.mediaQuery, + }); + } + return sources; + }, []); + + // console.log({ pictureFormats }); + + return sourceArray; + } + /** * Resolves the absolute URL for the given url. In most cases, this URL represents * the relative URL on the backend. In that case, we prefix the url with the baseUrl. From 1fd0a9a648b9c8ea0ace1a2b2c65a886ef42e687 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Tue, 23 Jul 2024 14:22:44 +0200 Subject: [PATCH 02/40] implementation of new media formats --- .../feature-toggles/config/feature-toggles.ts | 3 + .../spartacus/spartacus-features.module.ts | 5 +- .../recipes/config/default-media.config.ts | 1 + .../components/media/media.component.html | 135 +++++++------ .../components/media/media.component.ts | 3 - .../shared/components/media/media.config.ts | 21 ++ .../shared/components/media/media.model.ts | 5 + .../shared/components/media/media.module.ts | 3 +- .../shared/components/media/media.service.ts | 190 +++++++++++------- 9 files changed, 219 insertions(+), 147 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index b2e18982179..8e4d6da89c8 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -397,6 +397,8 @@ export interface FeatureTogglesInterface { * customization buttons in the BottomHeaderSlot in SmartEdit. */ cmsBottomHeaderSlotUsingFlexStyles?: boolean; + + useAdvancedMediaComponent?: boolean; } export const defaultFeatureToggles: Required = { @@ -463,4 +465,5 @@ export const defaultFeatureToggles: Required = { a11yDeliveryModeRadiogroup: false, occCartNameAndDescriptionInHttpRequestBody: false, cmsBottomHeaderSlotUsingFlexStyles: false, + useAdvancedMediaComponent: false, }; diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index ffff3bc7d0f..2fc24696e2d 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -73,6 +73,7 @@ import { WishListFeatureModule } from './features/cart/wish-list-feature.module' import { CdcFeatureModule } from './features/cdc/cdc-feature.module'; import { CdsFeatureModule } from './features/cds/cds-feature.module'; import { CheckoutFeatureModule } from './features/checkout/checkout-feature.module'; +import { CpqQuoteFeatureModule } from './features/cpq-quote/cpq-quote-feature.module'; import { CustomerTicketingFeatureModule } from './features/customer-ticketing/customer-ticketing-feature.module'; import { DigitalPaymentsFeatureModule } from './features/digital-payments/digital-payments-feature.module'; import { EpdVisualizationFeatureModule } from './features/epd-visualization/epd-visualization-feature.module'; @@ -95,14 +96,13 @@ import { QualtricsFeatureModule } from './features/qualtrics/qualtrics-feature.m import { QuoteFeatureModule } from './features/quote-feature.module'; import { OrganizationUserRegistrationFeatureModule } from './features/registration-feature.module'; import { RequestedDeliveryDateFeatureModule } from './features/requested-delivery-date/requested-delivery-date-feature.module'; +import { S4ServiceFeatureModule } from './features/s4-service/s4-service-feature.module'; import { S4OMFeatureModule } from './features/s4om/s4om-feature.module'; import { SegmentRefsFeatureModule } from './features/segment-refs/segment-refs-feature.module'; import { SmartEditFeatureModule } from './features/smartedit/smartedit-feature.module'; import { StorefinderFeatureModule } from './features/storefinder/storefinder-feature.module'; import { TrackingFeatureModule } from './features/tracking/tracking-feature.module'; import { UserFeatureModule } from './features/user/user-feature.module'; -import { S4ServiceFeatureModule } from './features/s4-service/s4-service-feature.module'; -import { CpqQuoteFeatureModule } from './features/cpq-quote/cpq-quote-feature.module'; const featureModules = []; @@ -340,6 +340,7 @@ if (environment.cpq) { a11yLinkBtnsToTertiaryBtns: true, a11yDeliveryModeRadiogroup: true, cmsBottomHeaderSlotUsingFlexStyles: true, + useAdvancedMediaComponent: true, }; return appFeatureToggles; }), diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index eca3b953c64..4e65e636ccd 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -43,4 +43,5 @@ export const mediaConfig: MediaConfig = { minDevicePixelRatio: 2, }, }, + pictureFormatsOrder: ['widescreen', 'mobile', 'tablet'], }; diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index 51494a5cc01..4cc4041ccb7 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,72 +1,77 @@ - - - + + + - - + + - - - - + + + + - diff --git a/projects/storefrontlib/shared/components/media/media.component.ts b/projects/storefrontlib/shared/components/media/media.component.ts index 6e78b72e4f7..1201cdef6aa 100644 --- a/projects/storefrontlib/shared/components/media/media.component.ts +++ b/projects/storefrontlib/shared/components/media/media.component.ts @@ -108,9 +108,6 @@ export class MediaComponent implements OnChanges { this.create(); } - log(log) { - console.log(log); - } /** * Creates the `Media` object */ diff --git a/projects/storefrontlib/shared/components/media/media.config.ts b/projects/storefrontlib/shared/components/media/media.config.ts index 63062eb3d89..76fa5e57dad 100644 --- a/projects/storefrontlib/shared/components/media/media.config.ts +++ b/projects/storefrontlib/shared/components/media/media.config.ts @@ -56,6 +56,27 @@ export abstract class MediaConfig { */ pictureFormatsOrder?: string[]; + /** + * A map that associates the keys of `PictureElementQueries` with their corresponding CSS media query features. + * + * @type {Record} + * + * The `queryMap` is used to translate the query properties provided in a `PictureElementQueries` object + * to their respective CSS media query feature names. This map includes the following mappings: + * + * @example + * - `minWidth` -> `min-width` + * - `maxWidth` -> `max-width` + * - `minHeight` -> `min-height` + * - `maxHeight` -> `max-height` + * - `minDevicePixelRatio` -> `min-device-pixel-ratio` + * - `maxDevicePixelRatio` -> `max-device-pixel-ratio` + * - `orientation` -> `orientation` + * - `minAspectRatio` -> `min-aspect-ratio` + * - `maxAspectRatio` -> `max-aspect-ratio` + */ + mediaQueryMap?: Record; + /** * Specify when need to use element * over the element diff --git a/projects/storefrontlib/shared/components/media/media.model.ts b/projects/storefrontlib/shared/components/media/media.model.ts index aba18de7298..fbe99d2dc4b 100644 --- a/projects/storefrontlib/shared/components/media/media.model.ts +++ b/projects/storefrontlib/shared/components/media/media.model.ts @@ -33,6 +33,11 @@ export interface Media { sources?: any[]; } +export interface PictureElementAttributes { + srcset: string; + media: string; +} + /** * Contains multiple media for different formats */ diff --git a/projects/storefrontlib/shared/components/media/media.module.ts b/projects/storefrontlib/shared/components/media/media.module.ts index e2fa182bb0d..6f8156a6493 100644 --- a/projects/storefrontlib/shared/components/media/media.module.ts +++ b/projects/storefrontlib/shared/components/media/media.module.ts @@ -6,11 +6,12 @@ import { CommonModule } from '@angular/common'; import { ModuleWithProviders, NgModule } from '@angular/core'; +import { FeaturesConfigModule } from '@spartacus/core'; import { MediaSourcesPipe } from './media-sources.pipe'; import { MediaComponent } from './media.component'; @NgModule({ - imports: [CommonModule], + imports: [CommonModule, FeaturesConfigModule], declarations: [MediaComponent, MediaSourcesPipe], exports: [MediaComponent], }) diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index 980651460ff..676684a8de6 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -12,6 +12,7 @@ import { Media, MediaContainer, MediaFormatSize, + PictureElementAttributes, PictureElementQueries, } from './media.model'; @@ -36,6 +37,7 @@ export class MediaService { * size is sorted on top. */ private _sortedFormats: { code: string; size: MediaFormatSize }[]; + private _sortedPictureFormats: { code: string; mediaQuery: string }[]; private _reversedFormats: { code: string; size: MediaFormatSize }[]; constructor(protected config: Config) {} @@ -58,21 +60,24 @@ export class MediaService { const mainMedia: Image = mediaContainer.url ? mediaContainer : this.resolveMedia(mediaContainer as MediaContainer, format); - // console.log('*********', format); - return usePictureElement + const commonMediaProperties = { + src: this.resolveAbsoluteUrl(mainMedia?.url ?? ''), + alt: alt ?? mainMedia?.altText, + role: role ?? mainMedia?.role, + }; + + const media = usePictureElement ? { - src: this.resolveAbsoluteUrl(mainMedia?.url ?? ''), - alt: alt ?? mainMedia?.altText, - role: role ?? mainMedia?.role, + ...commonMediaProperties, sources: this.resolveSources(mediaContainer, format), } : { - src: this.resolveAbsoluteUrl(mainMedia?.url ?? ''), - alt: alt ?? mainMedia?.altText, - role: role ?? mainMedia?.role, + ...commonMediaProperties, srcset: this.resolveSrcSet(mediaContainer, format), }; + + return media; } /** @@ -109,6 +114,11 @@ export class MediaService { return this._sortedFormats ?? []; } + /** + * Creates the media formats in a logical sorted order. The map contains the + * format key and the format media query information. We do this only once for performance + * benefits. + */ protected get sortedPictureFormats(): { code: string; mediaQuery: string; @@ -120,50 +130,57 @@ export class MediaService { return []; } - const mediaFormatsArray = Object.keys(pictureElementMediaFormats).map( - (key) => ({ - code: key, - mediaQuery: this.generateMediaQuery(pictureElementMediaFormats[key]), - }) - ); - - if (pictureFormatsOrder) { - mediaFormatsArray.sort((a, b) => { - const orderA = pictureFormatsOrder.indexOf(a.code); - const orderB = pictureFormatsOrder.indexOf(b.code); - - return ( - (orderA !== -1 ? orderA : Infinity) - - (orderB !== -1 ? orderB : Infinity) - ); - }); + if (!this._sortedPictureFormats && pictureElementMediaFormats) { + this._sortedPictureFormats = Object.keys(pictureElementMediaFormats).map( + (key) => ({ + code: key, + mediaQuery: this.generateMediaQuery(pictureElementMediaFormats[key]), + }) + ); + + if (pictureFormatsOrder) { + this._sortedPictureFormats.sort((a, b) => { + const orderA = pictureFormatsOrder.indexOf(a.code); + const orderB = pictureFormatsOrder.indexOf(b.code); + + return ( + (orderA !== -1 ? orderA : Infinity) - + (orderB !== -1 ? orderB : Infinity) + ); + }); + } } - return mediaFormatsArray; + return this._sortedPictureFormats ?? []; } - private generateMediaQuery(queries: PictureElementQueries): string { - const queryMap: { [key in keyof PictureElementQueries]: string } = { - minWidth: 'min-width', - maxWidth: 'max-width', - minHeight: 'min-height', - maxHeight: 'max-height', - minDevicePixelRatio: 'min-device-pixel-ratio', - maxDevicePixelRatio: 'max-device-pixel-ratio', - orientation: 'orientation', - minAspectRatio: 'min-aspect-ratio', - maxAspectRatio: 'max-aspect-ratio', - }; + /** + * Generates a CSS media query string from the given PictureElementQueries object. + * + * @param {PictureElementQueries} queries - An object containing media query properties. + * @returns {string} A string representing the CSS media query. + * + * This method constructs a media query string by mapping the provided query properties + * to their corresponding CSS media query features and joining them with "and". + */ + protected generateMediaQuery(queries: PictureElementQueries): string { + const queryMap = this.config?.mediaQueryMap; - const queryParts = Object.keys(queries) - .map((key) => { - const cssProperty = queryMap[key as keyof PictureElementQueries]; - const value = (queries as any)[key]; - return value !== undefined ? `(${cssProperty}: ${value})` : null; - }) - .filter((part) => part !== null); + if (!queryMap) { + return ''; + } - return queryParts.join(' and '); + return Object.keys(queries) + .filter( + (key): key is keyof PictureElementQueries => + key in queryMap && + queries[key as keyof PictureElementQueries] !== undefined + ) + .map( + (key) => + `(${queryMap[key as keyof PictureElementQueries]}: ${queries[key as keyof PictureElementQueries]})` + ) + .join(' and '); } /** @@ -226,12 +243,7 @@ export class MediaService { return undefined; } - // Only create srcset images that are smaller than the given `maxFormat` (if any) - let formats = this.sortedFormats; - const max: number = formats.findIndex((f) => f.code === maxFormat); - if (max > -1) { - formats = formats.slice(0, max + 1); - } + const formats = this.getFormatsUpToMaxFormat(this.sortedFormats, maxFormat); const srcset = formats.reduce((set, format) => { const image = (media as MediaContainer)[format.code]; @@ -249,42 +261,68 @@ export class MediaService { return srcset === '' ? undefined : srcset; } + /** + * Resolves the sources for a picture element based on the provided media container and maximum format. + * + * This method generates an array of picture element attributes (`srcset` and `media`) by filtering + * the sorted picture formats up to the specified maximum format. It then maps the corresponding + * media sources from the provided media container. + * + * The method will return an array of picture element attributes suitable for use + * in a `` element, or `undefined` if no media is provided. + */ protected resolveSources( media: MediaContainer | Image, maxFormat?: string - ): any[] | undefined { + ): PictureElementAttributes[] | undefined { if (!media) { return undefined; } - // maxFormat = 'widescreen'; - // Only create srcset images that are smaller than the given `maxFormat` (if any) - - let pictureFormats = this.sortedPictureFormats; - const max: number = pictureFormats.findIndex((f) => f.code === maxFormat); - if (max > -1) { - pictureFormats = pictureFormats.slice(0, max + 1); - } - - // console.log(pictureFormats, maxFormat); + const pictureFormats = this.getFormatsUpToMaxFormat( + this.sortedPictureFormats, + maxFormat + ); - const sourceArray = pictureFormats.reduce((sources: any[], format) => { - const image = (media as MediaContainer)[format.code]; + return pictureFormats.reduce( + (sources, format) => { + const image = (media as MediaContainer)[format.code]; - console.log(format); + if (image?.url) { + sources.push({ + srcset: this.resolveAbsoluteUrl(image.url), + media: format.mediaQuery, + }); + } + return sources; + }, + [] + ); + } - if (!!image) { - sources.push({ - srcset: this.resolveAbsoluteUrl(image.url ?? ''), - media: format.mediaQuery, - }); - } - return sources; - }, []); + /** + * Retrieves a list of formats up to and including the specified max format. + * + * @template T - A type that extends an object containing a `code` property of type `string`. + * @param {T[]} formats - An array of format objects, each containing at least a `code` property. + * @param {string} [maxFormat] - The maximum format code to include in the returned list. + * @returns {T[]} An array of formats up to and including the specified max format. If `maxFormat` is not found, returns the entire list. + * + * This method filters the provided list of formats to include only those up to and including + * the specified max format. If the `maxFormat` is not found, the entire list of formats is returned. + */ + protected getFormatsUpToMaxFormat( + formats: T[], + maxFormat?: string + ): T[] { + let pictureFormats = formats; + const maxIndex = pictureFormats.findIndex((f) => f.code === maxFormat); - // console.log({ pictureFormats }); + if (maxIndex > -1) { + pictureFormats = pictureFormats.slice(0, maxIndex + 1); + } - return sourceArray; + return pictureFormats; } /** From d0afd149c9f068ca0295c8bd0d0985f14d881330 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Wed, 24 Jul 2024 12:10:58 +0200 Subject: [PATCH 03/40] add map to config --- .../recipes/config/default-media.config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index 4e65e636ccd..ee5a3d75d04 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -44,4 +44,15 @@ export const mediaConfig: MediaConfig = { }, }, pictureFormatsOrder: ['widescreen', 'mobile', 'tablet'], + mediaQueryMap: { + minWidth: 'min-width', + maxWidth: 'max-width', + minHeight: 'min-height', + maxHeight: 'max-height', + minDevicePixelRatio: 'min-device-pixel-ratio', + maxDevicePixelRatio: 'max-device-pixel-ratio', + orientation: 'orientation', + minAspectRatio: 'min-aspect-ratio', + maxAspectRatio: 'max-aspect-ratio', + }, }; From f622e760cb59b0b94c983f8d6162031be905a135 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Wed, 24 Jul 2024 12:30:50 +0200 Subject: [PATCH 04/40] removed interface --- .../feature-toggles/config/feature-toggles.ts | 9 +++-- .../spartacus/spartacus-features.module.ts | 2 +- .../components/media/media.component.html | 4 +-- .../shared/components/media/media.model.ts | 5 --- .../shared/components/media/media.service.ts | 36 +++++++++++-------- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 8e4d6da89c8..be661cfc64f 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -398,7 +398,12 @@ export interface FeatureTogglesInterface { */ cmsBottomHeaderSlotUsingFlexStyles?: boolean; - useAdvancedMediaComponent?: boolean; + /** + * When enabled, allows to provide extended formats and media queries for element. + * + * For proper work requires `pictureElementFormats` and `mediaQueryMap` provided in media config. + */ + useMediaComponentWithConfigurableMediaQueries?: boolean; } export const defaultFeatureToggles: Required = { @@ -465,5 +470,5 @@ export const defaultFeatureToggles: Required = { a11yDeliveryModeRadiogroup: false, occCartNameAndDescriptionInHttpRequestBody: false, cmsBottomHeaderSlotUsingFlexStyles: false, - useAdvancedMediaComponent: false, + useMediaComponentWithConfigurableMediaQueries: false, }; diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index 2fc24696e2d..6b9b582e4cf 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -340,7 +340,7 @@ if (environment.cpq) { a11yLinkBtnsToTertiaryBtns: true, a11yDeliveryModeRadiogroup: true, cmsBottomHeaderSlotUsingFlexStyles: true, - useAdvancedMediaComponent: true, + useMediaComponentWithConfigurableMediaQueries: true, }; return appFeatureToggles; }), diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index 4cc4041ccb7..c1a6e13699b 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,5 +1,5 @@ - + +

- +

@@ -24,20 +21,14 @@ [target]="getTarget(data)" >

- +

diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index cd0e44a74ab..6cefe91f120 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -20,26 +20,28 @@ export const mediaConfig: MediaConfig = { }, pictureElementFormats: { mobile: { - maxWidth: '767px', + mediaQueries: { + maxWidth: '767px', + }, + width: 50, + height: 50, }, tablet: { - minWidth: '768px', - maxWidth: '1024px', + mediaQueries: { minWidth: '768px', maxWidth: '1024px' }, }, desktop: { - minWidth: '1025px', - maxWidth: '1439px', + mediaQueries: { minWidth: '1025px', maxWidth: '1439px' }, + width: 100, + height: 100, }, widescreen: { - minWidth: '1440px', + mediaQueries: { minWidth: '1440px' }, }, retina_mobile: { - maxWidth: '786px', - minDevicePixelRatio: 3, + mediaQueries: { maxWidth: '786px', minDevicePixelRatio: 3 }, }, retina_desktop: { - minWidth: '1440px', - minDevicePixelRatio: 2, + mediaQueries: { minWidth: '1440px', minDevicePixelRatio: 2 }, }, }, pictureFormatsOrder: [ diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index c1a6e13699b..34e79af66d3 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,15 +1,14 @@ @@ -32,6 +34,9 @@ [src]="media.src" [srcset]="media.srcset || media.src" [attr.role]="media.role" + [attr.width]="width" + [attr.height]="height" + [attr.sizes]="sizes" (load)="loadHandler()" (error)="errorHandler()" /> @@ -57,6 +62,9 @@ [attr.role]="media.role" (load)="loadHandler()" (error)="errorHandler()" + [attr.width]="width" + [attr.height]="height" + [attr.sizes]="sizes" /> @@ -71,6 +79,9 @@ [attr.role]="media.role" (load)="loadHandler()" (error)="errorHandler()" + [attr.width]="width" + [attr.height]="height" + [attr.sizes]="sizes" /> diff --git a/projects/storefrontlib/shared/components/media/media.component.ts b/projects/storefrontlib/shared/components/media/media.component.ts index 18c1a416d61..e926f631400 100644 --- a/projects/storefrontlib/shared/components/media/media.component.ts +++ b/projects/storefrontlib/shared/components/media/media.component.ts @@ -66,7 +66,29 @@ export class MediaComponent implements OnChanges { */ @Input() loading: ImageLoadingStrategy | null = this.loadingStrategy; - @Input() usePictureElement: boolean = false; + @Input() useImgElement: boolean = false; + + /** + * The intrinsic width of the image, in pixels + */ + @Input() width: number; + + /** + * The intrinsic height of the image, in pixels + */ + @Input() height: number; + + /** + * Specifies the sizes attribute for responsive images. + * + * The `sizes` attribute describes the layout width of the image for various viewport sizes. + * It helps the browser determine which image to download from the `srcset` attribute. + * + * - The sizes attribute is defined using media queries. + * - It allows specifying different sizes for various screen widths or other conditions (e.g., device orientation). + * - The browser uses the value to pick the most appropriate image source from the `srcset`. + */ + @Input() sizes: string; /** * Once the media is loaded, we emit an event. @@ -123,10 +145,10 @@ export class MediaComponent implements OnChanges { */ protected create(): void { const shouldGetMediaForPictureElement = - this.usePictureElement && this.featureConfigService.isEnabled( 'useMediaComponentWithConfigurableMediaQueries' - ); + ) && !this.useImgElement; + const getMedia = shouldGetMediaForPictureElement ? this.mediaService.getMediaForPictureElement.bind(this.mediaService) : this.mediaService.getMedia.bind(this.mediaService); diff --git a/projects/storefrontlib/shared/components/media/media.config.ts b/projects/storefrontlib/shared/components/media/media.config.ts index 1c72ba5c760..ef3eb13a5a7 100644 --- a/projects/storefrontlib/shared/components/media/media.config.ts +++ b/projects/storefrontlib/shared/components/media/media.config.ts @@ -42,7 +42,11 @@ export abstract class MediaConfig { * element in order and uses the first one that matches. */ pictureElementFormats?: { - [format: string]: PictureElementQueries; + [format: string]: { + mediaQueries: PictureElementQueries; + width?: number; + height?: number; + }; }; /** diff --git a/projects/storefrontlib/shared/components/media/media.model.ts b/projects/storefrontlib/shared/components/media/media.model.ts index be0307a3947..d4b538398db 100644 --- a/projects/storefrontlib/shared/components/media/media.model.ts +++ b/projects/storefrontlib/shared/components/media/media.model.ts @@ -30,9 +30,38 @@ export interface Media { */ role?: string; - sources?: any[]; + /** + * The sources holds a list of source element for picture html element + */ + sources?: PictureHTMLElementSources[]; + + /** + * Specifies the intrinsic width of the image in pixels. Allowed if the parent of `` is a `` + */ + width?: number; + + /** + * Specifies the intrinsic height of the image in pixels. Allowed if the parent of `` is a `` + */ + height?: number; } +/** + * Specifies most commonly used media queries for `picture` + */ +export type DefaultMediaQueryKeys = + | 'minWidth' + | 'maxWidth' + | 'minHeight' + | 'maxHeight' + | 'minDevicePixelRatio' + | 'maxDevicePixelRatio' + | 'orientation' + | 'minAspectRatio' + | 'maxAspectRatio' + | 'minResolution' + | 'maxResolution'; + /** * Contains multiple media for different formats */ @@ -53,12 +82,21 @@ export interface MediaFormatSize { width?: number; } +export type MediaQueryKeys = DefaultMediaQueryKeys & string; + /** * Specifies media queries that can be used to generate information for the * browser to resolve the right media for the right layout or device. */ -export interface PictureElementQueries { +export type PictureElementQueries = { [query: string]: string | number; +}; + +export interface PictureHTMLElementSources { + srcset: string; + media: string; + width: number | undefined; + height: number | undefined; } /** diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index 8ff3c04a4c7..f184c2dfe4d 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -13,6 +13,7 @@ import { MediaContainer, MediaFormatSize, PictureElementQueries, + PictureHTMLElementSources, } from './media.model'; /** @@ -181,7 +182,9 @@ export class MediaService { this._sortedPictureFormats = Object.keys(pictureElementMediaFormats).map( (key) => ({ code: key, - mediaQuery: this.generateMediaQuery(pictureElementMediaFormats[key]), + mediaQuery: this.generateMediaQuery( + pictureElementMediaFormats[key].mediaQueries + ), }) ); @@ -319,12 +322,7 @@ export class MediaService { protected resolveSources( media: MediaContainer | Image, maxFormat?: string - ): - | { - srcset: string; - media: string; - }[] - | undefined { + ): PictureHTMLElementSources[] | undefined { if (!media) { return undefined; } @@ -333,11 +331,14 @@ export class MediaService { this.sortedPictureFormats, maxFormat ); + const pictureElementMediaFormats = this.config?.pictureElementFormats; return pictureFormats.reduce< { srcset: string; media: string; + width: number | undefined; + height: number | undefined; }[] >((sources, format) => { const image = (media as MediaContainer)[format.code]; @@ -346,6 +347,8 @@ export class MediaService { sources.push({ srcset: this.resolveAbsoluteUrl(image.url), media: format.mediaQuery, + width: pictureElementMediaFormats?.[format?.code]?.width, + height: pictureElementMediaFormats?.[format?.code]?.height, }); } return sources; From 56086524eeb1ca53fd44766087df2b99328bfb3c Mon Sep 17 00:00:00 2001 From: rmch91 Date: Thu, 5 Sep 2024 11:48:41 +0200 Subject: [PATCH 15/40] some adjustements --- .../navigation/search-box/search-box.component.html | 1 + .../product-grid-item/product-grid-item.component.html | 1 + .../product-list-item/product-list-item.component.html | 1 + .../storefrontlib/shared/components/media/media.component.ts | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html index ee502a645db..2b690a0d8ed 100644 --- a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html +++ b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html @@ -163,6 +163,7 @@ [container]="product.images?.PRIMARY" format="thumbnail" role="presentation" + [useImgElement]="true" >
{{ product.price?.formattedValue }} diff --git a/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html b/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html index cbfce92adfd..09e4846ca19 100644 --- a/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html +++ b/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html @@ -8,6 +8,7 @@ [container]="product.images?.PRIMARY" format="product" [alt]="product.summary" + [useImgElement]="true" > diff --git a/projects/storefrontlib/shared/components/media/media.component.ts b/projects/storefrontlib/shared/components/media/media.component.ts index e926f631400..0eefb565a51 100644 --- a/projects/storefrontlib/shared/components/media/media.component.ts +++ b/projects/storefrontlib/shared/components/media/media.component.ts @@ -126,6 +126,10 @@ export class MediaComponent implements OnChanges { /** * @deprecated will be removed + * + * To use `img` HTML element instead of `picture` + * use `useMediaComponentWithConfigurableMediaQueries` feature flag + * and pass `[useImgElement]="true"` input to the component */ protected isLegacy = inject(USE_LEGACY_MEDIA_COMPONENT, { optional: true }) || From 16bf99c8e2b6cadc9671876afbb0bf9faf3b57ea Mon Sep 17 00:00:00 2001 From: rmch91 Date: Fri, 6 Sep 2024 12:41:10 +0200 Subject: [PATCH 16/40] fix tests --- .../components/media/media.component.spec.ts | 24 +++++---- .../shared/components/media/media.model.ts | 18 ------- .../components/media/media.service.spec.ts | 53 +++++++++++++++---- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/projects/storefrontlib/shared/components/media/media.component.spec.ts b/projects/storefrontlib/shared/components/media/media.component.spec.ts index 0e8900df19c..21540a5d8dd 100644 --- a/projects/storefrontlib/shared/components/media/media.component.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.component.spec.ts @@ -94,6 +94,8 @@ class MockMediaService { { srcset: 'test.url', media: '', + width: undefined, + height: undefined, }, ], }; @@ -145,7 +147,7 @@ function configureTestingModule( }).compileComponents(); } -function createComponent(usePictureElement = false) { +function createComponent(useImgElement = false) { const service = TestBed.inject(MediaService); const fixture = TestBed.createComponent(MediaComponent); const component = fixture.componentInstance; @@ -157,8 +159,8 @@ function createComponent(usePictureElement = false) { component.container = mockImageContainer; - if (usePictureElement) { - component.usePictureElement = usePictureElement; + if (useImgElement) { + component.useImgElement = useImgElement; } component.ngOnChanges(); @@ -175,37 +177,37 @@ function createComponent(usePictureElement = false) { describe('MediaComponent', () => { describe('with enabled useMediaComponentWithConfigurableMediaQueries', () => { - it('should have picture element if usePictureElement is true', () => { + it('should have picture element if useImgElement is false', () => { configureTestingModule(new MockMediaService('srcset'), false, true); - const { fixture } = createComponent(true); + const { fixture } = createComponent(false); const picture = fixture.debugElement.query(By.css('picture')); expect(picture).not.toBeNull(); }); - it('should not have picture element if usePictureElement is false', () => { + it('should not have picture element if useImgElement is true', () => { configureTestingModule(new MockMediaService('srcset'), false, true); - const { fixture } = createComponent(false); + const { fixture } = createComponent(true); const picture = fixture.debugElement.query(By.css('picture')); expect(picture).toBeNull(); }); - it('should call getMedia() method from service if usePictureElement is false', () => { + it('should call getMedia() method from service if useImgElement is true', () => { configureTestingModule(new MockMediaService('srcset'), false, true); const { getMediaSpy, getMediaForPictureElementSpy } = - createComponent(false); + createComponent(true); expect(getMediaForPictureElementSpy).not.toHaveBeenCalled(); expect(getMediaSpy).toHaveBeenCalled(); }); - it('should call getMediaForPictureElement() method from service if usePictureElement is true', () => { + it('should call getMediaForPictureElement() method from service if useImgElement is false', () => { configureTestingModule(new MockMediaService('srcset'), false, true); const { getMediaForPictureElementSpy, getMediaSpy } = - createComponent(true); + createComponent(false); expect(getMediaForPictureElementSpy).toHaveBeenCalled(); expect(getMediaSpy).not.toHaveBeenCalled(); diff --git a/projects/storefrontlib/shared/components/media/media.model.ts b/projects/storefrontlib/shared/components/media/media.model.ts index d4b538398db..2be42415a0b 100644 --- a/projects/storefrontlib/shared/components/media/media.model.ts +++ b/projects/storefrontlib/shared/components/media/media.model.ts @@ -46,22 +46,6 @@ export interface Media { height?: number; } -/** - * Specifies most commonly used media queries for `picture` - */ -export type DefaultMediaQueryKeys = - | 'minWidth' - | 'maxWidth' - | 'minHeight' - | 'maxHeight' - | 'minDevicePixelRatio' - | 'maxDevicePixelRatio' - | 'orientation' - | 'minAspectRatio' - | 'maxAspectRatio' - | 'minResolution' - | 'maxResolution'; - /** * Contains multiple media for different formats */ @@ -82,8 +66,6 @@ export interface MediaFormatSize { width?: number; } -export type MediaQueryKeys = DefaultMediaQueryKeys & string; - /** * Specifies media queries that can be used to generate information for the * browser to resolve the right media for the right layout or device. diff --git a/projects/storefrontlib/shared/components/media/media.service.spec.ts b/projects/storefrontlib/shared/components/media/media.service.spec.ts index 9683dba7a98..37b70f46f92 100644 --- a/projects/storefrontlib/shared/components/media/media.service.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.service.spec.ts @@ -30,19 +30,27 @@ const MockStorefrontConfig: Config = { }, pictureElementFormats: { format400: { - maxWidth: '786px', - minDevicePixelRatio: 3, + mediaQueries: { + maxWidth: '786px', + minDevicePixelRatio: 3, + }, }, format200: { - minWidth: '768px', - maxWidth: '1024px', + mediaQueries: { + minWidth: '768px', + maxWidth: '1024px', + }, }, format600: { - minWidth: '1025px', - maxWidth: '1439px', + mediaQueries: { + minWidth: '1025px', + maxWidth: '1439px', + }, }, format1: { - minWidth: '1440px', + mediaQueries: { + minWidth: '1440px', + }, }, }, pictureFormatsOrder: ['format1', 'format200', 'format400', 'format600'], @@ -443,14 +451,23 @@ describe('MediaService', () => { it('should return all images in sources', () => { const expectedResult = [ - { srcset: 'base:format-1.url', media: '(min-width: 1440px)' }, + { + srcset: 'base:format-1.url', + media: '(min-width: 1440px)', + width: undefined, + height: undefined, + }, { srcset: 'base:format-400.url', media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + width: undefined, + height: undefined, }, { srcset: 'base:format-600.url', media: '(min-width: 1025px) and (max-width: 1439px)', + width: undefined, + height: undefined, }, ]; @@ -468,24 +485,40 @@ describe('MediaService', () => { 'format400' )?.sources ).toEqual([ - { srcset: 'base:format-1.url', media: '(min-width: 1440px)' }, + { + srcset: 'base:format-1.url', + media: '(min-width: 1440px)', + width: undefined, + height: undefined, + }, { srcset: 'base:format-400.url', media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + width: undefined, + height: undefined, }, ]); }); it('should return all formats for unknown format', () => { const expectedResult = [ - { srcset: 'base:format-1.url', media: '(min-width: 1440px)' }, + { + srcset: 'base:format-1.url', + media: '(min-width: 1440px)', + width: undefined, + height: undefined, + }, { srcset: 'base:format-400.url', media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + width: undefined, + height: undefined, }, { srcset: 'base:format-600.url', media: '(min-width: 1025px) and (max-width: 1439px)', + width: undefined, + height: undefined, }, ]; From 132164f16b8de0996cc3784bec8575817c0e9a80 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Fri, 6 Sep 2024 13:05:56 +0200 Subject: [PATCH 17/40] add tests for width and height --- .../components/media/media.service.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/projects/storefrontlib/shared/components/media/media.service.spec.ts b/projects/storefrontlib/shared/components/media/media.service.spec.ts index 37b70f46f92..a3e6e419f75 100644 --- a/projects/storefrontlib/shared/components/media/media.service.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.service.spec.ts @@ -712,6 +712,76 @@ describe('MediaService', () => { }); }); + describe('width and height attributes for picture sources', () => { + it('should return all images with proper width and height properties based on values from config', () => { + const config = { + ...MockStorefrontConfig, + pictureElementFormats: { + format400: { + mediaQueries: { + maxWidth: '786px', + minDevicePixelRatio: 3, + }, + width: 200, + height: 300, + }, + format200: { + mediaQueries: { + minWidth: '768px', + maxWidth: '1024px', + }, + width: 700, + height: 1000, + }, + format600: { + mediaQueries: { + minWidth: '1025px', + maxWidth: '1439px', + }, + width: 1000, + height: 800, + }, + format1: { + mediaQueries: { + minWidth: '1440px', + }, + width: 1440, + height: 1000, + }, + }, + }; + configureTestingModule(config); + const service = TestBed.inject(MediaService); + + const expectedResult = [ + { + srcset: 'base:format-1.url', + media: '(min-width: 1440px)', + width: 1440, + height: 1000, + }, + { + srcset: 'base:format-400.url', + media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + width: 200, + height: 300, + }, + { + srcset: 'base:format-600.url', + media: '(min-width: 1025px) and (max-width: 1439px)', + width: 1000, + height: 800, + }, + ]; + + const result = service.getMediaForPictureElement( + mockBestFormatMediaContainer + )?.sources; + + expect(result).toEqual(expectedResult); + }); + }); + describe('without media config', () => { let mediaService: MediaService; From 64fe126e6ac7c522550c0a257bbf352f022beba0 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Fri, 11 Oct 2024 12:33:48 +0200 Subject: [PATCH 18/40] changes after review --- .../feature-toggles/config/feature-toggles.ts | 14 +++++++-- .../search-box/search-box.component.html | 2 +- .../product-grid-item.component.html | 2 +- .../product-list-item.component.html | 2 +- .../components/media/media.component.html | 11 +++---- .../components/media/media.component.spec.ts | 31 +++++++++++++------ .../components/media/media.component.ts | 27 ++++------------ .../shared/components/media/media.config.ts | 2 +- .../shared/components/media/media.model.ts | 3 ++ .../shared/components/media/media.service.ts | 27 ++++++++++++++-- .../shared/components/media/media.token.ts | 2 +- 11 files changed, 77 insertions(+), 46 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index aaeae7005ec..4a7a3670063 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -628,9 +628,19 @@ export interface FeatureTogglesInterface { allPageMetaResolversEnabledInCsr?: boolean; /** - * When enabled, allows to provide extended formats and media queries for element. + * When enabled, allows to provide extended formats and media queries for element if used in MediaComponent. * - * For proper work requires `pictureElementFormats` and `mediaQueryMap` provided in media config. + * For proper work requires `pictureElementFormats` and `mediaQueryMap` provided in media config: + * ```ts + * provideConfig({ + * pictureElementFormats: { + * ... + * }, + * mediaQueryMap: { + * ... + * } + * }) + * ``` */ useMediaComponentWithConfigurableMediaQueries?: boolean; } diff --git a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html index 40c9ec980cc..839f486fa75 100644 --- a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html +++ b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html @@ -176,7 +176,7 @@ [container]="product.images?.PRIMARY" format="thumbnail" role="presentation" - [useImgElement]="true" + [elementType]="'img'" >
{{ product.price?.formattedValue }} diff --git a/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html b/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html index 09e4846ca19..b5bf2252be5 100644 --- a/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html +++ b/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html @@ -8,7 +8,7 @@ [container]="product.images?.PRIMARY" format="product" [alt]="product.summary" - [useImgElement]="true" + [elementType]="'img'" > diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index 34e79af66d3..d72db61257c 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,7 +1,10 @@ @@ -79,9 +79,6 @@ [attr.role]="media.role" (load)="loadHandler()" (error)="errorHandler()" - [attr.width]="width" - [attr.height]="height" - [attr.sizes]="sizes" /> diff --git a/projects/storefrontlib/shared/components/media/media.component.spec.ts b/projects/storefrontlib/shared/components/media/media.component.spec.ts index 21540a5d8dd..7814d888e95 100644 --- a/projects/storefrontlib/shared/components/media/media.component.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.component.spec.ts @@ -17,7 +17,7 @@ import { ImageLoadingStrategy, Media } from './media.model'; import { MediaService } from './media.service'; import { USE_LEGACY_MEDIA_COMPONENT } from './media.token'; -export const IS_CONFIGURABLE_MEDIA_COMPONENT = new InjectionToken( +const IS_CONFIGURABLE_MEDIA_COMPONENT = new InjectionToken( 'IS_CONFIGURABLE_MEDIA_COMPONENT' ); @@ -100,6 +100,7 @@ class MockMediaService { ], }; } + getMissingImage() { return { src: 'missing.jpg', @@ -156,11 +157,15 @@ function createComponent(useImgElement = false) { service, 'getMediaForPictureElement' ).and.callThrough(); + const getMediaBasedOnHTMLElementType = spyOn( + service, + 'getMediaBasedOnHTMLElementType' + ).and.callThrough(); component.container = mockImageContainer; if (useImgElement) { - component.useImgElement = useImgElement; + component.elementType = 'img'; } component.ngOnChanges(); @@ -172,12 +177,13 @@ function createComponent(useImgElement = false) { component, getMediaSpy, getMediaForPictureElementSpy, + getMediaBasedOnHTMLElementType, }; } describe('MediaComponent', () => { describe('with enabled useMediaComponentWithConfigurableMediaQueries', () => { - it('should have picture element if useImgElement is false', () => { + it('should have picture element if elementType is `picture`', () => { configureTestingModule(new MockMediaService('srcset'), false, true); const { fixture } = createComponent(false); @@ -186,7 +192,7 @@ describe('MediaComponent', () => { expect(picture).not.toBeNull(); }); - it('should not have picture element if useImgElement is true', () => { + it('should not have picture element if elementType is `img`', () => { configureTestingModule(new MockMediaService('srcset'), false, true); const { fixture } = createComponent(true); @@ -195,16 +201,14 @@ describe('MediaComponent', () => { expect(picture).toBeNull(); }); - it('should call getMedia() method from service if useImgElement is true', () => { + it('should call getMediaBasedOnHTMLElementType() method from service', () => { configureTestingModule(new MockMediaService('srcset'), false, true); - const { getMediaSpy, getMediaForPictureElementSpy } = - createComponent(true); + const { getMediaSpy } = createComponent(true); - expect(getMediaForPictureElementSpy).not.toHaveBeenCalled(); expect(getMediaSpy).toHaveBeenCalled(); }); - it('should call getMediaForPictureElement() method from service if useImgElement is false', () => { + it('should call getMediaForPictureElement() method from service if elementType is `picture`', () => { configureTestingModule(new MockMediaService('srcset'), false, true); const { getMediaForPictureElementSpy, getMediaSpy } = createComponent(false); @@ -212,6 +216,15 @@ describe('MediaComponent', () => { expect(getMediaForPictureElementSpy).toHaveBeenCalled(); expect(getMediaSpy).not.toHaveBeenCalled(); }); + + it('should call getMedia() method from service if elementType is `img`', () => { + configureTestingModule(new MockMediaService('srcset'), false, true); + const { getMediaForPictureElementSpy, getMediaSpy } = + createComponent(true); + + expect(getMediaForPictureElementSpy).not.toHaveBeenCalled(); + expect(getMediaSpy).toHaveBeenCalled(); + }); }); it('should create', () => { diff --git a/projects/storefrontlib/shared/components/media/media.component.ts b/projects/storefrontlib/shared/components/media/media.component.ts index 0eefb565a51..b4308549a16 100644 --- a/projects/storefrontlib/shared/components/media/media.component.ts +++ b/projects/storefrontlib/shared/components/media/media.component.ts @@ -15,12 +15,7 @@ import { Output, TrackByFunction, } from '@angular/core'; -import { - Config, - FeatureConfigService, - Image, - ImageGroup, -} from '@spartacus/core'; +import { Config, Image, ImageGroup } from '@spartacus/core'; import { ImageLoadingStrategy, Media, MediaContainer } from './media.model'; import { MediaService } from './media.service'; import { USE_LEGACY_MEDIA_COMPONENT } from './media.token'; @@ -66,7 +61,7 @@ export class MediaComponent implements OnChanges { */ @Input() loading: ImageLoadingStrategy | null = this.loadingStrategy; - @Input() useImgElement: boolean = false; + @Input() elementType: 'img' | 'picture' = 'picture'; /** * The intrinsic width of the image, in pixels @@ -125,19 +120,17 @@ export class MediaComponent implements OnChanges { item.media; /** - * @deprecated will be removed + * @deprecated since 2211.30. It will be eventually removed in the future * * To use `img` HTML element instead of `picture` * use `useMediaComponentWithConfigurableMediaQueries` feature flag - * and pass `[useImgElement]="true"` input to the component + * and pass `[elementType]="'img'"` input to the component */ protected isLegacy = inject(USE_LEGACY_MEDIA_COMPONENT, { optional: true }) || (inject(Config) as any)['useLegacyMediaComponent'] || false; - protected readonly featureConfigService = inject(FeatureConfigService); - constructor(protected mediaService: MediaService) {} ngOnChanges(): void { @@ -148,16 +141,8 @@ export class MediaComponent implements OnChanges { * Creates the `Media` object */ protected create(): void { - const shouldGetMediaForPictureElement = - this.featureConfigService.isEnabled( - 'useMediaComponentWithConfigurableMediaQueries' - ) && !this.useImgElement; - - const getMedia = shouldGetMediaForPictureElement - ? this.mediaService.getMediaForPictureElement.bind(this.mediaService) - : this.mediaService.getMedia.bind(this.mediaService); - - this.media = getMedia( + this.media = this.mediaService.getMediaBasedOnHTMLElementType( + this.elementType, this.container instanceof Array ? this.container[0] : this.container, this.format, this.alt, diff --git a/projects/storefrontlib/shared/components/media/media.config.ts b/projects/storefrontlib/shared/components/media/media.config.ts index ef3eb13a5a7..ee52f28d689 100644 --- a/projects/storefrontlib/shared/components/media/media.config.ts +++ b/projects/storefrontlib/shared/components/media/media.config.ts @@ -94,7 +94,7 @@ export abstract class MediaConfig { imageLoadingStrategy?: ImageLoadingStrategy; /** - * @deprecated will be removed + * @deprecated since 2211.30. It will be eventually removed in the future * As of v7.0, Spartacus started using the element by default when a srcset is available. * * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture for more diff --git a/projects/storefrontlib/shared/components/media/media.model.ts b/projects/storefrontlib/shared/components/media/media.model.ts index 2be42415a0b..4e7a4a81ad4 100644 --- a/projects/storefrontlib/shared/components/media/media.model.ts +++ b/projects/storefrontlib/shared/components/media/media.model.ts @@ -69,6 +69,9 @@ export interface MediaFormatSize { /** * Specifies media queries that can be used to generate information for the * browser to resolve the right media for the right layout or device. + * + * You can check available queries in official docs + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries */ export type PictureElementQueries = { [query: string]: string | number; diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index f184c2dfe4d..594deaa6266 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; -import { Config, Image } from '@spartacus/core'; +import { Injectable, inject } from '@angular/core'; +import { Config, FeatureConfigService, Image } from '@spartacus/core'; import { MediaConfig } from './media.config'; import { ImageLoadingStrategy, @@ -40,11 +40,34 @@ export class MediaService { private _sortedPictureFormats: { code: string; mediaQuery: string }[]; private _reversedFormats: { code: string; size: MediaFormatSize }[]; + private readonly featureConfigService = inject(FeatureConfigService); + constructor(protected config: Config) {} + getMediaBasedOnHTMLElementType( + elementType: 'img' | 'picture', + mediaContainer?: MediaContainer | Image, + format?: string, + alt?: string, + role?: string + ): Media | undefined { + const shouldGetMediaForPictureElement = + this.featureConfigService.isEnabled( + 'useMediaComponentWithConfigurableMediaQueries' + ) && elementType !== 'img'; + + const getMediaFn = shouldGetMediaForPictureElement + ? this.getMediaForPictureElement + : this.getMedia; + + return getMediaFn(mediaContainer, format, alt, role); + } + /** * Returns a `Media` object with the main media (`src`) and various media (`src`) * for specific formats. + * + * This method is used for creating `Media` object that is used in `img` HTML element */ getMedia( mediaContainer?: MediaContainer | Image, diff --git a/projects/storefrontlib/shared/components/media/media.token.ts b/projects/storefrontlib/shared/components/media/media.token.ts index 609a7bf6666..0673c7cacc8 100644 --- a/projects/storefrontlib/shared/components/media/media.token.ts +++ b/projects/storefrontlib/shared/components/media/media.token.ts @@ -7,7 +7,7 @@ import { InjectionToken } from '@angular/core'; /** - * @deprecated will be removed + * @deprecated since 2211.30. It will be eventually removed in the future */ export const USE_LEGACY_MEDIA_COMPONENT = new InjectionToken( 'USE_LEGACY_MEDIA_COMPONENT' From d224542a8c4bec560fce2ab7ac472a170ef1793c Mon Sep 17 00:00:00 2001 From: rmch91 Date: Fri, 11 Oct 2024 14:31:43 +0200 Subject: [PATCH 19/40] changes --- .../feature-toggles/config/feature-toggles.ts | 11 +++--- .../recipes/config/default-media.config.ts | 35 +++++++------------ .../shared/components/media/media.config.ts | 21 ----------- .../components/media/media.service.spec.ts | 13 ------- .../shared/components/media/media.service.ts | 28 ++++----------- 5 files changed, 25 insertions(+), 83 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 4a7a3670063..4260df55c9c 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -630,15 +630,16 @@ export interface FeatureTogglesInterface { /** * When enabled, allows to provide extended formats and media queries for element if used in MediaComponent. * - * For proper work requires `pictureElementFormats` and `mediaQueryMap` provided in media config: + * For proper work requires `pictureElementFormats` provided in media config: * ```ts * provideConfig({ * pictureElementFormats: { - * ... + * mediaQueries: { + * 'max-width': '767px', + * }, + * width: 50, + * height: 50, * }, - * mediaQueryMap: { - * ... - * } * }) * ``` */ diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index 6cefe91f120..d7a38649a46 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -21,27 +21,29 @@ export const mediaConfig: MediaConfig = { pictureElementFormats: { mobile: { mediaQueries: { - maxWidth: '767px', + 'max-width': '767px', }, - width: 50, - height: 50, }, tablet: { - mediaQueries: { minWidth: '768px', maxWidth: '1024px' }, + mediaQueries: { 'min-width': '768px', 'max-width': '1024px' }, }, desktop: { - mediaQueries: { minWidth: '1025px', maxWidth: '1439px' }, - width: 100, - height: 100, + mediaQueries: { 'min-width': '1025px', 'max-width': '1439px' }, }, widescreen: { - mediaQueries: { minWidth: '1440px' }, + mediaQueries: { 'min-width': '1440px' }, }, retina_mobile: { - mediaQueries: { maxWidth: '786px', minDevicePixelRatio: 3 }, + mediaQueries: { + 'max-width': '786px', + '-webkit-min-device-pixel-ratio': 3, + }, }, retina_desktop: { - mediaQueries: { minWidth: '1440px', minDevicePixelRatio: 2 }, + mediaQueries: { + 'min-width': '1440px', + '-webkit-min-device-pixel-ratio': 2, + }, }, }, pictureFormatsOrder: [ @@ -52,17 +54,4 @@ export const mediaConfig: MediaConfig = { 'tablet', 'mobile', ], - mediaQueryMap: { - minWidth: 'min-width', - maxWidth: 'max-width', - minHeight: 'min-height', - maxHeight: 'max-height', - minDevicePixelRatio: '-webkit-min-device-pixel-ratio', - maxDevicePixelRatio: '-webkit-max-device-pixel-ratio', - orientation: 'orientation', - minAspectRatio: 'min-aspect-ratio', - maxAspectRatio: 'max-aspect-ratio', - minResolution: 'min-resolution', - maxResolution: 'max-resolution', - }, }; diff --git a/projects/storefrontlib/shared/components/media/media.config.ts b/projects/storefrontlib/shared/components/media/media.config.ts index ee52f28d689..2e46cc7c97b 100644 --- a/projects/storefrontlib/shared/components/media/media.config.ts +++ b/projects/storefrontlib/shared/components/media/media.config.ts @@ -60,27 +60,6 @@ export abstract class MediaConfig { */ pictureFormatsOrder?: string[]; - /** - * A map that associates the keys of `PictureElementQueries` with their corresponding CSS media query features. - * - * @type {Record} - * - * The `queryMap` is used to translate the query properties provided in a `PictureElementQueries` object - * to their respective CSS media query feature names. This map includes the following mappings: - * - * @example - * - `minWidth` -> `min-width` - * - `maxWidth` -> `max-width` - * - `minHeight` -> `min-height` - * - `maxHeight` -> `max-height` - * - `minDevicePixelRatio` -> `min-device-pixel-ratio` - * - `maxDevicePixelRatio` -> `max-device-pixel-ratio` - * - `orientation` -> `orientation` - * - `minAspectRatio` -> `min-aspect-ratio` - * - `maxAspectRatio` -> `max-aspect-ratio` - */ - mediaQueryMap?: Record; - /** * Indicates how the browser should load the image. There's only one * global strategy for all media cross media in Spartacus. diff --git a/projects/storefrontlib/shared/components/media/media.service.spec.ts b/projects/storefrontlib/shared/components/media/media.service.spec.ts index a3e6e419f75..de11d44e169 100644 --- a/projects/storefrontlib/shared/components/media/media.service.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.service.spec.ts @@ -54,19 +54,6 @@ const MockStorefrontConfig: Config = { }, }, pictureFormatsOrder: ['format1', 'format200', 'format400', 'format600'], - mediaQueryMap: { - minWidth: 'min-width', - maxWidth: 'max-width', - minHeight: 'min-height', - maxHeight: 'max-height', - minDevicePixelRatio: '-webkit-min-device-pixel-ratio', - maxDevicePixelRatio: '-webkit-max-device-pixel-ratio', - orientation: 'orientation', - minAspectRatio: 'min-aspect-ratio', - maxAspectRatio: 'max-aspect-ratio', - minResolution: 'min-resolution', - maxResolution: 'max-resolution', - }, }; const mockUnknownMediaContainer = { diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index 594deaa6266..1660e773825 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -56,11 +56,9 @@ export class MediaService { 'useMediaComponentWithConfigurableMediaQueries' ) && elementType !== 'img'; - const getMediaFn = shouldGetMediaForPictureElement - ? this.getMediaForPictureElement - : this.getMedia; - - return getMediaFn(mediaContainer, format, alt, role); + return shouldGetMediaForPictureElement + ? this.getMediaForPictureElement(mediaContainer, format, alt, role) + : this.getMedia(mediaContainer, format, alt, role); } /** @@ -233,24 +231,12 @@ export class MediaService { * @param {PictureElementQueries} queries - An object containing media query properties. * @returns {string} A string representing the CSS media query. * - * This method constructs a media query string by mapping the provided query properties - * to their corresponding CSS media query features and joining them with "and". + * This method constructs a media query string from the provided query + * properties in config and joining them with "and". */ protected generateMediaQuery(queries: PictureElementQueries): string { - const queryMap = this.config?.mediaQueryMap; - - if (!queryMap) { - return ''; - } - - return Object.keys(queries) - .filter((key) => key in queryMap && queries[key] !== undefined) - .map((key) => { - const mediaFeature = queryMap[key]; - const value = queries[key]; - - return `(${mediaFeature}: ${value})`; - }) + return Object.entries(queries) + .map(([key, value]) => `(${key}: ${value})`) .join(' and '); } From 5c6a462925503016f2eaf1805086af8632132b9a Mon Sep 17 00:00:00 2001 From: rmch91 Date: Mon, 14 Oct 2024 13:19:18 +0200 Subject: [PATCH 20/40] change unit tests --- .../components/media/media.component.spec.ts | 55 +++++++++++------ .../components/media/media.service.spec.ts | 61 ++++++------------- .../shared/components/media/media.service.ts | 1 + 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/projects/storefrontlib/shared/components/media/media.component.spec.ts b/projects/storefrontlib/shared/components/media/media.component.spec.ts index 7814d888e95..ee1ad0e4014 100644 --- a/projects/storefrontlib/shared/components/media/media.component.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.component.spec.ts @@ -72,10 +72,16 @@ export class MockMediaSourcesPipe implements PipeTransform { } class MockMediaService { - srcset: string | undefined; + srcset: any; + useMediaComponentWithConfigurableMediaQueries: boolean; - constructor(srcset?: string) { + constructor( + srcset: string | null, + useMediaComponentWithConfigurableMediaQueries: boolean + ) { this.srcset = srcset; + this.useMediaComponentWithConfigurableMediaQueries = + useMediaComponentWithConfigurableMediaQueries; } getMedia(media: any): Media { @@ -101,6 +107,19 @@ class MockMediaService { }; } + getMediaBasedOnHTMLElementType( + elementType: 'img' | 'picture', + mediaContainer?: any + ) { + const shouldGetMediaForPictureElement = + this.useMediaComponentWithConfigurableMediaQueries && + elementType !== 'img'; + + return shouldGetMediaForPictureElement + ? this.getMediaForPictureElement(mediaContainer) + : this.getMedia(mediaContainer); + } + getMissingImage() { return { src: 'missing.jpg', @@ -184,7 +203,7 @@ function createComponent(useImgElement = false) { describe('MediaComponent', () => { describe('with enabled useMediaComponentWithConfigurableMediaQueries', () => { it('should have picture element if elementType is `picture`', () => { - configureTestingModule(new MockMediaService('srcset'), false, true); + configureTestingModule(new MockMediaService('srcset', true), false, true); const { fixture } = createComponent(false); const picture = fixture.debugElement.query(By.css('picture')); @@ -193,7 +212,7 @@ describe('MediaComponent', () => { }); it('should not have picture element if elementType is `img`', () => { - configureTestingModule(new MockMediaService('srcset'), false, true); + configureTestingModule(new MockMediaService('srcset', true), false, true); const { fixture } = createComponent(true); const picture = fixture.debugElement.query(By.css('picture')); @@ -202,14 +221,14 @@ describe('MediaComponent', () => { }); it('should call getMediaBasedOnHTMLElementType() method from service', () => { - configureTestingModule(new MockMediaService('srcset'), false, true); + configureTestingModule(new MockMediaService('srcset', true), false, true); const { getMediaSpy } = createComponent(true); expect(getMediaSpy).toHaveBeenCalled(); }); it('should call getMediaForPictureElement() method from service if elementType is `picture`', () => { - configureTestingModule(new MockMediaService('srcset'), false, true); + configureTestingModule(new MockMediaService('srcset', true), false, true); const { getMediaForPictureElementSpy, getMediaSpy } = createComponent(false); @@ -218,7 +237,7 @@ describe('MediaComponent', () => { }); it('should call getMedia() method from service if elementType is `img`', () => { - configureTestingModule(new MockMediaService('srcset'), false, true); + configureTestingModule(new MockMediaService('srcset', true), false, true); const { getMediaForPictureElementSpy, getMediaSpy } = createComponent(true); @@ -228,21 +247,21 @@ describe('MediaComponent', () => { }); it('should create', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { component } = createComponent(); expect(component).toBeTruthy(); }); it('should create media object with valid image url', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { component } = createComponent(); expect(component?.media?.src).toEqual(mediaUrl); }); it('should update the img element with image url', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { fixture } = createComponent(); expect( @@ -253,7 +272,7 @@ describe('MediaComponent', () => { }); it('should not contain the loading attribute for the image element', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { fixture } = createComponent(); const el: HTMLElement = ( @@ -265,7 +284,7 @@ describe('MediaComponent', () => { }); it('should contain loading="lazy" for the image element', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { service } = createComponent(); spyOnProperty(service, 'loadingStrategy').and.returnValue( @@ -283,7 +302,7 @@ describe('MediaComponent', () => { }); it('should contain is-loading classes while loading', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { fixture } = createComponent(); expect( @@ -292,7 +311,7 @@ describe('MediaComponent', () => { }); it('should update classes when loaded', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { fixture } = createComponent(); const load = new UIEvent('load'); @@ -309,7 +328,7 @@ describe('MediaComponent', () => { }); it('should have is-missing class when there is no image', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { fixture, component, getMediaSpy } = createComponent(); component.container = mockImageContainer; @@ -329,7 +348,7 @@ describe('MediaComponent', () => { }); it('should not have picture element if there is no srcset in media', () => { - configureTestingModule(new MockMediaService()); + configureTestingModule(new MockMediaService(null, false)); const { fixture } = createComponent(); const picture = fixture.debugElement.query(By.css('picture')); @@ -338,7 +357,7 @@ describe('MediaComponent', () => { }); it('should have picture element if there is srcset in media', () => { - configureTestingModule(new MockMediaService('srcset'), false); + configureTestingModule(new MockMediaService('srcset', false), false); const { fixture } = createComponent(); const picture = fixture.debugElement.query(By.css('picture')); @@ -347,7 +366,7 @@ describe('MediaComponent', () => { }); it('should not have picture element if there is srcset in media but isLegacy mode', () => { - configureTestingModule(new MockMediaService('srcset')); + configureTestingModule(new MockMediaService('srcset', false)); const { fixture } = createComponent(); const picture = fixture.debugElement.query(By.css('picture')); diff --git a/projects/storefrontlib/shared/components/media/media.service.spec.ts b/projects/storefrontlib/shared/components/media/media.service.spec.ts index de11d44e169..6657ff0fa5d 100644 --- a/projects/storefrontlib/shared/components/media/media.service.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.service.spec.ts @@ -31,25 +31,25 @@ const MockStorefrontConfig: Config = { pictureElementFormats: { format400: { mediaQueries: { - maxWidth: '786px', - minDevicePixelRatio: 3, + 'max-width': '786px', + '-webkit-min-device-pixel-ratio': 3, }, }, format200: { mediaQueries: { - minWidth: '768px', - maxWidth: '1024px', + 'min-width': '768px', + 'max-width': '1024px', }, }, format600: { mediaQueries: { - minWidth: '1025px', - maxWidth: '1439px', + 'min-width': '1025px', + 'max-width': '1439px', }, }, format1: { mediaQueries: { - minWidth: '1440px', + 'min-width': '1440px', }, }, }, @@ -631,16 +631,6 @@ describe('MediaService', () => { }); describe('generateMediaQuery()', () => { - it('should return an empty string if config is not defined', () => { - configureTestingModule({}); - const service = TestBed.inject(MediaService); - - const queries: PictureElementQueries = { - minWidth: 768, - }; - expect(service['generateMediaQuery'](queries)).toBe(''); - }); - it('should return an empty string if queries are empty', () => { configureTestingModule({}); const service = TestBed.inject(MediaService); @@ -654,32 +644,21 @@ describe('MediaService', () => { const service = TestBed.inject(MediaService); const queries: PictureElementQueries = { - minWidth: 768, - maxWidth: 1024, + 'min-width': 768, + 'max-width': 1024, }; expect(service['generateMediaQuery'](queries)).toBe( '(min-width: 768) and (max-width: 1024)' ); }); - it('should ignore queries that are not in the query map', () => { - configureTestingModule(MockStorefrontConfig); - const service = TestBed.inject(MediaService); - - const queries: PictureElementQueries = { - minWidth: 768, - unknownQuery: 100, - }; - expect(service['generateMediaQuery'](queries)).toBe('(min-width: 768)'); - }); - it('should ignore undefined query values', () => { configureTestingModule(MockStorefrontConfig); const service = TestBed.inject(MediaService); const queries: PictureElementQueries = { - minWidth: 768, - maxWidth: undefined as unknown as any, + 'min-width': 768, + 'max-width': undefined as unknown as any, }; expect(service['generateMediaQuery'](queries)).toBe('(min-width: 768)'); }); @@ -689,8 +668,8 @@ describe('MediaService', () => { const service = TestBed.inject(MediaService); const queries: PictureElementQueries = { - minWidth: 768, - maxWidth: 1024, + 'min-width': 768, + 'max-width': 1024, orientation: 'landscape', }; expect(service['generateMediaQuery'](queries)).toBe( @@ -706,31 +685,31 @@ describe('MediaService', () => { pictureElementFormats: { format400: { mediaQueries: { - maxWidth: '786px', - minDevicePixelRatio: 3, + 'max-width': '786px', + '-webkit-min-device-pixel-ratio': 3, }, width: 200, height: 300, }, format200: { mediaQueries: { - minWidth: '768px', - maxWidth: '1024px', + 'min-width': '768px', + 'max-width': '1024px', }, width: 700, height: 1000, }, format600: { mediaQueries: { - minWidth: '1025px', - maxWidth: '1439px', + 'min-width': '1025px', + 'max-width': '1439px', }, width: 1000, height: 800, }, format1: { mediaQueries: { - minWidth: '1440px', + 'min-width': '1440px', }, width: 1440, height: 1000, diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index 1660e773825..641788fa925 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -236,6 +236,7 @@ export class MediaService { */ protected generateMediaQuery(queries: PictureElementQueries): string { return Object.entries(queries) + .filter(([key, value]) => !!key && !!value) .map(([key, value]) => `(${key}: ${value})`) .join(' and '); } From 06fbe155bb70173cab8a3914fe1cb592535697a0 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Mon, 14 Oct 2024 13:34:01 +0200 Subject: [PATCH 21/40] changes --- .../features-config/feature-toggles/config/feature-toggles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 4260df55c9c..073ee2faef8 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -630,12 +630,13 @@ export interface FeatureTogglesInterface { /** * When enabled, allows to provide extended formats and media queries for element if used in MediaComponent. * - * For proper work requires `pictureElementFormats` provided in media config: + * For proper work requires `pictureElementFormats` provided in media config: * ```ts * provideConfig({ * pictureElementFormats: { * mediaQueries: { * 'max-width': '767px', + * ... * }, * width: 50, * height: 50, From dc61ba69af6a608f4ac47167addf56d405f01cea Mon Sep 17 00:00:00 2001 From: rmch91 Date: Tue, 15 Oct 2024 14:15:34 +0200 Subject: [PATCH 22/40] Change default config --- .../recipes/config/default-media.config.ts | 21 +------------------ .../shared/components/media/media.config.ts | 2 +- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index d7a38649a46..0cdde14f300 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -33,25 +33,6 @@ export const mediaConfig: MediaConfig = { widescreen: { mediaQueries: { 'min-width': '1440px' }, }, - retina_mobile: { - mediaQueries: { - 'max-width': '786px', - '-webkit-min-device-pixel-ratio': 3, - }, - }, - retina_desktop: { - mediaQueries: { - 'min-width': '1440px', - '-webkit-min-device-pixel-ratio': 2, - }, - }, }, - pictureFormatsOrder: [ - 'retina_desktop', - 'retina_mobile', - 'widescreen', - 'desktop', - 'tablet', - 'mobile', - ], + pictureFormatsOrder: ['widescreen', 'desktop', 'tablet', 'mobile'], }; diff --git a/projects/storefrontlib/shared/components/media/media.config.ts b/projects/storefrontlib/shared/components/media/media.config.ts index 2e46cc7c97b..6b899b8264e 100644 --- a/projects/storefrontlib/shared/components/media/media.config.ts +++ b/projects/storefrontlib/shared/components/media/media.config.ts @@ -43,7 +43,7 @@ export abstract class MediaConfig { */ pictureElementFormats?: { [format: string]: { - mediaQueries: PictureElementQueries; + mediaQueries?: PictureElementQueries; width?: number; height?: number; }; From 8e39174adc411f55dfaff0a2bb7ff4c3b3b95158 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Tue, 15 Oct 2024 14:19:35 +0200 Subject: [PATCH 23/40] changes --- projects/storefrontapp/src/app/app.module.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/projects/storefrontapp/src/app/app.module.ts b/projects/storefrontapp/src/app/app.module.ts index 812c67a3a8a..0db5940588f 100644 --- a/projects/storefrontapp/src/app/app.module.ts +++ b/projects/storefrontapp/src/app/app.module.ts @@ -22,17 +22,13 @@ import { translationChunksConfig, translations } from '@spartacus/assets'; import { I18nConfig, OccConfig, - provideConfig, RoutingConfig, TestConfigModule, + provideConfig, } from '@spartacus/core'; import { StoreFinderConfig } from '@spartacus/storefinder/core'; import { GOOGLE_MAPS_DEVELOPMENT_KEY_CONFIG } from '@spartacus/storefinder/root'; -import { - AppRoutingModule, - StorefrontComponent, - USE_LEGACY_MEDIA_COMPONENT, -} from '@spartacus/storefront'; +import { AppRoutingModule, StorefrontComponent } from '@spartacus/storefront'; import { environment } from '../environments/environment'; import { TestOutletModule } from '../test-outlets/test-outlet.module'; import { SpartacusModule } from './spartacus/spartacus.module'; @@ -94,10 +90,6 @@ if (!environment.production) { // without a key, for development or demo purposes. googleMaps: { apiKey: GOOGLE_MAPS_DEVELOPMENT_KEY_CONFIG }, }), - { - provide: USE_LEGACY_MEDIA_COMPONENT, - useValue: false, - }, ], bootstrap: [StorefrontComponent], }) From 35641929d2efe74b9803fc902115fa947fc9d83c Mon Sep 17 00:00:00 2001 From: rmch91 Date: Tue, 15 Oct 2024 14:30:13 +0200 Subject: [PATCH 24/40] fix --- .../shared/components/media/media.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index 641788fa925..bd7194a2c4f 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -234,7 +234,13 @@ export class MediaService { * This method constructs a media query string from the provided query * properties in config and joining them with "and". */ - protected generateMediaQuery(queries: PictureElementQueries): string { + protected generateMediaQuery( + queries: PictureElementQueries | undefined + ): string { + if (!queries) { + return ''; + } + return Object.entries(queries) .filter(([key, value]) => !!key && !!value) .map(([key, value]) => `(${key}: ${value})`) From 7e2e3489d33c4078df2a464f1fd47100162b632a Mon Sep 17 00:00:00 2001 From: rmch91 Date: Tue, 15 Oct 2024 14:49:26 +0200 Subject: [PATCH 25/40] changes after review --- .../feature-toggles/config/feature-toggles.ts | 4 ++-- .../app/spartacus/spartacus-features.module.ts | 2 +- .../shared/components/media/media.component.html | 4 ++-- .../components/media/media.component.spec.ts | 15 +++++---------- .../shared/components/media/media.component.ts | 8 +++++++- .../shared/components/media/media.service.ts | 5 ++--- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 0cd302552ed..d1fe211f722 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -644,7 +644,7 @@ export interface FeatureTogglesInterface { * }) * ``` */ - useMediaComponentWithConfigurableMediaQueries?: boolean; + useExtendedMediaComponent?: boolean; } export const defaultFeatureToggles: Required = { @@ -743,5 +743,5 @@ export const defaultFeatureToggles: Required = { enableConsecutiveCharactersPasswordRequirement: false, enablePasswordsCannotMatchInPasswordUpdateForm: false, allPageMetaResolversEnabledInCsr: false, - useMediaComponentWithConfigurableMediaQueries: false, + useExtendedMediaComponent: false, }; diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index e6ffda32251..70ae3a08894 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -380,7 +380,7 @@ if (environment.cpq) { enableConsecutiveCharactersPasswordRequirement: true, enablePasswordsCannotMatchInPasswordUpdateForm: true, allPageMetaResolversEnabledInCsr: true, - useMediaComponentWithConfigurableMediaQueries: true, + useExtendedMediaComponent: true, }; return appFeatureToggles; }), diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index d72db61257c..2db3ba4b4ed 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,5 +1,5 @@ - + + Date: Tue, 15 Oct 2024 14:59:51 +0200 Subject: [PATCH 26/40] changes --- .../features-config/feature-toggles/config/feature-toggles.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index d1fe211f722..8423deb068a 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -643,6 +643,9 @@ export interface FeatureTogglesInterface { * }, * }) * ``` + * + * After activating this toggle, new inputs in `MediaComponent` — specifically + * `width`, `height`, and `size` — will be passed to the template as HTML attributes. */ useExtendedMediaComponent?: boolean; } From 2437bb08cfd55bca2e4ae55a32ca03a88ff9ed17 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Wed, 16 Oct 2024 12:42:33 +0200 Subject: [PATCH 27/40] changes after review --- .../feature-toggles/config/feature-toggles.ts | 8 ++++--- .../spartacus/spartacus-features.module.ts | 2 +- .../search-box/search-box.component.html | 1 - .../product-grid-item.component.html | 1 - .../product-list-item.component.html | 1 - .../components/media/media.component.html | 4 ++-- .../components/media/media.component.spec.ts | 14 ++++++++----- .../components/media/media.component.ts | 11 ++++++---- .../shared/components/media/media.service.ts | 21 +++++-------------- 9 files changed, 29 insertions(+), 34 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 8423deb068a..29012262e25 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -645,9 +645,11 @@ export interface FeatureTogglesInterface { * ``` * * After activating this toggle, new inputs in `MediaComponent` — specifically - * `width`, `height`, and `size` — will be passed to the template as HTML attributes. + * `width`, `height`, and `sizes` — will be passed to the template as HTML attributes. + * + * Toggle activates `@Input() elementType: 'img' | 'picture' = 'picture';` in `MediaComponent` */ - useExtendedMediaComponent?: boolean; + useExtendedMediaComponentConfiguration?: boolean; } export const defaultFeatureToggles: Required = { @@ -746,5 +748,5 @@ export const defaultFeatureToggles: Required = { enableConsecutiveCharactersPasswordRequirement: false, enablePasswordsCannotMatchInPasswordUpdateForm: false, allPageMetaResolversEnabledInCsr: false, - useExtendedMediaComponent: false, + useExtendedMediaComponentConfiguration: false, }; diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index 70ae3a08894..3ca844646f4 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -380,7 +380,7 @@ if (environment.cpq) { enableConsecutiveCharactersPasswordRequirement: true, enablePasswordsCannotMatchInPasswordUpdateForm: true, allPageMetaResolversEnabledInCsr: true, - useExtendedMediaComponent: true, + useExtendedMediaComponentConfiguration: true, }; return appFeatureToggles; }), diff --git a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html index 839f486fa75..7e2acbca3cc 100644 --- a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html +++ b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.html @@ -176,7 +176,6 @@ [container]="product.images?.PRIMARY" format="thumbnail" role="presentation" - [elementType]="'img'" >
{{ product.price?.formattedValue }} diff --git a/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html b/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html index b5bf2252be5..cbfce92adfd 100644 --- a/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html +++ b/projects/storefrontlib/cms-components/product/product-list/product-grid-item/product-grid-item.component.html @@ -8,7 +8,6 @@ [container]="product.images?.PRIMARY" format="product" [alt]="product.summary" - [elementType]="'img'" > diff --git a/projects/storefrontlib/shared/components/media/media.component.html b/projects/storefrontlib/shared/components/media/media.component.html index 2db3ba4b4ed..e4bcac48030 100644 --- a/projects/storefrontlib/shared/components/media/media.component.html +++ b/projects/storefrontlib/shared/components/media/media.component.html @@ -1,5 +1,5 @@ - + + ` ang ``. */ protected getCommonMediaObject( - mediaContainer?: MediaContainer | Image, + mediaContainer: MediaContainer | Image, format?: string, alt?: string, role?: string - ): Media | undefined { - if (!mediaContainer) { - return; - } - + ): Media { const mainMedia: Image = mediaContainer.url ? mediaContainer : this.resolveMedia(mediaContainer as MediaContainer, format); From 27241ebe4a1df41f6f41866298bbe0fcef4bde9b Mon Sep 17 00:00:00 2001 From: rmch91 Date: Wed, 16 Oct 2024 13:21:36 +0200 Subject: [PATCH 28/40] fix to pr --- .../recipes/config/default-media.config.ts | 10 +- .../shared/components/media/media.model.ts | 4 +- .../components/media/media.service.spec.ts | 102 +++--------------- .../shared/components/media/media.service.ts | 27 +---- 4 files changed, 21 insertions(+), 122 deletions(-) diff --git a/projects/storefrontlib/recipes/config/default-media.config.ts b/projects/storefrontlib/recipes/config/default-media.config.ts index 0cdde14f300..eb64a90671f 100644 --- a/projects/storefrontlib/recipes/config/default-media.config.ts +++ b/projects/storefrontlib/recipes/config/default-media.config.ts @@ -20,18 +20,16 @@ export const mediaConfig: MediaConfig = { }, pictureElementFormats: { mobile: { - mediaQueries: { - 'max-width': '767px', - }, + mediaQueries: '(max-width: 767px)', }, tablet: { - mediaQueries: { 'min-width': '768px', 'max-width': '1024px' }, + mediaQueries: '(min-width: 768px) and (max-width: 1024px)', }, desktop: { - mediaQueries: { 'min-width': '1025px', 'max-width': '1439px' }, + mediaQueries: '(min-width: 1025px) and (max-width: 1439px)', }, widescreen: { - mediaQueries: { 'min-width': '1440px' }, + mediaQueries: '(min-width: 1440px)', }, }, pictureFormatsOrder: ['widescreen', 'desktop', 'tablet', 'mobile'], diff --git a/projects/storefrontlib/shared/components/media/media.model.ts b/projects/storefrontlib/shared/components/media/media.model.ts index 4e7a4a81ad4..a4a0d04402f 100644 --- a/projects/storefrontlib/shared/components/media/media.model.ts +++ b/projects/storefrontlib/shared/components/media/media.model.ts @@ -73,9 +73,7 @@ export interface MediaFormatSize { * You can check available queries in official docs * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries */ -export type PictureElementQueries = { - [query: string]: string | number; -}; +export type PictureElementQueries = string; export interface PictureHTMLElementSources { srcset: string; diff --git a/projects/storefrontlib/shared/components/media/media.service.spec.ts b/projects/storefrontlib/shared/components/media/media.service.spec.ts index 6657ff0fa5d..8b9dc5d2b14 100644 --- a/projects/storefrontlib/shared/components/media/media.service.spec.ts +++ b/projects/storefrontlib/shared/components/media/media.service.spec.ts @@ -1,11 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { Config, Image } from '@spartacus/core'; import { LayoutConfig } from '../../../layout/config/layout-config'; -import { - ImageLoadingStrategy, - MediaContainer, - PictureElementQueries, -} from './media.model'; +import { ImageLoadingStrategy, MediaContainer } from './media.model'; import { MediaService } from './media.service'; const MockStorefrontConfig: Config = { @@ -30,27 +26,17 @@ const MockStorefrontConfig: Config = { }, pictureElementFormats: { format400: { - mediaQueries: { - 'max-width': '786px', - '-webkit-min-device-pixel-ratio': 3, - }, + mediaQueries: + '(max-width: 768px) and (-webkit-min-device-pixel-ratio: 3)', }, format200: { - mediaQueries: { - 'min-width': '768px', - 'max-width': '1024px', - }, + mediaQueries: '(min-width: 768px) and (max-width: 1024px)', }, format600: { - mediaQueries: { - 'min-width': '1025px', - 'max-width': '1439px', - }, + mediaQueries: '(min-width: 1025px) and (max-width: 1439px)', }, format1: { - mediaQueries: { - 'min-width': '1440px', - }, + mediaQueries: '(min-width: 1440px)', }, }, pictureFormatsOrder: ['format1', 'format200', 'format400', 'format600'], @@ -446,7 +432,7 @@ describe('MediaService', () => { }, { srcset: 'base:format-400.url', - media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + media: '(max-width: 768px) and (-webkit-min-device-pixel-ratio: 3)', width: undefined, height: undefined, }, @@ -480,7 +466,7 @@ describe('MediaService', () => { }, { srcset: 'base:format-400.url', - media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + media: '(max-width: 768px) and (-webkit-min-device-pixel-ratio: 3)', width: undefined, height: undefined, }, @@ -497,7 +483,7 @@ describe('MediaService', () => { }, { srcset: 'base:format-400.url', - media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + media: '(max-width: 768px) and (-webkit-min-device-pixel-ratio: 3)', width: undefined, height: undefined, }, @@ -630,87 +616,29 @@ describe('MediaService', () => { }); }); - describe('generateMediaQuery()', () => { - it('should return an empty string if queries are empty', () => { - configureTestingModule({}); - const service = TestBed.inject(MediaService); - - const queries: PictureElementQueries = {}; - expect(service['generateMediaQuery'](queries)).toBe(''); - }); - - it('should return correct media query for given queries', () => { - configureTestingModule(MockStorefrontConfig); - const service = TestBed.inject(MediaService); - - const queries: PictureElementQueries = { - 'min-width': 768, - 'max-width': 1024, - }; - expect(service['generateMediaQuery'](queries)).toBe( - '(min-width: 768) and (max-width: 1024)' - ); - }); - - it('should ignore undefined query values', () => { - configureTestingModule(MockStorefrontConfig); - const service = TestBed.inject(MediaService); - - const queries: PictureElementQueries = { - 'min-width': 768, - 'max-width': undefined as unknown as any, - }; - expect(service['generateMediaQuery'](queries)).toBe('(min-width: 768)'); - }); - - it('should return correct media query for mixed queries', () => { - configureTestingModule(MockStorefrontConfig); - const service = TestBed.inject(MediaService); - - const queries: PictureElementQueries = { - 'min-width': 768, - 'max-width': 1024, - orientation: 'landscape', - }; - expect(service['generateMediaQuery'](queries)).toBe( - '(min-width: 768) and (max-width: 1024) and (orientation: landscape)' - ); - }); - }); - describe('width and height attributes for picture sources', () => { it('should return all images with proper width and height properties based on values from config', () => { const config = { ...MockStorefrontConfig, pictureElementFormats: { format400: { - mediaQueries: { - 'max-width': '786px', - '-webkit-min-device-pixel-ratio': 3, - }, + mediaQueries: + '(max-width: 768px) and (-webkit-min-device-pixel-ratio: 3)', width: 200, height: 300, }, format200: { - mediaQueries: { - 'min-width': '768px', - 'max-width': '1024px', - }, + mediaQueries: '(min-width: 768px) and (max-width: 1024px)', width: 700, height: 1000, }, format600: { - mediaQueries: { - 'min-width': '1025px', - 'max-width': '1439px', - }, + mediaQueries: '(min-width: 1025px) and (max-width: 1439px)', width: 1000, height: 800, }, format1: { - mediaQueries: { - 'min-width': '1440px', - }, + mediaQueries: '(min-width: 1440px)', width: 1440, height: 1000, }, @@ -728,7 +656,7 @@ describe('MediaService', () => { }, { srcset: 'base:format-400.url', - media: '(max-width: 786px) and (-webkit-min-device-pixel-ratio: 3)', + media: '(max-width: 768px) and (-webkit-min-device-pixel-ratio: 3)', width: 200, height: 300, }, diff --git a/projects/storefrontlib/shared/components/media/media.service.ts b/projects/storefrontlib/shared/components/media/media.service.ts index 9d833e2c2af..4d2a258d71b 100644 --- a/projects/storefrontlib/shared/components/media/media.service.ts +++ b/projects/storefrontlib/shared/components/media/media.service.ts @@ -12,7 +12,6 @@ import { Media, MediaContainer, MediaFormatSize, - PictureElementQueries, PictureHTMLElementSources, } from './media.model'; @@ -191,9 +190,7 @@ export class MediaService { this._sortedPictureFormats = Object.keys(pictureElementMediaFormats).map( (key) => ({ code: key, - mediaQuery: this.generateMediaQuery( - pictureElementMediaFormats[key].mediaQueries - ), + mediaQuery: pictureElementMediaFormats[key]?.mediaQueries || '', }) ); @@ -213,28 +210,6 @@ export class MediaService { return this._sortedPictureFormats ?? []; } - /** - * Generates a CSS media query string from the given PictureElementQueries object. - * - * @param {PictureElementQueries} queries - An object containing media query properties. - * @returns {string} A string representing the CSS media query. - * - * This method constructs a media query string from the provided query - * properties in config and joining them with "and". - */ - protected generateMediaQuery( - queries: PictureElementQueries | undefined - ): string { - if (!queries) { - return ''; - } - - return Object.entries(queries) - .filter(([key, value]) => !!key && !!value) - .map(([key, value]) => `(${key}: ${value})`) - .join(' and '); - } - /** * Creates the media formats in a reversed sorted order. */ From d8adfdb58448b7bec9b5a8dafa74410100a70e03 Mon Sep 17 00:00:00 2001 From: rmch91 Date: Wed, 16 Oct 2024 16:57:26 +0200 Subject: [PATCH 29/40] add elementType img --- ...m-customer-360-product-item.component.html | 8 ++ .../asm-customer-360.component.html | 10 ++- .../cart-item-list-row.component.html | 7 ++ .../cart-item/cart-item.component.html | 7 ++ .../form/quick-order-form.component.html | 64 +++++++++++---- .../item/quick-order-item.component.html | 9 ++ .../wish-list-item.component.html | 7 ++ .../amend-order-items.component.html | 7 ++ .../my-account-v2-orders.component.html | 9 ++ ...er-consolidated-information.component.html | 9 ++ .../return-request-items.component.html | 7 ++ .../pickup-items-details.component.html | 7 ++ ...ator-attribute-product-card.component.html | 8 ++ ...r-overview-bundle-attribute.component.html | 8 ++ .../configurator-product-title.component.html | 7 ++ ...-multi-dimensional-selector.component.html | 12 ++- ...t-image-zoom-product-images.component.html | 76 ++++++++++++----- ...oduct-image-zoom-thumbnails.component.html | 58 +++++++++---- .../product-image-zoom-view.component.html | 82 +++++++++++++++---- .../merchandising-carousel.component.html | 24 ++++-- ...visual-picking-product-list.component.html | 7 ++ .../my-interests/my-interests.component.html | 7 ++ .../search-box/search-box.component.html | 24 ++++-- .../product-carousel-item.component.html | 8 ++ .../product-references.component.html | 12 ++- .../product-images.component.html | 21 ++++- .../product-grid-item.component.html | 9 ++ .../product-list-item.component.html | 9 ++ 28 files changed, 439 insertions(+), 84 deletions(-) diff --git a/feature-libs/asm/customer-360/components/asm-customer-360-product-item/asm-customer-360-product-item.component.html b/feature-libs/asm/customer-360/components/asm-customer-360-product-item/asm-customer-360-product-item.component.html index a870d0590fa..aed4ddf04da 100644 --- a/feature-libs/asm/customer-360/components/asm-customer-360-product-item/asm-customer-360-product-item.component.html +++ b/feature-libs/asm/customer-360/components/asm-customer-360-product-item/asm-customer-360-product-item.component.html @@ -4,6 +4,14 @@ (click)="selectProduct.emit(product)" > + " >
- + +
diff --git a/feature-libs/cart/base/components/cart-shared/cart-item-list-row/cart-item-list-row.component.html b/feature-libs/cart/base/components/cart-shared/cart-item-list-row/cart-item-list-row.component.html index 16df0befbfc..cf91d6e2041 100644 --- a/feature-libs/cart/base/components/cart-shared/cart-item-list-row/cart-item-list-row.component.html +++ b/feature-libs/cart/base/components/cart-shared/cart-item-list-row/cart-item-list-row.component.html @@ -14,6 +14,13 @@ tabindex="0" > + diff --git a/feature-libs/cart/base/components/cart-shared/cart-item/cart-item.component.html b/feature-libs/cart/base/components/cart-shared/cart-item/cart-item.component.html index 7af182f121d..cf8dafde3de 100644 --- a/feature-libs/cart/base/components/cart-shared/cart-item/cart-item.component.html +++ b/feature-libs/cart/base/components/cart-shared/cart-item/cart-item.component.html @@ -12,6 +12,13 @@ tabindex="0" > + diff --git a/feature-libs/cart/quick-order/components/quick-order/form/quick-order-form.component.html b/feature-libs/cart/quick-order/components/quick-order/form/quick-order-form.component.html index 82564346715..26e7f19333a 100644 --- a/feature-libs/cart/quick-order/components/quick-order/form/quick-order-form.component.html +++ b/feature-libs/cart/quick-order/components/quick-order/form/quick-order-form.component.html @@ -82,14 +82,29 @@ class="quick-order-results-product" role="option" > - + + + + + + + +
{{ @@ -139,14 +154,31 @@ class="quick-order-results-product" role="option" > - + + + + + + + +
{{ diff --git a/feature-libs/cart/quick-order/components/quick-order/table/item/quick-order-item.component.html b/feature-libs/cart/quick-order/components/quick-order/table/item/quick-order-item.component.html index 2f6ace69729..48ef7e0e2a6 100644 --- a/feature-libs/cart/quick-order/components/quick-order/table/item/quick-order-item.component.html +++ b/feature-libs/cart/quick-order/components/quick-order/table/item/quick-order-item.component.html @@ -9,6 +9,15 @@ tabindex="-1" > + + + diff --git a/feature-libs/order/components/amend-order/amend-order-items/amend-order-items.component.html b/feature-libs/order/components/amend-order/amend-order-items/amend-order-items.component.html index f58e4660990..648baffd69f 100644 --- a/feature-libs/order/components/amend-order/amend-order-items/amend-order-items.component.html +++ b/feature-libs/order/components/amend-order/amend-order-items/amend-order-items.component.html @@ -54,6 +54,13 @@
+ diff --git a/feature-libs/order/components/my-account-v2/my-account-v2-orders.component.html b/feature-libs/order/components/my-account-v2/my-account-v2-orders.component.html index 596ae775554..edbfa2c9b2d 100644 --- a/feature-libs/order/components/my-account-v2/my-account-v2-orders.component.html +++ b/feature-libs/order/components/my-account-v2/my-account-v2-orders.component.html @@ -60,6 +60,15 @@