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/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/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 = () => { 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) => { 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 ( )} /> ); } 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; } 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})} + /> + ); +};