Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/pos-list-template-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions': minor
---

Add the `s-pos-list` web component for POS and the `posListTemplate` tagged template that compiles its `<s-pos-list-item>` row markup.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -137,6 +138,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -257,6 +259,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -379,6 +382,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -501,6 +505,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -623,6 +628,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -745,6 +751,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -827,6 +834,7 @@
"Modal",
"NumberField",
"POSBlock",
"POSList",
"Page",
"PosBlock",
"Route",
Expand Down Expand Up @@ -1644,6 +1652,18 @@
"pos.register-details.block.render"
]
},
"POSList": {
"targets": [
"pos.cart.line-item-details.action.render",
"pos.customer-details.action.render",
"pos.draft-order-details.action.render",
"pos.home.modal.render",
"pos.order-details.action.render",
"pos.product-details.action.render",
"pos.purchase.post.action.render",
"pos.register-details.action.render"
]
},
"Page": {
"targets": [
"pos.cart.line-item-details.action.render",
Expand Down
1 change: 1 addition & 0 deletions packages/ui-extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
}
},
"dependencies": {
"htm": "^3.1.1",
"ts-morph": "^25.0.1"
}
}
13 changes: 13 additions & 0 deletions packages/ui-extensions/src/surfaces/point-of-sale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@ export * from './point-of-sale/events';
export * from './point-of-sale/extension-targets';
export * from './point-of-sale/event/data';
export * from './point-of-sale/event/output';
export {posListTemplate} from './point-of-sale/pos-list-template';
export type {
POSListTemplateTag,
POSListTemplates,
POSListItemTemplate,
POSListTemplateNode,
POSListTemplateProp,
POSListTemplateSegment,
} from './point-of-sale/pos-list-template';
export type {
POSListRow,
POSListRowClickEvent,
} from './point-of-sale/components/POSList';
209 changes: 209 additions & 0 deletions packages/ui-extensions/src/surfaces/point-of-sale/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5435,6 +5435,150 @@ declare module 'preact' {
}
}

/**
* The serialized template AST carried by `s-pos-list`'s `itemTemplates`
* property.
*
* Templates are authored in the extension sandbox with the `posListTemplate`
* tagged template, compiled once into this plain-JSON shape, and shipped
* across the bridge as a single property. The host hydrates the AST per
* visible row, so no RemoteDOM nodes exist for template content and no
* extension code runs while scrolling.
*/
/** A literal string piece or a row-field lookup within interpolated text. */
type POSListTemplateSegment =
| string
| {
path: string;
};
type POSListTemplateProp =
/** A static value. Web component attributes yield strings, or `true` when valueless. */
| {
kind: 'literal';
value: string | boolean;
}
/** The row field at `path`, passed through with its original type (`bind:prop="path"`). */
| {
kind: 'field';
path: string;
}
/** A string built from literal pieces and `{{path}}` lookups. */
| {
kind: 'segments';
segments: POSListTemplateSegment[];
};
type POSListTemplateNode =
| {
kind: 'element';
tag: string;
props?: Record<string, POSListTemplateProp>;
children?: POSListTemplateNode[];
}
/** Literal text, possibly interpolated with `{{path}}` lookups. */
| {
kind: 'text';
segments: POSListTemplateSegment[];
}
/** `{{#if path}}…{{/if}}`: children render only when the field is truthy. */
| {
kind: 'if';
path: string;
children: POSListTemplateNode[];
};
/** One `<s-pos-list-item templateId="…">` root. */
interface POSListItemTemplate {
templateId: string;
/** `button` rows fire `rowclick`; `text` rows are static content. */
type: 'button' | 'text';
children: POSListTemplateNode[];
}
interface POSListTemplates {
/**
* The API version whose `posListTemplate` compiled these templates, e.g. `'2026-10'`. The host
* renders them only for an extension declaring the same API version.
*/
version: `${number}-${number}` | 'unstable';
templates: POSListItemTemplate[];
}

