Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b0a3f1a
feat(gallery): add new gallery component
brandyscarney Apr 23, 2026
74332ac
style(core): add TODOs for migrating global breakpoints
brandyscarney Apr 23, 2026
cca764d
test(gallery): add spec test for columns property
brandyscarney Apr 23, 2026
ad2d0a2
refactor(gallery): convert columns css variable to an internal variable
brandyscarney Apr 29, 2026
6a19314
test(gallery): add basic e2e test
brandyscarney Apr 29, 2026
fdd55c4
chore(): add updated snapshots
brandyscarney Apr 29, 2026
e9cdc62
test(gallery): add layout e2e test
brandyscarney Apr 29, 2026
f865311
chore(): add updated snapshots
brandyscarney Apr 29, 2026
ca9305e
refactor(gallery): move types to separate file
brandyscarney Apr 29, 2026
8e9c9b0
test(gallery): add spec tests for layout and order
brandyscarney Apr 29, 2026
576c569
style: lint
brandyscarney Apr 29, 2026
f40ea8b
chore: pass non-snapshot images to Vercel
brandyscarney Apr 29, 2026
970e4f2
Merge branch 'next' into FW-7280
brandyscarney Apr 29, 2026
f862f49
fix(gallery): reset margin on slotted elements to properly position them
brandyscarney Apr 30, 2026
eb6d900
test(gallery): add tests for dynamically appended images and fix the …
brandyscarney Apr 30, 2026
89e6886
chore(): add updated snapshots
brandyscarney Apr 30, 2026
613db2e
Merge branch 'next' into FW-7280
brandyscarney Apr 30, 2026
9dfc81c
fix(gallery): remove the bottom margin from the last masonry item in …
brandyscarney May 1, 2026
32817bd
chore(): add updated snapshots
brandyscarney May 1, 2026
4009a8a
test(gallery): import default columns directly from gallery
brandyscarney May 5, 2026
63aa6df
style: fix nested describes in test
brandyscarney May 5, 2026
69938a8
style(gallery): update comment for sanitizeColumns
brandyscarney May 6, 2026
5366cdc
refactor(gallery): return early when columns is undefined instead of …
brandyscarney May 7, 2026
2561ebc
test(gallery): improve spec test organization and add more function t…
brandyscarney May 7, 2026
f166cd3
test(gallery): remove no longer used attribute
brandyscarney May 7, 2026
5f10f44
refactor(gallery): migrate Sass from import to use
brandyscarney May 7, 2026
4a333f3
test(gallery): dynamically added images wrapped in figures need to in…
brandyscarney May 7, 2026
30a04ab
test(gallery): resize viewport to include the height of the gallery
brandyscarney May 7, 2026
924102e
chore(): add updated snapshots
brandyscarney May 7, 2026
38f7f4d
Merge branch 'next' into FW-7280
brandyscarney May 7, 2026
ca9d61d
chore(): add updated snapshots
brandyscarney May 7, 2026
d494fab
feat(gallery): add gap as a property with responsive breakpoints
brandyscarney May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vercelignore
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this because this rule prevented the images in my assets/ directory from being served on Vercel.

Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
core/src/components/**/*/*.png
# Exclude visual-regression snapshot artifacts only
core/src/**/*-snapshots/*.png
8 changes: 8 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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,GalleryBreakpoints<string | number> | number | string,DEFAULT_COLUMNS,false,false
ion-gallery,prop,gap,GalleryBreakpoints<string | number> | number | string,DEFAULT_GAP,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-grid,shadow
ion-grid,prop,fixed,boolean,false,false,false
ion-grid,prop,mode,"ios" | "md",undefined,false,false
Expand Down
77 changes: 77 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, GalleryGap } 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";
Expand Down Expand Up @@ -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, GalleryGap } 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";
Expand Down Expand Up @@ -1469,6 +1471,36 @@ 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 space between gallery items. Accepts CSS lengths like `16px`, `1rem`, or numbers (treated as pixel values). Can also be set as a breakpoint map (e.g. `{ xs: '8px', sm: '1rem', md: '24px' }`).
* @default DEFAULT_GAP
*/
"gap": GalleryGap;
/**
* 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.
Expand Down Expand Up @@ -5009,6 +5041,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: {
Expand Down Expand Up @@ -5966,6 +6004,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;
Expand Down Expand Up @@ -7481,6 +7520,36 @@ 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 space between gallery items. Accepts CSS lengths like `16px`, `1rem`, or numbers (treated as pixel values). Can also be set as a breakpoint map (e.g. `{ xs: '8px', sm: '1rem', md: '24px' }`).
* @default DEFAULT_GAP
*/
"gap"?: GalleryGap;
/**
* 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.
Expand Down Expand Up @@ -10900,6 +10969,12 @@ declare namespace LocalJSX {
"collapse": 'fade';
"translucent": boolean;
}
interface IonGalleryAttributes {
"layout": 'uniform' | 'masonry';
"order": 'sequential' | 'best-fit';
"columns": string;
"gap": string;
}
interface IonGridAttributes {
"fixed": boolean;
}
Expand Down Expand Up @@ -11465,6 +11540,7 @@ declare namespace LocalJSX {
"ion-fab-button": Omit<IonFabButton, keyof IonFabButtonAttributes> & { [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<IonFabList, keyof IonFabListAttributes> & { [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<IonFooter, keyof IonFooterAttributes> & { [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<IonGallery, keyof IonGalleryAttributes> & { [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<IonGrid, keyof IonGridAttributes> & { [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<IonHeader, keyof IonHeaderAttributes> & { [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<IonImg, keyof IonImgAttributes> & { [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] };
Expand Down Expand Up @@ -11569,6 +11645,7 @@ declare module "@stencil/core" {
"ion-fab-button": LocalJSX.IntrinsicElements["ion-fab-button"] & JSXBase.HTMLAttributes<HTMLIonFabButtonElement>;
"ion-fab-list": LocalJSX.IntrinsicElements["ion-fab-list"] & JSXBase.HTMLAttributes<HTMLIonFabListElement>;
"ion-footer": LocalJSX.IntrinsicElements["ion-footer"] & JSXBase.HTMLAttributes<HTMLIonFooterElement>;
"ion-gallery": LocalJSX.IntrinsicElements["ion-gallery"] & JSXBase.HTMLAttributes<HTMLIonGalleryElement>;
"ion-grid": LocalJSX.IntrinsicElements["ion-grid"] & JSXBase.HTMLAttributes<HTMLIonGridElement>;
"ion-header": LocalJSX.IntrinsicElements["ion-header"] & JSXBase.HTMLAttributes<HTMLIonHeaderElement>;
"ion-img": LocalJSX.IntrinsicElements["ion-img"] & JSXBase.HTMLAttributes<HTMLIonImgElement>;
Expand Down
1 change: 1 addition & 0 deletions core/src/components/col/col.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand Down
12 changes: 12 additions & 0 deletions core/src/components/gallery/gallery-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { GalleryColumns, GalleryGap } from './gallery-interface';

export const DEFAULT_COLUMNS = {
xs: 2,
sm: 3,
md: 4,
lg: 6,
xl: 8,
xxl: 10,
} satisfies GalleryColumns;

export const DEFAULT_GAP = '16px' satisfies GalleryGap;
11 changes: 11 additions & 0 deletions core/src/components/gallery/gallery-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface GalleryBreakpoints<T = string | number> {
xs?: T;
sm?: T;
md?: T;
lg?: T;
xl?: T;
xxl?: T;
}

export type GalleryColumns = GalleryBreakpoints | string | number;
export type GalleryGap = GalleryBreakpoints | string | number;
62 changes: 62 additions & 0 deletions core/src/components/gallery/gallery.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@use "../../themes/native/native.globals" as globals;

// Gallery
// --------------------------------------------------

:host {
display: grid;
grid-template-columns: repeat(var(--internal-gallery-columns, 2), minmax(0, 1fr));
}

// Layout: Uniform
// --------------------------------------------------

:host(.gallery-layout-uniform) {
gap: var(--internal-gallery-gap, 16px);
}

// Target all slotted elements in the uniform layout. This ensures that divs
// and images have an aspect ratio of 1/1. Nested images must inherit the
// aspect ratio of their parent.
:host(.gallery-layout-uniform) ::slotted(*) {
aspect-ratio: 1/1;
}

// Layout: Masonry
// --------------------------------------------------

:host(.gallery-layout-masonry) {
align-items: start;

column-gap: var(--internal-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(--internal-gallery-gap, 16px);
}

// Slotted elements
// --------------------------------------------------

// Reset the default margin for slotted elements so wrapper elements
// (such as <figure>) align properly with other gallery items.
::slotted(*) {
@include globals.margin(0);

width: 100%;
}

::slotted(img) {
display: block;

object-fit: cover;
object-position: center;
}
Loading
Loading