Guidance

Use the gallery component within content areas or the @uol-widget-gallery to display single or groups of images, videos or combinations of both images and videos. Images and videos can also have an accompanying caption.

In addition to the image or video each item can have an accompanying title and text content.

Clicking on the item expand button will open the item in a full-screen carousel. If a title and text content is available these can be accessed by clicking in the “I” button for the corresponding carousel item.

For videos YouTube is currently the only supported platform.

When to use

Use the Gallery component when you want to present images or videos and have them viewable in full screen.

When not to use

  • Do not use to display essential information as content may be missed within the carousel layout.
  • Do not use for the following content types
    • Graphs
    • Maps
  • Do not use when very high resolution images are needed and the user needs to see the image at full natural resolution.

Developer guidance

Images

For performance and image quality it is recommended that a lower and higher resolution version of each image is provided. e.g.

"img": {
  "srcHighQuality": "/placeholders/campus/full/29940.jpeg",
  "src": "/placeholders/campus/medium/29940.jpeg",
  ...
},
  • Lower resolution image width 1200px
  • Higher resolution image maximum 2500px on longest axis.

For photographic images JPEG compression should be set to 85% quality

For graphical images PNG format should be used.

Videos

Ensure you set the item type to video.

At this point the only video host supported is YouTube. You can use full youtube URLS or the shortened versions. Eg. Both the following will display the YouTube video with id PpilTVi5Yk4

Do not use the ‘embed’ code that YouTube provides as this will not work within the @uol-gallery component. The component makes use of the oEmbed API that YouTube provides.

Do not provide an image for videos as video images are retrieved directly from YouTube. You may provide a caption.

{
  "title": "Ear tickle therapy and the world's thinnest gold",
  "type": "video",
  "video": "https://www.youtube.com/watch?v=PpilTVi5Yk4",
  "img": {
    "caption": "Ear tickle therapy and the world's thinnest gold",
  },
  ...
}

A single video with alternate play icon

In certain scenarios (following guidance from the design team) there maybe a need to replace the usual ‘expanded’ icon for the larger orange one with the play button icon.

This can be done through the config by setting the ‘videoPlayIcon: true’:

{
  "name": "Single video with play icon",
  "label": "Single video with play icon",
  "context": {
    "gallery": {
      "headingLevel": 2,
      "videoPlayIcon": true,
      "items": galleryVideos.slice(2, 3),
    },
  },
}

Please see the ‘with video’ variant of the @featured-content for more details.


{% if gallery.items.length %}
  {% set headingTag = 'h' + gallery.headingLevel if gallery.headingLevel else 'h3' %}
  <section class="uol-gallery-container" aria-label="Gallery of {{ gallery.items.length }} items">
    <div class="uol-gallery uol-gallery--count-{{ gallery.items.length }}">
      {% for item in gallery.items %}
        <div class="uol-gallery__item {{ 'uol-gallery__item--' + item.type if item.type }} {{ 'uol-gallery__item--image' if item.img.src }} {{ 'uol-gallery__item--video-play-icon' if gallery.videoPlayIcon }}"
        {% if item.type and item.video %} data-video="{{ item.video }}" {% endif %}>
          <{{ headingTag }} class="uol-gallery__item__title">{{ item.title | safe }}</{{ headingTag }}>
          <figure class="uol-gallery__figure">
            <div class="uol-gallery__image-container">
              <img
                src="{{ item.img.src if item.img.src else '#' }}"
                alt="{{ item.img.alt }}"
                {% if item.img.srcHighQuality %} data-src-high-quality="{{ item.img.srcHighQuality }}" {% endif %}>
            </div>
            {% if item.img.caption %}
              <figcaption class="uol-gallery__image-caption">{{ item.img.caption | safe }}</figcaption>
            {% endif %}
          </figure>
          {% if item.type and item.video %}
            <noscript>
              <a href="{{ item.video }}">{{ item.video }}</a>
            </noscript>
          {% endif %}
          {% if item.content %}
            <div class="uol-gallery__item__content">
              {{ item.content | safe }}
            </div>
          {% endif %}
        </div>
      {% endfor %}
    </div>
  </section>
{% endif %}
<section class="uol-gallery-container" aria-label="Gallery of 2 items">
    <div class="uol-gallery uol-gallery--count-2">

        <div class="uol-gallery__item  uol-gallery__item--image ">
            <h2 class="uol-gallery__item__title">&ldquo;Dual Form&rdquo; by Barbara Hepworth</h2>
            <figure class="uol-gallery__figure">
                <div class="uol-gallery__image-container">
                    <img src="/placeholders/campus/medium/29940.jpeg" alt="Dual Form sculpture by Barbara Hepworth with people relaxing on grass in background" data-src-high-quality="/placeholders/campus/full/29940.jpeg">
                </div>

                <figcaption class="uol-gallery__image-caption">&ldquo;Dual Form&rdquo; by Barbara Hepworth</figcaption>

            </figure>

            <div class="uol-gallery__item__content">
                <p>Lorem ipsum <a href='/some-text-link'>dolor sit amet consectetur</a> adipisicing elit. Corrupti, quasi nostrum blanditiis hic totam a id architecto molestias, sunt vitae iste consectetur cupiditate incidunt autem illo, consequuntur recusandae? Ipsam, asperiores.</p>
                <p>Expedita saepe illo vero sit et! Eveniet, deserunt. Nihil omnis fugit ut veniam ullam, non maiores, consequatur amet enim dolore totam, laborum accusantium voluptatum iure est ab aspernatur reiciendis explicabo.</p>
                <p>Eos reprehenderit suscipit, at eveniet, minus ea quod quis provident, nisi fugit maiores molestias culpa. Rerum rem pariatur quo mollitia autem omnis eum officiis, natus beatae eos saepe culpa earum!</p>
                <p>Eum ut tempore delectus quos unde tenetur neque perspiciatis. Dicta sunt rem dolore, in ab impedit assumenda, quaerat neque quos veritatis consequatur accusantium dignissimos eius natus iusto nostrum eos maxime.</p>
            </div>

        </div>

        <div class="uol-gallery__item  uol-gallery__item--image ">
            <h2 class="uol-gallery__item__title">Edward Boyle Library</h2>
            <figure class="uol-gallery__figure">
                <div class="uol-gallery__image-container">
                    <img src="/placeholders/campus/medium/28573.jpeg" alt="Steps outside Edward Boyle Library" data-src-high-quality="/placeholders/campus/full/28573.jpeg">
                </div>

            </figure>

            <div class="uol-gallery__item__content">
                <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Corrupti, quasi nostrum blanditiis hic totam a id architecto molestias, sunt vitae iste consectetur cupiditate incidunt autem illo, consequuntur recusandae? Ipsam, asperiores.</p>
                <p>Expedita saepe illo vero sit et! Eveniet, deserunt. Nihil omnis fugit ut veniam ullam, non maiores, consequatur amet enim dolore totam, laborum accusantium voluptatum iure est ab aspernatur reiciendis explicabo.</p>
                <p>Eos reprehenderit suscipit, at eveniet, <a href='/some-text-link'>minus ea quod</a> quis provident, nisi fugit maiores molestias culpa. Rerum rem pariatur quo mollitia autem omnis eum officiis, natus beatae eos saepe culpa earum!</p>
                <p>Eum ut tempore delectus quos unde tenetur neque perspiciatis. Dicta sunt rem dolore, in ab impedit assumenda, quaerat neque quos veritatis consequatur accusantium dignissimos eius natus iusto nostrum eos maxime.</p>
            </div>

        </div>

    </div>
