Button.svelte
1 <script lang="ts"> 2 // TODO: rdar://92270447 (JMOTW: Refactor ButtonAction component to use Button component) 3 import { createEventDispatcher, onMount } from 'svelte'; 4 import { makeSafeTick } from '@amp/web-app-components/src/utils/makeSafeTick'; 5 6 const dispatch = createEventDispatcher(); 7 8 const handleButtonClick = () => { 9 dispatch('buttonClick'); 10 }; 11 12 // Button A, B, etc. refers to the button spec 13 // https://pd-hi.apple.com/viewvc/Common/Modules/macOS/Music/-Common%20Elements/Buttons.png 14 // alertButton and alertButtonSecondary refer to Alert Modal spec 15 // https://pd-hi.apple.com/viewvc/Common/Modules/macOS/-Cross%20Product/_web%20-%20Alerts.png 16 type ButtonType = 17 | 'buttonA' 18 | 'buttonB' 19 | 'buttonD' 20 | 'alertButton' 21 | 'alertButtonSecondary' 22 | 'pillButton' 23 | 'socialProfileButton' 24 | 'textButton' 25 | null; 26 27 export let buttonStyle: string | null = null; 28 export let makeFocused = false; 29 export let ariaLabel: string | null = null; 30 export let type: 'button' | 'submit' = 'button'; 31 export let disabled = false; 32 export let buttonElement: HTMLButtonElement = null; 33 34 // Need to do this to resolve TS error: 35 // Type 'string' is not assignable to type 'ButtonType' 36 $: buttonType = buttonStyle as ButtonType; 37 38 function handleKeyUp(e: KeyboardEvent) { 39 if (e.key === 'Enter' || e.key === 'Escape') { 40 handleButtonClick(); 41 } 42 } 43 44 const safeTick = makeSafeTick(); 45 46 onMount(async () => { 47 await safeTick(async (tick) => { 48 await tick(); 49 if (makeFocused) { 50 buttonElement.focus(); 51 } 52 }); 53 }); 54 </script> 55 56 <div 57 class="button" 58 class:primary={buttonType === 'buttonA'} 59 class:secondary={buttonType === 'buttonB'} 60 class:tertiary={buttonType === 'buttonD'} 61 class:alert={buttonType && buttonType.startsWith('alertButton')} 62 class:alert-secondary={buttonType === 'alertButtonSecondary'} 63 class:pill={buttonType === 'pillButton'} 64 class:button--text-button={buttonType === 'textButton'} 65 class:socialProfileButton={buttonType === 'socialProfileButton'} 66 data-testid="button-base-wrapper" 67 > 68 <button 69 on:click={handleButtonClick} 70 data-testid="button-base" 71 aria-label={ariaLabel} 72 bind:this={buttonElement} 73 on:keyup={handleKeyUp} 74 class:link={buttonType === 'textButton'} 75 {type} 76 {disabled} 77 > 78 {#if $$slots['icon-before']} 79 <div class="button__icon button__icon--before"> 80 <slot name="icon-before" /> 81 </div> 82 {/if} 83 <slot /> 84 {#if $$slots['icon-after']} 85 <div class="button__icon button__icon--after"> 86 <slot name="icon-after" /> 87 </div> 88 {/if} 89 </button> 90 </div> 91 92 <style lang="scss"> 93 @use '@amp/web-shared-styles/app/core/globalvars' as *; 94 @use '@amp/web-shared-styles/app/core/mixins/keycolor-button-states' as *; 95 96 // TODO: rdar://104573582 (Refactor <Button> and <ButtonAction> styles) 97 .button { 98 width: var(--buttonWrapperWidth, 100%); 99 100 @media (--medium) { 101 width: var(--buttonWrapperWidth, auto); 102 } 103 104 /* TODO: rdar://78161351: this is kind of messy */ 105 button { 106 width: var(--buttonWidth, 100%); 107 height: var(--buttonHeight, 36px); 108 display: var(--buttonDisplay, flex); 109 color: var(--buttonTextColor, white); 110 background-color: var( 111 --buttonBackgroundColor, 112 var(--keyColorBG, var(--systemBlue)) 113 ); 114 align-items: center; 115 justify-content: var(--buttonJustifyContent, center); 116 border-radius: var(--buttonRadius, #{$global-border-radius-xsmall}); 117 font: var(--buttonFont, var(--body-emphasized)); 118 119 @media (--medium) { 120 width: var(--buttonWidth, auto); 121 min-width: 100px; 122 height: var(--buttonHeight, #{$action-button-size}); 123 } 124 125 &[disabled] { 126 opacity: var(--buttonDisabledOpacity, 0.75); 127 background-color: var( 128 --buttonDisabledBGColor, 129 var(--systemQuinary) 130 ); 131 color: var(--buttonDisabledTextColor, var(--systemTertiary)); 132 cursor: default; 133 134 @media (prefers-color-scheme: dark) { 135 opacity: var(--buttonDisabledOpacityDark, 1); 136 background-color: var( 137 --buttonDisabledBGColorDark, 138 rgba(255, 255, 255, 0.5) 139 ); 140 color: var( 141 --buttonDisabledTextColorDark, 142 var(--systemTertiary-onLight) 143 ); 144 } 145 } 146 } 147 148 &.primary button { 149 color: var(--buttonTextColor, white); 150 background-color: var( 151 --buttonBackgroundColor, 152 var(--keyColorBG, var(--systemBlue)) 153 ); 154 padding: 0 10px; 155 156 &:disabled { 157 opacity: 0.5; 158 } 159 } 160 161 &.secondary { 162 width: auto; 163 164 button { 165 --buttonBackgroundColor: transparent; 166 min-width: var(--buttonMinWidth, 108px); 167 color: var(--buttonTextColor, var(--keyColor)); 168 border: 1px solid 169 var(--buttonBorderColor, var(--keyColor, var(--systemBlue))); 170 font: var(--body-tall); 171 padding-inline-start: 16px; 172 padding-inline-end: 16px; 173 } 174 } 175 176 // the tertiary styles are used for button type D 177 // currently only used in the snapshot project 178 &.tertiary { 179 width: auto; 180 181 button { 182 --buttonBackgroundColor: var(--keyColorBG, var(--systemBlue)); 183 --buttonTextColor: white; 184 padding-inline-start: 22px; 185 padding-inline-end: 22px; 186 width: var(--buttonWidth, auto); 187 height: var(--buttonHeight, 45px); 188 font: var(--buttonFont, var(--body-reduced-semibold)); 189 190 &:hover, 191 &:focus, 192 &:focus-within { 193 --buttonBackgroundColor: var( 194 --buttonBackgroundColorHover, 195 var(--keyColorBG, var(--systemBlue)) 196 ); 197 transition: all 100ms ease-in-out; 198 } 199 } 200 } 201 202 &.alert { 203 // Prevent button inside modal from shrinking in wide viewport 204 --buttonWrapperWidth: 100%; 205 --buttonWidth: 100%; 206 --buttonHeight: 28px; 207 --buttonRadius: 6px; 208 } 209 210 &.alert-secondary { 211 --buttonTextColor: var(--systemPrimary); 212 --buttonBackgroundColor: var(--systemQuinary); 213 214 @media (prefers-color-scheme: dark) { 215 --buttonBackgroundColor: var(--systemTertiary); 216 } 217 } 218 219 &.pill { 220 --buttonBackgroundColor: rgba(var(--keyColor-rgb), 0.06); 221 --buttonTextColor: var(--keyColor); 222 223 button { 224 min-width: var(--buttonMinWidth, 90px); 225 width: var(--buttonWidth, auto); 226 height: var(--buttonHeight, 28px); 227 border-radius: var(--buttonBorderRadius, 16px); 228 padding-inline-start: var(--buttonPadding, 16px); 229 padding-inline-end: var(--buttonPadding, 16px); 230 font: var(--body-semibold-tall); 231 } 232 } 233 234 &.socialProfileButton { 235 height: auto; 236 border-radius: 10px; 237 margin-top: 27px; 238 width: unset; /* unset inherited value from .button */ 239 min-width: 90px; 240 background-color: var(--keyColorBG); 241 z-index: var(--z-default); 242 243 @include keycolor-button-states; 244 } 245 246 &.socialProfileButton button { 247 padding-top: 9px; 248 padding-bottom: 9px; 249 color: var(--systemPrimary-onDark); 250 height: auto; 251 font: var(--title-2); 252 padding-inline-start: 22px; 253 padding-inline-end: 22px; 254 255 :global(.web-to-native__action) { 256 fill: var(--systemPrimary-onDark); 257 } 258 } 259 } 260 261 // Works in conjuction with `link` class in @amp-stylekit/base/typography 262 .button--text-button { 263 --buttonBackgroundColor: transparent; 264 --buttonTextColor: var(--keyColor); // `link` class will inherit this 265 --linkHoverTextDecoration: none; // `link` custom property 266 267 button { 268 white-space: nowrap; 269 font: var(--buttonFont, var(--body)); 270 } 271 } 272 273 .button__icon { 274 display: flex; 275 fill: var(--buttonIconFill, currentColor); 276 height: var(--buttonIconHeight, 1em); 277 width: var(--buttonIconWidth, 1em); 278 padding: var(--buttonIconPadding, 0); 279 margin-top: var(--buttonIconMarginTop, 0); 280 margin-bottom: var(--buttonIconMarginBottom, 0); 281 282 &:empty, 283 &:has(div:empty) { 284 margin: 0; 285 } 286 287 @media (hover: hover) { 288 button:hover & { 289 fill: var( 290 --buttonIconFillHover, 291 var(--buttonIconFill, currentColor) 292 ); 293 } 294 } 295 296 @supports #{'selector(:has(:focus-visible))'} { 297 button:focus-visible & { 298 fill: var( 299 --buttonIconFillFocus, 300 var(--buttonIconFill, currentColor) 301 ); 302 } 303 } 304 305 &:active { 306 button:active & { 307 fill: var( 308 --buttonIconFillActive, 309 var(--buttonIconFill, currentColor) 310 ); 311 } 312 } 313 } 314 315 .button__icon--before { 316 margin-inline-end: var(--buttonIconMargin-inlineEnd, 0.25em); 317 margin-inline-start: var(--buttonIconMargin-inlineStart, 0); 318 } 319 320 .button__icon--after { 321 margin-inline-start: var(--buttonIconMargin-inlineStart, 0.25em); 322 margin-inline-end: var(--buttonIconMargin-inlineEnd, 0); 323 } 324 </style>