@pnx-mixtape/mxds 0.1.0-rc.3 → 0.1.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/create-mxds-component/SKILL.md +77 -0
- package/DESIGN.md +595 -0
- package/README.md +1 -2
- package/dist/build/accordion.css +1 -1
- package/dist/build/accordion.entry.js +2 -1
- package/dist/build/autocomplete.entry.js +1 -1
- package/dist/build/back-to-top.css +1 -0
- package/dist/build/back-to-top.entry.js +1 -0
- package/dist/build/base.css +1 -1
- package/dist/build/button.css +1 -1
- package/dist/build/card.css +1 -1
- package/dist/build/carousel.css +1 -1
- package/dist/build/chunks/Accordion-D2wp8hBG.js +44 -0
- package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
- package/dist/build/chunks/AccordionMobile-D3sTeiM5.js +26 -0
- package/dist/build/chunks/Autocomplete-kRo8RNbC.js +135 -0
- package/dist/build/chunks/BackToTop-4M-Wl7z3.js +64 -0
- package/dist/build/chunks/ClosableAlert-yy84gae6.js +55 -0
- package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
- package/dist/build/chunks/Dialog-DWoXW-7Z.js +48 -0
- package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
- package/dist/build/chunks/FeedbackForm-BochXEDx.js +64 -0
- package/dist/build/chunks/Gallery-Rkfv7Mbb.js +165 -0
- package/dist/build/chunks/GlobalToggle-CzlkpLpc.js +82 -0
- package/dist/build/chunks/InPageNavigation-D1Kk-rCm.js +86 -0
- package/dist/build/chunks/Navigation-CDQnN61h.js +147 -0
- package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
- package/dist/build/chunks/Sticky-DoVndENm.js +51 -0
- package/dist/build/chunks/Tabs-BDlxCDNh.js +160 -0
- package/dist/build/chunks/Translate-bmM38j6z.js +46 -0
- package/dist/build/chunks/cookie-compliance-CG6x-TcV.js +119 -0
- package/dist/build/chunks/enums-BrHxPZZ_.js +36 -0
- package/dist/build/chunks/polyfills-D-qh1a_2.js +33 -0
- package/dist/build/chunks/utilities-Dd-08SW6.js +255 -0
- package/dist/build/constants.css +1 -1
- package/dist/build/content-block.css +1 -1
- package/dist/build/cookie-consent.css +1 -1
- package/dist/build/cookie-consent.entry.js +1 -1
- package/dist/build/dialog.css +1 -1
- package/dist/build/dialog.entry.js +1 -1
- package/dist/build/drop-menu.entry.js +1 -1
- package/dist/build/elements.js +147 -1
- package/dist/build/feedback-form.css +1 -0
- package/dist/build/feedback-form.entry.js +1 -0
- package/dist/build/filters.entry.js +145 -1
- package/dist/build/form.css +1 -1
- package/dist/build/gallery.entry.js +1 -1
- package/dist/build/global-alert.css +1 -1
- package/dist/build/global-alert.entry.js +1 -1
- package/dist/build/grid.css +1 -1
- package/dist/build/header.css +1 -1
- package/dist/build/header.entry.js +1 -1
- package/dist/build/icon.css +1 -1
- package/dist/build/in-page-navigation.entry.js +1 -1
- package/dist/build/navigation.css +1 -1
- package/dist/build/navigation.entry.js +1 -1
- package/dist/build/pagination.css +1 -1
- package/dist/build/popover.css +1 -1
- package/dist/build/popover.entry.js +1 -1
- package/dist/build/react.js +2270 -1
- package/dist/build/sticky.entry.js +1 -1
- package/dist/build/tabs.css +1 -1
- package/dist/build/tabs.entry.js +1 -1
- package/dist/build/translate.css +1 -0
- package/dist/build/translate.entry.js +1 -0
- package/dist/build/utility-list.entry.js +53 -1
- package/dist/types/Component/BackToTop/BackToTop.d.ts +20 -0
- package/dist/types/Component/BackToTop/BackToTop.d.ts.map +1 -0
- package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts +27 -0
- package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts.map +1 -0
- package/dist/types/Component/BackToTop/back-to-top.entry.d.ts +2 -0
- package/dist/types/Component/BackToTop/back-to-top.entry.d.ts.map +1 -0
- package/dist/types/Component/DropMenu/DropMenu.d.ts +2 -1
- package/dist/types/Component/DropMenu/DropMenu.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Components/TabDropMenu.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
- package/dist/types/Component/Translate/Elements/Translate.d.ts +23 -0
- package/dist/types/Component/Translate/Elements/Translate.d.ts.map +1 -0
- package/dist/types/Component/Translate/Translate.d.ts +37 -0
- package/dist/types/Component/Translate/Translate.d.ts.map +1 -0
- package/dist/types/Component/Translate/languages.d.ts +11 -0
- package/dist/types/Component/Translate/languages.d.ts.map +1 -0
- package/dist/types/Component/Translate/translate.entry.d.ts +2 -0
- package/dist/types/Component/Translate/translate.entry.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts +37 -0
- package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts +36 -0
- package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts +2 -0
- package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts.map +1 -0
- package/dist/types/Utility/Elements/google-translate-widget.d.ts +91 -0
- package/dist/types/Utility/Elements/google-translate-widget.d.ts.map +1 -0
- package/dist/types/Utility/utilities.d.ts +2 -0
- package/dist/types/Utility/utilities.d.ts.map +1 -1
- package/dist/types/elements.d.ts +3 -0
- package/dist/types/elements.d.ts.map +1 -1
- package/dist/types/enums.d.ts +3 -1
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/react.d.ts +3 -0
- package/dist/types/react.d.ts.map +1 -1
- package/docs/API.md +1215 -0
- package/docs/CSSGuidelines.md +891 -0
- package/docs/CodeQuality.md +940 -0
- package/docs/ComponentArchitecture.md +714 -0
- package/docs/Contributing.md +626 -0
- package/docs/MemoryManagement.md +598 -0
- package/docs/Migrations.md +6 -0
- package/docs/README.md +100 -0
- package/docs/Security.md +444 -0
- package/docs/Testing.md +739 -0
- package/docs/Troubleshooting.md +674 -0
- package/package.json +6 -3
- package/src/Atom/Button/Button.tsx +1 -1
- package/src/Atom/Button/__snapshots__/Button.stories.tsx.snap +2 -2
- package/src/Atom/Button/_buttons.css +1 -0
- package/src/Atom/Icon/README.md +1 -1
- package/src/Atom/Icon/_icon.css +1 -0
- package/src/Atom/Icon/images/thumb.svg +1 -0
- package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +4 -9
- package/src/Atom/Media/media.twig +4 -5
- package/src/Atom/_generic.css +0 -10
- package/src/Component/Accordion/accordion.css +3 -1
- package/src/Component/BackToTop/BackToTop.stories.ts +127 -0
- package/src/Component/BackToTop/BackToTop.stories.tsx +53 -0
- package/src/Component/BackToTop/BackToTop.tsx +120 -0
- package/src/Component/BackToTop/Elements/BackToTop.ts +119 -0
- package/src/Component/BackToTop/README.md +164 -0
- package/src/Component/BackToTop/__snapshots__/BackToTop.stories.ts.snap +24 -0
- package/src/Component/BackToTop/__snapshots__/BackToTop.stories.tsx.snap +3 -0
- package/src/Component/BackToTop/back-to-top.css +70 -0
- package/src/Component/BackToTop/back-to-top.entry.js +1 -0
- package/src/Component/BackToTop/back-to-top.twig +26 -0
- package/src/Component/Card/Components/CardContent.tsx +1 -1
- package/src/Component/Card/Components/CardMedia.tsx +1 -1
- package/src/Component/Card/__snapshots__/Card.stories.tsx.snap +3 -3
- package/src/Component/Card/card.css +1 -1
- package/src/Component/Carousel/Carousel.stories.ts +66 -36
- package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -5
- package/src/Component/Carousel/carousel.css +1 -1
- package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
- package/src/Component/ContentBlock/content-block.css +1 -1
- package/src/Component/CookieConsent/cookie-consent.css +3 -0
- package/src/Component/Dialog/dialog.css +5 -2
- package/src/Component/DropMenu/DropMenu.stories.ts +21 -2
- package/src/Component/DropMenu/DropMenu.tsx +14 -4
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +3 -3
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +2 -2
- package/src/Component/DropMenu/drop-menu.twig +7 -1
- package/src/Component/GlobalAlert/global-alert.css +3 -0
- package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.tsx.snap +1 -1
- package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +48 -13
- package/src/Component/InPageNavigation/in-page-navigation.twig +4 -6
- package/src/Component/ListItem/Components/ListItemMedia.tsx +1 -1
- package/src/Component/Navigation/_navigation-collapsible.css +3 -1
- package/src/Component/Navigation/_navigation-dropdown.css +3 -1
- package/src/Component/Pagination/Pagination.stories.ts +57 -27
- package/src/Component/Pagination/pagination.css +16 -7
- package/src/Component/Popover/__snapshots__/Popover.stories.tsx.snap +3 -3
- package/src/Component/Popover/popover.css +3 -1
- package/src/Component/Tabs/Components/TabDropMenu.tsx +7 -1
- package/src/Component/Tabs/Elements/Tabs.ts +13 -3
- package/src/Component/Tabs/Tabs.stories.ts +33 -12
- package/src/Component/Tabs/Tabs.stories.tsx +28 -14
- package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +3 -3
- package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +2 -2
- package/src/Component/Tabs/tabs.css +4 -5
- package/src/Component/Translate/Elements/Translate.ts +76 -0
- package/src/Component/Translate/README.md +283 -0
- package/src/Component/Translate/Translate.stories.ts +87 -0
- package/src/Component/Translate/Translate.stories.tsx +77 -0
- package/src/Component/Translate/Translate.tsx +213 -0
- package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +18 -0
- package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +3 -0
- package/src/Component/Translate/languages.ts +39 -0
- package/src/Component/Translate/translate.css +62 -0
- package/src/Component/Translate/translate.entry.js +1 -0
- package/src/Component/Translate/translate.twig +44 -0
- package/src/Form/Autocomplete/Autocomplete.stories.tsx +2 -1
- package/src/Form/FeedbackForm/Elements/FeedbackForm.ts +122 -0
- package/src/Form/FeedbackForm/FeedbackForm.stories.ts +99 -0
- package/src/Form/FeedbackForm/FeedbackForm.stories.tsx +73 -0
- package/src/Form/FeedbackForm/FeedbackForm.tsx +196 -0
- package/src/Form/FeedbackForm/README.md +233 -0
- package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.ts.snap +125 -0
- package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.tsx.snap +5 -0
- package/src/Form/FeedbackForm/feedback-form.css +74 -0
- package/src/Form/FeedbackForm/feedback-form.entry.js +1 -0
- package/src/Form/FeedbackForm/feedback-form.twig +80 -0
- package/src/Form/form.css +5 -3
- package/src/Layout/Grid/Grid.stories.ts +1 -0
- package/src/Layout/Grid/Grid.stories.tsx +26 -0
- package/src/Layout/Grid/__snapshots__/Grid.stories.tsx.snap +2 -0
- package/src/Layout/Grid/grid.css +12 -0
- package/src/Layout/Header/Components/GlobalToggle.tsx +1 -1
- package/src/Layout/Header/Header.stories.ts +53 -48
- package/src/Layout/Header/Header.stories.tsx +35 -22
- package/src/Layout/Header/Header.tsx +2 -2
- package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +14 -10
- package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
- package/src/Layout/Header/_header.css +4 -2
- package/src/Layout/Header/_toggles.css +9 -6
- package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
- package/src/Utility/Elements/google-translate-widget.ts +293 -0
- package/src/Utility/_mixins.css +35 -13
- package/src/Utility/global.d.ts +4 -0
- package/src/Utility/utilities.ts +2 -0
- package/src/_custom-media.css +2 -2
- package/src/constants.css +3 -13
- package/src/elements.ts +3 -0
- package/src/enums.ts +2 -0
- package/src/react.ts +3 -0
- package/dist/build/chunks/Accordion-GN0sME4W.js +0 -1
- package/dist/build/chunks/AccordionGroup-Bzqp5__q.js +0 -1
- package/dist/build/chunks/AccordionMobile-DG5jSe8Q.js +0 -1
- package/dist/build/chunks/Autocomplete-B4RtkAyh.js +0 -1
- package/dist/build/chunks/ClosableAlert-DEhj8Nxm.js +0 -1
- package/dist/build/chunks/CookieConsent-BkCMRVST.js +0 -1
- package/dist/build/chunks/Dialog-Haahn-Yn.js +0 -1
- package/dist/build/chunks/DropMenu-1baQc8Yc.js +0 -1
- package/dist/build/chunks/Gallery-D8NGXy2Q.js +0 -1
- package/dist/build/chunks/GlobalToggle-B52fdBQu.js +0 -1
- package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +0 -1
- package/dist/build/chunks/Navigation-hOrfLq7h.js +0 -1
- package/dist/build/chunks/Popover-Cxg-s5Jn.js +0 -1
- package/dist/build/chunks/Sticky-BGCuuEJK.js +0 -1
- package/dist/build/chunks/Tabs-DYy1YTdU.js +0 -15
- package/dist/build/chunks/cookie-compliance-Bbm-0vpj.js +0 -1
- package/dist/build/chunks/enums-DLV_1NAf.js +0 -1
- package/dist/build/chunks/polyfills-C7pRt54M.js +0 -1
- package/dist/build/chunks/utilities-_Bq3TxAQ.js +0 -1
package/src/Atom/Icon/README.md
CHANGED
|
@@ -66,7 +66,7 @@ import { IconColourModifier, IconSizeModifier, IconRotateModifier } from "@pnx-m
|
|
|
66
66
|
|
|
67
67
|
## Available icons (`Icons` enum)
|
|
68
68
|
|
|
69
|
-
`arrow-down`, `arrow-left`, `arrow-up`, `arrow-right`, `chevron-down`, `chevron-left`, `chevron-right`, `chevron-up`, `close`, `collapse`, `download`, `drupal`, `error`, `expand`, `external-link`, `facebook`, `google`, `info`, `instagram`, `linkedin`, `mastodon`, `bluesky`, `menu`, `search`, `success`, `twitter`, `warning`, `youtube`, `email`, `file`, `heart-selected`, `heart-unselected`, `link`, `lock`, `login`, `more`, `pencil`, `phone`, `pin`, `print`, `question`, `restart`, `share`, `star-selected`, `star-unselected`, `tick`, `trash`, `user`
|
|
69
|
+
`arrow-down`, `arrow-left`, `arrow-up`, `arrow-right`, `chevron-down`, `chevron-left`, `chevron-right`, `chevron-up`, `close`, `collapse`, `download`, `drupal`, `error`, `expand`, `external-link`, `facebook`, `globe`, `google`, `info`, `instagram`, `linkedin`, `mastodon`, `bluesky`, `menu`, `search`, `success`, `twitter`, `warning`, `youtube`, `email`, `file`, `heart-selected`, `heart-unselected`, `link`, `lock`, `login`, `more`, `pencil`, `phone`, `pin`, `print`, `question`, `restart`, `share`, `star-selected`, `star-unselected`, `thumb`, `tick`, `trash`, `user`
|
|
70
70
|
|
|
71
71
|
## Colour modifiers
|
|
72
72
|
|
package/src/Atom/Icon/_icon.css
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up preview-icon"><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/><path d="M7 10v12"/></svg>
|
|
@@ -6,9 +6,7 @@ exports[`Attribution 1`] = `
|
|
|
6
6
|
<figure class="mx-media mx-media--image"> <picture>
|
|
7
7
|
<img src="https://picsum.photos/id/56/558/418?grayscale" alt="Some blurry bubble image">
|
|
8
8
|
</picture>
|
|
9
|
-
|
|
10
|
-
<figcaption><b>Figure 1.</b> A picsum image
|
|
11
|
-
- Jane Doe </figcaption>
|
|
9
|
+
<figcaption><b>Figure 1.</b> A picsum image - Jane Doe</figcaption>
|
|
12
10
|
</figure>"
|
|
13
11
|
`;
|
|
14
12
|
|
|
@@ -18,8 +16,7 @@ exports[`Media 1`] = `
|
|
|
18
16
|
<figure class="mx-media mx-media--image"> <picture>
|
|
19
17
|
<img src="https://picsum.photos/id/56/558/418?grayscale" alt="Some blurry bubble image">
|
|
20
18
|
</picture>
|
|
21
|
-
|
|
22
|
-
<figcaption><b>Figure 1.</b> A picsum image </figcaption>
|
|
19
|
+
<figcaption><b>Figure 1.</b> A picsum image</figcaption>
|
|
23
20
|
</figure>"
|
|
24
21
|
`;
|
|
25
22
|
|
|
@@ -29,8 +26,7 @@ exports[`No Figure 1`] = `
|
|
|
29
26
|
<picture>
|
|
30
27
|
<img src="https://picsum.photos/id/56/558/418?grayscale" alt="Some blurry bubble image">
|
|
31
28
|
</picture>
|
|
32
|
-
|
|
33
|
-
"
|
|
29
|
+
"
|
|
34
30
|
`;
|
|
35
31
|
|
|
36
32
|
exports[`Remote Video 1`] = `
|
|
@@ -39,7 +35,6 @@ exports[`Remote Video 1`] = `
|
|
|
39
35
|
<figure class="mx-media mx-media--video"> <figure class="mx-video" data-src="https://www.youtube.com/embed/NpEaa2P7qZI" data-title="Something from youtube">
|
|
40
36
|
<iframe src="https://www.youtube.com/embed/NpEaa2P7qZI" title="Something from youtube" allow="fullscreen; picture-in-picture; encrypted-media; clipboard-write" allowfullscreen=""></iframe>
|
|
41
37
|
</figure>
|
|
42
|
-
|
|
43
|
-
<figcaption>A remote video </figcaption>
|
|
38
|
+
<figcaption>A remote video</figcaption>
|
|
44
39
|
</figure>"
|
|
45
40
|
`;
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
{% if wrap %}<figure{{ attributes }}>{% endif %}
|
|
12
12
|
{{ item }}
|
|
13
13
|
{% if caption is not empty or attribution is not empty %}
|
|
14
|
-
<figcaption>
|
|
15
|
-
{{ caption }}
|
|
16
|
-
{
|
|
17
|
-
{{ attribution }}
|
|
18
|
-
{% endapply %}
|
|
14
|
+
<figcaption>
|
|
15
|
+
{{- caption -}}
|
|
16
|
+
{%- if caption is not empty and attribution is not empty %} - {% endif -%}
|
|
17
|
+
{{- attribution -}}
|
|
19
18
|
</figcaption>
|
|
20
19
|
{% endif %}
|
|
21
20
|
{% if wrap %}</figure>{% endif %}
|
package/src/Atom/_generic.css
CHANGED
|
@@ -9,16 +9,6 @@
|
|
|
9
9
|
box-sizing: border-box;
|
|
10
10
|
background-repeat: no-repeat;
|
|
11
11
|
margin: 0;
|
|
12
|
-
opacity: 1;
|
|
13
|
-
transition-property:
|
|
14
|
-
opacity, color, background-color, border-color, transform, rotate, display, overlay;
|
|
15
|
-
transition-duration: 0.5s;
|
|
16
|
-
transition-timing-function: var(--ease);
|
|
17
|
-
transition-behavior: allow-discrete;
|
|
18
|
-
|
|
19
|
-
@starting-style {
|
|
20
|
-
opacity: 0;
|
|
21
|
-
}
|
|
22
12
|
}
|
|
23
13
|
|
|
24
14
|
:focus {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Accordion
|
|
3
3
|
*/
|
|
4
|
+
@import "../../Utility/_mixins.css";
|
|
4
5
|
|
|
5
6
|
@layer design-system.defaults {
|
|
6
7
|
details {
|
|
@@ -59,8 +60,9 @@
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
& > .mx-accordion__content {
|
|
63
|
+
@add-mixin transition;
|
|
64
|
+
|
|
62
65
|
display: none;
|
|
63
|
-
opacity: 0;
|
|
64
66
|
overflow: hidden;
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/html-vite"
|
|
2
|
+
import { expect, waitFor } from "storybook/test"
|
|
3
|
+
import Component from "./back-to-top.twig"
|
|
4
|
+
import "./back-to-top.css"
|
|
5
|
+
import "./Elements/BackToTop"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A trigger that appears once the reader has scrolled past a threshold and jumps back to a
|
|
9
|
+
* target element on the page, with focus management and reduced-motion support.
|
|
10
|
+
* It requires the `BackToTop` custom element javascript.
|
|
11
|
+
*
|
|
12
|
+
* By default it watches how far the reader has scrolled (`visibleAfter`, `100vh`) — pass
|
|
13
|
+
* `target` to watch a specific element (e.g. a page's `<h1>`) leaving the viewport instead.
|
|
14
|
+
*/
|
|
15
|
+
const meta: Meta<typeof Component> = {
|
|
16
|
+
tags: ["autodocs"],
|
|
17
|
+
component: Component,
|
|
18
|
+
args: {
|
|
19
|
+
title: "Back to top",
|
|
20
|
+
href: "#main",
|
|
21
|
+
visibleAfter: "1vh",
|
|
22
|
+
},
|
|
23
|
+
argTypes: {
|
|
24
|
+
target: {
|
|
25
|
+
description:
|
|
26
|
+
"CSS selector of an element to observe — visible once it scrolls above the viewport top. Omit to use `visibleAfter` instead.",
|
|
27
|
+
control: "text",
|
|
28
|
+
type: "string",
|
|
29
|
+
},
|
|
30
|
+
visibleAfter: {
|
|
31
|
+
description:
|
|
32
|
+
"Distance down the page before the trigger appears. Only used when `target` is unset.",
|
|
33
|
+
control: "text",
|
|
34
|
+
type: "string",
|
|
35
|
+
table: {
|
|
36
|
+
defaultValue: { summary: "100vh" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
href: {
|
|
40
|
+
description: "Scroll destination — a same-page hash.",
|
|
41
|
+
control: "text",
|
|
42
|
+
type: {
|
|
43
|
+
name: "string",
|
|
44
|
+
required: true,
|
|
45
|
+
},
|
|
46
|
+
table: {
|
|
47
|
+
defaultValue: { summary: "#main" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
offset: {
|
|
51
|
+
description:
|
|
52
|
+
"Pixel offset to avoid overlapping another fixed element (e.g. a sticky CTA bar).",
|
|
53
|
+
control: "number",
|
|
54
|
+
type: "number",
|
|
55
|
+
},
|
|
56
|
+
title: {
|
|
57
|
+
description: "Trigger label.",
|
|
58
|
+
control: "text",
|
|
59
|
+
type: {
|
|
60
|
+
name: "string",
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
table: {
|
|
64
|
+
defaultValue: { summary: "Back to top" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
breakpoint: {
|
|
68
|
+
description: "Restrict display to a breakpoint and up. Omit to show at all viewport sizes.",
|
|
69
|
+
control: "select",
|
|
70
|
+
options: ["", "l"],
|
|
71
|
+
type: "string",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
render: args => `
|
|
75
|
+
<div style="block-size: 100px; padding-block-start: 2rem;">
|
|
76
|
+
<p>Scroll down to reveal the "Back to top" trigger.</p>
|
|
77
|
+
</div>
|
|
78
|
+
<div id="main">
|
|
79
|
+
<h2>Main content</h2>
|
|
80
|
+
<p>This is the scroll destination.</p>
|
|
81
|
+
</div>
|
|
82
|
+
${Component(args)}
|
|
83
|
+
`,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default meta
|
|
87
|
+
type Story = StoryObj<typeof meta>
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Visibility is driven by real window scroll position (see the component's `setVisible`), which
|
|
91
|
+
* isn't reliably simulated via `window.scrollTo` in this test canvas — so this play function
|
|
92
|
+
* exercises the same toggle the scroll listener would call, directly, along with the click
|
|
93
|
+
* behaviour, rather than simulating a real scroll.
|
|
94
|
+
*/
|
|
95
|
+
export const BackToTop: Story = {
|
|
96
|
+
play: async ({ canvasElement, userEvent, step }) => {
|
|
97
|
+
const wrapper = canvasElement.querySelector("mx-back-to-top") as InstanceType<
|
|
98
|
+
typeof HTMLElement
|
|
99
|
+
> & { setVisible: (isVisible: boolean) => void }
|
|
100
|
+
const trigger = canvasElement.querySelector("a") as HTMLAnchorElement
|
|
101
|
+
const main = canvasElement.querySelector("#main") as HTMLElement
|
|
102
|
+
|
|
103
|
+
await step("Starts hidden", async () => {
|
|
104
|
+
await expect(wrapper.classList.contains("is-visible")).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await step("Becomes visible once toggled", async () => {
|
|
108
|
+
let detail: { isVisible: boolean } | undefined
|
|
109
|
+
wrapper.addEventListener("back-to-top-toggle", (event: Event) => {
|
|
110
|
+
detail = (event as CustomEvent<{ isVisible: boolean }>).detail
|
|
111
|
+
})
|
|
112
|
+
wrapper.setVisible(true)
|
|
113
|
+
await expect(wrapper.classList.contains("is-visible")).toBe(true)
|
|
114
|
+
await expect(detail?.isVisible).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await step("Activating it moves focus to the destination", async () => {
|
|
118
|
+
await userEvent.click(trigger)
|
|
119
|
+
await waitFor(() => expect(document.activeElement).toBe(main))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await step("Restores toggled state", async () => {
|
|
123
|
+
wrapper.setVisible(false)
|
|
124
|
+
await expect(wrapper.classList.contains("is-visible")).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
},
|
|
127
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react-vite"
|
|
2
|
+
import { expect, waitFor } from "storybook/test"
|
|
3
|
+
import Component from "./BackToTop"
|
|
4
|
+
import "./back-to-top.css"
|
|
5
|
+
import { Page } from "../../../.storybook-react/decorators"
|
|
6
|
+
|
|
7
|
+
const fillerStyle = { blockSize: "50vh", paddingBlockStart: "2rem" }
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Component> = {
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
component: Component,
|
|
12
|
+
decorators: [Page],
|
|
13
|
+
args: {
|
|
14
|
+
title: "Back to top",
|
|
15
|
+
to: "#main",
|
|
16
|
+
},
|
|
17
|
+
render: args => (
|
|
18
|
+
<>
|
|
19
|
+
<div style={fillerStyle}>
|
|
20
|
+
<p>Scroll down to reveal the "Back to top" trigger.</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div id="main">
|
|
23
|
+
<h2>Main content</h2>
|
|
24
|
+
<p>This is the scroll destination.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<Component {...args} />
|
|
27
|
+
</>
|
|
28
|
+
),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default meta
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Visibility is driven by real window scroll position, which isn't reliably simulated via
|
|
35
|
+
* `window.scrollTo` in this test canvas — see the Twig/Web Component story, which exercises the
|
|
36
|
+
* toggle directly instead. This story covers the click behaviour, which doesn't depend on it.
|
|
37
|
+
*/
|
|
38
|
+
export const BackToTop: StoryObj<typeof meta> = {
|
|
39
|
+
play: async ({ canvasElement, userEvent, step }) => {
|
|
40
|
+
const wrapper = canvasElement.querySelector(".mx-back-to-top") as HTMLElement
|
|
41
|
+
const trigger = canvasElement.querySelector("a") as HTMLAnchorElement
|
|
42
|
+
const main = canvasElement.querySelector("#main") as HTMLElement
|
|
43
|
+
|
|
44
|
+
await step("Starts hidden", async () => {
|
|
45
|
+
await expect(wrapper.classList.contains("is-visible")).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await step("Activating it moves focus to the destination", async () => {
|
|
49
|
+
await userEvent.click(trigger)
|
|
50
|
+
await waitFor(() => expect(document.activeElement).toBe(main))
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { JSX, MouseEventHandler, useEffect, useMemo, useState } from "react"
|
|
2
|
+
import classNames from "classnames"
|
|
3
|
+
import { ButtonModifiers } from "@pnx-mixtape/ids-shape"
|
|
4
|
+
import { ButtonLink } from "../../Atom/Button/Button"
|
|
5
|
+
import { Icons } from "../../enums"
|
|
6
|
+
|
|
7
|
+
const TRIGGER_MODIFIERS = [ButtonModifiers.LIGHT]
|
|
8
|
+
|
|
9
|
+
/** Resolves a CSS length (`px` or `vh`) to a pixel value. */
|
|
10
|
+
const resolveThreshold = (value: string): number => {
|
|
11
|
+
const amount = Number.parseFloat(value)
|
|
12
|
+
if (Number.isNaN(amount)) return window.innerHeight
|
|
13
|
+
if (value.endsWith("vh")) return (amount / 100) * window.innerHeight
|
|
14
|
+
return amount
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type BackToTopProps = {
|
|
18
|
+
/** CSS selector of an element to observe — visible once it scrolls above the viewport top. */
|
|
19
|
+
target?: string
|
|
20
|
+
/** When `target` is unset, distance down the page before the trigger appears. */
|
|
21
|
+
visibleAfter?: string
|
|
22
|
+
/** Scroll destination. Must be a same-page hash (`#id`). */
|
|
23
|
+
to?: string
|
|
24
|
+
/** Pixel offset to avoid overlapping another fixed element (e.g. a sticky CTA bar). */
|
|
25
|
+
offset?: number
|
|
26
|
+
title?: string
|
|
27
|
+
/** Restricts display to a breakpoint and up. Omit to show at all viewport sizes. */
|
|
28
|
+
breakpoint?: "l"
|
|
29
|
+
className?: string
|
|
30
|
+
/** Called whenever the visible state changes. React analogue of the Web Component's `back-to-top-toggle` event. */
|
|
31
|
+
onVisibleChange?: (isVisible: boolean) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BackToTop = ({
|
|
35
|
+
target,
|
|
36
|
+
visibleAfter = "100vh",
|
|
37
|
+
to = "#main",
|
|
38
|
+
offset,
|
|
39
|
+
title = "Back to top",
|
|
40
|
+
breakpoint,
|
|
41
|
+
className,
|
|
42
|
+
onVisibleChange,
|
|
43
|
+
}: BackToTopProps): JSX.Element => {
|
|
44
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const setNext = (next: boolean): void => {
|
|
48
|
+
setIsVisible(prev => {
|
|
49
|
+
if (prev !== next) onVisibleChange?.(next)
|
|
50
|
+
return next
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const observed = target ? document.querySelector(target) : null
|
|
55
|
+
|
|
56
|
+
if (observed) {
|
|
57
|
+
const io = new IntersectionObserver(([{ boundingClientRect }]) => {
|
|
58
|
+
setNext(boundingClientRect.y < 0)
|
|
59
|
+
})
|
|
60
|
+
io.observe(observed)
|
|
61
|
+
return () => io.disconnect()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const threshold = resolveThreshold(visibleAfter)
|
|
65
|
+
const handleScroll = (): void => setNext(window.scrollY > threshold)
|
|
66
|
+
handleScroll()
|
|
67
|
+
window.addEventListener("scroll", handleScroll, { passive: true })
|
|
68
|
+
return () => window.removeEventListener("scroll", handleScroll)
|
|
69
|
+
}, [target, visibleAfter, onVisibleChange])
|
|
70
|
+
|
|
71
|
+
const handleClick: MouseEventHandler<HTMLAnchorElement> = event => {
|
|
72
|
+
if (!to.startsWith("#")) return
|
|
73
|
+
const dest = document.querySelector<HTMLElement>(to)
|
|
74
|
+
if (!dest) return
|
|
75
|
+
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
|
|
78
|
+
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
79
|
+
dest.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" })
|
|
80
|
+
|
|
81
|
+
const hadTabindex = dest.hasAttribute("tabindex")
|
|
82
|
+
if (!hadTabindex) dest.setAttribute("tabindex", "-1")
|
|
83
|
+
dest.focus({ preventScroll: true })
|
|
84
|
+
if (!hadTabindex) {
|
|
85
|
+
dest.addEventListener("blur", () => dest.removeAttribute("tabindex"), { once: true })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.history?.pushState?.(null, "", to)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const style = useMemo<Record<string, string | number>>(() => {
|
|
92
|
+
const next: Record<string, string | number> = {}
|
|
93
|
+
if (offset) next["--back-to-top-offset"] = `${offset}px`
|
|
94
|
+
return next
|
|
95
|
+
}, [offset])
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
className={classNames(
|
|
100
|
+
breakpoint ? `mx-back-to-top--${breakpoint}` : "mx-back-to-top",
|
|
101
|
+
className,
|
|
102
|
+
// Always "enhanced" — React has already confirmed JS is running.
|
|
103
|
+
{ "is-enhanced": true, "is-visible": isVisible },
|
|
104
|
+
)}
|
|
105
|
+
style={style}
|
|
106
|
+
>
|
|
107
|
+
<ButtonLink
|
|
108
|
+
to={to}
|
|
109
|
+
onClick={handleClick}
|
|
110
|
+
modifiers={TRIGGER_MODIFIERS}
|
|
111
|
+
className="mx-back-to-top__trigger mx-button--small"
|
|
112
|
+
iconStart={Icons.ARROW_UP}
|
|
113
|
+
>
|
|
114
|
+
{title}
|
|
115
|
+
</ButtonLink>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default BackToTop
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackToTop — shows/hides a "jump to top" trigger based on scroll position,
|
|
3
|
+
* and manages focus/scroll when it's activated.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type ToggleDetail = {
|
|
7
|
+
isVisible: boolean
|
|
8
|
+
target: BackToTop
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Resolves a CSS length (`px` or `vh`) to a pixel value. */
|
|
12
|
+
const resolveThreshold = (value: string): number => {
|
|
13
|
+
const amount = Number.parseFloat(value)
|
|
14
|
+
if (Number.isNaN(amount)) return window.innerHeight
|
|
15
|
+
if (value.endsWith("vh")) return (amount / 100) * window.innerHeight
|
|
16
|
+
return amount
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default class BackToTop extends HTMLElement {
|
|
20
|
+
internals_: ElementInternals
|
|
21
|
+
controller: AbortController
|
|
22
|
+
io?: IntersectionObserver
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super()
|
|
26
|
+
this.internals_ = this.attachInternals()
|
|
27
|
+
this.controller = new AbortController()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
connectedCallback(): void {
|
|
31
|
+
const { signal } = this.controller
|
|
32
|
+
|
|
33
|
+
// Only hide-until-scrolled once JS is confirmed running — see back-to-top.css.
|
|
34
|
+
this.classList.add("is-enhanced")
|
|
35
|
+
|
|
36
|
+
this.trigger?.addEventListener("click", this.handleClick, { signal })
|
|
37
|
+
|
|
38
|
+
const selector = this.getAttribute("target")
|
|
39
|
+
const target = selector ? document.querySelector(selector) : null
|
|
40
|
+
|
|
41
|
+
if (target) {
|
|
42
|
+
this.io = new IntersectionObserver(([{ boundingClientRect }]) => {
|
|
43
|
+
this.setVisible(boundingClientRect.y < 0)
|
|
44
|
+
})
|
|
45
|
+
this.io.observe(target)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const threshold = resolveThreshold(this.getAttribute("visibleafter") || "100vh")
|
|
50
|
+
this.setVisible(window.scrollY > threshold)
|
|
51
|
+
window.addEventListener("scroll", () => this.setVisible(window.scrollY > threshold), {
|
|
52
|
+
signal,
|
|
53
|
+
passive: true,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disconnectedCallback(): void {
|
|
58
|
+
this.controller.abort()
|
|
59
|
+
this.io?.disconnect()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get trigger(): HTMLAnchorElement | null {
|
|
63
|
+
return this.querySelector("a")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setVisible(isVisible: boolean): void {
|
|
67
|
+
if (this.classList.contains("is-visible") === isVisible) return
|
|
68
|
+
this.classList.toggle("is-visible", isVisible)
|
|
69
|
+
this.dispatchEvent(
|
|
70
|
+
new CustomEvent<ToggleDetail>("back-to-top-toggle", {
|
|
71
|
+
bubbles: true,
|
|
72
|
+
cancelable: true,
|
|
73
|
+
detail: { isVisible, target: this },
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Prevents the native anchor jump so we can scroll smoothly (honouring
|
|
80
|
+
* reduced-motion), then moves focus to the destination — a plain anchor
|
|
81
|
+
* jump scrolls the page but does not reliably move accessible focus
|
|
82
|
+
* unless the destination is already focusable.
|
|
83
|
+
*/
|
|
84
|
+
handleClick = (event: MouseEvent): void => {
|
|
85
|
+
const link = event.currentTarget as HTMLAnchorElement
|
|
86
|
+
const hash = link.getAttribute("href")
|
|
87
|
+
if (!hash?.startsWith("#")) return
|
|
88
|
+
|
|
89
|
+
const dest = document.querySelector<HTMLElement>(hash)
|
|
90
|
+
if (!dest) return
|
|
91
|
+
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
|
|
94
|
+
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
95
|
+
dest.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" })
|
|
96
|
+
|
|
97
|
+
const hadTabindex = dest.hasAttribute("tabindex")
|
|
98
|
+
if (!hadTabindex) dest.setAttribute("tabindex", "-1")
|
|
99
|
+
dest.focus({ preventScroll: true })
|
|
100
|
+
if (!hadTabindex) {
|
|
101
|
+
dest.addEventListener("blur", () => dest.removeAttribute("tabindex"), {
|
|
102
|
+
once: true,
|
|
103
|
+
signal: this.controller.signal,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
window.history?.pushState?.(null, "", hash)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!customElements.get("mx-back-to-top")) {
|
|
112
|
+
customElements.define("mx-back-to-top", BackToTop)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
declare global {
|
|
116
|
+
interface HTMLElementTagNameMap {
|
|
117
|
+
"mx-back-to-top": BackToTop
|
|
118
|
+
}
|
|
119
|
+
}
|