/ shared / components / src / components / Modal / ContentModal.svelte
ContentModal.svelte
  1  <script lang="ts">
  2      import { createEventDispatcher, onMount } from 'svelte';
  3      import CloseIcon from '@amp/web-app-components/assets/icons/close.svg';
  4      import { updateScrollAndWindowDependentVisuals } from '@amp/web-app-components/src/actions/updateScrollAndWindowDependentVisuals';
  5      import { focusNodeOnMount } from '@amp/web-app-components/src/actions/focus-node-on-mount';
  6      import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html';
  7  
  8      export let title: string | null;
  9      export let subtitle: string | null;
 10      export let text: string = null;
 11      export let translateFn: (key: string) => string;
 12      export let dialogTitleId: string | null = null;
 13  
 14      let contentContainerElement: HTMLElement;
 15      let contentIsScrolling = false;
 16      let hideGradient = false;
 17  
 18      const dispatch = createEventDispatcher();
 19  
 20      const handleCloseButton = (e: Event) => {
 21          e.preventDefault();
 22          e.stopPropagation();
 23  
 24          dispatch('close');
 25      };
 26  
 27      onMount(() => {
 28          // get initial state for hideGradient value, before user has scrolled
 29          let { scrollHeight, offsetHeight } = contentContainerElement;
 30          hideGradient = scrollHeight - offsetHeight === 0;
 31      });
 32  </script>
 33  
 34  <div
 35      data-testid="content-modal"
 36      class="content-modal-container"
 37      class:hide-gradient={hideGradient}
 38      dir="auto"
 39  >
 40      <div class="button-container">
 41          <button
 42              data-testid="content-modal-close-button"
 43              class="close-button"
 44              type="button"
 45              on:click={handleCloseButton}
 46              aria-label={translateFn('AMP.Shared.AX.Close')}
 47              use:focusNodeOnMount
 48          >
 49              <CloseIcon data-testid="content-modal-close-button-svg" />
 50          </button>
 51          {#if $$slots['button-container']}
 52              <slot name="button-container" />
 53          {/if}
 54      </div>
 55      {#if title || subtitle}
 56          <div
 57              class="header-container"
 58              class:content-is-scrolling={contentIsScrolling}
 59          >
 60              {#if title}
 61                  <h1
 62                      id={dialogTitleId}
 63                      data-testid="content-modal-title"
 64                      class="title"
 65                  >
 66                      {title}
 67                  </h1>
 68              {/if}
 69              {#if subtitle}
 70                  <h2 data-testid="content-modal-subtitle" class="subtitle">
 71                      {subtitle}
 72                  </h2>
 73              {/if}
 74          </div>
 75      {/if}
 76      {#if text || $$slots['content']}
 77          <div
 78              class="content-container"
 79              bind:this={contentContainerElement}
 80              use:updateScrollAndWindowDependentVisuals
 81              on:scrollStatus={(e) => {
 82                  contentIsScrolling = e.detail.contentIsScrolling;
 83                  hideGradient = e.detail.hideGradient;
 84              }}
 85          >
 86              {#if $$slots['content']}
 87                  <slot name="content" />
 88              {:else}
 89                  <p data-testid="content-modal-text">
 90                      {@html sanitizeHtml(text)}
 91                  </p>
 92              {/if}
 93          </div>
 94      {/if}
 95  </div>
 96  
 97  <style lang="scss">
 98      .content-modal-container {
 99          position: relative;
100          min-height: 230px;
101          max-height: calc(100vh - 160px);
102          height: auto;
103          display: flex;
104          flex-direction: column;
105          align-items: center;
106          max-width: 691px;
107          width: 80vw;
108          overflow: hidden;
109          background-color: var(--pageBG);
110          border-radius: var(--modalBorderRadius);
111  
112          @media (--range-xsmall-only) {
113              max-width: auto;
114              width: calc(100vw - 50px);
115          }
116  
117          &::after {
118              position: absolute;
119              bottom: 0;
120              height: 64px;
121              opacity: 1;
122              pointer-events: none;
123              transition-delay: 0s;
124              transition-duration: 300ms;
125              transition-property: height, width, background;
126              width: calc(100% - 60px);
127              content: '';
128              background: linear-gradient(
129                  to top,
130                  var(--pageBG) 0%,
131                  rgba(var(--pageBG-rgb), 0) 100%
132              );
133              z-index: var(--z-default);
134  
135              @media (--range-xsmall-only) {
136                  width: calc(100% - 40px);
137              }
138          }
139      }
140  
141      .header-container {
142          pointer-events: none;
143          position: sticky;
144          transition-delay: 0s;
145          transition-duration: 500ms;
146          transition-property: height, width;
147          width: 100%;
148          max-height: 120px;
149          padding-bottom: 22px;
150          z-index: var(--z-default);
151      }
152  
153      .content-is-scrolling {
154          box-shadow: 0 3px 5px var(--systemQuaternary);
155      }
156  
157      .button-container {
158          display: flex;
159          align-self: flex-start;
160          justify-content: space-between;
161          width: 100%;
162      }
163  
164      .close-button {
165          margin-top: 16px;
166          margin-bottom: 20px;
167          width: 18px;
168          height: 18px;
169          fill: var(--systemSecondary);
170          margin-inline-start: 20px;
171      }
172  
173      .title {
174          color: var(--systemPrimary);
175          padding: 0 30px;
176          font: var(--title-1-emphasized);
177  
178          @media (--range-xsmall-only) {
179              padding-inline-start: 20px;
180              padding-inline-end: 20px;
181          }
182  
183          @media (--small) {
184              font: var(--large-title-emphasized);
185          }
186      }
187  
188      .subtitle {
189          color: var(--systemSecondary);
190          padding: 0 30px;
191          font: var(--body);
192  
193          @media (--range-xsmall-only) {
194              padding-inline-start: 20px;
195              padding-inline-end: 20px;
196          }
197      }
198  
199      .content-container {
200          position: relative;
201          width: 100%;
202          height: calc(100% - 120px);
203          padding-bottom: 42px;
204          overflow-y: auto;
205          white-space: pre-wrap;
206          text-align: start;
207          font: var(--title-3-tall);
208          padding-inline-start: 30px;
209          padding-inline-end: 30px;
210  
211          @media (--range-xsmall-only) {
212              padding-inline-start: 20px;
213              padding-inline-end: 20px;
214          }
215      }
216  
217      .hide-gradient {
218          &::after {
219              opacity: 0;
220          }
221      }
222  </style>