/ shared / components / src / components / Shelf / Nav.svelte
Nav.svelte
  1  <script lang="ts">
  2      import type { ArrowOffset } from '@amp/web-app-components/src/components/Shelf/types';
  3      import ChevronCompactLeft from '@amp/web-app-components/assets/shelf/chevron-compact-left.svg';
  4      import { createEventDispatcher } from 'svelte';
  5  
  6      export let translateFn: (
  7          str: string,
  8          values?: Record<string, string | number>,
  9      ) => string;
 10      export let headerHeight: number;
 11      export let arrowOffset: ArrowOffset;
 12      export let hasNextPage: boolean;
 13      export let hasPreviousPage: boolean;
 14      export let isRTL: boolean;
 15  
 16      $: hasNavArrows = hasPreviousPage || hasNextPage;
 17  
 18      // Adjusting arrows to center on content.
 19      // This is a fallback for browsers that don't support CSS anchor positioning.
 20      $: addSpaceForHeader = (() => {
 21          let offsetStyle = '0px';
 22  
 23          // Custom adjustment provided by user
 24          if (arrowOffset && arrowOffset.length) {
 25              arrowOffset.forEach(({ direction, offset }) => {
 26                  if (direction == 'top') {
 27                      offsetStyle = `
 28                          ${offset}px;
 29                      `;
 30                  } else {
 31                      offsetStyle = `
 32                          calc(${offset}px * -1);
 33                      `;
 34                  }
 35              });
 36          }
 37          // Adjust for header
 38          if (headerHeight) {
 39              // adjust nav height to account for header
 40              offsetStyle = `
 41                  ${headerHeight}px;
 42              `;
 43          }
 44  
 45          return offsetStyle;
 46      })();
 47  
 48      const NAV = {
 49          PREVIOUS: 'previous',
 50          NEXT: 'next',
 51      } as const;
 52  
 53      const dispatch = createEventDispatcher();
 54      const handleNextPage = () => dispatch(NAV.NEXT);
 55      const handlePreviousPage = () => dispatch(NAV.PREVIOUS);
 56  
 57      $: NEXT_ARROW_PROPS = {
 58          disabled: !hasNextPage,
 59          'aria-label': translateFn('AMP.Shared.NextPage'),
 60      };
 61  
 62      $: PREV_ARROW_PROPS = {
 63          disabled: !hasPreviousPage,
 64          'aria-label': translateFn('AMP.Shared.PreviousPage'),
 65      };
 66  
 67      $: rightArrowProps = isRTL ? PREV_ARROW_PROPS : NEXT_ARROW_PROPS;
 68      $: rightClick = isRTL ? handlePreviousPage : handleNextPage;
 69  
 70      $: leftArrowProps = isRTL ? NEXT_ARROW_PROPS : PREV_ARROW_PROPS;
 71      $: leftClick = isRTL ? handleNextPage : handlePreviousPage;
 72  </script>
 73  
 74  {#if hasNavArrows}
 75      <button
 76          {...leftArrowProps}
 77          type="button"
 78          class="shelf-grid-nav__arrow shelf-grid-nav__arrow--left"
 79          data-testId="shelf-button-left"
 80          on:click={leftClick}
 81          style="--offset: {addSpaceForHeader};"
 82      >
 83          <ChevronCompactLeft />
 84      </button>
 85      <slot name="shelf-content" />
 86      <button
 87          {...rightArrowProps}
 88          type="button"
 89          class="shelf-grid-nav__arrow shelf-grid-nav__arrow--right"
 90          data-testId="shelf-button-right"
 91          on:click={rightClick}
 92          style="--offset: {addSpaceForHeader};"
 93      >
 94          <ChevronCompactLeft />
 95      </button>
 96  {:else}
 97      <slot name="shelf-content" />
 98  {/if}
 99  
100  <style lang="scss">
101      @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
102      @use './style/core.scss' as *;
103  
104      .shelf-grid-nav {
105          list-style: none;
106          margin: 0;
107  
108          ul {
109              list-style: none;
110              margin: 0;
111          }
112      }
113  
114      .shelf-grid-nav__arrow {
115          height: $shelf-grid-arrow-height;
116          width: $shelf-grid-arrow-width;
117          align-items: center;
118          border: none;
119          cursor: pointer;
120          display: flex;
121          justify-content: center;
122          overflow: hidden;
123          position: absolute;
124          top: 50%;
125          transition: $shelf-grid-nav-transition;
126          translate: 0 -50%;
127          border-radius: 6px;
128  
129          // Non GPU-accelerated layers must be below GPU-accelerated layers.
130          z-index: var(--z-default);
131  
132          // Fallback for browsers that don't support CSS anchor positioning
133          @supports not (top: anchor(--a center)) {
134              transform: translateY(calc(-50% + var(--offset)));
135              translate: none;
136          }
137  
138          // CSS Anchor Positioning to vertically center paddles with artwork
139          // Powerswoosh intentionally not targeted — doesn't have `shelf` class.
140          :global(.shelf:has(.shelf-grid__list--grid-rows-1)) & {
141              // Set `top` to align with center of first artwork in 1-row shelf.
142              // Targets anchor in `Shelf.svelte`.
143              top: anchor(--shelf-first-artwork center, 50%);
144          }
145  
146          :global(svg) {
147              width: 8.5px;
148              height: 30.5px;
149              fill: var(--systemSecondary);
150          }
151  
152          &:hover,
153          &:focus-visible {
154              text-decoration: none;
155              background: var(--systemQuinary);
156  
157              @media (prefers-color-scheme: dark) {
158                  background: var(--systemQuaternary);
159              }
160          }
161  
162          &:active {
163              background: var(--systemQuaternary);
164  
165              @media (prefers-color-scheme: dark) {
166                  background: var(--systemTertiary);
167              }
168  
169              :global(svg) {
170                  fill: var(--systemPrimary);
171              }
172          }
173  
174          &:disabled {
175              cursor: default;
176              opacity: 0;
177          }
178  
179          // Paddles not used in xsmall viewport
180          @media (--range-xsmall-down) {
181              display: none;
182          }
183      }
184  
185      .shelf-grid-nav__arrow--right {
186          right: $shelf-grid-arrow-position;
187          scale: -1 1; // Flip icon horizontally
188      }
189  
190      .shelf-grid-nav__arrow--left {
191          left: $shelf-grid-arrow-position;
192      }
193  
194      @media (--range-xsmall-down) {
195          .shelf-grid-nav {
196              display: none;
197          }
198      }
199  </style>