diff --git a/components/vf-hero/CHANGELOG.md b/components/vf-hero/CHANGELOG.md index e02adf7f95..9eb9d90328 100755 --- a/components/vf-hero/CHANGELOG.md +++ b/components/vf-hero/CHANGELOG.md @@ -1,3 +1,7 @@ +### 4.0.8 + +* Changed: Update vf-hero component for optimised responsive image delivery and performance [Tracking issue](https://github.com/visual-framework/vf-core/issues/2405) + ### 4.0.7 * Changed: Documentation updated [Tracking issue](https://github.com/visual-framework/vf-core/issues/2392) diff --git a/components/vf-hero/README.md b/components/vf-hero/README.md index 8d71acc14d..ddf34bd539 100755 --- a/components/vf-hero/README.md +++ b/components/vf-hero/README.md @@ -14,6 +14,10 @@ By default the `vf-hero` makes use of the roundels background image. To keep the You can bring your own image to use with the vf-hero. +- Dimensions - recommended dimensions for the hero image are: + - Desktop: 1920 × 1080 px + - Mobile: 360 × 200 px + - Large / high-res displays: 2560 × 1440 px - Size: Image size should be below 500KB - Format: WebP or AVIF provide better image quality at smaller file sizes - Positioning: people and other important imagery should be placed on the right side. The image will, by default, vertically centre and align to the right side. @@ -25,15 +29,16 @@ You can bring your own image to use with the vf-hero. | Content name | Usage | `.yml` key | | ------------ | ------------------------------------------------------------------------------ | -------------------- | -| Heading | To be used for the heading of the page. | `vf_hero_heading` | -| Heading HREF | To be used as a 'return to home' link for the micro site. | `vf_hero_heading_href` | -| Subheading | Optional content to be used along with the Heading for a terse explainer. | `vf_hero_subheading` | +| Heading | To be used for the heading of the page. | `vf_hero_heading` | +| Heading HREF | To be used as a 'return to home' link for the micro site. | `vf_hero_heading_href` | +| Subheading | Optional content to be used along with the Heading for a terse explainer. | `vf_hero_subheading` | | Kicker | Optional content that helps define the context of overall content of the page. | `vf_hero_kicker` | -| Text | Optional content that can help explain the page content in a brief paragraph. | `vf_hero_text` | -| Link Text | The text for the hero 'call to action' link. Only works if there is associated Link HREF. | `vf_hero_link_text` | -| Link HREF | The url that the Link Text would be pointing to and opens in the same browser tab. This only works if there is associated Link Text | `vf_hero_link_href` | -| Spacing | If added spacing can be any of these values only : 200, 400, 500, 600, 800, 1200, 1600 | 'spacing' +| Text | Optional content that can help explain the page content in a brief paragraph. | `vf_hero_text` | +| Link Text | The text for the hero 'call to action' link. Only works if there is associated Link HREF. | `vf_hero_link_text` | +| Link HREF | The url that the Link Text would be pointing to and opens in the same browser tab. This only works if there is associated Link Text | `vf_hero_link_href` | +| Spacing | If added spacing can be any of these values only : 200, 400, 500, 600, 800, 1200, 1600 | 'spacing' | +
| Content name | `.yml` key | CSS classname | | ------------ | -------------------- | --------------------- | @@ -44,6 +49,25 @@ You can bring your own image to use with the vf-hero. | Text | `vf_hero_text` | `vf-hero__text` | | Link Text | `vf_hero_link_text` | `vf-hero__link` | | Link HREF | `vf_hero_link_href` | n/a | +| Image Mobile | `vf_hero_image_mobile_src` | `vf-hero__image` | +| Image Desktop | `vf_hero_image_src` | `vf-hero__image` | +| Image Large | `vf_hero_image_large_src` | `vf-hero__image` | +| Image Width | `vf_hero_image_width` | `vf-hero__image` | +| Image Height | `vf_hero_image_height` | `vf-hero__image` | +| Image Priority | `vf_hero_image_fetchpriority` | `vf-hero__image` | +| Image Loading | `vf_hero_image_loading` | `vf-hero__image` | +| Image Sizes | `vf_hero_image_sizes` | `vf-hero__image` | +| Image Alt | `vf_hero_image_alt` | `vf-hero__image` | + +
+ +For responsive hero delivery, use Cloudimage transformation URLs on the image sources. The recommended pattern is: + +- Mobile image: `width=360&height=200&func=crop` +- Desktop image: `width=1920&height=1080&func=crop` +- Large/high-res image: `width=2560&height=1440&func=crop` + +The initial HTML renders the hero image with `fetchpriority="high"`, `loading="eager"`, and explicit `width` and `height` defaults to help LCP and layout stability. ### CSS Custom Properties @@ -57,7 +81,7 @@ IE 11 will get the background colour and the "roundels" image. Under the approac ### EMBL usage -For EMBL sites, the `vf-hero` can take an image (provided by Design) which should be uploaded to [the files site](https://www.embl.org/files) and applied using the custom property available (`--vf-hero--bg-image`). +For EMBL sites, the `vf-hero` can take an image (provided by Design) which should be uploaded to [the files site](https://www.embl.org/files) and applied using the custom property available (`--vf-hero--bg-image`). When you want the hero to be responsive in the initial HTML, prefer the Cloudimage transformation URLs shown above and pass them through the responsive image props instead of relying on the CSS background alone. ### Angular @@ -83,7 +107,14 @@ This package was generated with Angular version 15.2.0 and has been tested on ap [vf_hero_text]="['The Hentze group combines biochemical and systems–level approaches to investigate the connections between gene expression and cell metabolism, and their roles in human disease.']" [vf_hero_link_text]="'Learn more'" [vf_hero_link_href]="'JavaScript:Void(0);'" - [vf_hero_image_size]="'auto 28.5rem'"> + [vf_hero_image_size]="'auto 28.5rem'" + [vf_hero_image_mobile_src]="'https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=360&height=200&func=crop'" + [vf_hero_image_src]="'https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=1920&height=1080&func=crop'" + [vf_hero_image_large_src]="'https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=2560&height=1440&func=crop'" + [vf_hero_image_width]="'1920'" + [vf_hero_image_height]="'1080'" + [vf_hero_image_fetchpriority]="'high'" + [vf_hero_image_loading]="'eager'"> ``` 4. add to your styles.scss @@ -107,7 +138,14 @@ Usage: [vf_hero_text]="['The Hentze group combines biochemical and systems–level approaches to investigate the connections between gene expression and cell metabolism, and their roles in human disease.']" [vf_hero_link_text]="'Learn more'" [vf_hero_link_href]="'JavaScript:Void(0);'" - [vf_hero_image_size]="'auto 28.5rem'"> + [vf_hero_image_size]="'auto 28.5rem'" + [vf_hero_image_mobile_src]="'https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=360&height=200&func=crop'" + [vf_hero_image_src]="'https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=1920&height=1080&func=crop'" + [vf_hero_image_large_src]="'https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=2560&height=1440&func=crop'" + [vf_hero_image_width]="'1920'" + [vf_hero_image_height]="'1080'" + [vf_hero_image_fetchpriority]="'high'" + [vf_hero_image_loading]="'eager'"> ``` @@ -134,6 +172,13 @@ As of version 4.0.0-alpha.2 vf-hero has experimental React support which has bee vf_hero_link_text="Learn more" vf_hero_link_href="'JavaScript:Void(0);'" vf_hero_image_size="auto 28.5rem" + vf_hero_image_mobile_src="https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=360&height=200&func=crop" + vf_hero_image_src="https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=1920&height=1080&func=crop" + vf_hero_image_large_src="https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png?width=2560&height=1440&func=crop" + vf_hero_image_width="1920" + vf_hero_image_height="1080" + vf_hero_image_fetchpriority="high" + vf_hero_image_loading="eager" /> ``` 4. Styling changes diff --git a/components/vf-hero/vf-hero.angular.component.ts b/components/vf-hero/vf-hero.angular.component.ts index cce9e58cdf..fd94a1e754 100644 --- a/components/vf-hero/vf-hero.angular.component.ts +++ b/components/vf-hero/vf-hero.angular.component.ts @@ -5,6 +5,20 @@ import { Component, Input, OnInit } from "@angular/core"; selector: "vf-hero", template: `
+ + + + +

