diff --git a/.vercelignore b/.vercelignore index c18553dfa1e..7d2940991ff 100644 --- a/.vercelignore +++ b/.vercelignore @@ -1 +1,2 @@ -core/src/components/**/*/*.png +# Exclude visual-regression snapshot artifacts only +core/src/**/*-snapshots/*.png diff --git a/core/api.txt b/core/api.txt index 0c356bfa93c..5266536a49c 100644 --- a/core/api.txt +++ b/core/api.txt @@ -897,6 +897,14 @@ ion-footer,prop,mode,"ios" | "md",undefined,false,false ion-footer,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-footer,prop,translucent,boolean,false,false,false +ion-gallery,shadow +ion-gallery,prop,columns,GalleryBreakpointColumns | number | string,DEFAULT_COLUMNS,false,false +ion-gallery,prop,layout,"masonry" | "uniform",'uniform',false,true +ion-gallery,prop,mode,"ios" | "md",undefined,false,false +ion-gallery,prop,order,"best-fit" | "sequential",'sequential',false,true +ion-gallery,prop,theme,"ios" | "md" | "ionic",undefined,false,false +ion-gallery,css-prop,--ion-gallery-gap + ion-grid,shadow ion-grid,prop,fixed,boolean,false,false,false ion-grid,prop,mode,"ios" | "md",undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ea9f96efe77..56bf99906b9 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -16,6 +16,7 @@ import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/bre import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; +import { GalleryColumns } from "./components/gallery/gallery-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; import { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface"; @@ -54,6 +55,7 @@ export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/bre export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; +export { GalleryColumns } from "./components/gallery/gallery-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface"; @@ -1469,6 +1471,31 @@ export namespace Components { */ "translucent": boolean; } + interface IonGallery { + /** + * The number of columns to display. Can be set as a number or an object of breakpoint values (e.g. `{ xs: 2, sm: 3, md: 4 }`). + * @default DEFAULT_COLUMNS + */ + "columns": GalleryColumns; + /** + * The visual layout of the gallery. When `uniform`, rows take up the height of the tallest item and are spaced evenly across the gallery. Additionally, items will have an aspect ratio of 1/1, forcing them to be square unless a height is explicitly set. When `masonry`, items will be positioned under each other with only the specified gap between them. + * @default 'uniform' + */ + "layout": 'uniform' | 'masonry'; + /** + * The mode determines the platform behaviors of the component. + */ + "mode"?: "ios" | "md"; + /** + * The order in which items are positioned. Only applies when layout is `masonry`. When `sequential`, items are positioned in the order they are placed in the DOM. When `best-fit`, items are positioned under the column with the most available space. + * @default 'sequential' + */ + "order": 'sequential' | 'best-fit'; + /** + * The theme determines the visual appearance of the component. + */ + "theme"?: "ios" | "md" | "ionic"; + } interface IonGrid { /** * If `true`, the grid will have a fixed width based on the screen size. @@ -5009,6 +5036,12 @@ declare global { prototype: HTMLIonFooterElement; new (): HTMLIonFooterElement; }; + interface HTMLIonGalleryElement extends Components.IonGallery, HTMLStencilElement { + } + var HTMLIonGalleryElement: { + prototype: HTMLIonGalleryElement; + new (): HTMLIonGalleryElement; + }; interface HTMLIonGridElement extends Components.IonGrid, HTMLStencilElement { } var HTMLIonGridElement: { @@ -5966,6 +5999,7 @@ declare global { "ion-fab-button": HTMLIonFabButtonElement; "ion-fab-list": HTMLIonFabListElement; "ion-footer": HTMLIonFooterElement; + "ion-gallery": HTMLIonGalleryElement; "ion-grid": HTMLIonGridElement; "ion-header": HTMLIonHeaderElement; "ion-img": HTMLIonImgElement; @@ -7481,6 +7515,31 @@ declare namespace LocalJSX { */ "translucent"?: boolean; } + interface IonGallery { + /** + * The number of columns to display. Can be set as a number or an object of breakpoint values (e.g. `{ xs: 2, sm: 3, md: 4 }`). + * @default DEFAULT_COLUMNS + */ + "columns"?: GalleryColumns; + /** + * The visual layout of the gallery. When `uniform`, rows take up the height of the tallest item and are spaced evenly across the gallery. Additionally, items will have an aspect ratio of 1/1, forcing them to be square unless a height is explicitly set. When `masonry`, items will be positioned under each other with only the specified gap between them. + * @default 'uniform' + */ + "layout"?: 'uniform' | 'masonry'; + /** + * The mode determines the platform behaviors of the component. + */ + "mode"?: "ios" | "md"; + /** + * The order in which items are positioned. Only applies when layout is `masonry`. When `sequential`, items are positioned in the order they are placed in the DOM. When `best-fit`, items are positioned under the column with the most available space. + * @default 'sequential' + */ + "order"?: 'sequential' | 'best-fit'; + /** + * The theme determines the visual appearance of the component. + */ + "theme"?: "ios" | "md" | "ionic"; + } interface IonGrid { /** * If `true`, the grid will have a fixed width based on the screen size. @@ -10900,6 +10959,11 @@ declare namespace LocalJSX { "collapse": 'fade'; "translucent": boolean; } + interface IonGalleryAttributes { + "layout": 'uniform' | 'masonry'; + "order": 'sequential' | 'best-fit'; + "columns": string; + } interface IonGridAttributes { "fixed": boolean; } @@ -11465,6 +11529,7 @@ declare namespace LocalJSX { "ion-fab-button": Omit & { [K in keyof IonFabButton & keyof IonFabButtonAttributes]?: IonFabButton[K] } & { [K in keyof IonFabButton & keyof IonFabButtonAttributes as `attr:${K}`]?: IonFabButtonAttributes[K] } & { [K in keyof IonFabButton & keyof IonFabButtonAttributes as `prop:${K}`]?: IonFabButton[K] }; "ion-fab-list": Omit & { [K in keyof IonFabList & keyof IonFabListAttributes]?: IonFabList[K] } & { [K in keyof IonFabList & keyof IonFabListAttributes as `attr:${K}`]?: IonFabListAttributes[K] } & { [K in keyof IonFabList & keyof IonFabListAttributes as `prop:${K}`]?: IonFabList[K] }; "ion-footer": Omit & { [K in keyof IonFooter & keyof IonFooterAttributes]?: IonFooter[K] } & { [K in keyof IonFooter & keyof IonFooterAttributes as `attr:${K}`]?: IonFooterAttributes[K] } & { [K in keyof IonFooter & keyof IonFooterAttributes as `prop:${K}`]?: IonFooter[K] }; + "ion-gallery": Omit & { [K in keyof IonGallery & keyof IonGalleryAttributes]?: IonGallery[K] } & { [K in keyof IonGallery & keyof IonGalleryAttributes as `attr:${K}`]?: IonGalleryAttributes[K] } & { [K in keyof IonGallery & keyof IonGalleryAttributes as `prop:${K}`]?: IonGallery[K] }; "ion-grid": Omit & { [K in keyof IonGrid & keyof IonGridAttributes]?: IonGrid[K] } & { [K in keyof IonGrid & keyof IonGridAttributes as `attr:${K}`]?: IonGridAttributes[K] } & { [K in keyof IonGrid & keyof IonGridAttributes as `prop:${K}`]?: IonGrid[K] }; "ion-header": Omit & { [K in keyof IonHeader & keyof IonHeaderAttributes]?: IonHeader[K] } & { [K in keyof IonHeader & keyof IonHeaderAttributes as `attr:${K}`]?: IonHeaderAttributes[K] } & { [K in keyof IonHeader & keyof IonHeaderAttributes as `prop:${K}`]?: IonHeader[K] }; "ion-img": Omit & { [K in keyof IonImg & keyof IonImgAttributes]?: IonImg[K] } & { [K in keyof IonImg & keyof IonImgAttributes as `attr:${K}`]?: IonImgAttributes[K] } & { [K in keyof IonImg & keyof IonImgAttributes as `prop:${K}`]?: IonImg[K] }; @@ -11569,6 +11634,7 @@ declare module "@stencil/core" { "ion-fab-button": LocalJSX.IntrinsicElements["ion-fab-button"] & JSXBase.HTMLAttributes; "ion-fab-list": LocalJSX.IntrinsicElements["ion-fab-list"] & JSXBase.HTMLAttributes; "ion-footer": LocalJSX.IntrinsicElements["ion-footer"] & JSXBase.HTMLAttributes; + "ion-gallery": LocalJSX.IntrinsicElements["ion-gallery"] & JSXBase.HTMLAttributes; "ion-grid": LocalJSX.IntrinsicElements["ion-grid"] & JSXBase.HTMLAttributes; "ion-header": LocalJSX.IntrinsicElements["ion-header"] & JSXBase.HTMLAttributes; "ion-img": LocalJSX.IntrinsicElements["ion-img"] & JSXBase.HTMLAttributes; diff --git a/core/src/components/col/col.tsx b/core/src/components/col/col.tsx index ed6fba852a1..e929f4ed756 100644 --- a/core/src/components/col/col.tsx +++ b/core/src/components/col/col.tsx @@ -5,6 +5,7 @@ import { matchBreakpoint } from '@utils/media'; import { getIonTheme } from '../../global/ionic-global'; +// TODO(FW-7285): Replace with global breakpoints // eslint-disable-next-line @typescript-eslint/prefer-optional-chain const BREAKPOINTS = ['', 'xs', 'sm', 'md', 'lg', 'xl']; diff --git a/core/src/components/gallery/gallery-interface.ts b/core/src/components/gallery/gallery-interface.ts new file mode 100644 index 00000000000..ab9bc4721fc --- /dev/null +++ b/core/src/components/gallery/gallery-interface.ts @@ -0,0 +1,10 @@ +export interface GalleryBreakpointColumns { + xs?: string | number; + sm?: string | number; + md?: string | number; + lg?: string | number; + xl?: string | number; + xxl?: string | number; +} + +export type GalleryColumns = GalleryBreakpointColumns | string | number; diff --git a/core/src/components/gallery/gallery.scss b/core/src/components/gallery/gallery.scss new file mode 100644 index 00000000000..5ff3f593961 --- /dev/null +++ b/core/src/components/gallery/gallery.scss @@ -0,0 +1,64 @@ +@import "../../themes/native/native.globals"; + +// Gallery +// -------------------------------------------------- + +:host { + /** + * @prop --ion-gallery-gap: Space between gallery items + */ + display: grid; + grid-template-columns: repeat(var(--internal-gallery-columns, 2), minmax(0, 1fr)); +} + +// Layout: Uniform +// -------------------------------------------------- + +:host(.gallery-layout-uniform) { + gap: var(--ion-gallery-gap, 16px); +} + +// Target all slotted elements in the uniform layout. This ensures that nested +// images also have an aspect ratio of 1/1. +:host(.gallery-layout-uniform) ::slotted(*) { + aspect-ratio: 1/1; +} + +// Layout: Masonry +// -------------------------------------------------- + +:host(.gallery-layout-masonry) { + align-items: start; + + column-gap: var(--ion-gallery-gap, 16px); + row-gap: 0; + + grid-auto-rows: 2px; +} + +:host(.gallery-layout-masonry) ::slotted(*) { + display: block; + + // Clear min-height so items size to their content + min-height: unset; + + margin-bottom: var(--ion-gallery-gap, 16px); +} + +// Slotted elements +// -------------------------------------------------- + +// Reset the default margin for slotted elements so wrapper elements +// (such as
) align properly with other gallery items. +::slotted(*) { + @include margin(0); + + width: 100%; +} + +::slotted(img) { + display: block; + + object-fit: cover; + object-position: center; +} diff --git a/core/src/components/gallery/gallery.spec.ts b/core/src/components/gallery/gallery.spec.ts new file mode 100644 index 00000000000..622d0d773fa --- /dev/null +++ b/core/src/components/gallery/gallery.spec.ts @@ -0,0 +1,415 @@ +import * as logging from '@utils/logging'; + +import { Gallery } from './gallery'; + +let sharedGallery: Gallery; + +// This should match the default columns defined by the gallery component. +// It is hardcoded here instead of grabbing the value from the gallery so +// that changing it there without updating it here will break the tests. +const DEFAULT_COLUMNS_BY_BREAKPOINT = { + xs: 2, + sm: 3, + md: 4, + lg: 6, + xl: 8, + xxl: 10, +}; + +// The expected columns for each breakpoint when the columns property is +// not set or is set to an invalid value. +const DEFAULT_COLUMNS_BREAKPOINTS = [ + // xs + { width: 0, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xs'] }, + { width: 575, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xs'] }, + // sm + { width: 576, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['sm'] }, + { width: 767, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['sm'] }, + // md + { width: 768, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['md'] }, + { width: 991, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['md'] }, + // lg + { width: 992, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['lg'] }, + { width: 1199, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['lg'] }, + // xl + { width: 1200, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xl'] }, + { width: 1399, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xl'] }, + // xxl + { width: 1400, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xxl'] }, +]; + +describe('gallery: properties', () => { + beforeEach(() => { + sharedGallery = new Gallery(); + }); + describe('gallery columns', () => { + it('should resolve to the default columns for each breakpoint', () => { + const breakpoints = DEFAULT_COLUMNS_BREAKPOINTS; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + }); + + it('should fallback to the default columns when the columns property is set to a negative integer', () => { + const breakpoints = DEFAULT_COLUMNS_BREAKPOINTS; + + const warningSpy = jest.spyOn(logging, 'printIonWarning').mockImplementation(() => {}); + + const el = document.createElement('ion-gallery'); + Object.defineProperty(sharedGallery, 'el', { + value: el, + configurable: true, + }); + sharedGallery.columns = -3; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + + expect(warningSpy).toHaveBeenCalledWith( + expect.stringContaining('[ion-gallery] - Invalid "columns" value (-3).'), + el + ); + + warningSpy.mockRestore(); + }); + + it('should fallback to the default columns when the columns property is set to an invalid string', () => { + const breakpoints = DEFAULT_COLUMNS_BREAKPOINTS; + + const warningSpy = jest.spyOn(logging, 'printIonWarning').mockImplementation(() => {}); + + const el = document.createElement('ion-gallery'); + Object.defineProperty(sharedGallery, 'el', { + value: el, + configurable: true, + }); + sharedGallery.columns = 'invalid'; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + + expect(warningSpy).toHaveBeenCalledWith( + expect.stringContaining('[ion-gallery] - Invalid "columns" value ("invalid").'), + el + ); + + warningSpy.mockRestore(); + }); + + it('should fallback to the default columns when the columns property is set to an object with all invalid breakpoint values', () => { + const breakpoints = DEFAULT_COLUMNS_BREAKPOINTS; + + const warningSpy = jest.spyOn(logging, 'printIonWarning').mockImplementation(() => {}); + + const el = document.createElement('ion-gallery'); + Object.defineProperty(sharedGallery, 'el', { + value: el, + configurable: true, + }); + sharedGallery.columns = { xs: 'invalid', sm: -3 }; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + + expect(warningSpy).toHaveBeenCalledWith(expect.stringContaining('[ion-gallery] - Invalid "columns" value'), el); + + warningSpy.mockRestore(); + }); + + it('should properly set columns for the md breakpoint but fallback to the default columns for all others when the columns property is set to an object with one valid breakpoint and the rest invalid', () => { + const breakpoints = [ + // xs + { width: 0, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xs'] }, + { width: 575, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xs'] }, + // sm + { width: 576, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['sm'] }, + { width: 767, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['sm'] }, + // md + { width: 768, expectedColumns: 5 }, + { width: 991, expectedColumns: 5 }, + // lg + { width: 992, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['lg'] }, + { width: 1199, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['lg'] }, + // xl + { width: 1200, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xl'] }, + { width: 1399, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xl'] }, + // xxl + { width: 1400, expectedColumns: DEFAULT_COLUMNS_BY_BREAKPOINT['xxl'] }, + ]; + + const warningSpy = jest.spyOn(logging, 'printIonWarning').mockImplementation(() => {}); + + const el = document.createElement('ion-gallery'); + Object.defineProperty(sharedGallery, 'el', { + value: el, + configurable: true, + }); + sharedGallery.columns = { xs: 'invalid', sm: -3, md: 5, lg: 'foo', xl: 'bar', xxl: 'baz' }; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + + expect(warningSpy).toHaveBeenCalledWith(expect.stringContaining('[ion-gallery] - Invalid "columns" value'), el); + + warningSpy.mockRestore(); + }); + + it('should fallback to the default columns when the columns property is set to an unaccepted type', () => { + const breakpoints = DEFAULT_COLUMNS_BREAKPOINTS; + + const warningSpy = jest.spyOn(logging, 'printIonWarning').mockImplementation(() => {}); + + const el = document.createElement('ion-gallery'); + Object.defineProperty(sharedGallery, 'el', { + value: el, + configurable: true, + }); + sharedGallery.columns = [{ xs: 2 }] as any; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + + expect(warningSpy).toHaveBeenCalledWith( + expect.stringContaining('[ion-gallery] - Invalid "columns" value ([{"xs":2}]).'), + el + ); + + warningSpy.mockRestore(); + }); + + it('should resolve to 2 columns when the columns property is set to a number', () => { + const breakpoints = [ + // xs + { width: 0, expectedColumns: 2 }, + { width: 575, expectedColumns: 2 }, + // sm + { width: 576, expectedColumns: 2 }, + { width: 767, expectedColumns: 2 }, + // md + { width: 768, expectedColumns: 2 }, + { width: 991, expectedColumns: 2 }, + // lg + { width: 992, expectedColumns: 2 }, + { width: 1199, expectedColumns: 2 }, + // xl + { width: 1200, expectedColumns: 2 }, + { width: 1399, expectedColumns: 2 }, + // xxl + { width: 1400, expectedColumns: 2 }, + ]; + + sharedGallery.columns = 2; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + }); + + it('should resolve to 3 columns when the columns property is set to a string', () => { + const breakpoints = [ + // xs + { width: 0, expectedColumns: 3 }, + { width: 575, expectedColumns: 3 }, + // sm + { width: 576, expectedColumns: 3 }, + { width: 767, expectedColumns: 3 }, + // md + { width: 768, expectedColumns: 3 }, + { width: 991, expectedColumns: 3 }, + // lg + { width: 992, expectedColumns: 3 }, + { width: 1199, expectedColumns: 3 }, + // xl + { width: 1200, expectedColumns: 3 }, + { width: 1399, expectedColumns: 3 }, + // xxl + { width: 1400, expectedColumns: 3 }, + ]; + + sharedGallery.columns = '3'; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + }); + + it('should resolve to the proper columns when the columns property is set to an object', () => { + const breakpoints = [ + // xs + { width: 0, expectedColumns: 3 }, + { width: 575, expectedColumns: 3 }, + // sm + { width: 576, expectedColumns: 4 }, + { width: 767, expectedColumns: 4 }, + // md + { width: 768, expectedColumns: 5 }, + { width: 991, expectedColumns: 5 }, + // lg + { width: 992, expectedColumns: 7 }, + { width: 1199, expectedColumns: 7 }, + // xl + { width: 1200, expectedColumns: 9 }, + { width: 1399, expectedColumns: 9 }, + // xxl + { width: 1400, expectedColumns: 12 }, + ]; + + sharedGallery.columns = { xs: 3, sm: 4, md: 5, lg: 7, xl: 9, xxl: 12 }; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + }); + + it('should resolve to the proper columns when the columns property is set to an out of order object', () => { + const breakpoints = [ + // xs + { width: 0, expectedColumns: 3 }, + { width: 575, expectedColumns: 3 }, + // sm + { width: 576, expectedColumns: 4 }, + { width: 767, expectedColumns: 4 }, + // md + { width: 768, expectedColumns: 5 }, + { width: 991, expectedColumns: 5 }, + // lg + { width: 992, expectedColumns: 7 }, + { width: 1199, expectedColumns: 7 }, + // xl + { width: 1200, expectedColumns: 9 }, + { width: 1399, expectedColumns: 9 }, + // xxl + { width: 1400, expectedColumns: 12 }, + ]; + + sharedGallery.columns = { xxl: 12, xl: 9, lg: 7, md: 5, sm: 4, xs: 3 }; + + breakpoints.forEach(({ width, expectedColumns }) => { + expect((sharedGallery as any).getColumnsForWidth(width)).toBe(expectedColumns); + }); + }); + + describe('gallery layout', () => { + it('should update responsive columns and schedule masonry resize when layout changes', () => { + const updateResponsiveColumnsSpy = jest.spyOn(sharedGallery as any, 'updateResponsiveColumns'); + const scheduleMasonryResizeSpy = jest.spyOn(sharedGallery as any, 'scheduleMasonryResize'); + + (sharedGallery as any).layoutChanged(); + + expect(updateResponsiveColumnsSpy).toHaveBeenCalledWith(true); + expect(scheduleMasonryResizeSpy).toHaveBeenCalled(); + }); + + it('should clear masonry styles and not queue animation frame when layout is not masonry', () => { + sharedGallery.layout = 'uniform'; + + const clearMasonryStylesSpy = jest.spyOn(sharedGallery as any, 'clearMasonryStyles'); + const requestAnimationFrameSpy = jest.spyOn(globalThis, 'requestAnimationFrame'); + + (sharedGallery as any).scheduleMasonryResize(); + + expect(clearMasonryStylesSpy).toHaveBeenCalled(); + expect(requestAnimationFrameSpy).not.toHaveBeenCalled(); + }); + + it('should cancel pending animation frame and queue a new one when layout is masonry', () => { + sharedGallery.layout = 'masonry'; + (sharedGallery as any).masonryRaf = 77; + + const cancelAnimationFrameSpy = jest + .spyOn(globalThis, 'cancelAnimationFrame') + .mockImplementation(() => undefined); + const requestAnimationFrameSpy = jest.spyOn(globalThis, 'requestAnimationFrame').mockImplementation(() => 123); + + (sharedGallery as any).scheduleMasonryResize(); + + expect(cancelAnimationFrameSpy).toHaveBeenCalledWith(77); + expect(requestAnimationFrameSpy).toHaveBeenCalled(); + expect((sharedGallery as any).masonryRaf).toBe(123); + + cancelAnimationFrameSpy.mockRestore(); + requestAnimationFrameSpy.mockRestore(); + }); + + it('should not schedule masonry resize on child load when layout is not masonry', () => { + sharedGallery.layout = 'uniform'; + + const scheduleMasonryResizeSpy = jest.spyOn(sharedGallery as any, 'scheduleMasonryResize'); + + (sharedGallery as any).onChildLoad(new Event('load')); + + expect(scheduleMasonryResizeSpy).not.toHaveBeenCalled(); + }); + + it('should schedule masonry resize on child load when target is inside gallery and layout is masonry', () => { + sharedGallery.layout = 'masonry'; + const galleryEl = document.createElement('ion-gallery'); + const child = document.createElement('div'); + galleryEl.appendChild(child); + Object.defineProperty(sharedGallery, 'el', { + value: galleryEl, + configurable: true, + }); + + const scheduleMasonryResizeSpy = jest.spyOn(sharedGallery as any, 'scheduleMasonryResize'); + const event = new Event('load'); + Object.defineProperty(event, 'target', { value: child }); + + (sharedGallery as any).onChildLoad(event); + + expect(scheduleMasonryResizeSpy).toHaveBeenCalled(); + }); + + it('should not schedule masonry resize on child load when target is outside gallery', () => { + sharedGallery.layout = 'masonry'; + const galleryEl = document.createElement('ion-gallery'); + const outsideChild = document.createElement('div'); + Object.defineProperty(sharedGallery, 'el', { + value: galleryEl, + configurable: true, + }); + + const scheduleMasonryResizeSpy = jest.spyOn(sharedGallery as any, 'scheduleMasonryResize'); + const event = new Event('load'); + Object.defineProperty(event, 'target', { value: outsideChild }); + + (sharedGallery as any).onChildLoad(event); + + expect(scheduleMasonryResizeSpy).not.toHaveBeenCalled(); + }); + }); + + describe('gallery order', () => { + it('should place items sequentially when order is set to sequential', () => { + sharedGallery.order = 'sequential'; + + expect((sharedGallery as any).getColumnIndex(0, [0, 0, 0], 3)).toBe(0); + expect((sharedGallery as any).getColumnIndex(1, [1, 0, 0], 3)).toBe(1); + expect((sharedGallery as any).getColumnIndex(2, [1, 1, 0], 3)).toBe(2); + expect((sharedGallery as any).getColumnIndex(3, [1, 1, 1], 3)).toBe(0); + expect((sharedGallery as any).getColumnIndex(4, [2, 1, 1], 3)).toBe(1); + }); + + it('should place items in the shortest column when order is set to best-fit', () => { + sharedGallery.order = 'best-fit'; + + expect((sharedGallery as any).getColumnIndex(5, [4, 2, 6], 3)).toBe(1); + expect((sharedGallery as any).getColumnIndex(2, [3, 5, 1], 3)).toBe(2); + expect((sharedGallery as any).getColumnIndex(9, [2, 3, 4], 3)).toBe(0); + }); + + it('should prefer the first shortest column when best-fit columns are tied', () => { + sharedGallery.order = 'best-fit'; + + expect((sharedGallery as any).getColumnIndex(7, [2, 2, 5], 3)).toBe(0); + expect((sharedGallery as any).getColumnIndex(1, [3, 1, 1], 3)).toBe(1); + }); + }); + }); +}); diff --git a/core/src/components/gallery/gallery.tsx b/core/src/components/gallery/gallery.tsx new file mode 100644 index 00000000000..0e9a8877397 --- /dev/null +++ b/core/src/components/gallery/gallery.tsx @@ -0,0 +1,422 @@ +import type { ComponentInterface } from '@stencil/core'; +import { Component, Element, Host, Listen, Prop, Watch, h } from '@stencil/core'; +import { printIonWarning } from '@utils/logging'; + +import { getIonTheme } from '../../global/ionic-global'; + +import type { GalleryBreakpointColumns, GalleryColumns } from './gallery-interface'; + +// TODO(FW-7285): Replace with global breakpoints +const BREAKPOINTS = { + xs: 0, + sm: 576, + md: 768, + lg: 992, + xl: 1200, + xxl: 1400, +}; + +const DEFAULT_COLUMNS = { + xs: 2, + sm: 3, + md: 4, + lg: 6, + xl: 8, + xxl: 10, +}; + +type GalleryBreakpoint = keyof typeof BREAKPOINTS; +const BREAKPOINT_ORDER: GalleryBreakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl']; + +/** + * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component. + * @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component. + * + * @slot - Content is placed in a responsive gallery layout. + */ +@Component({ + tag: 'ion-gallery', + styleUrl: 'gallery.scss', + shadow: true, +}) +export class Gallery implements ComponentInterface { + @Element() el!: HTMLIonGalleryElement; + + private masonryRaf?: number; + private resizeObserver?: ResizeObserver; + private lastWidth?: number; + + // Keep track of whether we've warned about invalid columns to avoid + // duplicate warnings on screen resize. + private hasWarnedInvalidColumns = false; + + /** + * The visual layout of the gallery. When `uniform`, rows take up the height + * of the tallest item and are spaced evenly across the gallery. Additionally, + * items will have an aspect ratio of 1/1, forcing them to be square unless a + * height is explicitly set. When `masonry`, items will be positioned under each + * other with only the specified gap between them. + */ + @Prop({ reflect: true }) layout: 'uniform' | 'masonry' = 'uniform'; + + /** + * The order in which items are positioned. Only applies when layout is + * `masonry`. When `sequential`, items are positioned in the order they are + * placed in the DOM. When `best-fit`, items are positioned under the column + * with the most available space. + */ + @Prop({ reflect: true }) order: 'sequential' | 'best-fit' = 'sequential'; + + /** + * The number of columns to display. Can be set as a number or an object of + * breakpoint values (e.g. `{ xs: 2, sm: 3, md: 4 }`). + */ + @Prop() columns: GalleryColumns = DEFAULT_COLUMNS; + + @Watch('layout') + @Watch('order') + @Watch('columns') + protected layoutChanged() { + this.updateResponsiveColumns(true); + this.scheduleMasonryResize(); + } + + componentDidLoad() { + this.updateResponsiveColumns(true); + this.resizeObserver = new ResizeObserver(() => { + this.updateResponsiveColumns(); + this.scheduleMasonryResize(); + }); + this.resizeObserver.observe(this.el); + + this.scheduleMasonryResize(); + } + + disconnectedCallback() { + if (this.masonryRaf !== undefined) { + cancelAnimationFrame(this.masonryRaf); + this.masonryRaf = undefined; + } + + this.resizeObserver?.disconnect(); + this.resizeObserver = undefined; + } + + /** + * Listen for the load event on child elements. + * When the layout is `masonry`, this listener is used to schedule a resize + * of the masonry grid when the child elements load. This is useful for when + * images take time to load. + */ + @Listen('load', { capture: true }) + protected onChildLoad(ev: Event) { + if (this.layout !== 'masonry') { + return; + } + + const target = ev.target as HTMLElement | null; + if (target !== null && this.el.contains(target)) { + this.scheduleMasonryResize(); + } + } + + /** + * Listen for the slotchange event on the slot. + * When the layout is `masonry`, this listener is used to schedule a resize + * of the masonry grid when the slot changes. This is useful for when items + * are added or removed from the gallery. + */ + private onSlotChange = () => { + this.scheduleMasonryResize(); + }; + + /** + * Batch masonry measurements to a single animation frame. + * This avoids repeated sync layouts during rapid resize/load/slot changes. + */ + private scheduleMasonryResize() { + if (this.layout !== 'masonry') { + this.clearMasonryStyles(); + return; + } + + if (this.masonryRaf !== undefined) { + cancelAnimationFrame(this.masonryRaf); + } + + this.masonryRaf = requestAnimationFrame(this.resizeMasonryGrid); + } + + /** + * Normalize a columns value to a positive integer. + * Returns undefined when the input cannot be interpreted as a finite number. + */ + private sanitizeColumns(columns: number | string | undefined) { + if (columns === undefined) { + return undefined; + } + + const numericColumns = typeof columns === 'number' ? columns : Number(columns); + + if (!Number.isFinite(numericColumns) || !Number.isInteger(numericColumns) || numericColumns <= 0) { + return undefined; + } + + return numericColumns; + } + + /** + * Check if the value is a breakpoint columns object. + */ + private isBreakpointColumns(value: GalleryColumns): value is GalleryBreakpointColumns { + return typeof value === 'object' && value !== null && !Array.isArray(value); + } + + /** + * Check if the breakpoint columns object has any invalid values. + * Returns true if any value is undefined or not a positive integer. + */ + private hasInvalidBreakpointColumns(breakpointColumns: GalleryBreakpointColumns) { + for (const breakpoint of BREAKPOINT_ORDER) { + const value = breakpointColumns[breakpoint]; + if (value !== undefined && this.sanitizeColumns(value) === undefined) { + return true; + } + } + + return false; + } + + /** + * Get the columns from a responsive breakpoint map. + * Returns the columns for the last matching breakpoint. + */ + private getColumnsFromBreakpointColumns(width: number, breakpointColumns: GalleryBreakpointColumns) { + let columns: number | undefined; + for (const bp of BREAKPOINT_ORDER) { + const customValue = breakpointColumns[bp]; + const parsedCustom = this.sanitizeColumns(customValue); + const parsedDefault = this.sanitizeColumns(DEFAULT_COLUMNS[bp]); + + // Use valid custom values when present; otherwise fall back to defaults per breakpoint. + const resolved = customValue === undefined || parsedCustom === undefined ? parsedDefault : parsedCustom; + + if (resolved !== undefined && width >= BREAKPOINTS[bp]) { + columns = resolved; + } + } + return columns; + } + + /** + * Warn about an invalid columns value when it is set to a non-positive + * integer or a breakpoint map object with invalid values. + */ + private warnInvalidColumns(columns: GalleryColumns) { + printIonWarning( + `[ion-gallery] - Invalid "columns" value (${JSON.stringify( + columns + )}). Expected a positive integer or breakpoint map object (e.g. { xs: 2, md: 4 }). Falling back to default responsive columns.`, + this.el + ); + this.hasWarnedInvalidColumns = true; + } + + /** + * Resolve the active columns value for the current width. Falls back to + * the default responsive columns when the provided prop is invalid. + */ + private getColumnsForWidth(width: number) { + const { columns, hasWarnedInvalidColumns } = this; + const isBreakpointColumns = this.isBreakpointColumns(columns); + const hasInvalidBreakpointColumns = isBreakpointColumns && this.hasInvalidBreakpointColumns(columns); + + const sanitizedColumns = isBreakpointColumns + ? this.getColumnsFromBreakpointColumns(width, columns) + : this.sanitizeColumns(columns); + + if ( + !hasWarnedInvalidColumns && + (hasInvalidBreakpointColumns || (!isBreakpointColumns && sanitizedColumns === undefined)) + ) { + this.warnInvalidColumns(columns); + } + + if (sanitizedColumns !== undefined) { + return sanitizedColumns; + } + + return this.getColumnsFromBreakpointColumns(width, DEFAULT_COLUMNS); + } + + /** + * Update the responsive columns for the gallery. + * This is used to update the columns when the component width changes. + */ + private updateResponsiveColumns(force = false) { + const width = this.el.getBoundingClientRect().width; + + // Only update the columns if the component width has changed by more than + // 1px or if the force flag is true. This prevents unnecessary re-renders + // when the component width has not changed. + if (!width || (!force && this.lastWidth !== undefined && Math.abs(this.lastWidth - width) < 1)) { + return; + } + + this.lastWidth = width; + + const columns = this.getColumnsForWidth(width); + this.el.style.setProperty('--internal-gallery-columns', `${columns}`); + } + + /** + * Return all directly slotted HTMLElement children of the gallery. + */ + private getItems() { + return Array.from(this.el.children).filter((child): child is HTMLElement => child instanceof HTMLElement); + } + + /** + * Clear the item styles for the given item element. + * This is used to switch between uniform and masonry layouts. + */ + private clearItemStyles(itemEl: HTMLElement) { + itemEl.style.gridRowStart = ''; + itemEl.style.gridRowEnd = ''; + itemEl.style.gridColumn = ''; + itemEl.style.marginBottom = ''; + } + + /** + * Clear placement styles for all items when leaving masonry mode. + */ + private clearMasonryStyles() { + this.getItems().forEach((itemEl) => this.clearItemStyles(itemEl)); + } + + /** + * Convert a rendered item height to the number of grid rows it should span. + * Returns undefined for images that are not fully loaded yet. + */ + private calculateRowSpan(itemEl: HTMLElement, rowHeight: number, rowGap: number) { + if (itemEl instanceof HTMLImageElement && (!itemEl.complete || itemEl.naturalHeight === 0)) { + return undefined; + } + + const height = itemEl.getBoundingClientRect().height; + const itemStyles = getComputedStyle(itemEl); + const marginBottom = parseFloat(itemStyles.getPropertyValue('margin-bottom')) || 0; + const denominator = rowHeight + rowGap; + + if (!denominator || !Number.isFinite(denominator)) { + return 1; + } + + return Math.ceil((height + marginBottom + rowGap) / denominator) || 1; + } + + /** + * Get the index of the column to position the item in. + * When the order is `best-fit`, the column with the shortest height is + * returned. Otherwise, items are placed in the column that matches their + * natural DOM order. + */ + private getColumnIndex(index: number, columnHeights: number[], columns: number) { + const { order } = this; + + if (order === 'best-fit') { + let columnIndex = 0; + for (let i = 1; i < columns; i++) { + if (columnHeights[i] < columnHeights[columnIndex]) { + columnIndex = i; + } + } + return columnIndex; + } + + return index % columns; + } + + /** + * Apply masonry placement by assigning each item a column and row span. + */ + private layoutMasonry(items: HTMLElement[], rowHeight: number, rowGap: number, columns: number) { + const columnHeights = new Array(columns).fill(0); + const lastItemsByColumn = new Array(columns).fill(undefined); + + items.forEach((itemEl, i) => { + itemEl.style.marginBottom = ''; + const span = this.calculateRowSpan(itemEl, rowHeight, rowGap); + if (span === undefined) { + this.clearItemStyles(itemEl); + return; + } + + const columnIndex = this.getColumnIndex(i, columnHeights, columns); + const start = columnHeights[columnIndex] + 1; + + itemEl.style.gridColumn = `${columnIndex + 1}`; + itemEl.style.gridRowStart = `${start}`; + itemEl.style.gridRowEnd = `span ${span}`; + columnHeights[columnIndex] = start + span - 1; + lastItemsByColumn[columnIndex] = itemEl; + }); + + // Remove trailing space from the final item in each column while preserving + // spacing between all non-final items. + lastItemsByColumn.forEach((itemEl) => { + if (itemEl === undefined) { + return; + } + + itemEl.style.marginBottom = '0px'; + const spanWithoutTrailingGap = this.calculateRowSpan(itemEl, rowHeight, rowGap); + if (spanWithoutTrailingGap === undefined) { + this.clearItemStyles(itemEl); + return; + } + + itemEl.style.gridRowEnd = `span ${spanWithoutTrailingGap}`; + }); + } + + /** + * Measure the host and children, then compute masonry placement. + */ + private resizeMasonryGrid = () => { + this.masonryRaf = undefined; + + if (this.layout !== 'masonry') { + this.clearMasonryStyles(); + return; + } + + const styles = getComputedStyle(this.el); + const rowHeight = parseFloat(styles.getPropertyValue('grid-auto-rows')) || 0; + const rowGap = parseFloat(styles.getPropertyValue('row-gap')) || parseFloat(styles.getPropertyValue('gap')) || 0; + const columnsStr = styles.getPropertyValue('--internal-gallery-columns'); + // Fallback to 2 columns for masonry calculations when the resolved + // --internal-gallery-columns CSS value is missing or unparsable. + const columns = parseInt(columnsStr, 10) || 2; + const items = this.getItems(); + + this.layoutMasonry(items, rowHeight, rowGap, columns); + }; + + render() { + const { layout, order } = this; + const theme = getIonTheme(this); + + return ( + + + + ); + } +} diff --git a/core/src/components/gallery/test/assets/01.png b/core/src/components/gallery/test/assets/01.png new file mode 100644 index 00000000000..f3e49e2a22c Binary files /dev/null and b/core/src/components/gallery/test/assets/01.png differ diff --git a/core/src/components/gallery/test/assets/02.png b/core/src/components/gallery/test/assets/02.png new file mode 100644 index 00000000000..1c82befeb86 Binary files /dev/null and b/core/src/components/gallery/test/assets/02.png differ diff --git a/core/src/components/gallery/test/assets/03.png b/core/src/components/gallery/test/assets/03.png new file mode 100644 index 00000000000..3f454de0a06 Binary files /dev/null and b/core/src/components/gallery/test/assets/03.png differ diff --git a/core/src/components/gallery/test/assets/04.png b/core/src/components/gallery/test/assets/04.png new file mode 100644 index 00000000000..d026972f7e1 Binary files /dev/null and b/core/src/components/gallery/test/assets/04.png differ diff --git a/core/src/components/gallery/test/assets/05.png b/core/src/components/gallery/test/assets/05.png new file mode 100644 index 00000000000..461e1ef0e94 Binary files /dev/null and b/core/src/components/gallery/test/assets/05.png differ diff --git a/core/src/components/gallery/test/assets/06.png b/core/src/components/gallery/test/assets/06.png new file mode 100644 index 00000000000..dfb2ac54caf Binary files /dev/null and b/core/src/components/gallery/test/assets/06.png differ diff --git a/core/src/components/gallery/test/assets/07.png b/core/src/components/gallery/test/assets/07.png new file mode 100644 index 00000000000..a2e9d55ac6e Binary files /dev/null and b/core/src/components/gallery/test/assets/07.png differ diff --git a/core/src/components/gallery/test/assets/08.png b/core/src/components/gallery/test/assets/08.png new file mode 100644 index 00000000000..ce617520d89 Binary files /dev/null and b/core/src/components/gallery/test/assets/08.png differ diff --git a/core/src/components/gallery/test/assets/09.png b/core/src/components/gallery/test/assets/09.png new file mode 100644 index 00000000000..5fa6ee4e75f Binary files /dev/null and b/core/src/components/gallery/test/assets/09.png differ diff --git a/core/src/components/gallery/test/assets/10.png b/core/src/components/gallery/test/assets/10.png new file mode 100644 index 00000000000..fa28d4c1005 Binary files /dev/null and b/core/src/components/gallery/test/assets/10.png differ diff --git a/core/src/components/gallery/test/assets/11.png b/core/src/components/gallery/test/assets/11.png new file mode 100644 index 00000000000..8d2baefd257 Binary files /dev/null and b/core/src/components/gallery/test/assets/11.png differ diff --git a/core/src/components/gallery/test/assets/12.png b/core/src/components/gallery/test/assets/12.png new file mode 100644 index 00000000000..c20056d1579 Binary files /dev/null and b/core/src/components/gallery/test/assets/12.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts b/core/src/components/gallery/test/basic/gallery.e2e.ts new file mode 100644 index 00000000000..ebde0ef8b09 --- /dev/null +++ b/core/src/components/gallery/test/basic/gallery.e2e.ts @@ -0,0 +1,118 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +const DEFAULT_COLUMNS_BREAKPOINTS = [ + { name: 'xs', width: 384, expectedColumns: 2 }, + { name: 'sm', width: 576, expectedColumns: 3 }, + { name: 'md', width: 768, expectedColumns: 4 }, + { name: 'lg', width: 992, expectedColumns: 6 }, + { name: 'xl', width: 1200, expectedColumns: 8 }, + { name: 'xxl', width: 1400, expectedColumns: 10 }, +]; + +/** + * This behavior does not vary across modes/directions. + */ +configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('gallery: basic'), () => { + DEFAULT_COLUMNS_BREAKPOINTS.forEach((breakpoint) => { + test(`should display ${breakpoint.expectedColumns} columns of images on ${breakpoint.name} screens`, async ({ + page, + }) => { + await page.setViewportSize({ width: breakpoint.width, height: 900 }); + + await page.setContent( + ` + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect(gallery).toHaveScreenshot(screenshot(`gallery-basic-${breakpoint.name}-breakpoint`)); + }); + + test(`should always display 2 columns on ${breakpoint.name} screens when the columns prop is set to 2`, async ({ + page, + }) => { + await page.setViewportSize({ width: breakpoint.width, height: 900 }); + + await page.setContent( + ` + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect + .poll(() => + gallery.evaluate((el) => getComputedStyle(el).getPropertyValue('--internal-gallery-columns').trim()) + ) + .toBe('2'); + }); + + test(`should display ${breakpoint.expectedColumns} columns on ${breakpoint.name} screens when the internal CSS variable is set to 2`, async ({ + page, + }) => { + await page.setViewportSize({ width: breakpoint.width, height: 900 }); + + await page.setContent( + ` + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect + .poll(() => + gallery.evaluate((el) => getComputedStyle(el).getPropertyValue('--internal-gallery-columns').trim()) + ) + .toBe(`${breakpoint.expectedColumns}`); + }); + }); + }); +}); diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..329715aabd8 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b110b42202a Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..f72a9c845c6 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-lg-breakpoint-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..6f24852f45c Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..2a3cdd54f1b Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..a3e11fa0c9d Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-md-breakpoint-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..70614bcba17 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..8c5de273759 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..932ba28f670 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-sm-breakpoint-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..bac84fb52f4 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5387656d885 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..3b009338727 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xl-breakpoint-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..8c98d12b737 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..f0bdb740578 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..e1cd11b8e7c Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xs-breakpoint-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..fbb119957b9 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..196042a2aeb Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..4213682aaf0 Binary files /dev/null and b/core/src/components/gallery/test/basic/gallery.e2e.ts-snapshots/gallery-basic-xxl-breakpoint-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/basic/index.html b/core/src/components/gallery/test/basic/index.html new file mode 100644 index 00000000000..65b186fb795 --- /dev/null +++ b/core/src/components/gallery/test/basic/index.html @@ -0,0 +1,125 @@ + + + + + Gallery - Basic + + + + + + + + + + + + + Gallery - Basic + + + + + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + + +
One
+
Two
+
Three
+
Four
+
Five
+
Six
+
Seven
+
Eight
+
Nine
+
Ten
+
Eleven
+
Twelve
+
+
+
+ + + + diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts b/core/src/components/gallery/test/layout/gallery.e2e.ts new file mode 100644 index 00000000000..307c8c6bfa2 --- /dev/null +++ b/core/src/components/gallery/test/layout/gallery.e2e.ts @@ -0,0 +1,307 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +import { numberToWords, sharedStyles } from '../utils'; + +const LAYOUT_OPTIONS = ['uniform', 'masonry']; +const ORDER_OPTIONS = ['sequential', 'best-fit']; + +/** + * This behavior does not vary across modes/directions. + */ +configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ config, screenshot, title }) => { + LAYOUT_OPTIONS.forEach((layout) => { + const orders = layout === 'masonry' ? ORDER_OPTIONS : ['']; + + orders.forEach((order) => { + const orderAttribute = layout === 'masonry' ? ` order="${order}"` : ''; + const orderSuffix = layout === 'masonry' ? `-${order}` : ''; + + test.describe(title(`gallery: ${layout} layout${layout === 'masonry' ? ` (${order})` : ''}`), () => { + test(`should properly display same height divs with ${layout} layout${ + layout === 'masonry' ? ` and ${order} order` : '' + }`, async ({ page }) => { + await page.setContent( + ` + + + +
One
+
Two
+
Three
+
Four
+
Five
+
Six
+
Seven
+
Eight
+
Nine
+
Ten
+
Eleven
+
Twelve
+
+ `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect(gallery).toHaveScreenshot(screenshot(`gallery-${layout}${orderSuffix}-divs-same-height`)); + }); + + test(`should properly display variable height divs with ${layout} layout${ + layout === 'masonry' ? ` and ${order} order` : '' + }`, async ({ page }) => { + await page.setContent( + ` + + + +
One
+
Two
+
Three
+
Four
+
Five
+
Six
+
Seven
+
Eight
+
Nine
+
Ten
+
Eleven
+
Twelve
+
+ `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect(gallery).toHaveScreenshot(screenshot(`gallery-${layout}${orderSuffix}-divs-variable-height`)); + }); + + test(`should properly display same height images with ${layout} layout${ + layout === 'masonry' ? ` and ${order} order` : '' + }`, async ({ page }) => { + await page.setContent( + ` + + + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect(gallery).toHaveScreenshot(screenshot(`gallery-${layout}${orderSuffix}-images-same-height`)); + }); + + test(`should properly display variable height images with ${layout} layout${ + layout === 'masonry' ? ` and ${order} order` : '' + }`, async ({ page }) => { + await page.setContent( + ` + + + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + `, + config + ); + + const gallery = page.locator('ion-gallery'); + + await expect(gallery).toHaveScreenshot(screenshot(`gallery-${layout}${orderSuffix}-images-variable-height`)); + }); + + if (layout === 'masonry') { + test(`should properly display dynamically appended divs with ${order} order`, async ({ page }) => { + await page.setContent( + ` + + + +
One
+
Two
+
Three
+
Four
+
Five
+
Six
+
+ `, + config + ); + + const gallery = page.locator('ion-gallery'); + + const divHeights = [130, 80, 110, 90, 100, 150]; + const appendedItems = divHeights.map((height, i) => ({ + itemLabel: numberToWords(7 + i), + itemHeight: height, + })); + + await gallery.evaluate((galleryEl, items) => { + items.forEach(({ itemLabel, itemHeight }) => { + const divEl = document.createElement('div'); + divEl.style.height = `${itemHeight}px`; + divEl.textContent = itemLabel; + galleryEl.append(divEl); + }); + }, appendedItems); + + await expect(gallery).toHaveScreenshot( + screenshot(`gallery-${layout}${orderSuffix}-divs-dynamically-appended`) + ); + }); + + test(`should properly display dynamically appended images with ${order} order`, async ({ page }) => { + await page.setContent( + ` + + + + One + Two + Three + Four + Five + Six + + `, + config + ); + + const gallery = page.locator('ion-gallery'); + const appendedItems = [7, 8, 9, 10, 11, 12].map((n) => ({ + itemLabel: numberToWords(n), + itemSrc: `/src/components/gallery/test/assets/${n.toString().padStart(2, '0')}.png`, + })); + + await gallery.evaluate((galleryEl, items) => { + items.forEach(({ itemLabel, itemSrc }) => { + const imageEl = document.createElement('img'); + imageEl.src = itemSrc; + imageEl.alt = itemLabel; + + galleryEl.append(imageEl); + }); + }, appendedItems); + + await expect(gallery).toHaveScreenshot( + screenshot(`gallery-${layout}${orderSuffix}-images-dynamically-appended`) + ); + }); + + test(`should properly display dynamically appended figure-wrapped images with ${order} order`, async ({ + page, + }) => { + await page.setContent( + ` + + + +
+ One +
+
+ Two +
+
+ Three +
+
+ Four +
+
+ Five +
+
+ Six +
+
+ `, + config + ); + + const gallery = page.locator('ion-gallery'); + const appendedItems = [7, 8, 9, 10, 11, 12].map((n) => ({ + itemLabel: numberToWords(n), + itemSrc: `/src/components/gallery/test/assets/${n.toString().padStart(2, '0')}.png`, + })); + + await gallery.evaluate((galleryEl, items) => { + items.forEach(({ itemLabel, itemSrc }) => { + const figureEl = document.createElement('figure'); + figureEl.className = 'gallery-image-item'; + + const imageEl = document.createElement('img'); + imageEl.src = itemSrc; + imageEl.alt = itemLabel; + + figureEl.append(imageEl); + galleryEl.append(figureEl); + }); + }, appendedItems); + + await expect(gallery).toHaveScreenshot( + screenshot(`gallery-${layout}${orderSuffix}-figures-images-dynamically-appended`) + ); + }); + } + }); + }); + }); +}); diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..dcb8b8e60e5 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1a712ec2f53 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..af09cc87fd5 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-dynamically-appended-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2d4225ddf31 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..def74f24724 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d76a74c0619 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-same-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..dcb8b8e60e5 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1a712ec2f53 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..af09cc87fd5 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-divs-variable-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..54df9f10b48 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..67500e8f46a Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..fefe1f1ab35 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-figures-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..54df9f10b48 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..67500e8f46a Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..fefe1f1ab35 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..0cdaef065a7 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..99207db9ccc Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..5a922e59039 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-same-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..54df9f10b48 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..67500e8f46a Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..fefe1f1ab35 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-best-fit-images-variable-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..49911676b12 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..22b94971361 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..6c12b0d14a2 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-dynamically-appended-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2d4225ddf31 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..def74f24724 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d76a74c0619 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-same-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..49911676b12 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..22b94971361 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..6c12b0d14a2 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-divs-variable-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..daa324895eb Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6a859c62d68 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..928e4947e85 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-figures-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..daa324895eb Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6a859c62d68 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..928e4947e85 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-dynamically-appended-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..0cdaef065a7 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..99207db9ccc Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..5a922e59039 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-same-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..daa324895eb Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6a859c62d68 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..928e4947e85 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-masonry-sequential-images-variable-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2d4225ddf31 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..def74f24724 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d76a74c0619 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-same-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ebe504bc458 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5edda6c5847 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ab18381dbdf Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-divs-variable-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..0cdaef065a7 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..99207db9ccc Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..5a922e59039 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-same-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Chrome-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..dacf22b87a3 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Firefox-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b7e7c12db50 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Safari-linux.png b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..efe5abdc9d8 Binary files /dev/null and b/core/src/components/gallery/test/layout/gallery.e2e.ts-snapshots/gallery-uniform-images-variable-height-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/gallery/test/layout/index.html b/core/src/components/gallery/test/layout/index.html new file mode 100644 index 00000000000..10a0924c425 --- /dev/null +++ b/core/src/components/gallery/test/layout/index.html @@ -0,0 +1,373 @@ + + + + + Gallery - Layout + + + + + + + + + + + + + Gallery - Layout + + Add Item + + + + + Uniform + Masonry - Best Fit + Masonry - Sequential + + + + + +
+

Uniform

+
+
+

Divs

+ +
One
+
Two
+
Three
+
Four
+
Five
+
Six
+
Seven
+
Eight
+
Nine
+
Ten
+
Eleven
+
Twelve
+
+
+
+

Images

+ + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + +
+
+

Same Height Images

+ + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + +
+
+
+
+
+ + + + + + diff --git a/core/src/components/gallery/test/utils.ts b/core/src/components/gallery/test/utils.ts new file mode 100644 index 00000000000..3ffc0b076b5 --- /dev/null +++ b/core/src/components/gallery/test/utils.ts @@ -0,0 +1,100 @@ +export const sharedStyles = ` + ion-gallery { + width: 343px; + } + + div { + color: #fff; + height: 150px; + } + + div:nth-child(1) { + background: #ff6b6b; + } + + div:nth-child(2) { + background: #4ecdc4; + } + + div:nth-child(3) { + background: #ffe66d; + color: #333; + } + + div:nth-child(4) { + background: #5f27cd; + } + + div:nth-child(5) { + background: #7f8c8d; + } + + div:nth-child(6) { + background: #ff9f43; + } + + div:nth-child(7) { + background: #ff3f34; + } + + div:nth-child(8) { + background: #2ecc71; + } + + div:nth-child(9) { + background: #34495e; + } + + div:nth-child(10) { + background: #1abc9c; + } + + div:nth-child(11) { + background: #e67e22; + } + + div:nth-child(12) { + background: #9b59b6; + } +`; + +export const numberToWords = (value: number): string => { + const ones = [ + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + 'eleven', + 'twelve', + 'thirteen', + 'fourteen', + 'fifteen', + 'sixteen', + 'seventeen', + 'eighteen', + 'nineteen', + ]; + const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']; + + if (!Number.isInteger(value) || value < 0 || value >= 100) { + return `${value}`; + } + + let word = ''; + if (value < 20) { + word = ones[value]; + } else { + const tensValue = Math.floor(value / 10); + const onesValue = value % 10; + word = onesValue === 0 ? tens[tensValue] : `${tens[tensValue]}-${ones[onesValue]}`; + } + + return word.charAt(0).toUpperCase() + word.slice(1); +}; diff --git a/core/src/components/split-pane/split-pane.tsx b/core/src/components/split-pane/split-pane.tsx index 938a76c431e..95c8995f8ef 100644 --- a/core/src/components/split-pane/split-pane.tsx +++ b/core/src/components/split-pane/split-pane.tsx @@ -8,6 +8,7 @@ import { getIonTheme } from '../../global/ionic-global'; const SPLIT_PANE_MAIN = 'split-pane-main'; const SPLIT_PANE_SIDE = 'split-pane-side'; +// TODO(FW-7285): Replace with global breakpoints const QUERY: { [key: string]: string } = { xs: '(min-width: 0px)', sm: '(min-width: 576px)', diff --git a/core/src/utils/media.ts b/core/src/utils/media.ts index a3e201a7cae..858dbe608fe 100644 --- a/core/src/utils/media.ts +++ b/core/src/utils/media.ts @@ -1,3 +1,4 @@ +// TODO(FW-7285): Replace with global breakpoints export const SIZE_TO_MEDIA: any = { xs: '(min-width: 0px)', sm: '(min-width: 576px)', diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 9340a00f095..b9edee6c0b1 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -88,6 +88,7 @@ export const config: Config = { { components: ['ion-chip'] }, { components: ['ion-datetime', 'ion-picker-legacy', 'ion-picker-legacy-column'] }, { components: ['ion-fab', 'ion-fab-button', 'ion-fab-list'] }, + { components: ['ion-gallery'] }, { components: ['ion-grid', 'ion-row', 'ion-col'] }, { components: ['ion-infinite-scroll', 'ion-infinite-scroll-content'] }, { components: ['ion-input'] }, diff --git a/packages/angular/src/directives/proxies-list.ts b/packages/angular/src/directives/proxies-list.ts index 95d7dca0aee..9e7f5b18565 100644 --- a/packages/angular/src/directives/proxies-list.ts +++ b/packages/angular/src/directives/proxies-list.ts @@ -30,6 +30,7 @@ export const DIRECTIVES = [ d.IonFabButton, d.IonFabList, d.IonFooter, + d.IonGallery, d.IonGrid, d.IonHeader, d.IonIcon, diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 51d2d6a5883..cba29f1e829 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -824,6 +824,28 @@ export class IonFooter { export declare interface IonFooter extends Components.IonFooter {} +@ProxyCmp({ + inputs: ['columns', 'layout', 'mode', 'order', 'theme'] +}) +@Component({ + selector: 'ion-gallery', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['columns', 'layout', 'mode', 'order', 'theme'], +}) +export class IonGallery { + protected el: HTMLIonGalleryElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonGallery extends Components.IonGallery {} + + @ProxyCmp({ inputs: ['fixed', 'mode', 'theme'] }) diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index da7e74d4886..bac0dce8ece 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -32,6 +32,7 @@ import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion- import { defineCustomElement as defineIonFabButton } from '@ionic/core/components/ion-fab-button.js'; import { defineCustomElement as defineIonFabList } from '@ionic/core/components/ion-fab-list.js'; import { defineCustomElement as defineIonFooter } from '@ionic/core/components/ion-footer.js'; +import { defineCustomElement as defineIonGallery } from '@ionic/core/components/ion-gallery.js'; import { defineCustomElement as defineIonGrid } from '@ionic/core/components/ion-grid.js'; import { defineCustomElement as defineIonHeader } from '@ionic/core/components/ion-header.js'; import { defineCustomElement as defineIonImg } from '@ionic/core/components/ion-img.js'; @@ -864,6 +865,30 @@ export class IonFooter { export declare interface IonFooter extends Components.IonFooter {} +@ProxyCmp({ + defineCustomElementFn: defineIonGallery, + inputs: ['columns', 'layout', 'mode', 'order', 'theme'] +}) +@Component({ + selector: 'ion-gallery', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['columns', 'layout', 'mode', 'order', 'theme'], + standalone: true +}) +export class IonGallery { + protected el: HTMLIonGalleryElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonGallery extends Components.IonGallery {} + + @ProxyCmp({ defineCustomElementFn: defineIonGrid, inputs: ['fixed', 'mode', 'theme'] diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index a7750896971..47a8c6ac371 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -26,6 +26,7 @@ import { defineCustomElement as defineIonDivider } from '@ionic/core/components/ import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion-fab.js'; import { defineCustomElement as defineIonFabList } from '@ionic/core/components/ion-fab-list.js'; import { defineCustomElement as defineIonFooter } from '@ionic/core/components/ion-footer.js'; +import { defineCustomElement as defineIonGallery } from '@ionic/core/components/ion-gallery.js'; import { defineCustomElement as defineIonGrid } from '@ionic/core/components/ion-grid.js'; import { defineCustomElement as defineIonHeader } from '@ionic/core/components/ion-header.js'; import { defineCustomElement as defineIonImg } from '@ionic/core/components/ion-img.js'; @@ -100,6 +101,7 @@ export const IonDivider = /*@__PURE__*/createReactComponent('ion-fab', undefined, undefined, defineIonFab); export const IonFabList = /*@__PURE__*/createReactComponent('ion-fab-list', undefined, undefined, defineIonFabList); export const IonFooter = /*@__PURE__*/createReactComponent('ion-footer', undefined, undefined, defineIonFooter); +export const IonGallery = /*@__PURE__*/createReactComponent('ion-gallery', undefined, undefined, defineIonGallery); export const IonGrid = /*@__PURE__*/createReactComponent('ion-grid', undefined, undefined, defineIonGrid); export const IonHeader = /*@__PURE__*/createReactComponent('ion-header', undefined, undefined, defineIonHeader); export const IonImg = /*@__PURE__*/createReactComponent('ion-img', undefined, undefined, defineIonImg); diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 21d2afce02f..341ba4011e4 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -30,6 +30,7 @@ import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion- import { defineCustomElement as defineIonFabButton } from '@ionic/core/components/ion-fab-button.js'; import { defineCustomElement as defineIonFabList } from '@ionic/core/components/ion-fab-list.js'; import { defineCustomElement as defineIonFooter } from '@ionic/core/components/ion-footer.js'; +import { defineCustomElement as defineIonGallery } from '@ionic/core/components/ion-gallery.js'; import { defineCustomElement as defineIonGrid } from '@ionic/core/components/ion-grid.js'; import { defineCustomElement as defineIonHeader } from '@ionic/core/components/ion-header.js'; import { defineCustomElement as defineIonImg } from '@ionic/core/components/ion-img.js'; @@ -432,6 +433,13 @@ export const IonFooter: StencilVueComponent = /*@__PURE__*/ defin ]); +export const IonGallery: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-gallery', defineIonGallery, [ + 'layout', + 'order', + 'columns' +]); + + export const IonGrid: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-grid', defineIonGrid, [ 'fixed' ]);