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>