From 84cdde48e2892373c32a3ffdb3932d9747c5587a Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 12:24:47 +0100 Subject: [PATCH 01/34] feat(pie-css): DSW-4116 mixin for slotted element allowlists --- packages/components/pie-list/package.json | 3 ++- packages/components/pie-list/src/index.ts | 2 +- packages/components/pie-list/src/list.scss | 2 ++ .../scss/mixins/_enforceSlotElements.scss | 17 +++++++++++++++++ packages/tools/pie-css/scss/mixins/index.scss | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss diff --git a/packages/components/pie-list/package.json b/packages/components/pie-list/package.json index 9903bef517..b61414a17c 100644 --- a/packages/components/pie-list/package.json +++ b/packages/components/pie-list/package.json @@ -46,6 +46,7 @@ }, "customElements": "custom-elements.json", "sideEffects": [ - "dist/*.js" + "dist/*.js", + "dist/**/*.js" ] } diff --git a/packages/components/pie-list/src/index.ts b/packages/components/pie-list/src/index.ts index 504c5e822c..d192750dfc 100644 --- a/packages/components/pie-list/src/index.ts +++ b/packages/components/pie-list/src/index.ts @@ -16,7 +16,7 @@ const componentSelector = 'pie-list'; @safeCustomElement('pie-list') export class PieList extends RtlMixin(PieElement) implements ListProps { render () { - return html`

Hello world!

`; + return html``; } // Renders a `CSSResult` generated from SCSS by Vite diff --git a/packages/components/pie-list/src/list.scss b/packages/components/pie-list/src/list.scss index 649ce446fd..e8db54bc93 100644 --- a/packages/components/pie-list/src/list.scss +++ b/packages/components/pie-list/src/list.scss @@ -3,3 +3,5 @@ :host { display: block; } + +@include p.enforce-slot-elements(('pie-list-item')); diff --git a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss new file mode 100644 index 0000000000..5fd903af05 --- /dev/null +++ b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss @@ -0,0 +1,17 @@ +@use "sass:string"; +@use "sass:list"; + +@mixin enforce-slot-elements($allowed-elements) { + ::slotted(:not(#{$allowed-elements})) { + display: none; + } + + $allowlist-selectors: (); + @each $el in $allowed-elements { + $allowlist-selectors: list.append($allowlist-selectors, string.unquote("::slotted(#{$el})"), comma); + } + + #{$allowlist-selectors} { + @content; + } +} diff --git a/packages/tools/pie-css/scss/mixins/index.scss b/packages/tools/pie-css/scss/mixins/index.scss index 437d9efe58..066e409751 100644 --- a/packages/tools/pie-css/scss/mixins/index.scss +++ b/packages/tools/pie-css/scss/mixins/index.scss @@ -5,3 +5,4 @@ @forward './font-theme'; @forward './components'; @forward './disablePointerEventsInSlottedIcons'; +@forward './enforceSlotElements'; From fcccb12f77b68e4eb4bf851af33fe60bbe1d4dbb Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 12:27:55 +0100 Subject: [PATCH 02/34] fix lint --- .../tools/pie-css/scss/mixins/_enforceSlotElements.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss index 5fd903af05..be6087622d 100644 --- a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss +++ b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss @@ -1,5 +1,5 @@ -@use "sass:string"; -@use "sass:list"; +@use 'sass:string'; +@use 'sass:list'; @mixin enforce-slot-elements($allowed-elements) { ::slotted(:not(#{$allowed-elements})) { @@ -7,8 +7,9 @@ } $allowlist-selectors: (); + @each $el in $allowed-elements { - $allowlist-selectors: list.append($allowlist-selectors, string.unquote("::slotted(#{$el})"), comma); + $allowlist-selectors: list.append($allowlist-selectors, string.unquote('::slotted(#{$el})'), comma); } #{$allowlist-selectors} { From 3888ebb3b01166b7f0082274b95ed4313736e569 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 13:38:26 +0100 Subject: [PATCH 03/34] initial list-item markup and props --- .../stories/testing/pie-list.test.stories.ts | 63 +++++++++++++++++- .../pie-list/src/pie-list-item/defs.ts | 38 ++++++++++- .../pie-list/src/pie-list-item/index.ts | 66 +++++++++++++++++-- .../pie-list/src/pie-list-item/list-item.scss | 20 ++++++ 4 files changed, 178 insertions(+), 9 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index cd9b493976..cf0d8e326f 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -2,6 +2,9 @@ import { html } from 'lit'; import { type Meta } from '@storybook/web-components'; import '@justeattakeaway/pie-webc/components/list'; +import '@justeattakeaway/pie-webc/components/list-item'; +import '@justeattakeaway/pie-icons-webc/dist/IconPlaceholder'; + import { type ListProps } from '@justeattakeaway/pie-webc/components/list'; import { createStory } from '../../utilities'; @@ -28,7 +31,65 @@ export default listStoryMeta; // TODO: remove the eslint-disable rule when props are added // eslint-disable-next-line no-empty-pattern const Template = ({}: ListProps) => html` - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Padding override

+ + + + + + + + `; export const Default = createStory(Template, defaultArgs)(); diff --git a/packages/components/pie-list/src/pie-list-item/defs.ts b/packages/components/pie-list/src/pie-list-item/defs.ts index e92dad87f4..d8bbbe5234 100644 --- a/packages/components/pie-list/src/pie-list-item/defs.ts +++ b/packages/components/pie-list/src/pie-list-item/defs.ts @@ -1,3 +1,35 @@ -// TODO - please remove the eslint disable comment below when you add props to this interface -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ListItemProps {} +import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core'; + +export interface ListItemProps { + /** + * **Required:** Provides an overview of the content. + */ + primaryText: string, + /** + * Provides optional additional detail. + */ + secondaryText?: string, + /** + * Provides further optional information about the context, status or attributes of the primary content. + */ + metaText?: string + + /** + * The height of the component is decreased to reduce the vertical space, and used when space needs to be saved. + * + * **Note**: Do not use if you require secondary text, or if you want to slot a pie-avatar, pie-thumbnail or Icon with Background component into the list item. + */ + isCompact: boolean + + /** + * Sets the primary text to use a bold font-weight. + */ + isBold: boolean +} + +export type DefaultProps = ComponentDefaultProps>; + +export const defaultProps: DefaultProps = { + isCompact: false, + isBold: false, +}; diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index ce41edc8bd..1dd515c932 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -1,6 +1,9 @@ -import { LitElement } from 'lit'; -import { safeCustomElement } from '@justeattakeaway/pie-webc-core'; -import { type ListItemProps } from './defs'; +import { html, nothing, unsafeCSS } from 'lit'; +import { property } from 'lit/decorators.js'; +import { safeCustomElement, requiredProperty } from '@justeattakeaway/pie-webc-core'; +import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement'; +import { type ListItemProps, defaultProps } from './defs'; +import styles from './list-item.scss?inline'; const componentSelector = 'pie-list-item'; @@ -8,8 +11,61 @@ const componentSelector = 'pie-list-item'; * @tagname pie-list-item */ @safeCustomElement('pie-list-item') -export class PieListItem extends LitElement implements ListItemProps { - // component logic +export class PieListItem extends PieElement implements ListItemProps { + @property({ type: String }) + @requiredProperty(componentSelector) + primaryText!: ListItemProps['primaryText']; + + @property({ type: String }) + secondaryText: ListItemProps['metaText']; + + @property({ type: String }) + metaText: ListItemProps['metaText']; + + @property({ type: Boolean, attribute: 'is-compact', reflect: true }) + isCompact = defaultProps.isCompact; + + @property({ type: Boolean, attribute: 'is-bold', reflect: true }) + isBold = defaultProps.isBold; + + _renderSecondaryText () { + const { secondaryText } = this; + if (secondaryText) { + return html`${secondaryText}`; + } + + return nothing; + } + + // metaText is another form of trailing content. The component can only ever display either slotted trailing content or the metaText string + _renderTrailingContent () { + const { metaText } = this; + if (metaText) { + return html`${metaText}`; + } + + return html`
`; + } + + render () { + const { primaryText } = this; + + // We should never render a list item that doesn't have primary text. + if (!primaryText) return nothing; + + return html` +
+ +
+
+ ${primaryText} + ${this._renderSecondaryText()} +
+ ${this._renderTrailingContent()}`; + } + + // Renders a `CSSResult` generated from SCSS by Vite + static styles = unsafeCSS(styles); } declare global { diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 6ffaedad64..0e4f583daf 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -1 +1,21 @@ @use '@justeattakeaway/pie-css/scss' as p; + +:host { + --list-item-inline-padding-default: var(--dt-spacing-d); + + display: block; + border: 1px dashed hotpink; + + // TODO: document --list-item-inline-padding-override in component readme + padding-inline: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); +} + +:host([is-compact]) { + background: red; +} + +:host([is-bold]) .c-listItem-primaryText { + font-weight: bold; +} + +@include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag')); From 42ec0f1bcc2218a21909a7705ea1b38aa98116c8 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 14:50:52 +0100 Subject: [PATCH 04/34] basic list item structure styling --- .../stories/testing/pie-list.test.stories.ts | 2 + packages/components/pie-list/src/index.ts | 8 ++++ .../pie-list/src/pie-list-item/index.ts | 16 +++++-- .../pie-list/src/pie-list-item/list-item.scss | 43 +++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index cf0d8e326f..f2a314b25f 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -31,6 +31,7 @@ export default listStoryMeta; // TODO: remove the eslint-disable rule when props are added // eslint-disable-next-line no-empty-pattern const Template = ({}: ListProps) => html` + html` + `; export const Default = createStory(Template, defaultArgs)(); diff --git a/packages/components/pie-list/src/index.ts b/packages/components/pie-list/src/index.ts index d192750dfc..902ed1093c 100644 --- a/packages/components/pie-list/src/index.ts +++ b/packages/components/pie-list/src/index.ts @@ -15,6 +15,14 @@ const componentSelector = 'pie-list'; */ @safeCustomElement('pie-list') export class PieList extends RtlMixin(PieElement) implements ListProps { + connectedCallback (): void { + super.connectedCallback(); + + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'list'); + } + } + render () { return html``; } diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index 1dd515c932..5ce4923e50 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -28,10 +28,18 @@ export class PieListItem extends PieElement implements ListItemProps { @property({ type: Boolean, attribute: 'is-bold', reflect: true }) isBold = defaultProps.isBold; + connectedCallback () { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'listitem'); + } + + super.connectedCallback(); + } + _renderSecondaryText () { const { secondaryText } = this; if (secondaryText) { - return html`${secondaryText}`; + return html`${secondaryText}`; } return nothing; @@ -41,7 +49,7 @@ export class PieListItem extends PieElement implements ListItemProps { _renderTrailingContent () { const { metaText } = this; if (metaText) { - return html`${metaText}`; + return html`${metaText}`; } return html`
`; @@ -57,10 +65,12 @@ export class PieListItem extends PieElement implements ListItemProps {
+
- ${primaryText} + ${primaryText} ${this._renderSecondaryText()}
+ ${this._renderTrailingContent()}`; } diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 0e4f583daf..2b057311e2 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -2,12 +2,40 @@ :host { --list-item-inline-padding-default: var(--dt-spacing-d); + --list-item-block-padding-default: var(--dt-spacing-d); display: block; border: 1px dashed hotpink; // TODO: document --list-item-inline-padding-override in component readme padding-inline: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); + padding-block: var(--list-item-block-padding-default); + + // Layout + display: flex; + align-items: flex-start; + gap: var(--dt-spacing-c); +} + +.c-listItem-text { + flex: 1; + // Add a tiny bit of padding to the flex gap to make up 16px total + padding-inline-end: var(--dt-spacing-a); +} + +// Ensures that empty slots do not create a flex-gap in the container. +.c-listItem-leading, slot[name="leading"] { + display: contents; +} + +// Vertically stack the text items +.c-listItem-text > * { + display: block; +} + +// Flush trailing content to the end of the list-item +.c-listItem-trailing { + margin-inline-start: auto; } :host([is-compact]) { @@ -19,3 +47,18 @@ } @include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag')); + +// typography + +.c-listItem-primaryText { + @include p.font-theme('font-body-l'); + color: var(--dt-color-content-default); +} +.c-listItem-secondaryText { + @include p.font-theme('font-body-s'); + color: var(--dt-color-content-subdued); +} +.c-listItem-metaText { + @include p.font-theme('font-body-s'); + color: var(--dt-color-content-subdued); +} From 1d4999c755d092e619a23915081bb0ee1db154f3 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 17:10:27 +0100 Subject: [PATCH 05/34] typograpghy --- .../pie-list/src/pie-list-item/index.ts | 6 +++--- .../pie-list/src/pie-list-item/list-item.scss | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index 5ce4923e50..470ecb36f3 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -39,7 +39,7 @@ export class PieListItem extends PieElement implements ListItemProps { _renderSecondaryText () { const { secondaryText } = this; if (secondaryText) { - return html`${secondaryText}`; + return html`${secondaryText}`; } return nothing; @@ -49,7 +49,7 @@ export class PieListItem extends PieElement implements ListItemProps { _renderTrailingContent () { const { metaText } = this; if (metaText) { - return html`${metaText}`; + return html`${metaText}`; } return html`
`; @@ -67,7 +67,7 @@ export class PieListItem extends PieElement implements ListItemProps {
- ${primaryText} + ${primaryText} ${this._renderSecondaryText()}
diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 2b057311e2..b53d8b5562 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -3,13 +3,11 @@ :host { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); - - display: block; border: 1px dashed hotpink; // TODO: document --list-item-inline-padding-override in component readme - padding-inline: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); - padding-block: var(--list-item-block-padding-default); + padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default));padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); + padding-block-start: var(--list-item-block-padding-default);padding-block-end: var(--list-item-block-padding-default); // Layout display: flex; @@ -19,6 +17,7 @@ .c-listItem-text { flex: 1; + // Add a tiny bit of padding to the flex gap to make up 16px total padding-inline-end: var(--dt-spacing-a); } @@ -42,10 +41,6 @@ background: red; } -:host([is-bold]) .c-listItem-primaryText { - font-weight: bold; -} - @include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag')); // typography @@ -54,10 +49,16 @@ @include p.font-theme('font-body-l'); color: var(--dt-color-content-default); } + +:host([is-bold]) .c-listItem-primaryText { + @include p.font-theme('font-body-strong-l'); +} + .c-listItem-secondaryText { @include p.font-theme('font-body-s'); color: var(--dt-color-content-subdued); } + .c-listItem-metaText { @include p.font-theme('font-body-s'); color: var(--dt-color-content-subdued); From 8d4678767c225fb00e174f8b588ce839db72fd16 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 17:24:27 +0100 Subject: [PATCH 06/34] add named slots to the scss mixin --- .../pie-list/src/pie-list-item/list-item.scss | 8 ++- .../scss/mixins/_enforceSlotElements.scss | 57 +++++++++++++++++-- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index b53d8b5562..9237c5d34c 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -6,8 +6,10 @@ border: 1px dashed hotpink; // TODO: document --list-item-inline-padding-override in component readme - padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default));padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); - padding-block-start: var(--list-item-block-padding-default);padding-block-end: var(--list-item-block-padding-default); + padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); + padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); + padding-block-start: var(--list-item-block-padding-default); + padding-block-end: var(--list-item-block-padding-default); // Layout display: flex; @@ -41,7 +43,7 @@ background: red; } -@include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag')); +@include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag'), 'leading', 'trailing'); // typography diff --git a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss index be6087622d..3fe21a22df 100644 --- a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss +++ b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss @@ -1,15 +1,60 @@ @use 'sass:string'; @use 'sass:list'; -@mixin enforce-slot-elements($allowed-elements) { - ::slotted(:not(#{$allowed-elements})) { - display: none; - } +/// Enforces a strict allowlist of elements for one or more web component slots. +/// Any slotted elements not in the allowlist will be hidden (`display: none`). +/// +/// @param {List} $allowed-elements - A list of permitted CSS selectors (e.g., `('h1', 'p')`). +/// @param {ArgList} $slot-names... - 0 or more slot names. If omitted, targets the default slot. +/// @content The styles to apply exclusively to the allowed elements. +/// +/// @example scss - Usage for the default slot (0 slot names) +/// @include enforce-slot-elements(('p', 'span')) { +/// font-size: 1rem; +/// } +/// +/// @example scss - Usage for a specific named slot (1 slot name) +/// @include enforce-slot-elements(('h1', 'h2'), 'header-slot') { +/// color: blue; +/// } +/// +/// @example scss - Usage for multiple named slots (1+ slot names) +/// @include enforce-slot-elements(('img', 'picture'), 'hero-image', 'footer-image') { +/// max-width: 100%; +/// } +@mixin enforce-slot-elements($allowed-elements, $slot-names...) { + // 1. If no slot names are passed, default to an empty string (targets default slot) + $slots-to-process: if(list.length($slot-names) == 0, (''), $slot-names); + $hide-selectors: (); $allowlist-selectors: (); - @each $el in $allowed-elements { - $allowlist-selectors: list.append($allowlist-selectors, string.unquote('::slotted(#{$el})'), comma); + // 2. Loop through every slot name provided + @each $slot in $slots-to-process { + // Create the specific attribute selector for this loop iteration + $slot-selector: if($slot != '', '[slot="#{$slot}"]', ''); + + // 3. Build the 'hide' selector for this specific slot + $hide-selectors: list.append( + $hide-selectors, + string.unquote('::slotted(#{$slot-selector}:not(#{$allowed-elements}))'), + comma + ); + + // 4. Build the 'allowlist' selectors for this specific slot + @each $el in $allowed-elements { + $combined-selector: '#{$el}#{$slot-selector}'; + $allowlist-selectors: list.append( + $allowlist-selectors, + string.unquote('::slotted(#{$combined-selector})'), + comma + ); + } + } + + // 5. Output the final comma-separated selectors + #{$hide-selectors} { + display: none; } #{$allowlist-selectors} { From c4ebbd45a590e90e4d4a96a3284d2b75e5c9ced4 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 17:39:03 +0100 Subject: [PATCH 07/34] borders --- .../stories/testing/pie-list.test.stories.ts | 14 +++++++++++++- packages/components/pie-list/src/list.scss | 7 +++++++ .../pie-list/src/pie-list-item/list-item.scss | 19 +++++++++++++------ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index f2a314b25f..ba6979d5a3 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -80,8 +80,20 @@ const Template = ({}: ListProps) => html` -

Padding override

+

Custom Padding override

+ + + + + + + + +

Removed Padding - override

+ diff --git a/packages/components/pie-list/src/list.scss b/packages/components/pie-list/src/list.scss index e8db54bc93..3d0bd95843 100644 --- a/packages/components/pie-list/src/list.scss +++ b/packages/components/pie-list/src/list.scss @@ -2,6 +2,13 @@ :host { display: block; + + ::slotted(*:nth-last-child(n+2)) { + // This allows list-items following other list-items to have 1px stripped off of their padding to account for the added border. + --list-item-size-modifier: 1px; + + border-bottom: var(--list-item-size-modifier) solid var(--dt-color-divider-default); + } } @include p.enforce-slot-elements(('pie-list-item')); diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 9237c5d34c..f91e622cee 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -3,18 +3,29 @@ :host { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); - border: 1px dashed hotpink; + + --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); // TODO: document --list-item-inline-padding-override in component readme padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); + padding-block-start: var(--list-item-block-padding-default); - padding-block-end: var(--list-item-block-padding-default); + padding-block-end: var(--list-item-block-end-padding-default, var(--list-item-block-padding-default)); // Layout display: flex; align-items: flex-start; gap: var(--dt-spacing-c); + min-height: 56px; +} + +:host:has(.c-listItem-secondaryText) { + min-height: 76px; +} + +:host([is-compact]) { + min-height: 48px; } .c-listItem-text { @@ -39,10 +50,6 @@ margin-inline-start: auto; } -:host([is-compact]) { - background: red; -} - @include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag'), 'leading', 'trailing'); // typography From 6e7a7f8480c06b71f6fdb060a7c68e5b91f82f92 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 17:41:29 +0100 Subject: [PATCH 08/34] fix lint --- packages/components/pie-list/src/pie-list-item/list-item.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index f91e622cee..dc35de037b 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -3,7 +3,6 @@ :host { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); - --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); // TODO: document --list-item-inline-padding-override in component readme From 122d8315a9aff3994895ab085c9ed7a9121f2566 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 17:42:25 +0100 Subject: [PATCH 09/34] comment --- packages/components/pie-list/src/pie-list-item/list-item.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index dc35de037b..f5294dfb6f 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -3,6 +3,7 @@ :host { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); + // The list component will add a border to each list-item that follows another. This ensures the height accounts for the added border. --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); // TODO: document --list-item-inline-padding-override in component readme From 842e10a558df6f8b3f9933f4d394eae2e87f566b Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 17:45:01 +0100 Subject: [PATCH 10/34] compact padding --- .../stories/testing/pie-list.test.stories.ts | 16 ++++++++++++++++ .../pie-list/src/pie-list-item/list-item.scss | 2 ++ 2 files changed, 18 insertions(+) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index ba6979d5a3..7b213876a1 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -80,6 +80,22 @@ const Template = ({}: ListProps) => html` +

Compact list

+ + + + + + + + +

Custom Padding override

Date: Wed, 1 Jul 2026 17:46:06 +0100 Subject: [PATCH 11/34] fix lint --- packages/components/pie-list/src/pie-list-item/list-item.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 828765b2eb..37128a6ea3 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -3,6 +3,7 @@ :host { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); + // The list component will add a border to each list-item that follows another. This ensures the height accounts for the added border. --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); From 06126b2cf9ee7256846ebed246989b96eed8273e Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 18:02:19 +0100 Subject: [PATCH 12/34] icons, fix trailing slot display, flex alignment and lint fixes --- .../stories/testing/pie-list.test.stories.ts | 21 +++++++++++++++++-- .../pie-list/src/pie-list-item/list-item.scss | 11 +++++++--- .../scss/mixins/_enforceSlotElements.scss | 1 - 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 7b213876a1..a67c82d108 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -85,21 +85,38 @@ const Template = ({}: ListProps) => html` + + + + + + +

Alignment override

+ + + + + +

Custom Padding override

- + Date: Wed, 1 Jul 2026 18:03:15 +0100 Subject: [PATCH 13/34] todo readme comments --- packages/components/pie-list/src/pie-list-item/list-item.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 4e537fb890..d3d259a9a9 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -7,7 +7,7 @@ // The list component will add a border to each list-item that follows another. This ensures the height accounts for the added border. --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); - // TODO: document --list-item-inline-padding-override in component readme + // TODO: document --list-item-inline-padding-override in component readme (any spacing token or 0) padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); @@ -16,6 +16,7 @@ // Layout display: flex; + // TODO: document --list-item-alignment-override in component readme (only recommend using 'center') align-items: var(--list-item-alignment-override, flex-start); gap: var(--dt-spacing-c); min-height: 56px; From a2a95a1e00c9201c928617cd968fff7caa6d93a5 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 18:19:07 +0100 Subject: [PATCH 14/34] handle multiline text and more story demos --- .../stories/testing/pie-list.test.stories.ts | 96 ++++++++++++++++++- .../pie-list/src/pie-list-item/list-item.scss | 5 + 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index a67c82d108..615fa0a3b5 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -3,6 +3,7 @@ import { type Meta } from '@storybook/web-components'; import '@justeattakeaway/pie-webc/components/list'; import '@justeattakeaway/pie-webc/components/list-item'; +import '@justeattakeaway/pie-webc/components/tag'; import '@justeattakeaway/pie-icons-webc/dist/IconPlaceholder'; import { type ListProps } from '@justeattakeaway/pie-webc/components/list'; @@ -31,8 +32,13 @@ export default listStoryMeta; // TODO: remove the eslint-disable rule when props are added // eslint-disable-next-line no-empty-pattern const Template = ({}: ListProps) => html` + - + html` + Label html` +

Tags

+ + + Label + + + Label + + + Label + + + +

Tags (compact)

+ + + Label + + + Label + + + Label + + +

Alignment override

html` + +

Edge cases

+

Long text

+ + + + + + + + + + + + + + +

Long text + center aligned

+ + + + Label + + + + + Label + + + + + Label + + `; diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index d3d259a9a9..de6358499b 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -55,6 +55,11 @@ margin-inline-start: auto; } +.c-listItem-metaText { + display: block; + max-width: calc(100% / 3); +} + @include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag'), 'leading', 'trailing'); ::slotted(.c-pieIcon) { From 4df3e206de08da80dd263983ca0faeca3fa1e6d0 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 18:21:15 +0100 Subject: [PATCH 15/34] fix lint --- packages/components/pie-list/src/pie-list-item/list-item.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index de6358499b..5162b4c434 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -16,6 +16,7 @@ // Layout display: flex; + // TODO: document --list-item-alignment-override in component readme (only recommend using 'center') align-items: var(--list-item-alignment-override, flex-start); gap: var(--dt-spacing-c); From 168df5c3cfa50816955f5f66b33a325874e0b169 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 18:35:53 +0100 Subject: [PATCH 16/34] organise stories --- .../stories/testing/pie-list.test.stories.ts | 389 +++++++++++++----- 1 file changed, 279 insertions(+), 110 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 615fa0a3b5..70083f87a3 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -1,4 +1,4 @@ -import { html } from 'lit'; +import { html, type TemplateResult } from 'lit'; import { type Meta } from '@storybook/web-components'; import '@justeattakeaway/pie-webc/components/list'; @@ -29,208 +29,377 @@ const listStoryMeta: ListStoryMeta = { export default listStoryMeta; -// TODO: remove the eslint-disable rule when props are added -// eslint-disable-next-line no-empty-pattern -const Template = ({}: ListProps) => html` +// Shared building blocks ----------------------------------------------------- + +const leadingIcon = html``; +const trailingIcon = html``; +const trailingTag = html`Label`; + +/** + * Wraps a `pie-list` block in a fixed-width container so that every story + * renders at a consistent, readable width. + */ +const withLayout = (content: TemplateResult) => html` - + ${content} +`; + +// Stories -------------------------------------------------------------------- + +/** + * The minimal item: primary text only, with no secondary text, meta text or + * slotted content. + */ +const PrimaryTextOnlyTemplate = () => withLayout(html` - - - + + + + + +`); - - +export const PrimaryTextOnly = createStory(PrimaryTextOnlyTemplate, defaultArgs)(); - - +/** + * Text-only items: primary and secondary text with no leading/trailing content. + */ +const TextOnlyTemplate = () => withLayout(html` + + + + + + +`); - - Label - +export const TextOnly = createStory(TextOnlyTemplate, defaultArgs)(); - - +/** + * `is-bold` sets the primary text to a bold font-weight. + */ +const BoldTemplate = () => withLayout(html` + + + + + + +`); - - +export const Bold = createStory(BoldTemplate, defaultArgs)(); + +/** + * A leading icon slotted into each item. + */ +const LeadingIconTemplate = () => withLayout(html` + + + ${leadingIcon} + + + ${leadingIcon} + + + ${leadingIcon} + + + ${leadingIcon} + +`); - - +export const LeadingIcon = createStory(LeadingIconTemplate, defaultArgs)(); + +/** + * A trailing icon slotted into each item. + */ +const TrailingIconTemplate = () => withLayout(html` + + + ${trailingIcon} + + + ${trailingIcon} + + + ${trailingIcon} + + ${trailingIcon} + + +`); - - - +export const TrailingIcon = createStory(TrailingIconTemplate, defaultArgs)(); + +/** + * Both a leading and a trailing icon on each item. + */ +const LeadingAndTrailingTemplate = () => withLayout(html` + + + ${leadingIcon} + ${trailingIcon} + + + ${leadingIcon} + ${trailingIcon} + + + ${leadingIcon} + ${trailingIcon} + + + ${leadingIcon} + ${trailingIcon} +`); + +export const LeadingAndTrailing = createStory(LeadingAndTrailingTemplate, defaultArgs)(); -

Compact list

+/** + * `metaText` renders a trailing text string (mutually exclusive with the + * trailing slot). + */ +const MetaTextTemplate = () => withLayout(html` - - + + + + + +`); + +export const MetaText = createStory(MetaTextTemplate, defaultArgs)(); + +/** + * `is-compact` reduces the vertical space of each item. + */ +const CompactTemplate = () => withLayout(html` + + + ${trailingIcon} - - + + ${trailingIcon} - - + + ${trailingIcon} + + + ${trailingIcon} +`); -

Tags

+export const Compact = createStory(CompactTemplate, defaultArgs)(); + +/** + * A trailing `pie-tag` slotted into each item. + */ +const TagsTemplate = () => withLayout(html` - Label + ${trailingTag} + + + ${trailingTag} - Label + ${trailingTag} - Label + ${trailingTag} +`); + +export const Tags = createStory(TagsTemplate, defaultArgs)(); -

Tags (compact)

+/** + * Trailing tags within compact items. + */ +const TagsCompactTemplate = () => withLayout(html` - - Label + + ${trailingTag} - - Label + + ${trailingTag} - - Label + + ${trailingTag} + + + ${trailingTag} +`); + +export const TagsCompact = createStory(TagsCompactTemplate, defaultArgs)(); -

Alignment override

+/** + * `--list-item-alignment-override: center` vertically centres the item content. + */ +const AlignmentOverrideTemplate = () => withLayout(html` - + ${trailingIcon} + + + ${trailingIcon} - + ${trailingIcon} + + + ${trailingIcon} +`); + +export const AlignmentOverride = createStory(AlignmentOverrideTemplate, defaultArgs)(); -

Custom Padding override

+/** + * `--list-item-inline-padding-override` set to a larger spacing token. + */ +const CustomPaddingTemplate = () => withLayout(html` - - + + ${trailingIcon} - - + + ${trailingIcon} + + + ${trailingIcon} + + + ${trailingIcon} +`); -

Removed Padding - override

+export const CustomPadding = createStory(CustomPaddingTemplate, defaultArgs)(); + +/** + * `--list-item-inline-padding-override: 0` removes the inline padding entirely. + */ +const RemovedPaddingTemplate = () => withLayout(html` - - + + ${trailingIcon} - - + + ${trailingIcon} + + + ${trailingIcon} + + + ${trailingIcon} +`); + +export const RemovedPadding = createStory(RemovedPaddingTemplate, defaultArgs)(); -

Edge cases

-

Long text

+// Edge cases ----------------------------------------------------------------- + +/** + * Very long primary, secondary and meta text to verify wrapping behaviour. + */ +const LongTextTemplate = () => withLayout(html` + ${leadingIcon} + + + - + ${leadingIcon} - + ${leadingIcon} - + ${leadingIcon} +`); -

Long text + center aligned

+export const LongText = createStory(LongTextTemplate, defaultArgs)(); + +/** + * Long text combined with the centre alignment override, a leading icon and a + * trailing tag. + */ +const LongTextCentreAlignedTemplate = () => withLayout(html` - - Label + secondaryText="Secondary text that goes on far too long Secondary text that goes on far too long Secondary text that goes on far too long"> + ${leadingIcon} + ${trailingTag} + + + + ${leadingIcon} + ${trailingTag} - - Label + secondaryText="Secondary text that goes on far too long Secondary text that goes on far too long Secondary text that goes on far too long"> + ${leadingIcon} + ${trailingTag} - - Label + secondaryText="Secondary text that goes on far too long Secondary text that goes on far too long Secondary text that goes on far too long"> + ${leadingIcon} + ${trailingTag} - -`; +`); -export const Default = createStory(Template, defaultArgs)(); +export const LongTextCentreAligned = createStory(LongTextCentreAlignedTemplate, defaultArgs)(); From f8da6db12fe2bb0bf2325cf444ecb37040f3c394 Mon Sep 17 00:00:00 2001 From: personal Date: Wed, 1 Jul 2026 19:24:14 +0100 Subject: [PATCH 17/34] handle avatar padding quirk and disallow large comps in compact mode --- .../stories/testing/pie-list.test.stories.ts | 37 +++++++++++++++++++ .../pie-list/src/pie-list-item/defs.ts | 8 ++++ .../pie-list/src/pie-list-item/index.ts | 3 ++ .../pie-list/src/pie-list-item/list-item.scss | 15 ++++++++ 4 files changed, 63 insertions(+) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 70083f87a3..7a58123242 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -3,6 +3,7 @@ import { type Meta } from '@storybook/web-components'; import '@justeattakeaway/pie-webc/components/list'; import '@justeattakeaway/pie-webc/components/list-item'; +import '@justeattakeaway/pie-webc/components/thumbnail'; import '@justeattakeaway/pie-webc/components/tag'; import '@justeattakeaway/pie-icons-webc/dist/IconPlaceholder'; @@ -34,6 +35,7 @@ export default listStoryMeta; const leadingIcon = html``; const trailingIcon = html``; const trailingTag = html`Label`; +const leadingThumbnail = html``; /** * Wraps a `pie-list` block in a fixed-width container so that every story @@ -179,6 +181,41 @@ const MetaTextTemplate = () => withLayout(html` export const MetaText = createStory(MetaTextTemplate, defaultArgs)(); +/** + * `has-media` reduces the block padding to suit a larger slotted pie-thumbnail. + * The reduced padding only applies when there is no secondary text — the last + * two items show padding reverting to normal. + */ +const MediaTemplate = () => withLayout(html` +

Primary text only

+ + + ${leadingThumbnail} + + + ${leadingThumbnail} + + + ${leadingThumbnail} + + + +

With secondary text

+ + + ${leadingThumbnail} + + + ${leadingThumbnail} + + + ${leadingThumbnail} + + +`); + +export const Media = createStory(MediaTemplate, defaultArgs)(); + /** * `is-compact` reduces the vertical space of each item. */ diff --git a/packages/components/pie-list/src/pie-list-item/defs.ts b/packages/components/pie-list/src/pie-list-item/defs.ts index d8bbbe5234..121a976ae2 100644 --- a/packages/components/pie-list/src/pie-list-item/defs.ts +++ b/packages/components/pie-list/src/pie-list-item/defs.ts @@ -25,6 +25,13 @@ export interface ListItemProps { * Sets the primary text to use a bold font-weight. */ isBold: boolean + + /** + * Reduces the block padding to suit larger slotted media (such as a pie-thumbnail). + * + * **Note**: This has no effect when `secondaryText` is set, and should not be combined with `isCompact`. + */ + hasMedia: boolean } export type DefaultProps = ComponentDefaultProps>; @@ -32,4 +39,5 @@ export type DefaultProps = ComponentDefaultProps Date: Wed, 1 Jul 2026 19:44:09 +0100 Subject: [PATCH 18/34] readme and slot policing --- .../stories/testing/pie-list.test.stories.ts | 2 +- packages/components/pie-list/README.md | 291 +++++++++++++++--- .../pie-list/src/pie-list-item/list-item.scss | 10 +- 3 files changed, 249 insertions(+), 54 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 7a58123242..4093007a43 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -183,7 +183,7 @@ export const MetaText = createStory(MetaTextTemplate, defaultArgs)(); /** * `has-media` reduces the block padding to suit a larger slotted pie-thumbnail. - * The reduced padding only applies when there is no secondary text — the last + * The reduced padding only applies when there is no secondary text. The last * two items show padding reverting to normal. */ const MediaTemplate = () => withLayout(html` diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 17fb83c956..e5dfadebda 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -1,91 +1,282 @@ -

- -

+# @justeattakeaway/pie-list +[Source Code](https://github.com/justeattakeaway/pie/tree/main/packages/components/pie-list) | [Design Documentation](https://pie.design/components/list) | [NPM](https://www.npmjs.com/package/@justeattakeaway/pie-list) -

+

GitHub Workflow Status

-# Table of Contents +`@justeattakeaway/pie-list` is a Web Component built using the Lit library. It provides a simple, accessible list built from two elements: a `pie-list` container and one or more `pie-list-item` children. -1. [Introduction](#pie-list) -2. [Installation](#installation) -3. [Importing the component](#importing-the-component) -4. [Peer Dependencies](#peer-dependencies) -5. [Props](#props) -6. [Contributing](#contributing) +> [!NOTE] +> This component is still growing. More properties, slots and sub-components will be added over time. This documentation covers what is currently supported. -## pie-list +## Table of Contents -`pie-list` is a Web Component built using the Lit library. +- [Installation](#installation) +- [Documentation](#documentation) + - [Properties](#properties) + - [Slots](#slots) + - [CSS Variables](#css-variables) + - [Events](#events) +- [Accessibility](#accessibility) +- [Usage Examples](#usage-examples) +- [Usage Notes and Rules](#usage-notes-and-rules) +- [Questions and Support](#questions-and-support) +- [Contributing](#contributing) -This component can be easily integrated into various frontend frameworks and customized through a set of properties. +## Installation +> To install any of our web components in your application, we would suggest following the [getting started guide](https://webc.pie.design/?path=/docs/introduction-getting-started--docs) to set up your project. -## Installation +Ideally, you should install the component using the **`@justeattakeaway/pie-webc`** package, which includes all of the components. Or you can install the individual component package. + +## Documentation + +`pie-list` is a compound component. The `pie-list` element is the container (it applies the `list` role and renders the dividers between items), and each row is a `pie-list-item` (which applies the `listitem` role). + +> [!IMPORTANT] +> Because `pie-list` uses the `list` role, you **must always give it an accessible name** using either `aria-label` or `aria-labelledby`. Without one, screen reader users get no context for what the list contains. See [Accessibility](#accessibility). + +### Properties + +#### `pie-list` + +`pie-list` currently has no configurable properties. It acts as the semantic container for `pie-list-item` children. + +#### `pie-list-item` + +| Prop | Options | Description | Default | +|---|---|---|---| +| `primaryText` | Any string | **Required.** The main text of the item, providing an overview of the content. | `undefined` | +| `secondaryText` | Any string | Optional additional detail, rendered on a second line beneath the primary text. | `undefined` | +| `metaText` | Any string | Optional supporting information about the item's context, status or attributes. Rendered as a trailing text string. **Mutually exclusive with the `trailing` slot**: if `metaText` is set, the `trailing` slot is not rendered. | `undefined` | +| `isCompact` | `true`, `false` | Decreases the item height to save vertical space. See the [rules](#usage-notes-and-rules) below. | `false` | +| `isBold` | `true`, `false` | Sets the primary text to a bold font-weight. | `false` | +| `hasMedia` | `true`, `false` | **Required whenever you slot a media element (e.g. `pie-thumbnail`) into the item.** Slotted media is hidden unless this is set. It also reduces the block padding so single-line media sits correctly (this padding adjustment has no effect when `secondaryText` is set, but you should still set `hasMedia`). | `false` | + +### Slots + +Slots are provided by `pie-list-item`. + +| Slot | Description | +|---|---| +| `leading` | Content displayed at the start of the item, before the text. Intended for a small icon or a media element (e.g. `pie-thumbnail`). | +| `trailing` | Content displayed at the end of the item, after the text. Intended for a small icon, a `pie-tag`, etc. Not rendered when `metaText` is set. | + +The permitted slotted elements are: a PIE icon, `pie-tag`, `pie-thumbnail`, `pie-avatar`*, `pie-switch`, and radio/checkbox inputs. -To install `pie-list` in your application, run the following on your command line: +> \* `pie-avatar` is a permitted slot element but is not covered by usage examples here yet, as it is not ready for use in lists. -```bash -# npm -$ npm i @justeattakeaway/pie-list +### CSS Variables -# yarn -$ yarn add @justeattakeaway/pie-list +These custom properties can be set on a `pie-list-item` (or on `pie-list` to affect all items) to override the defaults. + +| Variable | Description | Accepted values | +|---|---|---| +| `--list-item-inline-padding-override` | Overrides the inline (start and end) padding of the item. | Any PIE spacing token (e.g. `var(--dt-spacing-f)`) or `0` | +| `--list-item-alignment-override` | Overrides the vertical alignment of the item's content. | Only `center` is recommended | + +### Events + +This component does not emit any custom events. To listen for interactions, treat slotted interactive elements like native HTML elements in your application. + +## Accessibility + +`pie-list` renders with `role="list"` and each `pie-list-item` with `role="listitem"`. + +> [!IMPORTANT] +> **Always provide an accessible name for `pie-list`** via `aria-label` or `aria-labelledby`. A list with the `list` role has no inherent name, so without one a screen reader announces "list" with no indication of what it contains. + +- Use **`aria-label`** when there is no visible heading for the list: + + ```html + + + + + ``` + +- Use **`aria-labelledby`** to reference a visible heading (preferred when one exists, so the visible and accessible names stay in sync): + + ```html +

Payment methods

+ + + + + ``` + +## Usage Examples + +**For HTML:** + +```js +// import as modules into a js file e.g. main.js +import '@justeattakeaway/pie-webc/components/list.js'; +import '@justeattakeaway/pie-webc/components/list-item.js'; ``` -For full information on using PIE components as part of an application, check out the [Getting Started Guide](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components). +A basic list with primary, secondary and meta text (note the required `aria-label`): +```html + + + + + + +``` -### Importing the component +**Leading and trailing content (icons):** -#### JavaScript ```js -// Default – for Native JS Applications, Vue, Angular, Svelte, etc. -import { PieList } from '@justeattakeaway/pie-list'; +import '@justeattakeaway/pie-icons-webc/dist/IconChevronRight.js'; +``` -// If you don't need to reference the imported object, you can simply -// import the module which registers the component as a custom element. -import '@justeattakeaway/pie-list'; +```html + + + + + + ``` -#### React +**Trailing `pie-tag`:** + ```js -// React -// For React, you will need to import our React-specific component build -// which wraps the web component using ​@lit/react -import { PieList } from '@justeattakeaway/pie-list/dist/react'; +import '@justeattakeaway/pie-webc/components/tag.js'; ``` -> [!NOTE] -> When using the React version of the component, please make sure to also -> include React as a [peer dependency](#peer-dependencies) in your project. +```html + + + Label + + +``` +**Meta text** (renders as trailing text; do not combine with the `trailing` slot): -## Peer Dependencies +```html + + + +``` -> [!IMPORTANT] -> When using `pie-list`, you will also need to include a couple of dependencies to ensure the component renders as expected. See [the PIE Wiki](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components#expected-dependencies) for more information and how to include these in your application. +**Bold primary text:** + +```html + + + +``` +**Compact list** (reduced height, see [rules](#usage-notes-and-rules)): -## Props +```html + + + + + + + + +``` -| Property | Type | Default | Description | -|---|---|---|---| -| - | - | - | - | +**Media (`pie-thumbnail`) in the leading slot:** -In your markup or JSX, you can then use these to set the properties for the `pie-list` component: +```js +import '@justeattakeaway/pie-webc/components/thumbnail.js'; +``` ```html - - + + + + + + - - + + + + + + ``` +**Overriding alignment and padding** (via CSS variables): + +```html + + + + + + + + + + + +``` + +**For Native JS Applications, Vue, Angular, Svelte etc.:** + +```js +// Vue templates (using Nuxt 3) +import '@justeattakeaway/pie-webc/components/list.js'; +import '@justeattakeaway/pie-webc/components/list-item.js'; + + + + +``` + +**For React Applications:** + +```jsx +import { PieList } from '@justeattakeaway/pie-webc/react/list.js'; +import { PieListItem } from '@justeattakeaway/pie-webc/react/list-item.js'; + + + + + + +``` + +## Usage Notes and Rules + +To keep lists consistent and correct, follow these rules: + +- **Always give `pie-list` an accessible name** with `aria-label` or `aria-labelledby` (use `aria-labelledby` when a visible heading exists). This is required for screen reader users to understand the list. See [Accessibility](#accessibility). +- **`primaryText` is required** on every `pie-list-item`. An item with no primary text will not render. +- **`metaText` and the `trailing` slot are mutually exclusive.** If `metaText` is set, any `trailing` slot content is ignored. Choose one. +- **Slotted `pie-thumbnail` must use `size="40"`.** This is the only size that fits the list-item layout correctly. +- **Always set `has-media` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. **Slotted media is hidden if `has-media` is not set.** This is deliberate, to guarantee the item has the correct padding and to prevent misuse. On single-line items `has-media` also reduces the block padding; with `secondaryText` the padding is unchanged but you must still set it. +- **Do not combine `is-compact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. As a safeguard, slotted media is hidden when `is-compact` is set (even if `has-media` is present), but you should not rely on this; simply avoid the combination. +- **Only use `center` for `--list-item-alignment-override`.** Other values are not supported. +- **`pie-avatar` is not yet ready** for use in lists. Prefer `pie-thumbnail` for media for now. + +## Questions and Support + +If you work at Just Eat Takeaway.com, please contact us on **#help-designsystem**. Otherwise, please raise an issue on [Github](https://github.com/justeattakeaway/pie/issues). + ## Contributing -Check out our [contributing guide](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide) for more information on [local development](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#local-development) and how to run specific [component tests](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#testing). \ No newline at end of file +Check out our [contributing guide](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide) for more information on [local development](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#local-development) and how to run specific [component tests](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#testing). diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index d1044cdcbe..616b55df4e 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -34,7 +34,7 @@ } // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only -// when the primary text stands alone (`:only-child` == no secondary text) — with +// when the primary text stands alone (`:only-child` == no secondary text). With // secondary text the default padding is kept so two lines of text sit correctly. :host([has-media]):has(.c-listItem-primaryText:only-child) { --list-item-block-padding-default: var(--dt-spacing-b); @@ -74,9 +74,13 @@ --icon-size-override: 24px } -// Media (pie-avatar / pie-thumbnail) is too large for a compact item, so it is -// not displayed when `is-compact` is set. +// Slotted media (pie-avatar / pie-thumbnail) is only shown when the consumer opts +// in with `has-media`. This guarantees the item always has the correct block +// padding for media and prevents accidental misuse. Media is also hidden in compact +// items, where it is not permitted (`is-compact` is for single-line items only). // TODO: add icon with background here once it exists +:host(:not([has-media])) ::slotted(pie-avatar), +:host(:not([has-media])) ::slotted(pie-thumbnail), :host([is-compact]) ::slotted(pie-avatar), :host([is-compact]) ::slotted(pie-thumbnail) { display: none; From e6dde0ff607c26db313db8f89de0781849191929 Mon Sep 17 00:00:00 2001 From: personal Date: Thu, 2 Jul 2026 13:39:35 +0100 Subject: [PATCH 19/34] add tests + readme and fix metatext single line alignment --- apps/pie-storybook/stories/pie-list.mdx | 6 + .../pie-storybook/stories/pie-list.stories.ts | 225 +++++++++++++++++- .../stories/testing/pie-list.test.stories.ts | 214 ++++++++++------- packages/components/pie-list/README.md | 4 +- .../pie-list/src/pie-list-item/list-item.scss | 11 +- .../test/accessibility/pie-list.spec.ts | 2 +- .../pie-list/test/component/pie-list.spec.ts | 77 +++++- .../pie-list/test/visual/pie-list.spec.ts | 31 ++- .../scss/mixins/_enforceSlotElements.scss | 4 +- 9 files changed, 455 insertions(+), 119 deletions(-) create mode 100644 apps/pie-storybook/stories/pie-list.mdx diff --git a/apps/pie-storybook/stories/pie-list.mdx b/apps/pie-storybook/stories/pie-list.mdx new file mode 100644 index 0000000000..40c77c43e2 --- /dev/null +++ b/apps/pie-storybook/stories/pie-list.mdx @@ -0,0 +1,6 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { Markdown } from '@storybook/addon-docs/blocks'; +import readme from '@justeattakeaway/pie-list/README.md?raw'; + + +{readme} diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index a06a9a00eb..dcad3478b5 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -1,19 +1,80 @@ -import { html } from 'lit'; +import { html, nothing } from 'lit'; import { type Meta } from '@storybook/web-components'; import '@justeattakeaway/pie-webc/components/list'; -import { type ListProps } from '@justeattakeaway/pie-webc/components/list'; +import '@justeattakeaway/pie-webc/components/list-item'; +import '@justeattakeaway/pie-webc/components/thumbnail'; +import '@justeattakeaway/pie-webc/components/tag'; +import '@justeattakeaway/pie-icons-webc/dist/IconPlaceholder'; -import { createStory } from '../utilities'; +import { createStory, type TemplateFunction } from '../utilities'; -type ListStoryMeta = Meta; +// The list itself has no props; these are the `pie-list-item` props plus two +// helper controls (`leadingContent` / `trailingContent`) for experimenting with +// the slots. The latter two are not component props. +type ListPlaygroundProps = { + primaryText: string; + secondaryText: string; + metaText: string; + isCompact: boolean; + isBold: boolean; + hasMedia: boolean; + leadingContent: 'none' | 'icon' | 'thumbnail'; + trailingContent: 'none' | 'icon' | 'tag'; +}; + +type ListStoryMeta = Meta; -const defaultArgs: ListProps = {}; +const defaultArgs: ListPlaygroundProps = { + primaryText: 'Primary text', + secondaryText: 'Secondary text', + metaText: '', + isCompact: false, + isBold: false, + hasMedia: false, + leadingContent: 'none', + trailingContent: 'icon', +}; const listStoryMeta: ListStoryMeta = { title: 'Components/List', component: 'pie-list', - argTypes: {}, + argTypes: { + primaryText: { + description: 'The main text of the item. **Required**. An item with no primary text renders nothing.', + control: 'text', + }, + secondaryText: { + description: 'Optional additional detail, rendered on a second line.', + control: 'text', + }, + metaText: { + description: 'Optional trailing text. Mutually exclusive with the `trailing` slot (if set, the trailing slot is not rendered).', + control: 'text', + }, + isCompact: { + description: 'Reduces the item height. Do not use with secondary text or slotted media.', + control: 'boolean', + }, + isBold: { + description: 'Sets the primary text to a bold font-weight.', + control: 'boolean', + }, + hasMedia: { + description: 'Required to display slotted media (e.g. `pie-thumbnail`). Also reduces block padding on single-line items.', + control: 'boolean', + }, + leadingContent: { + description: '**Not a component prop.** Chooses what to slot into the `leading` slot.', + control: 'select', + options: ['none', 'icon', 'thumbnail'], + }, + trailingContent: { + description: '**Not a component prop.** Chooses what to slot into the `trailing` slot.', + control: 'select', + options: ['none', 'icon', 'tag'], + }, + }, args: defaultArgs, parameters: { design: { @@ -25,10 +86,152 @@ const listStoryMeta: ListStoryMeta = { export default listStoryMeta; -// TODO: remove the eslint-disable rule when props are added -// eslint-disable-next-line no-empty-pattern -const Template = ({}: ListProps) => html` - +// Slot content helpers ------------------------------------------------------- + +const renderLeading = (leadingContent: ListPlaygroundProps['leadingContent']) => { + if (leadingContent === 'icon') return html``; + if (leadingContent === 'thumbnail') return html``; + return nothing; +}; + +const renderTrailing = (trailingContent: ListPlaygroundProps['trailingContent']) => { + if (trailingContent === 'icon') return html``; + if (trailingContent === 'tag') return html`Label`; + return nothing; +}; + +const renderItem = (args: ListPlaygroundProps) => html` + + ${renderLeading(args.leadingContent)} + ${renderTrailing(args.trailingContent)} + `; -export const Default = createStory(Template, defaultArgs)(); +// Surfaces the component's usage rules when the controls are set to a combination +// that the component intentionally handles (so the behaviour is not surprising). +const buildNotes = (args: ListPlaygroundProps) => { + const notes: string[] = []; + if (args.leadingContent === 'thumbnail' && !args.hasMedia) notes.push('Enable "hasMedia" to display the slotted thumbnail.'); + if (args.isCompact && args.leadingContent === 'thumbnail') notes.push('Media is not displayed in compact items.'); + if (args.isCompact && args.secondaryText) notes.push('Avoid secondary text in compact items.'); + if (args.metaText && args.trailingContent !== 'none') notes.push('"metaText" replaces the trailing slot, so the trailing content is not shown.'); + return notes; +}; + +/** + * Every story renders the same arg-driven list (so the controls are live on all of + * them). `pie-list` must always have an accessible name; we use `aria-labelledby` + * pointing at a visible heading (preferred over `aria-label`, so the visible and + * accessible names stay in sync). `headingId` is unique per story to keep the + * association valid when several stories render on one docs page. + */ +const makeListTemplate = (headingId: string, heading: string, listStyle = ''): TemplateFunction => (args) => { + const notes = buildNotes(args); + + return html` + + ${notes.length ? html`

Note: ${notes.join(' ')}

` : nothing} +

${heading}

+ + ${renderItem(args)} + ${renderItem(args)} + ${renderItem(args)} + + `; +}; + +// Stories -------------------------------------------------------------------- + +/** + * Interactive example. Use the controls to experiment with the `pie-list-item` + * properties and slotted content. + */ +export const Playground = createStory( + makeListTemplate('list-playground-heading', 'Example list'), + defaultArgs, +)(); + +/** + * `isBold` sets the primary text to a bold font-weight. + */ +export const Bold = createStory( + makeListTemplate('list-bold-heading', 'Bold primary text'), + defaultArgs, +)({ isBold: true }); + +/** + * `isCompact` reduces the vertical space of each item. Do not use it with + * secondary text or slotted media. + */ +export const Compact = createStory( + makeListTemplate('list-compact-heading', 'Compact'), + defaultArgs, +)({ isCompact: true, secondaryText: '' }); + +/** + * A trailing `pie-tag` slotted into each item. + */ +export const WithTag = createStory( + makeListTemplate('list-tag-heading', 'Trailing tag'), + defaultArgs, +)({ trailingContent: 'tag' }); + +/** + * `metaText` renders a trailing text string. It is mutually exclusive with the + * `trailing` slot. + */ +export const MetaText = createStory( + makeListTemplate('list-meta-heading', 'Meta text'), + defaultArgs, +)({ metaText: 'Meta text', trailingContent: 'none' }); + +/** + * Slotted media (`pie-thumbnail`, always `size="40"`) requires `hasMedia`. Toggle + * `secondaryText` in the controls to see the block padding adjust. + */ +export const Media = createStory( + makeListTemplate('list-media-heading', 'Media'), + defaultArgs, +)({ hasMedia: true, leadingContent: 'thumbnail', trailingContent: 'none' }); + +/** + * `--list-item-alignment-override: center` vertically centres the item content. + */ +export const AlignmentOverride = createStory( + makeListTemplate('list-alignment-heading', 'Centre aligned', '--list-item-alignment-override: center;'), + defaultArgs, +)(); + +/** + * `--list-item-inline-padding-override` overrides the inline padding of the items + * (any spacing token, or `0` to remove it). + */ +export const InlinePaddingOverride = createStory( + makeListTemplate('list-padding-heading', 'No inline padding', '--list-item-inline-padding-override: 0;'), + defaultArgs, +)({ leadingContent: 'none' }); + +/** + * Long text wraps within each item. + */ +export const LongText = createStory( + makeListTemplate('list-long-text-heading', 'Long content'), + defaultArgs, +)({ + primaryText: 'Primary text that goes on far too long Primary text that goes on far too long', + secondaryText: 'Secondary text that goes on far too long Secondary text that goes on far too long', + metaText: 'Some very long awful meta text', + leadingContent: 'icon', + trailingContent: 'none', +}); diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 4093007a43..0ffd9c6861 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -44,8 +44,9 @@ const leadingThumbnail = html` html` ${content} @@ -53,21 +54,6 @@ const withLayout = (content: TemplateResult) => html` // Stories -------------------------------------------------------------------- -/** - * The minimal item: primary text only, with no secondary text, meta text or - * slotted content. - */ -const PrimaryTextOnlyTemplate = () => withLayout(html` - - - - - - -`); - -export const PrimaryTextOnly = createStory(PrimaryTextOnlyTemplate, defaultArgs)(); - /** * Text-only items: primary and secondary text with no leading/trailing content. */ @@ -97,74 +83,57 @@ const BoldTemplate = () => withLayout(html` export const Bold = createStory(BoldTemplate, defaultArgs)(); /** - * A leading icon slotted into each item. + * Both a leading and a trailing icon on each item. */ -const LeadingIconTemplate = () => withLayout(html` +const LeadingAndTrailingTemplate = () => withLayout(html` ${leadingIcon} - - - ${leadingIcon} - - - ${leadingIcon} - - - ${leadingIcon} - - -`); - -export const LeadingIcon = createStory(LeadingIconTemplate, defaultArgs)(); - -/** - * A trailing icon slotted into each item. - */ -const TrailingIconTemplate = () => withLayout(html` - - ${trailingIcon} + ${leadingIcon} ${trailingIcon} + ${leadingIcon} ${trailingIcon} + ${leadingIcon} ${trailingIcon} `); -export const TrailingIcon = createStory(TrailingIconTemplate, defaultArgs)(); +export const LeadingAndTrailing = createStory(LeadingAndTrailingTemplate, defaultArgs)(); /** - * Both a leading and a trailing icon on each item. + * Leading and trailing icons with only primary text (no secondary text), to + * verify centre alignment on a single-line item. */ -const LeadingAndTrailingTemplate = () => withLayout(html` +const LeadingAndTrailingOnlyPrimaryTextTemplate = () => withLayout(html` - + ${leadingIcon} ${trailingIcon} - + ${leadingIcon} ${trailingIcon} - + ${leadingIcon} ${trailingIcon} - + ${leadingIcon} ${trailingIcon} `); -export const LeadingAndTrailing = createStory(LeadingAndTrailingTemplate, defaultArgs)(); +export const LeadingAndTrailingOnlyPrimaryText = createStory(LeadingAndTrailingOnlyPrimaryTextTemplate, defaultArgs)(); /** * `metaText` renders a trailing text string (mutually exclusive with the @@ -181,6 +150,22 @@ const MetaTextTemplate = () => withLayout(html` export const MetaText = createStory(MetaTextTemplate, defaultArgs)(); +/** + * `metaText` with only primary text (no secondary text). The meta text's + * line-height is adjusted to match the primary text so both sit on the same + * baseline. + */ +const MetaTextOnlyPrimaryTextTemplate = () => withLayout(html` + + + + + + +`); + +export const MetaTextOnlyPrimaryText = createStory(MetaTextOnlyPrimaryTextTemplate, defaultArgs)(); + /** * `has-media` reduces the block padding to suit a larger slotted pie-thumbnail. * The reduced padding only applies when there is no secondary text. The last @@ -268,28 +253,6 @@ const TagsTemplate = () => withLayout(html` export const Tags = createStory(TagsTemplate, defaultArgs)(); -/** - * Trailing tags within compact items. - */ -const TagsCompactTemplate = () => withLayout(html` - - - ${trailingTag} - - - ${trailingTag} - - - ${trailingTag} - - - ${trailingTag} - - -`); - -export const TagsCompact = createStory(TagsCompactTemplate, defaultArgs)(); - /** * `--list-item-alignment-override: center` vertically centres the item content. */ @@ -320,28 +283,6 @@ const AlignmentOverrideTemplate = () => withLayout(html` export const AlignmentOverride = createStory(AlignmentOverrideTemplate, defaultArgs)(); -/** - * `--list-item-inline-padding-override` set to a larger spacing token. - */ -const CustomPaddingTemplate = () => withLayout(html` - - - ${trailingIcon} - - - ${trailingIcon} - - - ${trailingIcon} - - - ${trailingIcon} - - -`); - -export const CustomPadding = createStory(CustomPaddingTemplate, defaultArgs)(); - /** * `--list-item-inline-padding-override: 0` removes the inline padding entirely. */ @@ -403,6 +344,40 @@ const LongTextTemplate = () => withLayout(html` export const LongText = createStory(LongTextTemplate, defaultArgs)(); +/** + * Very long primary and meta text (no secondary text) with a leading icon, to + * verify wrapping and baseline alignment behaviour. + */ +const LongTextMetaTextOnlyPrimaryTextTemplate = () => withLayout(html` + + + ${leadingIcon} + + + + ${leadingIcon} + + + + ${leadingIcon} + + + + ${leadingIcon} + + +`); + +export const LongTextMetaTextOnlyPrimaryText = createStory(LongTextMetaTextOnlyPrimaryTextTemplate, defaultArgs)(); + /** * Long text combined with the centre alignment override, a leading icon and a * trailing tag. @@ -440,3 +415,60 @@ const LongTextCentreAlignedTemplate = () => withLayout(html` `); export const LongTextCentreAligned = createStory(LongTextCentreAlignedTemplate, defaultArgs)(); + +// Test-only stories ----------------------------------------------------------- +// The stories below exist purely to drive browser tests. They are not intended +// as usage showcases. + +/** + * Test-only: an item with no `primaryText`. The item should render nothing. + */ +const NoPrimaryTextTemplate = () => withLayout(html` + + + +`); + +export const NoPrimaryText = createStory(NoPrimaryTextTemplate, defaultArgs)(); + +/** + * Test-only: `metaText` and the `trailing` slot set on the same item. `metaText` + * takes precedence and the trailing slot is not rendered. + */ +const MetaTextWithTrailingTemplate = () => withLayout(html` + + + ${trailingTag} + + +`); + +export const MetaTextWithTrailing = createStory(MetaTextWithTrailingTemplate, defaultArgs)(); + +/** + * Test-only: a thumbnail slotted without `has-media`. The thumbnail should be + * hidden (media requires an explicit `has-media` opt-in). + */ +const MediaWithoutHasMediaTemplate = () => withLayout(html` + + + ${leadingThumbnail} + + +`); + +export const MediaWithoutHasMedia = createStory(MediaWithoutHasMediaTemplate, defaultArgs)(); + +/** + * Test-only: a thumbnail slotted into a compact item (with `has-media`). The + * thumbnail should be hidden because media is not permitted in compact items. + */ +const MediaCompactTemplate = () => withLayout(html` + + + ${leadingThumbnail} + + +`); + +export const MediaCompact = createStory(MediaCompactTemplate, defaultArgs)(); diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index e5dfadebda..9d493da1e3 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -62,11 +62,13 @@ Slots are provided by `pie-list-item`. | Slot | Description | |---|---| -| `leading` | Content displayed at the start of the item, before the text. Intended for a small icon or a media element (e.g. `pie-thumbnail`). | +| `leading` | Content displayed at the start of the item, before the text. Intended for a small icon or a media element (e.g. `pie-thumbnail`). If slotting `pie-thumbnail`, it MUST use `size="40"`; this is the only size that fits the list-item layout correctly. | | `trailing` | Content displayed at the end of the item, after the text. Intended for a small icon, a `pie-tag`, etc. Not rendered when `metaText` is set. | The permitted slotted elements are: a PIE icon, `pie-tag`, `pie-thumbnail`, `pie-avatar`*, `pie-switch`, and radio/checkbox inputs. +> Slotted PIE icons are always sized by `pie-list-item` (24px). Consumers cannot override this size. + > \* `pie-avatar` is a permitted slot element but is not covered by usage examples here yet, as it is not ready for use in lists. ### CSS Variables diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 616b55df4e..bce045dec1 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -23,16 +23,25 @@ min-height: 56px; } +// Two lines of text (primary + secondary) need top-alignment so leading/trailing +// content sits level with the first line, not centred across both lines. :host:has(.c-listItem-secondaryText) { + align-items: var(--list-item-alignment-override, flex-start); min-height: 76px; } :host([is-compact]) { --list-item-block-padding-default: var(--dt-spacing-c); - align-items: center; + align-items: var(--list-item-alignment-override, center); min-height: 48px; } +:host(:not([is-compact])):has(.c-listItem-primaryText:only-child) .c-listItem-metaText { + // Ensures that meta text sits centrally with primary text when no secondary text present. Other trailing content is at least 24px height so they sit correctly already. + // The trade off is multi-line meta text will have a slightly larger gap between lines. But the choice is between that and misalignment. This edge case seems like a reasonable trade-off. + line-height: calc(var(--dt-font-body-l-line-height) * 1px); +} + // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only // when the primary text stands alone (`:only-child` == no secondary text). With // secondary text the default padding is kept so two lines of text sit correctly. diff --git a/packages/components/pie-list/test/accessibility/pie-list.spec.ts b/packages/components/pie-list/test/accessibility/pie-list.spec.ts index 425b0e79fe..b6916d5ea3 100644 --- a/packages/components/pie-list/test/accessibility/pie-list.spec.ts +++ b/packages/components/pie-list/test/accessibility/pie-list.spec.ts @@ -4,7 +4,7 @@ import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-obj test.describe('PieList - Accessibility tests', () => { test('a11y - should test the PieList component WCAG compliance', async ({ page, makeAxeBuilder }) => { // Arrange - const basePage = new BasePage(page, 'list--default'); + const basePage = new BasePage(page, 'list--leading-and-trailing'); await basePage.load(); diff --git a/packages/components/pie-list/test/component/pie-list.spec.ts b/packages/components/pie-list/test/component/pie-list.spec.ts index e2be4671ed..7157dfa473 100644 --- a/packages/components/pie-list/test/component/pie-list.spec.ts +++ b/packages/components/pie-list/test/component/pie-list.spec.ts @@ -1,19 +1,82 @@ import { test, expect } from '@playwright/test'; import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; -const componentSelector = '[data-test-id="pie-list"]'; - test.describe('PieList - Component tests', () => { - test('should render successfully', async ({ page }) => { + test('should render the list and its items with the correct ARIA roles', async ({ page }) => { // Arrange - const basePage = new BasePage(page, 'list--default'); - - await basePage.load(); + await new BasePage(page, 'list--text-only').load(); // Act - const list = page.locator(componentSelector); + const list = page.getByRole('list'); + const items = page.getByRole('listitem'); // Assert await expect(list).toBeVisible(); + await expect(items).toHaveCount(4); + }); + + test.describe('primaryText', () => { + test('should render nothing when primaryText is not provided', async ({ page }) => { + // Arrange + await new BasePage(page, 'list--no-primary-text').load(); + + // Act + const item = page.locator('pie-list-item'); + const primaryText = item.locator('.c-listItem-primaryText'); + + // Assert + await expect(item).toHaveCount(1); // the element still exists... + await expect(primaryText).toHaveCount(0); // ...but renders no content + }); + }); + + test.describe('metaText', () => { + test('should render metaText and not the trailing slot when both are provided', async ({ page }) => { + // Arrange + await new BasePage(page, 'list--meta-text-with-trailing').load(); + + // Act + const metaText = page.locator('.c-listItem-metaText'); + const trailingContent = page.locator('pie-tag[slot="trailing"]'); + + // Assert + await expect(metaText).toHaveText('Meta text'); + await expect(trailingContent).toBeHidden(); // no trailing slot is rendered, so it cannot project + }); + }); + + test.describe('slotted media (has-media)', () => { + test('should display slotted media when has-media is set', async ({ page }) => { + // Arrange + await new BasePage(page, 'list--media').load(); + + // Act + const thumbnail = page.locator('pie-thumbnail').first(); + + // Assert + await expect(thumbnail).toBeVisible(); + }); + + test('should hide slotted media when has-media is not set', async ({ page }) => { + // Arrange + await new BasePage(page, 'list--media-without-has-media').load(); + + // Act + const thumbnail = page.locator('pie-thumbnail'); + + // Assert + await expect(thumbnail).toBeHidden(); + }); + + test('should hide slotted media in a compact item even when has-media is set', async ({ page }) => { + // Arrange + await new BasePage(page, 'list--media-compact').load(); + + // Act + const thumbnail = page.locator('pie-thumbnail'); + + // Assert + await expect(thumbnail).toBeHidden(); + }); }); }); diff --git a/packages/components/pie-list/test/visual/pie-list.spec.ts b/packages/components/pie-list/test/visual/pie-list.spec.ts index 8cd6d38ddc..c3ea9bb570 100644 --- a/packages/components/pie-list/test/visual/pie-list.spec.ts +++ b/packages/components/pie-list/test/visual/pie-list.spec.ts @@ -2,11 +2,32 @@ import { test } from '@playwright/test'; import percySnapshot from '@percy/playwright'; import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; -test.describe('PieList - Visual tests`', () => { - test('should display the PieList component successfully', async ({ page }) => { - const basePage = new BasePage(page, 'list--default'); - await basePage.load(); +// Each entry is a story that exercises a visually distinct state. Appearance +// concerns (padding, spacing, dividers, alignment, typography, wrapping) are +// verified here rather than in the component (JS) tests. +const visualStories: { storyId: string, snapshotName: string }[] = [ + { storyId: 'list--text-only', snapshotName: 'PieList - Text only' }, + { storyId: 'list--leading-and-trailing', snapshotName: 'PieList - Leading and trailing content' }, + { storyId: 'list--leading-and-trailing-only-primary-text', snapshotName: 'PieList - Leading and trailing content (only primary text)' }, + { storyId: 'list--tags', snapshotName: 'PieList - Trailing tags' }, + { storyId: 'list--meta-text', snapshotName: 'PieList - Meta text' }, + { storyId: 'list--meta-text-only-primary-text', snapshotName: 'PieList - Meta text (only primary text)' }, + { storyId: 'list--bold', snapshotName: 'PieList - Bold primary text' }, + { storyId: 'list--compact', snapshotName: 'PieList - Compact' }, + { storyId: 'list--media', snapshotName: 'PieList - Media (has-media block padding)' }, + { storyId: 'list--long-text', snapshotName: 'PieList - Long text wrapping' }, + { storyId: 'list--long-text-meta-text-only-primary-text', snapshotName: 'PieList - Long text, meta text with only primary text' }, + { storyId: 'list--long-text-centre-aligned', snapshotName: 'PieList - Long text, centre aligned' }, + { storyId: 'list--alignment-override', snapshotName: 'PieList - Alignment override' }, + { storyId: 'list--removed-padding', snapshotName: 'PieList - Removed inline padding' }, +]; - await percySnapshot(page, 'PieList - Visual Test'); +test.describe('PieList - Visual tests', () => { + visualStories.forEach(({ storyId, snapshotName }) => { + test(`should display the ${snapshotName} story correctly`, async ({ page }) => { + await new BasePage(page, storyId).load(); + + await percySnapshot(page, snapshotName); + }); }); }); diff --git a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss index 282f3c83f1..d50d12af80 100644 --- a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss +++ b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss @@ -24,14 +24,14 @@ /// } @mixin enforce-slot-elements($allowed-elements, $slot-names...) { // 1. If no slot names are passed, default to an empty string (targets default slot) - $slots-to-process: if(list.length($slot-names) == 0, (''), $slot-names); + $slots-to-process: if(sass(list.length($slot-names) == 0): (''); else: $slot-names); $hide-selectors: (); $allowlist-selectors: (); // 2. Loop through every slot name provided @each $slot in $slots-to-process { // Create the specific attribute selector for this loop iteration - $slot-selector: if($slot != '', '[slot="#{$slot}"]', ''); + $slot-selector: if(sass($slot != ''): '[slot="#{$slot}"]'; else: ''); // 3. Build the 'hide' selector for this specific slot $hide-selectors: list.append( From 6ee328c4c81b0c58b14badfe38efafe77e83c537 Mon Sep 17 00:00:00 2001 From: personal Date: Thu, 2 Jul 2026 13:41:09 +0100 Subject: [PATCH 20/34] fix lint --- .../pie-list/src/pie-list-item/list-item.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index bce045dec1..4d21180405 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -36,12 +36,6 @@ min-height: 48px; } -:host(:not([is-compact])):has(.c-listItem-primaryText:only-child) .c-listItem-metaText { - // Ensures that meta text sits centrally with primary text when no secondary text present. Other trailing content is at least 24px height so they sit correctly already. - // The trade off is multi-line meta text will have a slightly larger gap between lines. But the choice is between that and misalignment. This edge case seems like a reasonable trade-off. - line-height: calc(var(--dt-font-body-l-line-height) * 1px); -} - // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only // when the primary text stands alone (`:only-child` == no secondary text). With // secondary text the default padding is kept so two lines of text sit correctly. @@ -115,3 +109,9 @@ @include p.font-theme('font-body-s'); color: var(--dt-color-content-subdued); } + +// Ensures that meta text sits centrally with primary text when no secondary text present. Other trailing content is at least 24px height so they sit correctly already. +// The trade off is multi-line meta text will have a slightly larger gap between lines. But the choice is between that and misalignment. This edge case seems like a reasonable trade-off. +:host(:not([is-compact])):has(.c-listItem-primaryText:only-child) .c-listItem-metaText { + line-height: calc(var(--dt-font-body-l-line-height) * 1px); +} From ad6a8e0b14b8e99eddc8dde0e8f5037399ec9371 Mon Sep 17 00:00:00 2001 From: personal Date: Thu, 2 Jul 2026 14:12:05 +0100 Subject: [PATCH 21/34] test scss mixin --- .changeset/fiery-terms-obey.md | 5 + .changeset/great-carpets-feel.md | 5 + .changeset/new-rules-repair.md | 5 + .../pie-list/src/pie-list-item/list-item.scss | 6 - .../scss/mixins/_enforceSlotElements.scss | 2 +- .../unit/mixins/enforceSlotElements.spec.ts | 216 ++++++++++++++++++ 6 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 .changeset/fiery-terms-obey.md create mode 100644 .changeset/great-carpets-feel.md create mode 100644 .changeset/new-rules-repair.md create mode 100644 packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts diff --git a/.changeset/fiery-terms-obey.md b/.changeset/fiery-terms-obey.md new file mode 100644 index 0000000000..4996e15cf6 --- /dev/null +++ b/.changeset/fiery-terms-obey.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-storybook": minor +--- + +[Added] - New pie list examples for the plain text list/list item diff --git a/.changeset/great-carpets-feel.md b/.changeset/great-carpets-feel.md new file mode 100644 index 0000000000..44e7708253 --- /dev/null +++ b/.changeset/great-carpets-feel.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-css": minor +--- + +[Added] - new `enforce-slot-elements` scss mixin that allows components to define allow-lists of elements that can be placed into a slot diff --git a/.changeset/new-rules-repair.md b/.changeset/new-rules-repair.md new file mode 100644 index 0000000000..b33f129c3d --- /dev/null +++ b/.changeset/new-rules-repair.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-list": minor +--- + +[Added] - Plain text list/list item component variation. diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 4d21180405..caefe8e154 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -109,9 +109,3 @@ @include p.font-theme('font-body-s'); color: var(--dt-color-content-subdued); } - -// Ensures that meta text sits centrally with primary text when no secondary text present. Other trailing content is at least 24px height so they sit correctly already. -// The trade off is multi-line meta text will have a slightly larger gap between lines. But the choice is between that and misalignment. This edge case seems like a reasonable trade-off. -:host(:not([is-compact])):has(.c-listItem-primaryText:only-child) .c-listItem-metaText { - line-height: calc(var(--dt-font-body-l-line-height) * 1px); -} diff --git a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss index d50d12af80..9e58fba68d 100644 --- a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss +++ b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss @@ -24,7 +24,7 @@ /// } @mixin enforce-slot-elements($allowed-elements, $slot-names...) { // 1. If no slot names are passed, default to an empty string (targets default slot) - $slots-to-process: if(sass(list.length($slot-names) == 0): (''); else: $slot-names); + $slots-to-process: if(sass(list.length($slot-names) == 0): ('') ; else: $slot-names); $hide-selectors: (); $allowlist-selectors: (); diff --git a/packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts b/packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts new file mode 100644 index 0000000000..b1f69fabce --- /dev/null +++ b/packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts @@ -0,0 +1,216 @@ +import { + describe, + it, + expect, +} from 'vitest'; + +import { compileCss, stripCSSWhitespace } from '../../../../utilities/compileCss'; + +describe('mixins.enforce-slot-elements', () => { + describe('default slot (no slot names provided)', () => { + it('should hide slotted elements that are not in the allowlist', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('p', 'span')) { + font-size: 1rem; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted(:not(p, span)) { + display: none; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toContain(expectedCss); + }); + + it('should apply the content block to each allowed element', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('p', 'span')) { + font-size: 1rem; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted(p), .foo ::slotted(span) { + font-size: 1rem; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toContain(expectedCss); + }); + + it('should support a single allowed element', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('span',)) { + font-weight: bold; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted(:not(span)) { + display: none; + } + .foo ::slotted(span) { + font-weight: bold; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toBe(expectedCss); + }); + }); + + describe('single named slot', () => { + it('should hide non-allowed elements scoped to the named slot', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('h1', 'h2'), 'header-slot') { + color: blue; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted([slot=header-slot]:not(h1, h2)) { + display: none; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toContain(expectedCss); + }); + + it('should apply the content block only to allowed elements in that slot', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('h1', 'h2'), 'header-slot') { + color: blue; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted(h1[slot=header-slot]), .foo ::slotted(h2[slot=header-slot]) { + color: blue; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toContain(expectedCss); + }); + + it('should not affect the default (unnamed) slot', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('h1', 'h2'), 'header-slot') { + color: blue; + } + } + `; + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).not.toContain('::slotted(:not(h1,h2))'); + }); + }); + + describe('multiple named slots', () => { + it('should build combined hide selectors for every slot', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('img', 'picture'), 'hero-image', 'footer-image') { + max-width: 100%; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted([slot=hero-image]:not(img, picture)), + .foo ::slotted([slot=footer-image]:not(img, picture)) { + display: none; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toContain(expectedCss); + }); + + it('should build combined allowlist selectors for every slot/element pairing', () => { + // Arrange + const scssToTest = ` + @use 'mixins'; + + .foo { + @include mixins.enforce-slot-elements(('img', 'picture'), 'hero-image', 'footer-image') { + max-width: 100%; + } + } + `; + + const expectedCss = stripCSSWhitespace(` + .foo ::slotted(img[slot=hero-image]), + .foo ::slotted(picture[slot=hero-image]), + .foo ::slotted(img[slot=footer-image]), + .foo ::slotted(picture[slot=footer-image]) { + max-width: 100%; + } + `); + + // Act + const css = stripCSSWhitespace(compileCss(scssToTest)); + + // Assert + expect(css).toContain(expectedCss); + }); + }); +}); From dc88e5b47738be12b0ef2d603d9d8d57ff4aff37 Mon Sep 17 00:00:00 2001 From: personal Date: Thu, 2 Jul 2026 14:19:31 +0100 Subject: [PATCH 22/34] tidy ts --- packages/components/pie-list/src/pie-list-item/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index 2cfea26a02..b06576433a 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -17,7 +17,7 @@ export class PieListItem extends PieElement implements ListItemProps { primaryText!: ListItemProps['primaryText']; @property({ type: String }) - secondaryText: ListItemProps['metaText']; + secondaryText: ListItemProps['secondaryText']; @property({ type: String }) metaText: ListItemProps['metaText']; @@ -32,11 +32,11 @@ export class PieListItem extends PieElement implements ListItemProps { hasMedia = defaultProps.hasMedia; connectedCallback () { + super.connectedCallback(); + if (!this.hasAttribute('role')) { this.setAttribute('role', 'listitem'); } - - super.connectedCallback(); } _renderSecondaryText () { From 81930b6797cf8e25f3530c721492395be665a887 Mon Sep 17 00:00:00 2001 From: personal Date: Thu, 2 Jul 2026 15:24:50 +0100 Subject: [PATCH 23/34] safari firefox style fix --- .../stories/testing/pie-list.test.stories.ts | 22 +++++++++++++++ packages/components/pie-list/src/list.scss | 3 +-- .../pie-list/src/pie-list-item/index.ts | 27 ++++++++++++------- .../pie-list/src/pie-list-item/list-item.scss | 23 ++++++++++------ .../pie-list/test/visual/pie-list.spec.ts | 1 + 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 0ffd9c6861..811a3a395e 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -305,6 +305,28 @@ const RemovedPaddingTemplate = () => withLayout(html` export const RemovedPadding = createStory(RemovedPaddingTemplate, defaultArgs)(); +/** + * Verifies the divider border pie-list applies between items. Deliberately + * omits `withLayout`'s decorative purple border so the real divider (and its + * absence on the last item) is visible without visual noise. + */ +const BordersTemplate = () => html` + + + + + + + +`; + +export const Borders = createStory(BordersTemplate, defaultArgs)(); + // Edge cases ----------------------------------------------------------------- /** diff --git a/packages/components/pie-list/src/list.scss b/packages/components/pie-list/src/list.scss index 3d0bd95843..ffc47c34f6 100644 --- a/packages/components/pie-list/src/list.scss +++ b/packages/components/pie-list/src/list.scss @@ -6,8 +6,7 @@ ::slotted(*:nth-last-child(n+2)) { // This allows list-items following other list-items to have 1px stripped off of their padding to account for the added border. --list-item-size-modifier: 1px; - - border-bottom: var(--list-item-size-modifier) solid var(--dt-color-divider-default); + --list-item-border-override: var(--list-item-size-modifier) solid var(--dt-color-divider-default); } } diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index b06576433a..77fcffe108 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -65,16 +65,23 @@ export class PieListItem extends PieElement implements ListItemProps { if (!primaryText) return nothing; return html` -
- -
- -
- ${primaryText} - ${this._renderSecondaryText()} -
- - ${this._renderTrailingContent()}`; +
+
+ +
+ +
+ ${primaryText} + ${this._renderSecondaryText()} +
+ + ${this._renderTrailingContent()} +
`; } // Renders a `CSSResult` generated from SCSS by Vite diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index caefe8e154..28b2ae3147 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -1,6 +1,12 @@ @use '@justeattakeaway/pie-css/scss' as p; :host { + // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell + display: contents; +} + +// Using an internal container div rather than applying directly to the host due to safari and firefox compat issues +.c-listItem-container { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); @@ -21,16 +27,17 @@ align-items: var(--list-item-alignment-override, flex-start); gap: var(--dt-spacing-c); min-height: 56px; + border-bottom: var(--list-item-border-override, none); } // Two lines of text (primary + secondary) need top-alignment so leading/trailing // content sits level with the first line, not centred across both lines. -:host:has(.c-listItem-secondaryText) { +.c-listItem-container:has(.c-listItem-secondaryText) { align-items: var(--list-item-alignment-override, flex-start); min-height: 76px; } -:host([is-compact]) { +.c-listItem-container[is-compact] { --list-item-block-padding-default: var(--dt-spacing-c); align-items: var(--list-item-alignment-override, center); min-height: 48px; @@ -39,7 +46,7 @@ // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only // when the primary text stands alone (`:only-child` == no secondary text). With // secondary text the default padding is kept so two lines of text sit correctly. -:host([has-media]):has(.c-listItem-primaryText:only-child) { +.c-listItem-container[has-media]:has(.c-listItem-primaryText:only-child) { --list-item-block-padding-default: var(--dt-spacing-b); } @@ -82,10 +89,10 @@ // padding for media and prevents accidental misuse. Media is also hidden in compact // items, where it is not permitted (`is-compact` is for single-line items only). // TODO: add icon with background here once it exists -:host(:not([has-media])) ::slotted(pie-avatar), -:host(:not([has-media])) ::slotted(pie-thumbnail), -:host([is-compact]) ::slotted(pie-avatar), -:host([is-compact]) ::slotted(pie-thumbnail) { +.c-listItem-container:not([has-media]) ::slotted(pie-avatar), +.c-listItem-container:not([has-media]) ::slotted(pie-thumbnail), +.c-listItem-container[is-compact] ::slotted(pie-avatar), +.c-listItem-container[is-compact] ::slotted(pie-thumbnail) { display: none; } @@ -96,7 +103,7 @@ color: var(--dt-color-content-default); } -:host([is-bold]) .c-listItem-primaryText { +.c-listItem-container[is-bold] .c-listItem-primaryText { @include p.font-theme('font-body-strong-l'); } diff --git a/packages/components/pie-list/test/visual/pie-list.spec.ts b/packages/components/pie-list/test/visual/pie-list.spec.ts index c3ea9bb570..63f20030ab 100644 --- a/packages/components/pie-list/test/visual/pie-list.spec.ts +++ b/packages/components/pie-list/test/visual/pie-list.spec.ts @@ -20,6 +20,7 @@ const visualStories: { storyId: string, snapshotName: string }[] = [ { storyId: 'list--long-text-centre-aligned', snapshotName: 'PieList - Long text, centre aligned' }, { storyId: 'list--alignment-override', snapshotName: 'PieList - Alignment override' }, { storyId: 'list--removed-padding', snapshotName: 'PieList - Removed inline padding' }, + { storyId: 'list--borders', snapshotName: 'PieList - Borders between items' }, ]; test.describe('PieList - Visual tests', () => { From d0a6b4828a1a801c83c8cdaa885aaace99119461 Mon Sep 17 00:00:00 2001 From: personal Date: Fri, 3 Jul 2026 13:13:01 +0100 Subject: [PATCH 24/34] display block list-items --- packages/components/pie-list/src/pie-list-item/list-item.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 28b2ae3147..074245e23b 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -2,7 +2,7 @@ :host { // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell - display: contents; + display: block; } // Using an internal container div rather than applying directly to the host due to safari and firefox compat issues From 8016ae424ab10651b62b913e75a1540cdd243700 Mon Sep 17 00:00:00 2001 From: personal Date: Fri, 3 Jul 2026 15:02:22 +0100 Subject: [PATCH 25/34] remove slot policing --- .changeset/great-carpets-feel.md | 5 - .../stories/testing/pie-list.test.stories.ts | 28 --- packages/components/pie-list/README.md | 13 +- packages/components/pie-list/src/list.scss | 2 - .../pie-list/src/pie-list-item/list-item.scss | 14 -- .../pie-list/test/component/pie-list.spec.ts | 22 -- .../scss/mixins/_enforceSlotElements.scss | 62 ----- packages/tools/pie-css/scss/mixins/index.scss | 1 - .../unit/mixins/enforceSlotElements.spec.ts | 216 ------------------ 9 files changed, 7 insertions(+), 356 deletions(-) delete mode 100644 .changeset/great-carpets-feel.md delete mode 100644 packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss delete mode 100644 packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts diff --git a/.changeset/great-carpets-feel.md b/.changeset/great-carpets-feel.md deleted file mode 100644 index 44e7708253..0000000000 --- a/.changeset/great-carpets-feel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@justeattakeaway/pie-css": minor ---- - -[Added] - new `enforce-slot-elements` scss mixin that allows components to define allow-lists of elements that can be placed into a slot diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 811a3a395e..6116bdd2f1 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -466,31 +466,3 @@ const MetaTextWithTrailingTemplate = () => withLayout(html` `); export const MetaTextWithTrailing = createStory(MetaTextWithTrailingTemplate, defaultArgs)(); - -/** - * Test-only: a thumbnail slotted without `has-media`. The thumbnail should be - * hidden (media requires an explicit `has-media` opt-in). - */ -const MediaWithoutHasMediaTemplate = () => withLayout(html` - - - ${leadingThumbnail} - - -`); - -export const MediaWithoutHasMedia = createStory(MediaWithoutHasMediaTemplate, defaultArgs)(); - -/** - * Test-only: a thumbnail slotted into a compact item (with `has-media`). The - * thumbnail should be hidden because media is not permitted in compact items. - */ -const MediaCompactTemplate = () => withLayout(html` - - - ${leadingThumbnail} - - -`); - -export const MediaCompact = createStory(MediaCompactTemplate, defaultArgs)(); diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 9d493da1e3..54bfaf8668 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -54,7 +54,7 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc | `metaText` | Any string | Optional supporting information about the item's context, status or attributes. Rendered as a trailing text string. **Mutually exclusive with the `trailing` slot**: if `metaText` is set, the `trailing` slot is not rendered. | `undefined` | | `isCompact` | `true`, `false` | Decreases the item height to save vertical space. See the [rules](#usage-notes-and-rules) below. | `false` | | `isBold` | `true`, `false` | Sets the primary text to a bold font-weight. | `false` | -| `hasMedia` | `true`, `false` | **Required whenever you slot a media element (e.g. `pie-thumbnail`) into the item.** Slotted media is hidden unless this is set. It also reduces the block padding so single-line media sits correctly (this padding adjustment has no effect when `secondaryText` is set, but you should still set `hasMedia`). | `false` | +| `hasMedia` | `true`, `false` | **Required whenever you slot a media element (e.g. `pie-thumbnail`) into the item.** Reduces the block padding so single-line media sits correctly (this padding adjustment has no effect when `secondaryText` is set, but you should still set `hasMedia`). | `false` | ### Slots @@ -65,7 +65,8 @@ Slots are provided by `pie-list-item`. | `leading` | Content displayed at the start of the item, before the text. Intended for a small icon or a media element (e.g. `pie-thumbnail`). If slotting `pie-thumbnail`, it MUST use `size="40"`; this is the only size that fits the list-item layout correctly. | | `trailing` | Content displayed at the end of the item, after the text. Intended for a small icon, a `pie-tag`, etc. Not rendered when `metaText` is set. | -The permitted slotted elements are: a PIE icon, `pie-tag`, `pie-thumbnail`, `pie-avatar`*, `pie-switch`, and radio/checkbox inputs. +The permitted slotted elements are: a PIE WEBC icon, `pie-tag`, `pie-thumbnail`, `pie-avatar`*, `pie-switch`, and native HTML radio/checkbox inputs. +Some slotted content is designed with specific properties being used. So please read the entire readme to understand correct slot usage. > Slotted PIE icons are always sized by `pie-list-item` (24px). Consumers cannot override this size. @@ -205,7 +206,7 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; ```html + block padding will be incorrect. --> @@ -213,7 +214,7 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; + in this case, but you should still set it. --> @@ -270,8 +271,8 @@ To keep lists consistent and correct, follow these rules: - **`primaryText` is required** on every `pie-list-item`. An item with no primary text will not render. - **`metaText` and the `trailing` slot are mutually exclusive.** If `metaText` is set, any `trailing` slot content is ignored. Choose one. - **Slotted `pie-thumbnail` must use `size="40"`.** This is the only size that fits the list-item layout correctly. -- **Always set `has-media` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. **Slotted media is hidden if `has-media` is not set.** This is deliberate, to guarantee the item has the correct padding and to prevent misuse. On single-line items `has-media` also reduces the block padding; with `secondaryText` the padding is unchanged but you must still set it. -- **Do not combine `is-compact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. As a safeguard, slotted media is hidden when `is-compact` is set (even if `has-media` is present), but you should not rely on this; simply avoid the combination. +- **Always set `has-media` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding. +- **Do not combine `is-compact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. - **Only use `center` for `--list-item-alignment-override`.** Other values are not supported. - **`pie-avatar` is not yet ready** for use in lists. Prefer `pie-thumbnail` for media for now. diff --git a/packages/components/pie-list/src/list.scss b/packages/components/pie-list/src/list.scss index ffc47c34f6..40aa64d3a5 100644 --- a/packages/components/pie-list/src/list.scss +++ b/packages/components/pie-list/src/list.scss @@ -9,5 +9,3 @@ --list-item-border-override: var(--list-item-size-modifier) solid var(--dt-color-divider-default); } } - -@include p.enforce-slot-elements(('pie-list-item')); diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 074245e23b..4cca8091e1 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -78,24 +78,10 @@ max-width: calc(100% / 3); } -@include p.enforce-slot-elements(('.c-pieIcon', 'input[type="radio"]', 'input[type="checkbox"]', 'pie-switch', 'pie-avatar', 'pie-thumbnail', 'pie-tag'), 'leading', 'trailing'); - ::slotted(.c-pieIcon) { --icon-size-override: 24px } -// Slotted media (pie-avatar / pie-thumbnail) is only shown when the consumer opts -// in with `has-media`. This guarantees the item always has the correct block -// padding for media and prevents accidental misuse. Media is also hidden in compact -// items, where it is not permitted (`is-compact` is for single-line items only). -// TODO: add icon with background here once it exists -.c-listItem-container:not([has-media]) ::slotted(pie-avatar), -.c-listItem-container:not([has-media]) ::slotted(pie-thumbnail), -.c-listItem-container[is-compact] ::slotted(pie-avatar), -.c-listItem-container[is-compact] ::slotted(pie-thumbnail) { - display: none; -} - // typography .c-listItem-primaryText { diff --git a/packages/components/pie-list/test/component/pie-list.spec.ts b/packages/components/pie-list/test/component/pie-list.spec.ts index 7157dfa473..b91941607d 100644 --- a/packages/components/pie-list/test/component/pie-list.spec.ts +++ b/packages/components/pie-list/test/component/pie-list.spec.ts @@ -56,27 +56,5 @@ test.describe('PieList - Component tests', () => { // Assert await expect(thumbnail).toBeVisible(); }); - - test('should hide slotted media when has-media is not set', async ({ page }) => { - // Arrange - await new BasePage(page, 'list--media-without-has-media').load(); - - // Act - const thumbnail = page.locator('pie-thumbnail'); - - // Assert - await expect(thumbnail).toBeHidden(); - }); - - test('should hide slotted media in a compact item even when has-media is set', async ({ page }) => { - // Arrange - await new BasePage(page, 'list--media-compact').load(); - - // Act - const thumbnail = page.locator('pie-thumbnail'); - - // Assert - await expect(thumbnail).toBeHidden(); - }); }); }); diff --git a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss b/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss deleted file mode 100644 index 9e58fba68d..0000000000 --- a/packages/tools/pie-css/scss/mixins/_enforceSlotElements.scss +++ /dev/null @@ -1,62 +0,0 @@ -@use 'sass:string'; -@use 'sass:list'; - -/// Enforces a strict allowlist of elements for one or more web component slots. -/// Any slotted elements not in the allowlist will be hidden (`display: none`). -/// -/// @param {List} $allowed-elements - A list of permitted CSS selectors (e.g., `('h1', 'p')`). -/// @param {ArgList} $slot-names... - 0 or more slot names. If omitted, targets the default slot. -/// @content The styles to apply exclusively to the allowed elements. -/// -/// @example scss - Usage for the default slot (0 slot names) -/// @include enforce-slot-elements(('p', 'span')) { -/// font-size: 1rem; -/// } -/// -/// @example scss - Usage for a specific named slot (1 slot name) -/// @include enforce-slot-elements(('h1', 'h2'), 'header-slot') { -/// color: blue; -/// } -/// -/// @example scss - Usage for multiple named slots (1+ slot names) -/// @include enforce-slot-elements(('img', 'picture'), 'hero-image', 'footer-image') { -/// max-width: 100%; -/// } -@mixin enforce-slot-elements($allowed-elements, $slot-names...) { - // 1. If no slot names are passed, default to an empty string (targets default slot) - $slots-to-process: if(sass(list.length($slot-names) == 0): ('') ; else: $slot-names); - $hide-selectors: (); - $allowlist-selectors: (); - - // 2. Loop through every slot name provided - @each $slot in $slots-to-process { - // Create the specific attribute selector for this loop iteration - $slot-selector: if(sass($slot != ''): '[slot="#{$slot}"]'; else: ''); - - // 3. Build the 'hide' selector for this specific slot - $hide-selectors: list.append( - $hide-selectors, - string.unquote('::slotted(#{$slot-selector}:not(#{$allowed-elements}))'), - comma - ); - - // 4. Build the 'allowlist' selectors for this specific slot - @each $el in $allowed-elements { - $combined-selector: '#{$el}#{$slot-selector}'; - $allowlist-selectors: list.append( - $allowlist-selectors, - string.unquote('::slotted(#{$combined-selector})'), - comma - ); - } - } - - // 5. Output the final comma-separated selectors - #{$hide-selectors} { - display: none; - } - - #{$allowlist-selectors} { - @content; - } -} diff --git a/packages/tools/pie-css/scss/mixins/index.scss b/packages/tools/pie-css/scss/mixins/index.scss index 066e409751..437d9efe58 100644 --- a/packages/tools/pie-css/scss/mixins/index.scss +++ b/packages/tools/pie-css/scss/mixins/index.scss @@ -5,4 +5,3 @@ @forward './font-theme'; @forward './components'; @forward './disablePointerEventsInSlottedIcons'; -@forward './enforceSlotElements'; diff --git a/packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts b/packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts deleted file mode 100644 index b1f69fabce..0000000000 --- a/packages/tools/pie-css/test/scss/unit/mixins/enforceSlotElements.spec.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { - describe, - it, - expect, -} from 'vitest'; - -import { compileCss, stripCSSWhitespace } from '../../../../utilities/compileCss'; - -describe('mixins.enforce-slot-elements', () => { - describe('default slot (no slot names provided)', () => { - it('should hide slotted elements that are not in the allowlist', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('p', 'span')) { - font-size: 1rem; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted(:not(p, span)) { - display: none; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toContain(expectedCss); - }); - - it('should apply the content block to each allowed element', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('p', 'span')) { - font-size: 1rem; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted(p), .foo ::slotted(span) { - font-size: 1rem; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toContain(expectedCss); - }); - - it('should support a single allowed element', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('span',)) { - font-weight: bold; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted(:not(span)) { - display: none; - } - .foo ::slotted(span) { - font-weight: bold; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toBe(expectedCss); - }); - }); - - describe('single named slot', () => { - it('should hide non-allowed elements scoped to the named slot', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('h1', 'h2'), 'header-slot') { - color: blue; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted([slot=header-slot]:not(h1, h2)) { - display: none; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toContain(expectedCss); - }); - - it('should apply the content block only to allowed elements in that slot', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('h1', 'h2'), 'header-slot') { - color: blue; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted(h1[slot=header-slot]), .foo ::slotted(h2[slot=header-slot]) { - color: blue; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toContain(expectedCss); - }); - - it('should not affect the default (unnamed) slot', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('h1', 'h2'), 'header-slot') { - color: blue; - } - } - `; - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).not.toContain('::slotted(:not(h1,h2))'); - }); - }); - - describe('multiple named slots', () => { - it('should build combined hide selectors for every slot', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('img', 'picture'), 'hero-image', 'footer-image') { - max-width: 100%; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted([slot=hero-image]:not(img, picture)), - .foo ::slotted([slot=footer-image]:not(img, picture)) { - display: none; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toContain(expectedCss); - }); - - it('should build combined allowlist selectors for every slot/element pairing', () => { - // Arrange - const scssToTest = ` - @use 'mixins'; - - .foo { - @include mixins.enforce-slot-elements(('img', 'picture'), 'hero-image', 'footer-image') { - max-width: 100%; - } - } - `; - - const expectedCss = stripCSSWhitespace(` - .foo ::slotted(img[slot=hero-image]), - .foo ::slotted(picture[slot=hero-image]), - .foo ::slotted(img[slot=footer-image]), - .foo ::slotted(picture[slot=footer-image]) { - max-width: 100%; - } - `); - - // Act - const css = stripCSSWhitespace(compileCss(scssToTest)); - - // Assert - expect(css).toContain(expectedCss); - }); - }); -}); From fb1a0597074ba1b5e4a16dab803af4d97a19f9e9 Mon Sep 17 00:00:00 2001 From: personal Date: Fri, 3 Jul 2026 15:52:45 +0100 Subject: [PATCH 26/34] apply gap padding to slotted trailing content and metatext instead of text --- .../components/pie-list/src/pie-list-item/list-item.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 4cca8091e1..546a01b25f 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -52,9 +52,12 @@ .c-listItem-text { flex: 1; +} +slot[name="trailing"]::slotted(*), +.c-listItem-metaText { // Add a tiny bit of padding to the flex gap to make up 16px total - padding-inline-end: var(--dt-spacing-a); + padding-inline-start: var(--dt-spacing-a); } // Ensures that empty slots do not create a flex-gap in the container. From d1b3cb33443aa5703e9da32e625aaaf750dc79cc Mon Sep 17 00:00:00 2001 From: personal Date: Fri, 3 Jul 2026 22:23:36 +0100 Subject: [PATCH 27/34] rename default story --- apps/pie-storybook/stories/pie-list.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index dcad3478b5..fd31ad7c94 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -157,7 +157,7 @@ const makeListTemplate = (headingId: string, heading: string, listStyle = ''): T * Interactive example. Use the controls to experiment with the `pie-list-item` * properties and slotted content. */ -export const Playground = createStory( +export const Default = createStory( makeListTemplate('list-playground-heading', 'Example list'), defaultArgs, )(); From 72f5f1e318dafab03399af5f228747e0bae62e60 Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 08:57:43 +0100 Subject: [PATCH 28/34] no longer reflect props --- packages/components/pie-list/src/pie-list-item/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index 77fcffe108..10c3c4f30f 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -22,13 +22,13 @@ export class PieListItem extends PieElement implements ListItemProps { @property({ type: String }) metaText: ListItemProps['metaText']; - @property({ type: Boolean, attribute: 'is-compact', reflect: true }) + @property({ type: Boolean, attribute: 'is-compact' }) isCompact = defaultProps.isCompact; - @property({ type: Boolean, attribute: 'is-bold', reflect: true }) + @property({ type: Boolean, attribute: 'is-bold' }) isBold = defaultProps.isBold; - @property({ type: Boolean, attribute: 'has-media', reflect: true }) + @property({ type: Boolean, attribute: 'has-media' }) hasMedia = defaultProps.hasMedia; connectedCallback () { From 16cf329afec251e7efbe188dee415d7b6b9809ee Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 09:09:30 +0100 Subject: [PATCH 29/34] classmap --- .../pie-list/src/pie-list-item/index.ts | 15 +++++++------ .../pie-list/src/pie-list-item/list-item.scss | 21 ++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index 10c3c4f30f..d6902d040d 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -2,6 +2,7 @@ import { html, nothing, unsafeCSS } from 'lit'; import { property } from 'lit/decorators.js'; import { safeCustomElement, requiredProperty } from '@justeattakeaway/pie-webc-core'; import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement'; +import { classMap } from 'lit/directives/class-map.js'; import { type ListItemProps, defaultProps } from './defs'; import styles from './list-item.scss?inline'; @@ -64,13 +65,15 @@ export class PieListItem extends PieElement implements ListItemProps { // We should never render a list item that doesn't have primary text. if (!primaryText) return nothing; + const containerClasses = { + 'c-listItem-container': true, + 'is-compact': this.isCompact, + 'is-bold': this.isBold, + 'has-media': this.hasMedia, + }; + return html` -
+
diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 546a01b25f..fddf8aad3d 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -1,19 +1,18 @@ @use '@justeattakeaway/pie-css/scss' as p; :host { - // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell - display: block; -} - -// Using an internal container div rather than applying directly to the host due to safari and firefox compat issues -.c-listItem-container { --list-item-inline-padding-default: var(--dt-spacing-d); --list-item-block-padding-default: var(--dt-spacing-d); // The list component will add a border to each list-item that follows another. This ensures the height accounts for the added border. --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); - // TODO: document --list-item-inline-padding-override in component readme (any spacing token or 0) + // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell + display: block; +} + +// Using an internal container div rather than applying directly to the host due to safari and firefox compat issues +.c-listItem-container { padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); @@ -22,8 +21,6 @@ // Layout display: flex; - - // TODO: document --list-item-alignment-override in component readme (only recommend using 'center') align-items: var(--list-item-alignment-override, flex-start); gap: var(--dt-spacing-c); min-height: 56px; @@ -37,7 +34,7 @@ min-height: 76px; } -.c-listItem-container[is-compact] { +.c-listItem-container.is-compact { --list-item-block-padding-default: var(--dt-spacing-c); align-items: var(--list-item-alignment-override, center); min-height: 48px; @@ -46,7 +43,7 @@ // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only // when the primary text stands alone (`:only-child` == no secondary text). With // secondary text the default padding is kept so two lines of text sit correctly. -.c-listItem-container[has-media]:has(.c-listItem-primaryText:only-child) { +.c-listItem-container.has-media:has(.c-listItem-primaryText:only-child) { --list-item-block-padding-default: var(--dt-spacing-b); } @@ -92,7 +89,7 @@ slot[name="trailing"]::slotted(*), color: var(--dt-color-content-default); } -.c-listItem-container[is-bold] .c-listItem-primaryText { +.c-listItem-container.is-bold .c-listItem-primaryText { @include p.font-theme('font-body-strong-l'); } From 490c8e3ca67ccd9f650ae2ee670092efa1703f3a Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 10:05:07 +0100 Subject: [PATCH 30/34] tidy up css overrides and variable names --- .../pie-storybook/stories/pie-list.stories.ts | 4 +-- .../stories/testing/pie-list.test.stories.ts | 4 +-- packages/components/pie-list/README.md | 4 +-- .../pie-list/src/pie-list-item/list-item.scss | 25 +++++++++++-------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index fd31ad7c94..b478d146ac 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -214,11 +214,11 @@ export const AlignmentOverride = createStory( )(); /** - * `--list-item-inline-padding-override` overrides the inline padding of the items + * `--list-item-inline-padding` sets the inline padding of the items * (any spacing token, or `0` to remove it). */ export const InlinePaddingOverride = createStory( - makeListTemplate('list-padding-heading', 'No inline padding', '--list-item-inline-padding-override: 0;'), + makeListTemplate('list-padding-heading', 'No inline padding', '--list-item-inline-padding: 0;'), defaultArgs, )({ leadingContent: 'none' }); diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 6116bdd2f1..cf69666b20 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -284,10 +284,10 @@ const AlignmentOverrideTemplate = () => withLayout(html` export const AlignmentOverride = createStory(AlignmentOverrideTemplate, defaultArgs)(); /** - * `--list-item-inline-padding-override: 0` removes the inline padding entirely. + * `--list-item-inline-padding: 0` removes the inline padding entirely. */ const RemovedPaddingTemplate = () => withLayout(html` - + ${trailingIcon} diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 54bfaf8668..61036cea6c 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -78,7 +78,7 @@ These custom properties can be set on a `pie-list-item` (or on `pie-list` to aff | Variable | Description | Accepted values | |---|---|---| -| `--list-item-inline-padding-override` | Overrides the inline (start and end) padding of the item. | Any PIE spacing token (e.g. `var(--dt-spacing-f)`) or `0` | +| `--list-item-inline-padding` | Sets the inline (start and end) padding of the item. Defaults to `var(--dt-spacing-d)`. | Any PIE spacing token (e.g. `var(--dt-spacing-f)`) or `0` | | `--list-item-alignment-override` | Overrides the vertical alignment of the item's content. | Only `center` is recommended | ### Events @@ -233,7 +233,7 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; - + ``` diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index fddf8aad3d..2d75b74e0a 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -1,11 +1,9 @@ @use '@justeattakeaway/pie-css/scss' as p; :host { - --list-item-inline-padding-default: var(--dt-spacing-d); - --list-item-block-padding-default: var(--dt-spacing-d); - - // The list component will add a border to each list-item that follows another. This ensures the height accounts for the added border. - --list-item-block-end-padding-default: calc(var(--list-item-block-padding-default) - var(--list-item-size-modifier)); + // Defined on the host so consumers can override it directly (a declaration on the element + // takes precedence over this default). + --list-item-inline-padding: var(--dt-spacing-d); // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell display: block; @@ -13,11 +11,16 @@ // Using an internal container div rather than applying directly to the host due to safari and firefox compat issues .c-listItem-container { - padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); - padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding-default)); + --list-item-block-padding: var(--dt-spacing-d); + + // The list component will add a border to each list-item that follows another. This ensures the height accounts for the added border. + --list-item-block-end-padding: calc(var(--list-item-block-padding) - var(--list-item-size-modifier)); + + padding-inline-start: var(--list-item-inline-padding); + padding-inline-end: var(--list-item-inline-padding); - padding-block-start: var(--list-item-block-padding-default); - padding-block-end: var(--list-item-block-end-padding-default, var(--list-item-block-padding-default)); + padding-block-start: var(--list-item-block-padding); + padding-block-end: var(--list-item-block-end-padding, var(--list-item-block-padding)); // Layout display: flex; @@ -35,7 +38,7 @@ } .c-listItem-container.is-compact { - --list-item-block-padding-default: var(--dt-spacing-c); + --list-item-block-padding: var(--dt-spacing-c); align-items: var(--list-item-alignment-override, center); min-height: 48px; } @@ -44,7 +47,7 @@ // when the primary text stands alone (`:only-child` == no secondary text). With // secondary text the default padding is kept so two lines of text sit correctly. .c-listItem-container.has-media:has(.c-listItem-primaryText:only-child) { - --list-item-block-padding-default: var(--dt-spacing-b); + --list-item-block-padding: var(--dt-spacing-b); } .c-listItem-text { From 8770bcba2f6ed89fc8a935ef262d8b00a1179f9e Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 10:32:00 +0100 Subject: [PATCH 31/34] update css --- apps/pie-storybook/stories/pie-list.stories.ts | 17 ++++++++++------- .../stories/testing/pie-list.test.stories.ts | 14 ++++++++------ packages/components/pie-list/README.md | 9 +++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index b478d146ac..57269351bc 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -100,8 +100,9 @@ const renderTrailing = (trailingContent: ListPlaygroundProps['trailingContent']) return nothing; }; -const renderItem = (args: ListPlaygroundProps) => html` +const renderItem = (args: ListPlaygroundProps, itemStyle = '') => html` { * them). `pie-list` must always have an accessible name; we use `aria-labelledby` * pointing at a visible heading (preferred over `aria-label`, so the visible and * accessible names stay in sync). `headingId` is unique per story to keep the - * association valid when several stories render on one docs page. + * association valid when several stories render on one docs page. `itemStyle` is applied to each + * `pie-list-item` (not the list) so item-level CSS variables such as `--list-item-inline-padding` + * take effect (a value on `pie-list` cannot override the item's own default). */ -const makeListTemplate = (headingId: string, heading: string, listStyle = ''): TemplateFunction => (args) => { +const makeListTemplate = (headingId: string, heading: string, itemStyle = ''): TemplateFunction => (args) => { const notes = buildNotes(args); return html` @@ -143,10 +146,10 @@ const makeListTemplate = (headingId: string, heading: string, listStyle = ''): T ${notes.length ? html`

Note: ${notes.join(' ')}

` : nothing}

${heading}

- - ${renderItem(args)} - ${renderItem(args)} - ${renderItem(args)} + + ${renderItem(args, itemStyle)} + ${renderItem(args, itemStyle)} + ${renderItem(args, itemStyle)} `; }; diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index cf69666b20..0f971e68f8 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -284,20 +284,22 @@ const AlignmentOverrideTemplate = () => withLayout(html` export const AlignmentOverride = createStory(AlignmentOverrideTemplate, defaultArgs)(); /** - * `--list-item-inline-padding: 0` removes the inline padding entirely. + * `--list-item-inline-padding: 0` removes the inline padding entirely. It must be set on the + * `pie-list-item` (here via a class), not on `pie-list` — the item defines the default on its host. */ const RemovedPaddingTemplate = () => withLayout(html` - - + + + ${trailingIcon} - + ${trailingIcon} - + ${trailingIcon} - + ${trailingIcon} diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 61036cea6c..6f686de333 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -78,7 +78,7 @@ These custom properties can be set on a `pie-list-item` (or on `pie-list` to aff | Variable | Description | Accepted values | |---|---|---| -| `--list-item-inline-padding` | Sets the inline (start and end) padding of the item. Defaults to `var(--dt-spacing-d)`. | Any PIE spacing token (e.g. `var(--dt-spacing-f)`) or `0` | +| `--list-item-inline-padding` | Sets the inline (start and end) padding of the item. Defaults to `var(--dt-spacing-d)`. **Must be set on the `pie-list-item`** (directly or via a rule targeting it), not on `pie-list` — the default lives on the item's host, so an inherited value from `pie-list` will not override it. | Any PIE spacing token (e.g. `var(--dt-spacing-f)`) or `0` | | `--list-item-alignment-override` | Overrides the vertical alignment of the item's content. | Only `center` is recommended | ### Events @@ -232,9 +232,10 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; - - - + + + ``` From 73d4dbe57486128264001027f025a04d386b11c3 Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 10:38:04 +0100 Subject: [PATCH 32/34] update alignment override var --- apps/pie-storybook/stories/pie-list.stories.ts | 4 ++-- .../stories/testing/pie-list.test.stories.ts | 17 ++++++++++++++--- packages/components/pie-list/README.md | 10 ++++++---- .../pie-list/src/pie-list-item/list-item.scss | 15 ++++++++++----- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index 57269351bc..31e7536213 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -209,10 +209,10 @@ export const Media = createStory( )({ hasMedia: true, leadingContent: 'thumbnail', trailingContent: 'none' }); /** - * `--list-item-alignment-override: center` vertically centres the item content. + * `--list-item-alignment: center` vertically centres the item content. */ export const AlignmentOverride = createStory( - makeListTemplate('list-alignment-heading', 'Centre aligned', '--list-item-alignment-override: center;'), + makeListTemplate('list-alignment-heading', 'Centre aligned', '--list-item-alignment: center;'), defaultArgs, )(); diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 0f971e68f8..0cda1b55f7 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -254,26 +254,32 @@ const TagsTemplate = () => withLayout(html` export const Tags = createStory(TagsTemplate, defaultArgs)(); /** - * `--list-item-alignment-override: center` vertically centres the item content. + * `--list-item-alignment: center` vertically centres the item content. It is set on the + * `pie-list-item` (here via a class), not on `pie-list` — the item defines the default on its host. */ const AlignmentOverrideTemplate = () => withLayout(html` - + + ${trailingIcon} ${trailingIcon} ${trailingIcon} ${trailingIcon} @@ -407,8 +413,10 @@ export const LongTextMetaTextOnlyPrimaryText = createStory(LongTextMe * trailing tag. */ const LongTextCentreAlignedTemplate = () => withLayout(html` - + + ${leadingIcon} @@ -416,6 +424,7 @@ const LongTextCentreAlignedTemplate = () => withLayout(html` ${leadingIcon} @@ -423,6 +432,7 @@ const LongTextCentreAlignedTemplate = () => withLayout(html` ${leadingIcon} @@ -430,6 +440,7 @@ const LongTextCentreAlignedTemplate = () => withLayout(html` ${leadingIcon} diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 6f686de333..349e9a27ff 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -79,7 +79,7 @@ These custom properties can be set on a `pie-list-item` (or on `pie-list` to aff | Variable | Description | Accepted values | |---|---|---| | `--list-item-inline-padding` | Sets the inline (start and end) padding of the item. Defaults to `var(--dt-spacing-d)`. **Must be set on the `pie-list-item`** (directly or via a rule targeting it), not on `pie-list` — the default lives on the item's host, so an inherited value from `pie-list` will not override it. | Any PIE spacing token (e.g. `var(--dt-spacing-f)`) or `0` | -| `--list-item-alignment-override` | Overrides the vertical alignment of the item's content. | Only `center` is recommended | +| `--list-item-alignment` | Sets the vertical alignment of the item's content. Defaults to `flex-start`. **Must be set on the `pie-list-item`** (directly or via a rule targeting it), not on `pie-list` — the default lives on the item's host, so an inherited value from `pie-list` will not override it. | Only `center` is recommended | ### Events @@ -225,8 +225,10 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; **Overriding alignment and padding** (via CSS variables): ```html - - + + + @@ -274,7 +276,7 @@ To keep lists consistent and correct, follow these rules: - **Slotted `pie-thumbnail` must use `size="40"`.** This is the only size that fits the list-item layout correctly. - **Always set `has-media` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding. - **Do not combine `is-compact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. -- **Only use `center` for `--list-item-alignment-override`.** Other values are not supported. +- **Only use `center` for `--list-item-alignment`.** Other values are not supported. - **`pie-avatar` is not yet ready** for use in lists. Prefer `pie-thumbnail` for media for now. ## Questions and Support diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 2d75b74e0a..b7fb1bcbee 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -1,9 +1,11 @@ @use '@justeattakeaway/pie-css/scss' as p; :host { - // Defined on the host so consumers can override it directly (a declaration on the element - // takes precedence over this default). + // Defined on the host so consumers can override them directly (a declaration on the element + // takes precedence over these defaults). Setting them on `pie-list` does not work, since the + // host declaration wins over a value inherited from an ancestor. --list-item-inline-padding: var(--dt-spacing-d); + --list-item-alignment: flex-start; // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell display: block; @@ -24,7 +26,7 @@ // Layout display: flex; - align-items: var(--list-item-alignment-override, flex-start); + align-items: var(--list-item-alignment); gap: var(--dt-spacing-c); min-height: 56px; border-bottom: var(--list-item-border-override, none); @@ -33,13 +35,16 @@ // Two lines of text (primary + secondary) need top-alignment so leading/trailing // content sits level with the first line, not centred across both lines. .c-listItem-container:has(.c-listItem-secondaryText) { - align-items: var(--list-item-alignment-override, flex-start); + align-items: var(--list-item-alignment); min-height: 76px; } .c-listItem-container.is-compact { --list-item-block-padding: var(--dt-spacing-c); - align-items: var(--list-item-alignment-override, center); + + // Compact items are centre-aligned. + --list-item-alignment: center; + align-items: var(--list-item-alignment); min-height: 48px; } From 5fbfca9866c82b4a8af8d96e6ba6bd7d2397ec5c Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 14:10:43 +0100 Subject: [PATCH 33/34] remove requiredpropertyvalue decorator use --- .../pie-storybook/stories/pie-list.stories.ts | 8 ++--- .../stories/testing/pie-list.test.stories.ts | 34 +++++++++---------- packages/components/pie-list/README.md | 22 ++++++------ .../pie-list/src/pie-list-item/defs.ts | 8 ++--- .../pie-list/src/pie-list-item/index.ts | 12 +++---- .../pie-list/test/component/pie-list.spec.ts | 13 ++++--- 6 files changed, 48 insertions(+), 49 deletions(-) diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index 31e7536213..e4bd05a5b1 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -41,7 +41,7 @@ const listStoryMeta: ListStoryMeta = { component: 'pie-list', argTypes: { primaryText: { - description: 'The main text of the item. **Required**. An item with no primary text renders nothing.', + description: 'The main text of the item.', control: 'text', }, secondaryText: { @@ -106,9 +106,9 @@ const renderItem = (args: ListPlaygroundProps, itemStyle = '') => html` .primaryText=${args.primaryText} .secondaryText=${args.secondaryText || undefined} .metaText=${args.metaText || undefined} - ?is-compact=${args.isCompact} - ?is-bold=${args.isBold} - ?has-media=${args.hasMedia}> + .isCompact=${args.isCompact} + .isBold=${args.isBold} + .hasMedia=${args.hasMedia}> ${renderLeading(args.leadingContent)} ${renderTrailing(args.trailingContent)} diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 0cda1b55f7..5e70ec13e0 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -69,14 +69,14 @@ const TextOnlyTemplate = () => withLayout(html` export const TextOnly = createStory(TextOnlyTemplate, defaultArgs)(); /** - * `is-bold` sets the primary text to a bold font-weight. + * `isbold` sets the primary text to a bold font-weight. */ const BoldTemplate = () => withLayout(html` - - - - + + + + `); @@ -167,33 +167,33 @@ const MetaTextOnlyPrimaryTextTemplate = () => withLayout(html` export const MetaTextOnlyPrimaryText = createStory(MetaTextOnlyPrimaryTextTemplate, defaultArgs)(); /** - * `has-media` reduces the block padding to suit a larger slotted pie-thumbnail. + * `hasmedia` reduces the block padding to suit a larger slotted pie-thumbnail. * The reduced padding only applies when there is no secondary text. The last * two items show padding reverting to normal. */ const MediaTemplate = () => withLayout(html`

Primary text only

- + ${leadingThumbnail} - + ${leadingThumbnail} - + ${leadingThumbnail}

With secondary text

- + ${leadingThumbnail} - + ${leadingThumbnail} - + ${leadingThumbnail} @@ -202,20 +202,20 @@ const MediaTemplate = () => withLayout(html` export const Media = createStory(MediaTemplate, defaultArgs)(); /** - * `is-compact` reduces the vertical space of each item. + * `iscompact` reduces the vertical space of each item. */ const CompactTemplate = () => withLayout(html` - + ${trailingIcon} - + ${trailingIcon} - + ${trailingIcon} - + ${trailingIcon} diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 349e9a27ff..2e4a4ec10d 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -49,7 +49,7 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc | Prop | Options | Description | Default | |---|---|---|---| -| `primaryText` | Any string | **Required.** The main text of the item, providing an overview of the content. | `undefined` | +| `primaryText` | Any string | The main text of the item, providing an overview of the content. | `undefined` | | `secondaryText` | Any string | Optional additional detail, rendered on a second line beneath the primary text. | `undefined` | | `metaText` | Any string | Optional supporting information about the item's context, status or attributes. Rendered as a trailing text string. **Mutually exclusive with the `trailing` slot**: if `metaText` is set, the `trailing` slot is not rendered. | `undefined` | | `isCompact` | `true`, `false` | Decreases the item height to save vertical space. See the [rules](#usage-notes-and-rules) below. | `false` | @@ -181,7 +181,7 @@ import '@justeattakeaway/pie-webc/components/tag.js'; ```html - + ``` @@ -189,10 +189,10 @@ import '@justeattakeaway/pie-webc/components/tag.js'; ```html - + - + @@ -205,18 +205,18 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; ``` ```html - - + - - + @@ -271,11 +271,11 @@ import { PieListItem } from '@justeattakeaway/pie-webc/react/list-item.js'; To keep lists consistent and correct, follow these rules: - **Always give `pie-list` an accessible name** with `aria-label` or `aria-labelledby` (use `aria-labelledby` when a visible heading exists). This is required for screen reader users to understand the list. See [Accessibility](#accessibility). -- **`primaryText` is required** on every `pie-list-item`. An item with no primary text will not render. +- **Provide `primaryText`** on every `pie-list-item`; it is the item's main line of content. - **`metaText` and the `trailing` slot are mutually exclusive.** If `metaText` is set, any `trailing` slot content is ignored. Choose one. - **Slotted `pie-thumbnail` must use `size="40"`.** This is the only size that fits the list-item layout correctly. -- **Always set `has-media` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding. -- **Do not combine `is-compact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. +- **Always set `hasmedia` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding. +- **Do not combine `iscompact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. - **Only use `center` for `--list-item-alignment`.** Other values are not supported. - **`pie-avatar` is not yet ready** for use in lists. Prefer `pie-thumbnail` for media for now. diff --git a/packages/components/pie-list/src/pie-list-item/defs.ts b/packages/components/pie-list/src/pie-list-item/defs.ts index 121a976ae2..37680a4fde 100644 --- a/packages/components/pie-list/src/pie-list-item/defs.ts +++ b/packages/components/pie-list/src/pie-list-item/defs.ts @@ -4,7 +4,7 @@ export interface ListItemProps { /** * **Required:** Provides an overview of the content. */ - primaryText: string, + primaryText?: string, /** * Provides optional additional detail. */ @@ -19,19 +19,19 @@ export interface ListItemProps { * * **Note**: Do not use if you require secondary text, or if you want to slot a pie-avatar, pie-thumbnail or Icon with Background component into the list item. */ - isCompact: boolean + isCompact?: boolean /** * Sets the primary text to use a bold font-weight. */ - isBold: boolean + isBold?: boolean /** * Reduces the block padding to suit larger slotted media (such as a pie-thumbnail). * * **Note**: This has no effect when `secondaryText` is set, and should not be combined with `isCompact`. */ - hasMedia: boolean + hasMedia?: boolean } export type DefaultProps = ComponentDefaultProps>; diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index d6902d040d..f958bf5e72 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -1,6 +1,6 @@ import { html, nothing, unsafeCSS } from 'lit'; import { property } from 'lit/decorators.js'; -import { safeCustomElement, requiredProperty } from '@justeattakeaway/pie-webc-core'; +import { safeCustomElement } from '@justeattakeaway/pie-webc-core'; import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement'; import { classMap } from 'lit/directives/class-map.js'; import { type ListItemProps, defaultProps } from './defs'; @@ -14,7 +14,6 @@ const componentSelector = 'pie-list-item'; @safeCustomElement('pie-list-item') export class PieListItem extends PieElement implements ListItemProps { @property({ type: String }) - @requiredProperty(componentSelector) primaryText!: ListItemProps['primaryText']; @property({ type: String }) @@ -23,13 +22,13 @@ export class PieListItem extends PieElement implements ListItemProps { @property({ type: String }) metaText: ListItemProps['metaText']; - @property({ type: Boolean, attribute: 'is-compact' }) + @property({ type: Boolean }) isCompact = defaultProps.isCompact; - @property({ type: Boolean, attribute: 'is-bold' }) + @property({ type: Boolean }) isBold = defaultProps.isBold; - @property({ type: Boolean, attribute: 'has-media' }) + @property({ type: Boolean }) hasMedia = defaultProps.hasMedia; connectedCallback () { @@ -62,9 +61,6 @@ export class PieListItem extends PieElement implements ListItemProps { render () { const { primaryText } = this; - // We should never render a list item that doesn't have primary text. - if (!primaryText) return nothing; - const containerClasses = { 'c-listItem-container': true, 'is-compact': this.isCompact, diff --git a/packages/components/pie-list/test/component/pie-list.spec.ts b/packages/components/pie-list/test/component/pie-list.spec.ts index b91941607d..cf8afd3cd5 100644 --- a/packages/components/pie-list/test/component/pie-list.spec.ts +++ b/packages/components/pie-list/test/component/pie-list.spec.ts @@ -16,7 +16,7 @@ test.describe('PieList - Component tests', () => { }); test.describe('primaryText', () => { - test('should render nothing when primaryText is not provided', async ({ page }) => { + test('should still render the item when primaryText is not provided', async ({ page }) => { // Arrange await new BasePage(page, 'list--no-primary-text').load(); @@ -25,8 +25,11 @@ test.describe('PieList - Component tests', () => { const primaryText = item.locator('.c-listItem-primaryText'); // Assert - await expect(item).toHaveCount(1); // the element still exists... - await expect(primaryText).toHaveCount(0); // ...but renders no content + await expect(item).toHaveCount(1); + // The item renders as normal; the primary text span is present but empty (the component + // no longer short-circuits to rendering nothing when primaryText is missing). + await expect(primaryText).toHaveCount(1); + await expect(primaryText).toHaveText(''); }); }); @@ -45,8 +48,8 @@ test.describe('PieList - Component tests', () => { }); }); - test.describe('slotted media (has-media)', () => { - test('should display slotted media when has-media is set', async ({ page }) => { + test.describe('slotted media (hasMedia)', () => { + test('should display slotted media when hasMedia is set', async ({ page }) => { // Arrange await new BasePage(page, 'list--media').load(); From 13cb968c188e6acf180cc79b64ab0952e190147a Mon Sep 17 00:00:00 2001 From: personal Date: Tue, 7 Jul 2026 15:09:52 +0100 Subject: [PATCH 34/34] camelCase and boolean bindings --- .../pie-storybook/stories/pie-list.stories.ts | 6 ++-- .../stories/testing/pie-list.test.stories.ts | 34 +++++++++---------- packages/components/pie-list/README.md | 18 +++++----- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index e4bd05a5b1..65c0452f64 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -106,9 +106,9 @@ const renderItem = (args: ListPlaygroundProps, itemStyle = '') => html` .primaryText=${args.primaryText} .secondaryText=${args.secondaryText || undefined} .metaText=${args.metaText || undefined} - .isCompact=${args.isCompact} - .isBold=${args.isBold} - .hasMedia=${args.hasMedia}> + ?isCompact=${args.isCompact} + ?isBold=${args.isBold} + ?hasMedia=${args.hasMedia}> ${renderLeading(args.leadingContent)} ${renderTrailing(args.trailingContent)}
diff --git a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts index 5e70ec13e0..fc68009aec 100644 --- a/apps/pie-storybook/stories/testing/pie-list.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-list.test.stories.ts @@ -69,14 +69,14 @@ const TextOnlyTemplate = () => withLayout(html` export const TextOnly = createStory(TextOnlyTemplate, defaultArgs)(); /** - * `isbold` sets the primary text to a bold font-weight. + * `isBold` sets the primary text to a bold font-weight. */ const BoldTemplate = () => withLayout(html` - - - - + + + + `); @@ -167,33 +167,33 @@ const MetaTextOnlyPrimaryTextTemplate = () => withLayout(html` export const MetaTextOnlyPrimaryText = createStory(MetaTextOnlyPrimaryTextTemplate, defaultArgs)(); /** - * `hasmedia` reduces the block padding to suit a larger slotted pie-thumbnail. + * `hasMedia` reduces the block padding to suit a larger slotted pie-thumbnail. * The reduced padding only applies when there is no secondary text. The last * two items show padding reverting to normal. */ const MediaTemplate = () => withLayout(html`

Primary text only

- + ${leadingThumbnail} - + ${leadingThumbnail} - + ${leadingThumbnail}

With secondary text

- + ${leadingThumbnail} - + ${leadingThumbnail} - + ${leadingThumbnail} @@ -202,20 +202,20 @@ const MediaTemplate = () => withLayout(html` export const Media = createStory(MediaTemplate, defaultArgs)(); /** - * `iscompact` reduces the vertical space of each item. + * `isCompact` reduces the vertical space of each item. */ const CompactTemplate = () => withLayout(html` - + ${trailingIcon} - + ${trailingIcon} - + ${trailingIcon} - + ${trailingIcon} diff --git a/packages/components/pie-list/README.md b/packages/components/pie-list/README.md index 2e4a4ec10d..7fc3621643 100644 --- a/packages/components/pie-list/README.md +++ b/packages/components/pie-list/README.md @@ -181,7 +181,7 @@ import '@justeattakeaway/pie-webc/components/tag.js'; ```html - + ``` @@ -189,10 +189,10 @@ import '@justeattakeaway/pie-webc/components/tag.js'; ```html - + - + @@ -205,18 +205,18 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js'; ``` ```html - - + - - + @@ -274,8 +274,8 @@ To keep lists consistent and correct, follow these rules: - **Provide `primaryText`** on every `pie-list-item`; it is the item's main line of content. - **`metaText` and the `trailing` slot are mutually exclusive.** If `metaText` is set, any `trailing` slot content is ignored. Choose one. - **Slotted `pie-thumbnail` must use `size="40"`.** This is the only size that fits the list-item layout correctly. -- **Always set `hasmedia` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding. -- **Do not combine `iscompact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. +- **Always set `hasMedia` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding. +- **Do not combine `isCompact` with `secondaryText` or with slotted media.** Compact items are single-line and too short for these. - **Only use `center` for `--list-item-alignment`.** Other values are not supported. - **`pie-avatar` is not yet ready** for use in lists. Prefer `pie-thumbnail` for media for now.