</section>
  • Content:
    $nav-depth: 77px;
    // button sizes used in calculation for right hand nav spacings
    $small-button-size: 2.81rem;
    $standard-button-size: 3.12rem;
    
    @keyframes info-text-fade {
      0% {
        display: none;
        opacity: 0;
      }
      1% {
        display: block;
        opacity: 0;
        // transform: scale(0);
      }
      30% {
        display: block;
        opacity: 0;
        // transform: scale(0);
      }
      100% {
        display: block;
        opacity: 1;
        // transform: scale(1);
      }
    }
    
    .uol-gallery-modal {
      position: absolute;
      width: 100vw;
      height: calc(var(--vh, 1vh) * 99.9); // 99.9% to avoid distortion when zoomed in
      left: 0;
      top: 0;
      background: $color-black--dark;
      z-index: 10;
      color: $color-white;
    }
    
      .uol-gallery-modal__button-close {
        .js .uol-gallery-modal & {
          position: absolute;
          top: $spacing-4;
          right: $spacing-3;
          z-index: 2;
    
          @media (orientation: portrait) {
            @include media(">=uol-media-xs") {
              right: $spacing-5;
            }
          }
    
          @media (orientation: landscape) {
            top: $spacing-2;
            right: ($nav-depth / 2);
            transform: translateX(50%);
            
            @include media(">=uol-media-s") {
              top: $spacing-5;
            }
          }
    
          svg {
            path {
              fill: $color-white;
    
              @media (-ms-high-contrast: active), (forced-colors: active) {
                fill: ButtonText;
              }
            }
          }
        }
      }
    
      .uol-gallery-modal__track {
        margin: 0;
        padding: 0;
        height: calc(var(--vh, 1vh) * 99.9);
        width: 100vw;
        list-style: none;
        scroll-snap-type: x mandatory;
        display: flex;
        flex-wrap: wrap;
        flex-direction: column;
        overflow-x: auto;
    
        &::-webkit-scrollbar {
          display: none;
        }
    
        // TODO: IE 11 remove scrollbar
        -ms-overflow-style: none;
      }
    
        .uol-gallery-modal__track--smooth {
          scroll-behavior: smooth;
        }
    
      .uol-gallery-modal__item {
        position: relative;
        box-sizing: border-box;
        display: flex;
        width: 100vw;
        height: 100%;
        scroll-snap-align: start;
        background: $color-black;
    
        @media (orientation: portrait) {
          margin-top: $nav-depth;
          height: calc(100% - #{$nav-depth});
        }
      }
    
      .uol-gallery-modal__info-container {
        box-sizing: border-box;
        background: rgba($color-black--dark, 0.92);
        z-index: 1;
        transition: all 0.3s ease;
    
        @media (orientation: landscape) {
          position: absolute;
          width: $nav-depth;
          height: 100%;
          border-right: 3px solid $color-border--light;
          display: flex;
    
          &::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            bottom: 0;
            width: calc(#{$nav-depth} - 3px);
            background-color: $color-black--dark;
          }
        }
    
        @media (orientation: portrait) {
          position: absolute;
          bottom: 0;
          width: 100%;
          max-height: 100%;
          overflow-y: hidden;
          border-top: 3px solid $color-border--light;
          padding: $spacing-4 $spacing-5;
        }
      }
    
        .uol-gallery-modal__info-container--open {
    
          @media (orientation: portrait) {
            padding-bottom: $spacing-6;
          }
    
          @media (orientation: landscape) {
            width: calc(100vw - (#{$nav-depth} * 1.5));
    
            @include media(">=uol-media-s") {
              width: calc(80vw - (#{$nav-depth}));
            }
    
            @include media(">=uol-media-m") {
              width: calc(70vw - (#{$nav-depth}));
            }
    
            @include media(">=uol-media-l") {
              width: calc(60vw - (#{$nav-depth}));
            }
    
            @include media(">=uol-media-xl") {
              width: calc(50vw - (#{$nav-depth}));
            }
          }
        }
    
      .uol-gallery-modal__button-info {
        @media (orientation: landscape) {
          position: absolute;
          left: $spacing-4;
          top: $spacing-2;
    
          @include media(">=uol-media-s") {
            top: $spacing-5;
          }
    
          @include media(">=uol-media-l") {
            left: $spacing-3;
          }
        }
    
        .uol-gallery-modal__info-container--open & {
          border: 2px solid $color-white;
        }
      }
    
      .uol-gallery-modal__info {
        @include ds-scrollbars();
    
        display: none;
        box-sizing: border-box;
        overflow-y: auto;
    
        @media (orientation: portrait) {
          margin-top: $spacing-4;
          max-height: calc(var(--vh, 1vh) * 100 - (#{$nav-depth} * 2) - #{$spacing-6});
          padding-right: $spacing-4;
        }
    
        @media (orientation: landscape) {
          box-sizing: border-box;
          flex-basis: calc(100% - #{$nav-depth});
          margin: $spacing-5 $spacing-2 0 auto;
          padding-right: $spacing-2;
          padding-left: $spacing-4;
        }
    
        .uol-gallery-modal__button-info[aria-expanded="true"] + & {
          display: block;
    
          @media (orientation: landscape) {
            animation: info-text-fade 0.7s ease;
            animation-fill-mode: both;
          }
        }
      }
    
        .uol-gallery-modal__info__title {
          @extend .uol-typography-heading-2;
    
          margin-top: 0;
          margin-bottom: $spacing-4;
    
          @media (orientation: landscape) {
            margin-top: $spacing-4;
    
            @include media(">=uol-media-m") {
              margin-top: $spacing-2;
            }
          }
        }
    
      .uol-gallery-modal__figure {
        position: relative;
        display: flex;
        flex-wrap: wrap;
        width: 100%;
        height: 100%;
        justify-content: center;
        align-items: center;
    
        @media (orientation: landscape) {
          height: 100%;
          width: calc(100% - #{$nav-depth});
    
          .uol-gallery-modal__info-container + & {
            margin-left: $nav-depth;
            width: calc(100% - (#{$nav-depth} * 2));
          }
        }
    
        @media (orientation: portrait) {
          .uol-gallery-modal__item--has-info & {
            height: calc(100% - #{$nav-depth} - 3px);
          }
        }
      }
    
      .uol-gallery-modal__image-container {
        position: absolute;
        top: 50%;
        height: 100%;
        right: 0;
        bottom: 0;
        left: 0;
        transform: translateY(-50%);
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-items: center;
    
        img {
          max-width: 100%;
          max-height: 100%;
          object-fit: contain;
        }
      }
    
        .uol-gallery-modal__image-container--video {
          top: auto;
          transform: none;
          position: relative;
          aspect-ratio: 16/9;
          overflow: hidden;
    
          iframe {
            border: none;
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
          }
        }
    
      .uol-gallery-modal__image-caption {
        @extend %text-size-caption;
    
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        padding: $spacing-2 $spacing-4;
        background: rgba($color-black, 0.88);
        color: $color-white;
    
        // TODO: This is a hack to stop caption overlapping video controls
        @media (orientation: landscape) {
          .uol-gallery-modal__figure--video & {
            display: none;
          }
        }
      }
    
      .uol-gallery-modal__nav-container {
        box-sizing: border-box;
        position: fixed;
        background: $color-black--dark;
        display: flex;
        z-index: 1;
    
        @media (orientation: landscape) {
          top: 0;
          right: 0;
          width: $nav-depth;
          flex-direction: column;
          height: 100%;
          padding: $small-button-size + $spacing-2 $spacing-3 0;
          
          @include media(">=uol-media-s") {
            padding: $small-button-size + $spacing-5 $spacing-3 0;
          }
          
          @include media(">=uol-media-l") {
            padding: $standard-button-size + $spacing-5 $spacing-3 0;
          }
    
          button {
            margin-bottom: $spacing-2;
            margin-left: auto;
            margin-right: auto;
    
            @include media(">=uol-media-s") {
              margin-bottom: $spacing-5;
              padding: $spacing-9 $spacing-3 $spacing-4;
            }
          }
        }
    
        @media (orientation: portrait) {
          top: 0;
          height: $nav-depth;
          width: 100%;
          justify-content: center;
          align-items: center;
        }
      }
    
      .uol-gallery-modal__progress {
        font-variant-numeric: lining-nums;
    
        @media (orientation: landscape) {
          order: -1;
          text-align: center;
          margin: $spacing-2 0;
          
          @include media(">=uol-media-s") {
            margin: $spacing-5 0;
          }
        }
    
        @media (orientation: portrait) {
          margin: 0 $spacing-5;
    
          @include media(">=uol-media-xs") {
            margin: 0 $spacing-7;
          }
    
          @include media(">=uol-media-s") {
            margin: 0 $spacing-8;
          }
        }
      }
    
        .uol-gallery-modal__progress__current,
        .uol-gallery-modal__progress__total {
          position: relative;
          top: 0.05em;
        }
    
        .uol-gallery-modal__progress__current {
          color: $color-brand--bright;
        }
    
  • URL: /components/raw/uol-gallery/_gallery-modal.scss
  • Filesystem Path: src/library/02-components/gallery/_gallery-modal.scss
  • Size: 8.6 KB
  • Content:
    @mixin galleryItemHalf {
      flex-basis: calc(50% - #{$spacing-4});
      margin: 0 $spacing-2 $spacing-4;
    
      @include media(">=uol-media-l") {
        flex-basis: calc(50% - #{$spacing-5});
        margin: 0 $spacing-3 $spacing-4;
      }
    
      @include media(">=uol-media-xl") {
        flex-basis: calc(50% - #{$spacing-6});
        margin: 0 $spacing-4 $spacing-5;
      }
    }
    
    // Keyframe animations
    @keyframes skeletonBg {
      0% {
        background-color: $color-grey--light;
      }
      50% {
        background-color: rgba($color-grey--light, 0.2);
      }
      100% {
        background-color: $color-grey--light;
      }
    }
    
    @keyframes galleryImageFadeIn {
      0% {
        display: none;
        opacity: 0;
      }
      1% {
        display: block;
      }
      100% {
        display: block;
        opacity: 1;
      }
    }
    
    .uol-gallery-container {
      width: 100%;
      overflow-x: hidden;
    }
    
    .uol-gallery {
      display: flex;
      flex-wrap: wrap;
      margin-left: -#{$spacing-2};
      margin-right: -#{$spacing-2};
    
      @include media(">=uol-media-l") {
        margin-left: -#{$spacing-3};
        margin-right: -#{$spacing-3};
      }
    
      @include media(">=uol-media-xl") {
        margin-left: -#{$spacing-4};
        margin-right: -#{$spacing-4};
      }
    
      .uol-rich-text & {
        max-width: none;
      }
    }
    
      .uol-gallery__item {
        box-sizing: border-box;
    
        .js & {
          @include galleryItemHalf;
        }
    
        &:first-of-type {
          flex-basis: 100%;
        }
    
        .uol-gallery--count-2 &,
        .uol-gallery--count-4 & {
    
          &:first-of-type {
            @include galleryItemHalf;
          }
        }
    
        // Hide all after 5
        &:nth-of-type(5) ~ & {
          .js & {
            display: none;
          }
        }
      }
    
      .uol-gallery__item__title {
        .js & {
          @extend .hide-accessible;
        }
      }
    
      .uol-gallery__figure {
    
        .uol-gallery__item--video & {
          display: none;
    
          .js & {
            display: block;
          }
        }
    
        .uol-rich-text & {
          margin: 0;
        }
      }
    
      .uol-gallery__image-caption {
        @extend %text-size-caption;
    
        color: $color-font--light;
        padding-top: $spacing-2;
    
        @include media(">=uol-media-l") {
          padding-top: $spacing-3;
        }
      }
    
      .uol-gallery__image-container {
        font-size: 0;
        @include imageFit(66.6%);
    
        .uol-gallery__item:first-of-type & {
          @include imageFit(50%);
        }
    
        .uol-gallery--count-2 .uol-gallery__item:nth-of-type(2) & {
          @include imageFit(50%);
        }
    
        .uol-gallery--count-4 .uol-gallery__item:first-of-type & {
          @include imageFit(66.6%);
        }
    
        .uol-gallery__item--video & {
          outline: 1px solid rgba($color-grey--dark, 0.7);
          outline-offset: -1px;
          background-color: $color-grey--light;
          animation-name: skeletonBg;
          animation-duration: 2.49s;
          animation-iteration-count: 2;
    
          img {
            opacity: 0;
          }
        }
    
          .uol-gallery__item--video[aria-busy=false] & {
            outline: none;
    
            img {
              opacity: 0;
              display: block;
              animation: galleryImageFadeIn 1s ease-out forwards;
            }
          }
    
        @for $i from 1 through 5 {
          .uol-gallery__item--video:nth-of-type(5n + #{$i}) & {
            animation-delay: #{$i * 0.2}s;
          }
        }
      }
    
      .uol-gallery__item__content {
        .js & {
          display: none;
        }
      }
    
      .uol-gallery__button {
        background: rgba($color-black, 0.75);
        border: none;
      }
    
      .uol-gallery__button--open-item {
        @include button_focus(-3px, false, $color-brand--bright);
    
        position: absolute;
        width: 45px;
        height: 45px;
        border-radius: 50%;
        left: $spacing-2;
        bottom: $spacing-2;
        transition: all 0.15s;
    
        &:hover,
        &:focus {
          background: $color-black;
          color: $color-brand--bright;
        }
    
        @include media(">=uol-media-m") {
          left: $spacing-5;
          bottom: $spacing-5;
        }
    
        // Override the default uol-icon positioning
        .js .uol-gallery__item & {
          position: absolute;
        }
      }
    
      .uol-gallery__button--with-count {
        @include button_focus(-2px, false, $color-brand--bright);
        @extend %text-size-heading-2;
    
        position: absolute;
        width: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        background: rgba($color-black, 0.65);
        color: $color-white;
        z-index: 2;
        text-decoration-color: $color-brand--bright;
        transition: all 0.15s;
    
        &:hover,
        &:focus {
          background: rgba($color-black, 0.75);
          text-decoration: underline;
          text-decoration-color: $color-brand--bright;
    
          @media (-ms-high-contrast: active), (forced-colors: active) {
            text-decoration-color: ButtonText;
            border: 2px solid ButtonText;
          }
        }
      }
    
    // Update for video variant using play icon
    .js .uol-gallery__item--video-play-icon 
    .uol-button.uol-icon--icon-only {
      @extend %text-size-heading-2;
    
      background: $color-brand;
      border: 2px solid $color-white;
      border-radius: 50%;
      color: $color-white;
      aspect-ratio: 1/1;
      width: $spacing-8;
      height: $spacing-8;
      left: $spacing-5;
      bottom: $spacing-5;
    
      svg {
        width: $spacing-7;
        height: $spacing-7;
      }
    }
  • URL: /components/raw/uol-gallery/_gallery.scss
  • Filesystem Path: src/library/02-components/gallery/_gallery.scss
  • Size: 5 KB
  • Content:
    // TODO: IE11 resizeObserver polyfill
    import ResizeObserver from "resize-observer-polyfill";
    
    /*
     * Set shift keydown listener
     * Needed to overcome tab oder/focus issues when reverse tabbing.
     * See listenGalleryScroll()
    */
    let shiftKeyDown = false;
    function isShiftKeyDown(event) {
      shiftKeyDown = event.shiftKey;
    }
    document.addEventListener("keydown", isShiftKeyDown);
    document.addEventListener("keyup", isShiftKeyDown);
    
    /**
     * Returns the modal outer to contain the new gallery
     * @returns {element} Modal element
     */
    const modalOuter = () => {
      const modal = document.createElement("div");
      modal.setAttribute("role", "dialog");
      modal.setAttribute("tabindex", "-1");
      modal.setAttribute("aria-modal", "true");
      modal.setAttribute("aria-label", "Gallery carousel");
      modal.classList.add("uol-gallery-modal");
    
      return modal;
    };
    
    /**
     * HTML fragment for close button
     */
    const buttonClose = `
      <button class="uol-button uol-icon uol-icon--icon-only uol-icon--mdiClose uol-gallery-modal__button-close" type="button">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true">
          <path fill="#000000" fill-rule="nonzero" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"></path>
        </svg>
        <span class="uol-icon__label">Close</span>
      </button>
    `;
    
    /**
     * Create the modal gallery navigation.
     * Returns '' if only 1 item and HTML string otherwise
     * @param {object} items - NodeList of the gallery's initial items
     * @param {number} index - the index of the gallery item to open on
     * @returns {string} HTML for modal navigation
     */
    const galleryNavigation = (items, index) => {
      // if there is only one item we do not need navigation
      if (items.length === 1) return ''
      // Otherwise return navigation
      return `
      <div class="uol-gallery-modal__nav-container" aria-hidden="true">
        <button
          tabindex="-1"
          class="
            uol-gallery-modal__button-nav
            uol-gallery-modal__button-nav--prev
            uol-button uol-button--bright uol-icon uol-icon--icon-only uol-icon--mdiArrowLeft"
          type="button" ${index === 0 ? "disabled" : ""}>
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true">
            <path fill="#000000" fill-rule="nonzero" d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"></path>
          </svg>
          <span class="uol-icon__label">Previous</span>
        </button>
        <span class="uol-gallery-modal__progress">
          <span class="uol-gallery-modal__progress__current">${index + 1}</span> /
          <span class="uol-gallery-modal__progress__total">${items.length}</span>
        </span>
        <button tabindex="-1" class="uol-gallery-modal__button-nav uol-gallery-modal__button-nav--next uol-button uol-button--bright uol-icon uol-icon--icon-only uol-icon--mdiArrowRight" type="button" ${
          index === items.length - 1 ? "disabled" : ""
        }>
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true">
            <path fill="#000000" fill-rule="nonzero" d="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z"></path>
          </svg>
          <span class="uol-icon__label">Next</span>
        </button>
      </div>
      `;
    };
    
    /**
     * Create the list of gallery items
     * @param  {Object} items - NodeList of the gallery's initial items
     * @returns {string} - HTML fragment for gallery items list
     */
    const galleryItems = (items) => {
      return `
        <ol class="uol-gallery-modal__track">
          ${[...items]
            .map((item, idx) => {
              return galleryItem(item, idx, items.length);
            })
            .join("")}
        </ol>
      `;
    };
    
    /**
     * Create the gallery item for inclusion in the list
     * @param  {object} item - item from NodeList of items
     * @param  {number} idx - the index of this item in the Items object
     * @returns {string} - HTML fragment for gallery item
     */
    const galleryItem = (item, idx, itemsLength) => {
      const hasInfo = item.querySelector(".uol-gallery__item__content");
      return `
        <li class="uol-gallery-modal__item ${
          hasInfo ? "uol-gallery-modal__item--has-info" : ""
        }" tabindex="0" aria-label="Item ${idx + 1} of ${itemsLength}">
          ${galleryItemInfo(item, idx)}
          ${galleryItemFigure(item, idx)}
        </li>
      `;
    };
    
    /**
     * Create the gallery item info for inclusion in the gallery item
     * @param  {object} item - item from NodeList of items
     * @param  {number} idx - the index of this item in the Items object
     * @returns {string} - HTML fragment for gallery item info
     */
    const galleryItemInfo = (item, idx) => {
      // Get item title
      const itemTitle = item.querySelector(".uol-gallery__item__title");
      const itemTitleText = itemTitle ? itemTitle.innerText : null;
    
      // Get item text content
      const itemContent = item.querySelector(".uol-gallery__item__content");
      const itemContentHTML = itemContent ? itemContent.innerHTML : "";
    
      // If no info content return empty string
      if (!itemContentHTML) return "";
      // Otherwise return the info fragment
      return `
        <div class="uol-gallery-modal__info-container">
          <button aria-expanded="false" class="uol-gallery-modal__button-info uol-button uol-button--bright uol-icon uol-icon--icon-only uol-icon--icon-only--large uol-icon--mdiInformationVariant" type="button">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true">
              <path fill="#000000" fill-rule="nonzero" d="M13.5,4A1.5,1.5 0 0,0 12,5.5A1.5,1.5 0 0,0 13.5,7A1.5,1.5 0 0,0 15,5.5A1.5,1.5 0 0,0 13.5,4M13.14,8.77C11.95,8.87 8.7,11.46 8.7,11.46C8.5,11.61 8.56,11.6 8.72,11.88C8.88,12.15 8.86,12.17 9.05,12.04C9.25,11.91 9.58,11.7 10.13,11.36C12.25,10 10.47,13.14 9.56,18.43C9.2,21.05 11.56,19.7 12.17,19.3C12.77,18.91 14.38,17.8 14.54,17.69C14.76,17.54 14.6,17.42 14.43,17.17C14.31,17 14.19,17.12 14.19,17.12C13.54,17.55 12.35,18.45 12.19,17.88C12,17.31 13.22,13.4 13.89,10.71C14,10.07 14.3,8.67 13.14,8.77Z"></path>
            </svg>
            <span class="uol-icon__label">Show information</span>
          </button>
          <aside class="uol-gallery-modal__info" tabindex="0" aria-labelledby="item-title-${idx}">
            ${
              itemTitle
                ? `<h2 id="item-title-${idx}" class="uol-gallery-modal__info__title">${itemTitleText}</h2>`
                : ""
            }
            ${itemContentHTML}
          </aside>
        </div>
      `;
    };
    
    /**
     * Create the <figure> element the the gallery item
     * @param {object} item - item from NodeList of items
     * @param {number} idx - the index of this item in the Items object
     * @returns {string} - HTML fragment for gallery item figure element
     */
    const galleryItemFigure = (item, idx) => {
      // Get image and attributes
      const itemImage = item.querySelector(".uol-gallery__image-container img");
      const imgAlt = itemImage ? itemImage.getAttribute("alt") : "";
    
      // Set image src to either image data attribute or native src
      let imgSrc
      if ( itemImage.dataset.srcHighQuality ) {
        imgSrc = itemImage.dataset.srcHighQuality;
      } else if (itemImage.getAttribute("src")) {
        imgSrc = itemImage.getAttribute("src");
      }
    
      // Set image caption if present in item
      const caption = item.querySelector(".uol-gallery__image-caption");
      const captionText = caption ? caption.innerText : null;
    
      // Set YouTube constants
      const youtubeUrl = item.dataset.youtubeUrl;
      const youtubeTitle = item.dataset.youtubeTitle;
      const youtubeId = item.dataset.youtubeId;
    
      let figureContent;
      if (youtubeUrl) {
        // As the YouTube iframe breaks the focus trap
        // we include a visually hidden text link so that
        // there is a focusable element after the iframe
        figureContent = `
        <iframe
          class="youtube"
          tabindex="0"
          id="video-${idx}"
          data-youtube-id="${youtubeId}"
          title="YouTube: ${youtubeTitle}"
          src="${youtubeUrl}"
          allow="autoplay"
          loading="lazy"></iframe>
        <a class="uol-gallery-modal__embed-link" href="${youtubeUrl}" target="_blank" rel="noopener">Open <span class="hide-accessible">${youtubeTitle}</span> in new window</a>`;
      } else if (itemImage) {
        figureContent = `<img src="${imgSrc}" alt="${imgAlt}" loading="lazy" />`;
      }
      return `
        <figure class="uol-gallery-modal__figure${
          youtubeUrl ? " uol-gallery-modal__figure--video" : ""
        }">
          <div class="uol-gallery-modal__image-container ${
            youtubeUrl ? "uol-gallery-modal__image-container--video" : ""
          }">
            ${figureContent}
          </div>
            ${
              captionText
                ? `<figcaption class="uol-gallery-modal__image-caption">${captionText}</figcaption>`
                : ""
            }
        </figure>
      `;
    };
    
    /**
     * "Close gallery" tasks
     * @param {object} modal - The current modal gallery
     * @param {object} focusedElementBeforeModal - HTML node that triggered the gallery open
     */
    const closeGalleryModal = (modal, focusedElementBeforeModal) => {
      galleryPageContent.hidden = false;
      modal.remove();
      focusedElementBeforeModal.focus();
      focusedElementBeforeModal.parentElement.scrollIntoView();
    };
    
    /**
     * Container function for listeners
     * @param {object} modal - The current modal gallery
     * @param {object} focusedElementBeforeModal - HTML node that triggered the gallery open
     */
    const addListeners = (modal, focusedElementBeforeModal) => {
      const closeButton = modal.querySelector(".uol-gallery-modal__button-close");
      // Listen for close actions
      listenCloseActions(modal, focusedElementBeforeModal, closeButton);
      // Listen for tab keys
      listenTabKey(modal, closeButton);
      // Listen for button navigation
      listenNavButtons(modal);
      // Listen for swipe and scroll navigation
      listenGalleryScroll(modal);
      // Listen to info buttons clicks
      listenInfoButtons(modal);
    
      listenOrientationChange(modal);
    };
    
    /**
     * Listen for close actions. ie Close button press or escape key
     * @param {object} modal
     * @param {object} focusedElementBeforeModal
     * @param {object} closeButton
     */
    const listenCloseActions = (modal, focusedElementBeforeModal, closeButton) => {
      closeButton.onclick = () => {
        closeGalleryModal(modal, focusedElementBeforeModal);
      };
    
      // Handle escape key
      modal.addEventListener("keydown", (event) => {
        if (event.keyCode === 27) {
          closeGalleryModal(modal, focusedElementBeforeModal);
        }
      });
    };
    
    /**
     * Listen for Tab key and ensure modal focus trap
     */
    const listenTabKey = (modal, closeButton) => {
      const modelItems = modal.querySelectorAll(".uol-gallery-modal__item");
      modal.addEventListener("keydown", (event) => {
        // Handle TAB key to produce a modal trap
        if (event.keyCode === 9) {
          // Find all focusable modal elements
          let focusableElements = modal.querySelectorAll(
            ".uol-gallery-modal__item, .uol-gallery-modal__track button:not([disabled]), .uol-gallery-modal__track a"
          );
    
          // We're only interested in visible elements so we create a new array containing only those elements where the offsetWidth !== 0
          let visibleFocusableElements = [];
          focusableElements.forEach((element) => {
            if (element.offsetWidth !== 0) {
              visibleFocusableElements.push(element);
            }
          });
    
          // Define the last focusable element
          const lastElementOfModal =
            visibleFocusableElements[visibleFocusableElements.length - 1];
    
          // If last item in gallery carousel
          if (document.activeElement === lastElementOfModal) {
            event.preventDefault();
            closeButton.focus();
          }
    
          // Shift + Tab on close button - return to last slide
          if (event.shiftKey && document.activeElement === closeButton) {
            event.preventDefault();
            modelItems[modelItems.length - 1].focus();
          }
        }
      });
    };
    
    /**
     * Listen to navigation buttons and scroll the gallery track to the new item
     * @param {Element} modal
     */
    const listenNavButtons = (modal) => {
      const navButtons = modal.querySelectorAll(".uol-gallery-modal__button-nav");
      const navButtonPrev = modal.querySelector(
        ".uol-gallery-modal__button-nav--prev"
      );
      const navButtonNext = modal.querySelector(
        ".uol-gallery-modal__button-nav--next"
      );
      const modalTrack = modal.querySelector(".uol-gallery-modal__track");
      const modelItems = modalTrack.querySelectorAll(".uol-gallery-modal__item");
      const counter = modal.querySelector(".uol-gallery-modal__progress__current");
    
      navButtons.forEach((navButton) => {
        navButton.onclick = () => {
          let current = parseInt(counter.innerText) - 1;
          if (navButton == navButtonPrev) {
            current--;
          } else if (navButton == navButtonNext) {
            current++;
          }
    
          // Scroll to item
          modalTrack.scrollLeft = modelItems[current].offsetLeft;
    
          // Update counter
          counter.innerText = current + 1;
    
          if (current === 0) {
            navButtonPrev.disabled = true;
            navButtonNext.disabled = false;
          } else if (current === modelItems.length - 1) {
            navButtonNext.disabled = true;
            navButtonPrev.disabled = false;
          } else {
            navButtonNext.disabled = false;
            navButtonPrev.disabled = false;
          }
        };
      });
    };
    
    /**
     * Listen for modal track scroll and:
     * - Update current item display
     * - Update previous and next buttons
     * - Pause offscreen YouTube videos
     * @param {Element} modal
     */
    const listenGalleryScroll = (modal) => {
      // TODO: Add all listeners to wrapper function and declare these all once
      const modalTrack = modal.querySelector(".uol-gallery-modal__track");
      const modelItems = modalTrack.querySelectorAll(".uol-gallery-modal__item");
      const counter = modal.querySelector(".uol-gallery-modal__progress__current");
      const navButtonPrev = modal.querySelector(
        ".uol-gallery-modal__button-nav--prev"
      );
      const navButtonNext = modal.querySelector(
        ".uol-gallery-modal__button-nav--next"
      );
    
      let isScrolling = null;
      modalTrack.addEventListener("scroll", () => {
        window.clearTimeout(isScrolling);
    
        isScrolling = setTimeout(() => {
          modelItems.forEach((item, itemIndex) => {
            let itemLeft = item.getBoundingClientRect().x;
            if (!itemLeft)
              // Handle IE11
              itemLeft = item.getBoundingClientRect().left;
    
            // If item is more than half in view
            if (itemLeft >= -10 && itemLeft < modalTrack.clientWidth / 2) {
              // Update counter text
              counter.innerText = itemIndex + 1;
    
              /*
               * Focus on current item unless the shift key is down
               * to overcome tab oder/focus issues when reverse tabbing.
               */
              if (!shiftKeyDown) {
                item.focus();
              }
    
              // Update buttons on scroll
              if (itemIndex === 0) {
                navButtonPrev.disabled = true;
                navButtonNext.disabled = false;
              } else if (itemIndex === modelItems.length - 1) {
                navButtonNext.disabled = true;
                navButtonPrev.disabled = false;
              } else {
                navButtonNext.disabled = false;
                navButtonPrev.disabled = false;
              }
    
              // Pause all youtube videos
              pauseYouTubeVideos(modal);
            }
          });
        }, 100);
      });
    };
    
    const pauseYouTubeVideos = (modal) => {
      const videoIframes = modal.querySelectorAll('iframe')
    
      videoIframes.forEach((iframe) => {
    
        // Check if loaded after lazy by seeing if postMessage is available
        const iframeDoc = iframe.contentWindow;
    
        // Only post message if iframe accepts postMessage
        // NB: Relies on undocumented YouTube postMessage API.
        // If this stops working we will need to replace with the YouTube iframe API
        if ( iframeDoc.postMessage ) {
          iframe.contentWindow.postMessage(
            JSON.stringify({ event: "command", func: "pauseVideo" }),
            "https://www.youtube-nocookie.com"
          );
        }
    
      });
    }
    
    const listenInfoButtons = (modal) => {
      const infoButtons = modal.querySelectorAll(".uol-gallery-modal__button-info");
    
      infoButtons.forEach((infoButton) => {
        const infoParent = infoButton.closest(".uol-gallery-modal__info-container");
    
        infoButton.onclick = () => {
          let expanded =
            infoButton.getAttribute("aria-expanded") === "true" || false;
    
          infoButton.setAttribute("aria-expanded", !expanded);
    
          if (!expanded) {
            infoParent.classList.add("uol-gallery-modal__info-container--open");
          } else {
            infoParent.classList.remove("uol-gallery-modal__info-container--open");
          }
        };
      });
    };
    
    /**
     * Listen for orientation changes and move the info container
     * to ensure correct tab order
     * @param {*} modal
     */
    const listenOrientationChange = modal => {
      // Get the gallery items
      const items = modal.querySelectorAll(".uol-gallery-modal__item");
    
      // Create the query list.
      const mediaQueryList = window.matchMedia("(orientation: portrait)");
    
      // Define a callback function for the event listener.
      function handleOrientationChange(mql) {
    
        items.forEach((item) => {
          // Get the info container
          const itemInfo = item.querySelector(
            ".uol-gallery-modal__info-container"
          );
    
          // If item contains info container
          if (itemInfo) {
            // if portrait orientation
            if (mql.matches) {
              item.insertAdjacentElement("beforeend", itemInfo);
            } else {
              item.insertAdjacentElement("afterbegin", itemInfo);
            }
          }
        });
      }
    
      // Run the orientation change handler once.
      handleOrientationChange(mediaQueryList);
    
      // Add the callback function as a listener to the query list.
      // TODO: IE11 support - revert to addEventListener when we drop is
      // mediaQueryList.addEventListener("change", handleOrientationChange);
      mediaQueryList.addListener(handleOrientationChange);
    }
    
    const galleryPageContent = document.querySelector(".site-outer");
    
    /**
     * Add open modal buttons to each item in initial gallery.
     * @param {NodeList} items
     */
    const galleryAddButtons = (items) => {
      items.forEach((item, index) => {
        const button = document.createElement("button");
        button.classList.add("uol-gallery__button");
    
        // Set basic buttonText
        let buttonText = `Open gallery at item ${ index + 1 } of ${items.length}`
    
        // Enhance buttonText
        // If item has title append title
        if (item.querySelector(".uol-gallery__item__title")) {
          buttonText +=
            ": " + item.querySelector(".uol-gallery__item__title").innerHTML;
        }
        // If item does not have title and is type video append ": Video"
        else if (item.classList.includes("uol-gallery__item--video")) {
          buttonText += ": Video";
        }
        // If item does not have title and is type image append ": Image"
        else if (item.classList.includes("uol-gallery__item--image")) {
          buttonText += ": Image";
        }
    
        button.innerHTML = `<span class="hide-accessible">${buttonText}</span>`;
        if (index === 4 && items.length > 5) {
          button.classList.add("uol-gallery__button--with-count");
          button.innerHTML =
            button.innerHTML +
            `<span aria-hidden="true">+${items.length - (index + 1)}</span>`;
        } else {
          button.classList.add("uol-button");
          button.classList.add("uol-icon");
          button.classList.add("uol-icon--icon-only");
          button.classList.add("uol-gallery__button--open-item");
    
          // Check if 'uol-gallery__item--video-play-icon' exists on video item (this can be passed through config)
          const isVideoPlayIcon = item.classList.contains('uol-gallery__item--video-play-icon');
          isVideoPlayIcon ? button.classList.add("uol-icon--mdiPlay") : button.classList.add("uol-icon--mdiArrowExpand");
        }
        button.onclick = (event) => {
          openGalleryModal(items, index, event.target);
        };
        item.querySelector(".uol-gallery__image-container").appendChild(button);
      });
    };
    
    /*
     * youtube_parser
     * Get YouTube IDs from URLs
     */
    const youtube_parser = (url) => {
      var regExp =
        /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
      var match = url.match(regExp);
      return match && match[7].length == 11 ? match[7] : false;
    };
    
    /**
     * Get images for gallery items from YouTube
     * @param {NodeList} items
     */
    const galleryVideoImages = (items) => {
      // TODO: Add full oEmbed support
      items.forEach((item) => {
        const videoUrl = item.dataset.video;
    
        if (videoUrl) {
          const youtubeId = youtube_parser(videoUrl);
          const itemImg = item.querySelector("img");
          itemImg.hidden = true;
          item.setAttribute("aria-busy", true )
    
          itemImg.onload = () => {
            item.setAttribute("aria-busy", false);
            itemImg.hidden = false;
          };
    
          if (youtubeId) {
            itemImg.src =
              "https://i.ytimg.com/vi/" + youtubeId + "/maxresdefault.jpg";
    
            fetch(
              "https://www.youtube.com/oembed?url=http%3A//www.youtube.com/watch?v%3D" +
                youtubeId +
                "&format=json",
              {
                method: "get",
              }
            )
              .then((response) => response.json())
              .then((data) => {
                const youtubeSrc = data.html.match(
                  /\<iframe.+src\=(?:\"|\')(.+?)(?:\"|\')(?:.+?)\>/
                );
                const youtubeURL = youtubeSrc[1].replace(
                  "youtube.com",
                  "youtube-nocookie.com"
                );
    
                item.dataset.youtubeUrl =
                  youtubeURL + "&rel=0&enablejsapi=1";
                item.dataset.youtubeTitle = data.title;
                item.dataset.youtubeId = youtubeId;
              });
          }
        }
      });
    };
    
    /**
     * Creates the gallery modal on button click
     * @param {NodeList} items - NodeList of the gallery's initial items
     * @param {number} index - the index of the gallery item to open on
     * @param {Node} focusedElementBeforeModal - HTML node that triggered the gallery open
     */
    const openGalleryModal = (items, index, focusedElementBeforeModal) => {
    
      // Create modal
      const modal = modalOuter();
    
      // Add close button to modal
      modal.innerHTML += buttonClose;
    
      // Add gallery items to modal
      modal.innerHTML += galleryItems(items);
    
      // Add navigation to modal
      modal.innerHTML += galleryNavigation(items, index);
    
      // Add listeners
      addListeners(modal, focusedElementBeforeModal);
    
      // Add modal to page
      document.body.appendChild(modal);
    
      // Scroll to selected item
      const modelItems = modal.querySelectorAll(".uol-gallery-modal__item");
      // Delay scroll to work around iOS Safari rendering issues
      setTimeout(() => {
        modelItems[index].focus();
        modelItems[index].scrollIntoView();
        // Add smooth scroll class for future interactions
        const modalTrack = modal.querySelector(".uol-gallery-modal__track");
        modalTrack.classList.add("uol-gallery-modal__track--smooth");
      }, 10);
    
      // Resize iframes to keep aspect ratio
      const videoFigures = modal.querySelectorAll(
        ".uol-gallery-modal__figure--video"
      );
    
      videoFigures.forEach( videoFigure => {
        const container = videoFigure;
        const object = videoFigure.querySelector(
          ".uol-gallery-modal__image-container--video"
        );
        const aspectRatio = 16 / 9;
    
        function update() {
          const isTall =
            container.clientWidth / container.clientHeight < aspectRatio;
    
          // if IE 11 TODO: IE11 hack
          if (window.msCrypto) {
            object.style.width = "100%";
            object.style.height = "100%";
          } else {
            object.style.width = isTall ? "100%" : "auto";
            object.style.height = isTall ? "auto" : "100%";
          }
        }
    
        new ResizeObserver(update).observe(container);
      })
    
      // Hide other page content
      if (galleryPageContent) {
        galleryPageContent.hidden = true;
      }
    };
    
    
    export const uolGallery = () => {
      const galleries = document.querySelectorAll(".uol-gallery");
    
      galleries.forEach((gallery) => {
        const items = gallery.querySelectorAll(".uol-gallery__item");
    
        galleryVideoImages(items);
    
        galleryAddButtons(items);
      });
    };
    
  • URL: /components/raw/uol-gallery/gallery.module.js
  • Filesystem Path: src/library/02-components/gallery/gallery.module.js
  • Size: 24 KB
{
  "gallery": {
    "headingLevel": 2,
    "items": [
      {
        "title": "&ldquo;Dual Form&rdquo; by Barbara Hepworth",
        "img": {
          "srcHighQuality": "/placeholders/campus/full/29940.jpeg",
          "src": "/placeholders/campus/medium/29940.jpeg",
          "alt": "Dual Form sculpture by Barbara Hepworth with people relaxing on grass in background",
          "caption": "&ldquo;Dual Form&rdquo; by Barbara Hepworth"
        },
        "content": "<p>Lorem ipsum <a href='/some-text-link'>dolor sit amet consectetur</a> adipisicing elit. Corrupti, quasi nostrum blanditiis hic totam a id architecto molestias, sunt vitae iste consectetur cupiditate incidunt autem illo, consequuntur recusandae? Ipsam, asperiores.</p><p>Expedita saepe illo vero sit et! Eveniet, deserunt. Nihil omnis fugit ut veniam ullam, non maiores, consequatur amet enim dolore totam, laborum accusantium voluptatum iure est ab aspernatur reiciendis explicabo.</p><p>Eos reprehenderit suscipit, at eveniet, minus ea quod quis provident, nisi fugit maiores molestias culpa. Rerum rem pariatur quo mollitia autem omnis eum officiis, natus beatae eos saepe culpa earum!</p><p>Eum ut tempore delectus quos unde tenetur neque perspiciatis. Dicta sunt rem dolore, in ab impedit assumenda, quaerat neque quos veritatis consequatur accusantium dignissimos eius natus iusto nostrum eos maxime.</p>"
      },
      {
        "title": "Edward Boyle Library",
        "img": {
          "srcHighQuality": "/placeholders/campus/full/28573.jpeg",
          "src": "/placeholders/campus/medium/28573.jpeg",
          "alt": "Steps outside Edward Boyle Library"
        },
        "content": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Corrupti, quasi nostrum blanditiis hic totam a id architecto molestias, sunt vitae iste consectetur cupiditate incidunt autem illo, consequuntur recusandae? Ipsam, asperiores.</p><p>Expedita saepe illo vero sit et! Eveniet, deserunt. Nihil omnis fugit ut veniam ullam, non maiores, consequatur amet enim dolore totam, laborum accusantium voluptatum iure est ab aspernatur reiciendis explicabo.</p><p>Eos reprehenderit suscipit, at eveniet, <a href='/some-text-link'>minus ea quod</a> quis provident, nisi fugit maiores molestias culpa. Rerum rem pariatur quo mollitia autem omnis eum officiis, natus beatae eos saepe culpa earum!</p><p>Eum ut tempore delectus quos unde tenetur neque perspiciatis. Dicta sunt rem dolore, in ab impedit assumenda, quaerat neque quos veritatis consequatur accusantium dignissimos eius natus iusto nostrum eos maxime.</p>"
      }
    ]
  }
}