From 4679e71972f70f8eebc52a1f7f115bf204b0731a Mon Sep 17 00:00:00 2001 From: Per Fryking Date: Thu, 30 Apr 2026 14:27:35 +0200 Subject: [PATCH 1/5] feat(image): add `referrerpolicy` to the `Image` interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional `referrerpolicy?: ReferrerPolicy` field to the shared `Image` type. Lets consumers express that the rendered `` should carry a specific referrer-policy attribute — useful when the `src` points to a third-party service that should not receive the originating page URL in the `Referer` header (e.g. external favicon or avatar services). This commit only widens the type. The components that accept `Image` do not yet honor the new field; that follows in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- etc/lime-elements.api.md | 1 + src/global/shared-types/image.types.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/etc/lime-elements.api.md b/etc/lime-elements.api.md index 827c8417fb..62c0164c67 100644 --- a/etc/lime-elements.api.md +++ b/etc/lime-elements.api.md @@ -1238,6 +1238,7 @@ export type IconSize = 'x-small' | 'small' | 'medium' | 'large'; interface Image_2 { alt: string; loading?: 'lazy' | 'eager'; + referrerpolicy?: ReferrerPolicy; src: string; } export { Image_2 as Image } diff --git a/src/global/shared-types/image.types.ts b/src/global/shared-types/image.types.ts index a91be7a31a..f11b21e084 100644 --- a/src/global/shared-types/image.types.ts +++ b/src/global/shared-types/image.types.ts @@ -20,4 +20,13 @@ export interface Image { * - `eager` means that the image will be loaded as soon as possible. */ loading?: 'lazy' | 'eager'; + + /** + * The `referrerpolicy` attribute of the image. Set to `'no-referrer'` + * when `src` points to a third-party service that should not receive + * the originating page URL in the `Referer` header (e.g. external + * favicon or avatar services). When omitted, the attribute is not + * emitted and the browser's default referrer policy applies. + */ + referrerpolicy?: ReferrerPolicy; } From 974c6719a644ab76e0a8aafb0616655b8f52f6bc Mon Sep 17 00:00:00 2001 From: Per Fryking Date: Thu, 30 Apr 2026 14:27:41 +0200 Subject: [PATCH 2/5] chore(image): add `ImageTemplate` helper for shared image rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an `ImageTemplate` `FunctionalComponent` that returns an `` JSX node with the `Image` fields applied — including the spread-cast workaround needed today for the `referrerpolicy` attribute (Stencil's `ImgHTMLAttributes` typing omits it; tracked upstream at stenciljs/core#6692). Lives in `src/util/image.template.tsx`. Internal helper, not exported from the public API. Honors `image.loading`, which was on the public `Image` type but silently ignored at every render site before this PR. Wired up to `limel-list-item`, `limel-chip`, and `limel-card` in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/util/image.template.tsx | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/util/image.template.tsx diff --git a/src/util/image.template.tsx b/src/util/image.template.tsx new file mode 100644 index 0000000000..de92a4a992 --- /dev/null +++ b/src/util/image.template.tsx @@ -0,0 +1,37 @@ +import { FunctionalComponent, h } from '@stencil/core'; +import { Image } from '../global/shared-types/image.types'; + +interface ImageTemplateProps { + image: Image; +} + +/** + * Renders an `Image` as a plain `` element. Centralises the + * attribute forwarding (including the spread-cast workaround for + * `referrerpolicy`) so consumer components do not have to reimplement + * it. Intended for internal use by lime-elements components that + * accept an `Image`-shaped prop. + * + * @param props + * @param props.image - the image to render + * @internal + */ +export const ImageTemplate: FunctionalComponent = ({ + image, +}) => { + // Spread-cast: Stencil's `ImgHTMLAttributes` typing omits + // `referrerpolicy`, so the attribute can't be passed directly. + // Tracked upstream at https://github.com/stenciljs/core/issues/6692 + // — once that lands, this cast can be removed. + const referrerPolicyAttr: { referrerpolicy?: Image['referrerpolicy'] } = + image.referrerpolicy ? { referrerpolicy: image.referrerpolicy } : {}; + + return ( + {image.alt})} + /> + ); +}; From c7af85bb3329d76b886abb8beb1d63c9a814530a Mon Sep 17 00:00:00 2001 From: Per Fryking Date: Thu, 30 Apr 2026 14:27:48 +0200 Subject: [PATCH 3/5] refactor(card,chip,list-item): render `Image` via `ImageTemplate` helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the three components that accept an `Image`-shaped prop to delegate their `` rendering to the shared `ImageTemplate` helper. This both removes the duplicated inline `` JSX and lets the `referrerpolicy` field added to `Image` flow through to the rendered element — previously it was on the type but ignored at every render site. Adds two e2e cases on `limel-list-item` to verify the attribute is emitted when set and absent otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/card/card.tsx | 3 ++- src/components/chip/chip.tsx | 5 ++--- src/components/list-item/list-item.e2e.tsx | 20 ++++++++++++++++++++ src/components/list-item/list-item.tsx | 3 ++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/card/card.tsx b/src/components/card/card.tsx index 18befd5033..5eadbf44cf 100644 --- a/src/components/card/card.tsx +++ b/src/components/card/card.tsx @@ -9,6 +9,7 @@ import { State, } from '@stencil/core'; import { Image } from '../../global/shared-types/image.types'; +import { ImageTemplate } from '../../util/image.template'; import { Icon, IconName } from '../../global/shared-types/icon.types'; import { isItem } from '../action-bar/is-item'; import { getIconName } from '../icon/get-icon-props'; @@ -243,7 +244,7 @@ export class Card { return (
- {this.image.alt} +
); } diff --git a/src/components/chip/chip.tsx b/src/components/chip/chip.tsx index 6faf753601..046b638d43 100644 --- a/src/components/chip/chip.tsx +++ b/src/components/chip/chip.tsx @@ -8,6 +8,7 @@ import { Prop, } from '@stencil/core'; import { Icon, IconName } from '../../global/shared-types/icon.types'; +import { ImageTemplate } from '../../util/image.template'; import { Languages } from '../date-picker/date.types'; import { Link } from '../../global/shared-types/link.types'; import { getRel } from '../../util/link-helper'; @@ -295,9 +296,7 @@ export class Chip implements ChipInterface { } if (!isEmpty(this.image)) { - return ( - {this.image.alt} - ); + return ; } return ( diff --git a/src/components/list-item/list-item.e2e.tsx b/src/components/list-item/list-item.e2e.tsx index da6976b0e0..1bdc2ab3ea 100644 --- a/src/components/list-item/list-item.e2e.tsx +++ b/src/components/list-item/list-item.e2e.tsx @@ -108,5 +108,25 @@ describe('limel-list-item', () => { const imgEl = root.querySelector('img'); expect(imgEl).not.toBeNull(); + expect(imgEl?.hasAttribute('referrerpolicy')).toBe(false); + }); + + it('forwards referrerpolicy to the rendered image when set', async () => { + const imgSrc = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; + const { root, waitForChanges } = await render( + + ); + await waitForChanges(); + + const imgEl = root.querySelector('img'); + expect(imgEl?.getAttribute('referrerpolicy')).toEqual('no-referrer'); }); }); diff --git a/src/components/list-item/list-item.tsx b/src/components/list-item/list-item.tsx index 8d65582eb8..d43c341198 100644 --- a/src/components/list-item/list-item.tsx +++ b/src/components/list-item/list-item.tsx @@ -9,6 +9,7 @@ import { ListSeparator } from '../../global/shared-types/separator.types'; import { CheckboxTemplate } from '../checkbox/checkbox.template'; import translate from '../../global/translations'; import { Languages } from '../date-picker/date.types'; +import { ImageTemplate } from '../../util/image.template'; /** * This components displays the list item. @@ -253,7 +254,7 @@ export class ListItemComponent implements ListItem { return; } - return {this.image.alt}; + return ; }; private renderActionMenu = (actions: Array) => { From 13f5f7fde889b25611b1c533025e9e399bafd7b9 Mon Sep 17 00:00:00 2001 From: Per Fryking Date: Mon, 11 May 2026 11:53:01 +0200 Subject: [PATCH 4/5] refactor(file-viewer): render image via `ImageTemplate` helper `limel-file-viewer`'s image branch previously rendered its own inline `` with hardcoded `loading="lazy"`. Switch it to the shared `ImageTemplate` helper used by `limel-card`, `limel-chip`, and `limel-list-item`, so the four `Image`-rendering surfaces in lime-elements share one rendering path. The component's `alt` prop is optional (`alt?: string`); the `Image` type's `alt` is required, so `this.alt ?? ''` covers the gap. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/file-viewer/file-viewer.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/file-viewer/file-viewer.tsx b/src/components/file-viewer/file-viewer.tsx index 10c41cac33..30d73d4525 100644 --- a/src/components/file-viewer/file-viewer.tsx +++ b/src/components/file-viewer/file-viewer.tsx @@ -1,6 +1,7 @@ import { Component, Element, + Fragment, h, Prop, State, @@ -17,6 +18,7 @@ import { FileType, OfficeViewer } from './file-viewer.types'; import { LimelMenuCustomEvent } from '../../components'; import { Email } from '../email-viewer/email-viewer.types'; import { loadEmail } from '../email-viewer/email-loader'; +import { ImageTemplate } from '../../util/image.template'; /** * This is a smart component that automatically detects @@ -236,14 +238,17 @@ export class FileViewer { }; private renderImage = () => { - return [ - this.renderButtons(), - {this.alt}, - ]; + return ( + + {this.renderButtons()} + + + ); }; private renderVideo = () => { From 807914c37fcc13fae3c80534e28b20bd6cef7b2b Mon Sep 17 00:00:00 2001 From: Per Fryking Date: Mon, 11 May 2026 12:26:01 +0200 Subject: [PATCH 5/5] fix(profile-picture): suppress referrer on rendered `` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avatar URLs frequently point at third-party services (Gravatar, Microsoft Graph for Entra users, Google profile, etc.). By default, the browser sends the originating page URL in the `Referer` header on the cross-origin avatar fetch, leaking the tenant hostname to the avatar provider. Set `referrerpolicy="no-referrer"` directly on the `` so the attribute applies regardless of which avatar source the consumer configures. Same spread-cast workaround as elsewhere — Stencil's `ImgHTMLAttributes` typing omits the attribute, tracked upstream at stenciljs/core#6692. `limec-profile-picture` does not consume the `Image` type (it carries its own `style` and `onError` plumbing), so it can't use the `ImageTemplate` helper; the policy is applied directly here. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/profile-picture/profile-picture.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/profile-picture/profile-picture.tsx b/src/components/profile-picture/profile-picture.tsx index d68c7ffe59..3c5973feeb 100644 --- a/src/components/profile-picture/profile-picture.tsx +++ b/src/components/profile-picture/profile-picture.tsx @@ -245,6 +245,10 @@ export class ProfilePicture { const src = this.getImageSrc(); if (src) { + // Spread-cast: Stencil's `ImgHTMLAttributes` typing omits + // `referrerpolicy`, so the attribute can't be passed directly. + // Tracked upstream at https://github.com/stenciljs/core/issues/6692 + // — once that lands, this cast can be removed. return ( )} /> ); }