declare const posListTagName = 's-pos-list';
/**
* A row supplied to `s-pos-list`. Rows are plain data; every member other than `id` and
* `templateFor` is available to the row's item template through `{{path}}`, `bind:prop`,
* and `{{#if path}}` bindings.
*/
interface POSListRow {
/** A unique identifier for the row. Keys virtualization and identity across incremental loads. */
id: string;
/** The `templateId` of the item template that renders this row. */
templateFor: string;
/** Any additional data the row's template reads. */
[field: string]: unknown;
}
/**
* The event fired when a `button` row is activated. `detail.item` is the activated row and
* `detail.index` its position in `rows`. POS delivers the row data in `detail` because its
* RemoteDOM bridge forwards only `detail` when dispatching an event to the extension; the shared
* `POSListRowClickEvent` contract declares `item` and `index` on the event, which POS exposes once
* the bridge forwards custom event properties.
*/
type POSListRowClickEvent = CallbackEvent<typeof posListTagName> & {
detail: {
item: POSListRow;
index: number;
};
};
/**
* Displays a virtualized list of rows rendered from plain data and item templates compiled with
* `posListTemplate`.
*
* @publicDocs
*/
interface POSListJSXProps {
/** A unique identifier for the element. */
id?: string;
/**
* The rows displayed in the list. Each row names the item template that renders it through
* `templateFor`.
*
* @default []
*/
rows?: POSListRow[];
/**
* The compiled item templates, one per `<s-pos-list-item>`, produced by `posListTemplate`.
* A row whose `templateFor` matches no template renders nothing.
*/
itemTemplates?: POSListTemplates;
/**
* Whether additional rows are being loaded. Renders a progress indicator after the last row.
*
* @default false
*/
loadingMore?: boolean;
/**
* Callback when a `button` row is activated. `event.detail.item` is the row and
* `event.detail.index` its position in `rows`. Rows rendered by a `text` template never fire it.
*/
onRowClick?: ((event: POSListRowClickEvent) => void) | null;
/** Callback when the list has scrolled near its end and more rows should be loaded. */
onLoadMore?: ((event: CallbackEvent<typeof posListTagName>) => void) | null;
/** Content displayed before the rows as part of the list's scrollable content. */
header?: ComponentChild;
}
type POSListElementProps = Omit<POSListJSXProps, 'header'>;
declare global {
interface HTMLElementTagNameMap {
[posListTagName]: HtmlElementTagNameProps<POSListElementProps>;
}
}
declare module 'preact' {
namespace createElement.JSX {
interface IntrinsicElements {
[posListTagName]: IntrinsicElementProps<POSListElementProps>;
}
}
}

export type {
BadgeJSXProps,
BannerJSXProps,
Expand All @@ -5457,6 +5601,7 @@ export type {
ModalJSXProps,
NumberFieldJSXProps,
PageJSXProps,
POSListJSXProps,
PosBlockJSXProps,
QrCodeJSXProps,
ScrollBoxJSXProps,
Expand All @@ -5477,6 +5622,55 @@ export type {
TimePickerJSXProps,
};

/**
* The POS list component provides event callbacks for handling user interactions. Learn more about [handling events](/docs/api/polaris/using-polaris-web-components#handling-events).
* @publicDocs
*/
interface POSListEvents {
/**
* Callback when a `button` row is activated. `event.detail.item` is the row and
* `event.detail.index` its position in `rows`. Rows rendered by a `text` template never fire it.
*/
rowclick?: (event: POSListRowClickEvent) => void;
/** Callback when the list has scrolled near its end and more rows should be loaded. */
loadmore?: (event: CallbackEvent<typeof posListTagName>) => void;
}

/**
* Content slots for the POS list.
* @publicDocs
*/
interface POSListSlots {
/** Content displayed before the rows as part of the list's scrollable content. */
header?: HTMLElement;
}

/**
* Displays a virtualized list of rows rendered from plain data and item templates compiled with
* `posListTemplate`.
* @publicDocs
*/
interface POSList {
/** A unique identifier for the element. */
id?: string;
/**
* The rows displayed in the list. Each row names the item template that renders it through
* `templateFor`.
* @default []
*/
rows?: POSListRow[];
/**
* The compiled item templates, one per `<s-pos-list-item>`, produced by `posListTemplate`.
* A row whose `templateFor` matches no template renders nothing.
*/
itemTemplates?: POSListTemplates;
/**
* Whether additional rows are being loaded. Renders a progress indicator after the last row.
* @default false
*/
loadingMore?: boolean;
}

/**
* The link component provides event callbacks for handling user interactions. Learn more about [handling events](/docs/api/polaris/using-polaris-web-components#handling-events).
* @publicDocs
Expand Down Expand Up @@ -7809,3 +8003,18 @@ declare global {
}
}
}

declare module 'react' {
namespace JSX {
interface IntrinsicElements {
[posListTagName]: IntrinsicElementProps<POSListElementProps>;
}
}
}
declare global {
namespace JSX {
interface IntrinsicElements {
[posListTagName]: IntrinsicElementProps<POSListElementProps>;
}
}
}
Loading
Loading