/ shared / components / src / components / buttons / Button.svelte
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>