@@ -33,6 +47,15 @@ export class VfHeroAngularComponent implements OnInit { /* Initialize values based on input values */ @Input() vf_hero_image = ''; @Input() vf_hero_image_size = ''; + @Input() vf_hero_image_src = ''; + @Input() vf_hero_image_mobile_src = ''; + @Input() vf_hero_image_large_src = ''; + @Input() vf_hero_image_alt = ''; + @Input() vf_hero_image_width = ''; + @Input() vf_hero_image_height = ''; + @Input() vf_hero_image_fetchpriority = ''; + @Input() vf_hero_image_loading = ''; + @Input() vf_hero_image_sizes = ''; @Input() vf_hero_kicker = ''; @Input() vf_hero_heading = ''; @Input() vf_hero_heading_href = ''; @@ -47,6 +70,7 @@ export class VfHeroAngularComponent implements OnInit { class = ''; style = ''; + hasResponsiveMedia = false; vf_hero_link_innerhtml = ''; ngOnInit(): void { @@ -62,6 +86,7 @@ export class VfHeroAngularComponent implements OnInit { /* Initialize/Reset the values for class and style */ this.class = 'vf-hero '; this.style = ''; + this.hasResponsiveMedia = false; if (this.vf_hero_heading_additional !== '') { this.vf_hero_kicker = this.vf_hero_heading_additional; @@ -69,6 +94,8 @@ export class VfHeroAngularComponent implements OnInit { /* Set values ass per the input */ this.class += this.spacing !== undefined ? 'vf-hero--' + this.spacing + ' ' : ''; + this.hasResponsiveMedia = this.vf_hero_image_src !== '' || this.vf_hero_image_mobile_src !== '' || this.vf_hero_image_large_src !== ''; + this.class += this.hasResponsiveMedia ? 'vf-hero--has-media ' : ''; this.class += '| vf-u-fullbleed'; this.class += this.modifier_class !== '' ? ' ' + this.modifier_class : ''; this.style += this.vf_hero_image !== '' ? '--vf-hero--bg-image: ' + this.vf_hero_image : ''; diff --git a/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.d.ts b/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.d.ts index ebe48a7cc4..9547eb3954 100644 --- a/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.d.ts +++ b/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.d.ts @@ -3,6 +3,15 @@ import * as i0 from "@angular/core"; export declare class VfHeroAngularComponent implements OnInit { vf_hero_image: string; vf_hero_image_size: string; + vf_hero_image_src: string; + vf_hero_image_mobile_src: string; + vf_hero_image_large_src: string; + vf_hero_image_alt: string; + vf_hero_image_width: string; + vf_hero_image_height: string; + vf_hero_image_fetchpriority: string; + vf_hero_image_loading: string; + vf_hero_image_sizes: string; vf_hero_kicker: string; vf_hero_heading: string; vf_hero_heading_href: string; @@ -16,10 +25,11 @@ export declare class VfHeroAngularComponent implements OnInit { vf_hero_heading_additional: string; class: string; style: string; + hasResponsiveMedia: boolean; vf_hero_link_innerhtml: string; ngOnInit(): void; ngOnChanges(): void; setValues(): void; static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; } diff --git a/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.spec.ts b/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.spec.ts index e5b7746981..2094ad8e80 100644 --- a/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.spec.ts +++ b/components/vf-hero/vf-hero.angular/lib/vf-hero.angular.component.spec.ts @@ -75,4 +75,30 @@ describe('VfHeroAngularComponent', () => { expect(linkElement.nativeElement.innerHTML).toContain('Link Text'); expect(linkElement.nativeElement.getAttribute('href')).toBe('http://example.com'); }); + + it('should render responsive media with high priority defaults', () => { + component.vf_hero_image_mobile_src = '/hero-mobile-360x200.jpg'; + component.vf_hero_image_src = '/hero-desktop-1920x1080.jpg'; + component.vf_hero_image_large_src = '/hero-large-2560x1440.jpg'; + fixture.detectChanges(); + + expect(component.class).toContain('vf-hero--has-media'); + + const imageElement = fixture.debugElement.query(By.css('.vf-hero__image')); + expect(imageElement.nativeElement.getAttribute('src')).toBe('/hero-desktop-1920x1080.jpg'); + expect(imageElement.nativeElement.getAttribute('fetchpriority')).toBe('high'); + expect(imageElement.nativeElement.getAttribute('loading')).toBe('eager'); + expect(imageElement.nativeElement.getAttribute('width')).toBe('1920'); + expect(imageElement.nativeElement.getAttribute('height')).toBe('1080'); + expect(imageElement.nativeElement.getAttribute('sizes')).toBe('(max-width: 767px) 100vw, 1920px'); + }); + + it('should use the default sizes hint when one is not provided', () => { + component.vf_hero_image_mobile_src = '/hero-mobile-360x200.jpg'; + component.vf_hero_image_src = '/hero-desktop-1920x1080.jpg'; + fixture.detectChanges(); + + const imageElement = fixture.debugElement.query(By.css('.vf-hero__image')); + expect(imageElement.nativeElement.getAttribute('sizes')).toBe('(max-width: 767px) 100vw, 1920px'); + }); }); diff --git a/components/vf-hero/vf-hero.config.yml b/components/vf-hero/vf-hero.config.yml index 86f588c1ca..2630893818 100755 --- a/components/vf-hero/vf-hero.config.yml +++ b/components/vf-hero/vf-hero.config.yml @@ -12,6 +12,19 @@ preview: "@preview--nogrid" # hidden: true context: component-type: container + # Recommended responsive image settings: + # - mobile: 360x200 + # - desktop: 1920x1080 + # - large/high-res: 2560x1440 + # Example Cloudimage URLs: + # vf_hero_image_mobile_src: "https://acxngcvroo.cloudimg.io/v7/https://example.org/hero.jpg?width=360&height=200&func=crop" + # vf_hero_image_src: "https://acxngcvroo.cloudimg.io/v7/https://example.org/hero.jpg?width=1920&height=1080&func=crop" + # vf_hero_image_large_src: "https://acxngcvroo.cloudimg.io/v7/https://example.org/hero.jpg?width=2560&height=1440&func=crop" + # vf_hero_image_width: 1920 + # vf_hero_image_height: 1080 + # vf_hero_image_sizes: "(max-width: 767px) 100vw, 1920px" + # vf_hero_image_fetchpriority: high + # vf_hero_image_loading: eager @@ -50,6 +63,60 @@ variants: vf_hero_link_href="'JavaScript:Void(0);'" vf_hero_image_size="auto 28.5rem" /> + - name: default with image (responsive) + context: + vf_hero_kicker: VF Hamburg | Structural Biology + vf_hero_heading: About the Hentze group! + vf_hero_subheading: an example of some ancillary text + vf_hero_text: + - The Hentze group combines biochemical and systems–level approaches to investigate the connections between gene expression and cell metabolism, and their roles in human disease. + vf_hero_link_text: Learn more + vf_hero_link_href: JavaScript:Void(0); + vf_hero_image: url('https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif') + vf_hero_image_size: null + vf_hero_image_mobile_src: https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif?width=360&height=200&func=crop + vf_hero_image_src: https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif?width=1920&height=1080&func=crop + vf_hero_image_large_src: https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif?width=2560&height=1440&func=crop + vf_hero_image_width: 1920 + vf_hero_image_height: 1080 + vf_hero_image_sizes: '(max-width: 767px) 100vw, 1920px' + vf_hero_image_fetchpriority: high + vf_hero_image_loading: eager + angular: + + + react: + systems–level approaches to investigate the connections between gene expression and cell metabolism, and their roles in human disease."]} + vf_hero_link_text="Learn more" + vf_hero_link_href="'JavaScript:Void(0);'" + vf_hero_image="url(https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif)" + vf_hero_image_mobile_src="https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif?width=360&height=200&func=crop" + vf_hero_image_src="https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif?width=1920&height=1080&func=crop" + vf_hero_image_large_src="https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.avif?width=2560&height=1440&func=crop" + vf_hero_image_width="1920" + vf_hero_image_height="1080" + vf_hero_image_fetchpriority="high" + vf_hero_image_loading="eager" + /> - name: default with image context: vf_hero_kicker: VF Hamburg | Structural Biology @@ -59,7 +126,8 @@ variants: - The Hentze group combines biochemical and systems–level approaches to investigate the connections between gene expression and cell metabolism, and their roles in human disease. vf_hero_link_text: Learn more vf_hero_link_href: JavaScript:Void(0); - vf_hero_image: url('https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png'); + vf_hero_image_size: null + vf_hero_image: url('https://acxngcvroo.cloudimg.io/v7/https://www.embl.org/files/wp-content/uploads/vf-hero-intense.png') angular: - - name: spacing (400) context: spacing: 400 diff --git a/components/vf-hero/vf-hero.njk b/components/vf-hero/vf-hero.njk index c692338fe9..ae05fc7e7d 100755 --- a/components/vf-hero/vf-hero.njk +++ b/components/vf-hero/vf-hero.njk @@ -1,6 +1,16 @@ {% spaceless %} {%- if context %} {% set vf_hero_image = context.vf_hero_image %} + {% set vf_hero_image_size = context.vf_hero_image_size %} + {% set vf_hero_image_src = context.vf_hero_image_src %} + {% set vf_hero_image_mobile_src = context.vf_hero_image_mobile_src %} + {% set vf_hero_image_large_src = context.vf_hero_image_large_src %} + {% set vf_hero_image_alt = context.vf_hero_image_alt %} + {% set vf_hero_image_width = context.vf_hero_image_width %} + {% set vf_hero_image_height = context.vf_hero_image_height %} + {% set vf_hero_image_fetchpriority = context.vf_hero_image_fetchpriority %} + {% set vf_hero_image_loading = context.vf_hero_image_loading %} + {% set vf_hero_image_sizes = context.vf_hero_image_sizes %} {% set vf_hero_kicker = context.vf_hero_kicker %} {% set vf_hero_heading = context.vf_hero_heading %} @@ -26,10 +36,26 @@ {%- endif -%} {% endspaceless %}
+ {%- if vf_hero_image_src or vf_hero_image_mobile_src or vf_hero_image_large_src -%} + + {%- if vf_hero_image_large_src %}{% endif -%} + {%- if vf_hero_image_mobile_src %}{% endif -%} + {{ vf_hero_image_alt | default('', true) }} + + {%- endif -%}
{%- if vf_hero_kicker -%}

{{- vf_hero_kicker | safe -}}

diff --git a/components/vf-hero/vf-hero.react.js b/components/vf-hero/vf-hero.react.js index 3199ab0102..bd6b9b254f 100644 --- a/components/vf-hero/vf-hero.react.js +++ b/components/vf-hero/vf-hero.react.js @@ -14,14 +14,31 @@ function VfHero({ vf_hero_link_href, vf_hero_image, vf_hero_image_size, + vf_hero_image_src, + vf_hero_image_mobile_src, + vf_hero_image_large_src, + vf_hero_image_alt, + vf_hero_image_width = "1920", + vf_hero_image_height = "1080", + vf_hero_image_fetchpriority = "high", + vf_hero_image_loading = "eager", + vf_hero_image_sizes, vf_hero_kicker, vf_hero_heading_additional, modifier_class }) { - const classNames = `vf-hero vf-u-fullbleed ${modifier_class || ""}`; + const hasResponsiveMedia = + !!vf_hero_image_src || + !!vf_hero_image_mobile_src || + !!vf_hero_image_large_src; + const resolvedImageSizes = + vf_hero_image_sizes || "(max-width: 767px) 100vw, 1920px"; + const classNames = `vf-hero${ + hasResponsiveMedia ? " vf-hero--has-media" : "" + } vf-u-fullbleed ${modifier_class || ""}`; const styles = { - "--vf-hero--bg-image": vf_hero_image, - "--vf-hero--bg-image-size": vf_hero_image_size + ...(vf_hero_image && { "--vf-hero--bg-image": vf_hero_image }), + ...(vf_hero_image_size && { "--vf-hero--bg-image-size": vf_hero_image_size }) }; const attributes = { ...(id && { id }) @@ -34,6 +51,30 @@ function VfHero({ className={classNames} style={styles} > + {hasResponsiveMedia ? ( + + {vf_hero_image_large_src ? ( + + ) : null} + {vf_hero_image_mobile_src ? ( + + ) : null} + {vf_hero_image_alt + + ) : null}
{vf_hero_kicker ? (

{vf_hero_kicker}

@@ -41,7 +82,9 @@ function VfHero({ {vf_hero_heading ? (

{vf_hero_heading_href ? ( - {vf_hero_heading} + + {vf_hero_heading} + ) : ( vf_hero_heading )} diff --git a/components/vf-hero/vf-hero.react.test.js b/components/vf-hero/vf-hero.react.test.js index ba5459a065..967935f470 100644 --- a/components/vf-hero/vf-hero.react.test.js +++ b/components/vf-hero/vf-hero.react.test.js @@ -27,6 +27,41 @@ describe("VfHero", () => { }); }); + test("renders responsive hero media with high-priority image attrs", () => { + render( + + ); + + const banner = screen.getByRole("banner"); + expect(banner).toHaveClass("vf-hero--has-media"); + + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", "/hero-desktop-1920x1080.jpg"); + expect(img).toHaveAttribute("fetchpriority", "high"); + expect(img).toHaveAttribute("loading", "eager"); + expect(img).toHaveAttribute("width", "1920"); + expect(img).toHaveAttribute("height", "1080"); + expect(img).toHaveAttribute("sizes", "(max-width: 767px) 100vw, 1920px"); + }); + + test("uses the default sizes hint when none is provided", () => { + render( + + ); + + expect(screen.getByRole("img")).toHaveAttribute( + "sizes", + "(max-width: 767px) 100vw, 1920px" + ); + }); + test("renders with heading and link", () => { render(