@pnx-mixtape/mxds 0.1.0-rc.4 → 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.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/button.css +1 -1
- package/dist/build/card.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/content-block.css +1 -1
- package/dist/build/cookie-consent.entry.js +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/gallery.entry.js +1 -1
- package/dist/build/global-alert.entry.js +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.entry.js +1 -1
- package/dist/build/pagination.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.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/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/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
- package/src/Component/ContentBlock/content-block.css +1 -1
- 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/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/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/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/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/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/_toggles.css +7 -7
- package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
- package/src/Utility/Elements/google-translate-widget.ts +293 -0
- package/src/Utility/global.d.ts +4 -0
- package/src/Utility/utilities.ts +2 -0
- 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 %}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# BackToTop
|
|
2
|
+
|
|
3
|
+
A trigger that appears once the reader has scrolled past a threshold and jumps back to a target
|
|
4
|
+
element on the page. Shows/hides itself via `IntersectionObserver`, and manages focus and
|
|
5
|
+
reduced-motion-aware smooth scrolling when activated.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## CSS Import
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
@import "@pnx-mixtape/mxds/src/Component/BackToTop/back-to-top.css";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Twig
|
|
18
|
+
|
|
19
|
+
```twig
|
|
20
|
+
{% include '@mixtape/Component/BackToTop/back-to-top.twig' with {
|
|
21
|
+
href: '#main',
|
|
22
|
+
title: 'Back to top',
|
|
23
|
+
} %}
|
|
24
|
+
|
|
25
|
+
{# Watch a specific element (e.g. the page's <h1>) instead of a scroll distance #}
|
|
26
|
+
{% include '@mixtape/Component/BackToTop/back-to-top.twig' with {
|
|
27
|
+
target: '#page-title',
|
|
28
|
+
href: '#main',
|
|
29
|
+
} %}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**back-to-top.twig props:**
|
|
33
|
+
|
|
34
|
+
| Prop | Type | Default | Description |
|
|
35
|
+
| -------------- | ----------- | ------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
36
|
+
| `href` | `string` | `#main` | Scroll destination — a same-page hash |
|
|
37
|
+
| `title` | `string` | `Back to top` | Trigger label |
|
|
38
|
+
| `target` | `string` | — | CSS selector of an element to observe — visible once it scrolls above the viewport top |
|
|
39
|
+
| `visibleAfter` | `string` | `100vh` | Distance down the page before the trigger appears. Only used when `target` is unset |
|
|
40
|
+
| `offset` | `number` | — | Pixel offset to avoid overlapping another fixed element (e.g. a sticky CTA bar). Sets `--back-to-top-offset` |
|
|
41
|
+
| `breakpoint` | `string` | — | `l` restricts display to the large breakpoint and up. Omit to show at all viewport sizes |
|
|
42
|
+
| `attributes` | `Attribute` | — | Optional Drupal-style attributes object — modifier classes are merged on top |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## HTML + Web Component
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<mx-back-to-top class="mx-back-to-top" target="#page-title">
|
|
50
|
+
<a class="mx-button mx-button--light mx-button--small mx-back-to-top__trigger" href="#main">
|
|
51
|
+
<span class="mx-icon mx-icon--arrow-up"></span>
|
|
52
|
+
<span>Back to top</span>
|
|
53
|
+
</a>
|
|
54
|
+
</mx-back-to-top>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Registers as `<mx-back-to-top>`. Import the element to activate its behaviour:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import "@pnx-mixtape/mxds/src/Component/BackToTop/Elements/BackToTop"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**`<mx-back-to-top>` attributes:**
|
|
64
|
+
|
|
65
|
+
| Attribute | Description |
|
|
66
|
+
| -------------- | ----------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `target` | CSS selector of an element to observe — visible once it scrolls above the viewport top |
|
|
68
|
+
| `visibleafter` | Distance down the page before the trigger appears (default `100vh`). Used only when `target` is unset |
|
|
69
|
+
|
|
70
|
+
If no `target` is set, visibility is driven by a plain scroll-position check instead (`window.scrollY` compared against `visibleafter`, resolved from a `px` or `vh` value) rather than an `IntersectionObserver` — this avoids depending on the host page's containing-block/positioning context, which a synthetic absolutely-positioned sentinel would be sensitive to.
|
|
71
|
+
|
|
72
|
+
**Behaviour on click:** the Web Component intercepts the click, scrolls the destination into view
|
|
73
|
+
(`behavior: smooth`, or `auto` when `prefers-reduced-motion: reduce`), temporarily makes the
|
|
74
|
+
destination focusable if it isn't already (`tabindex="-1"`, removed again on blur), moves focus
|
|
75
|
+
to it, and updates the URL hash via `history.pushState` — all without a full native anchor jump,
|
|
76
|
+
so the smooth scroll and reduced-motion behaviour are never a race against the browser's own
|
|
77
|
+
default jump.
|
|
78
|
+
|
|
79
|
+
**Event:**
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
document.querySelector("mx-back-to-top").addEventListener("back-to-top-toggle", e => {
|
|
83
|
+
console.log(e.detail.isVisible) // true when the trigger becomes visible, false when it hides
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## React
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { BackToTop } from "@pnx-mixtape/mxds/react"
|
|
93
|
+
|
|
94
|
+
<BackToTop />
|
|
95
|
+
|
|
96
|
+
// Watch a specific element instead of a scroll distance
|
|
97
|
+
<BackToTop target="#page-title" href="#main" />
|
|
98
|
+
|
|
99
|
+
// Desktop-only, offset to avoid overlapping a sticky CTA bar
|
|
100
|
+
<BackToTop breakpoint="l" offset={56} />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**`BackToTop` props:**
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Default | Description |
|
|
106
|
+
| ----------------- | ------------------------------ | ------------- | -------------------------------------------------------------------------------------- |
|
|
107
|
+
| `to` | `string` | `#main` | Scroll destination — a same-page hash |
|
|
108
|
+
| `title` | `string` | `Back to top` | Trigger label |
|
|
109
|
+
| `target` | `string` | — | CSS selector of an element to observe — visible once it scrolls above the viewport top |
|
|
110
|
+
| `visibleAfter` | `string` | `100vh` | Distance down the page before the trigger appears. Only used when `target` is unset |
|
|
111
|
+
| `offset` | `number` | — | Pixel offset to avoid overlapping another fixed element. Sets `--back-to-top-offset` |
|
|
112
|
+
| `breakpoint` | `"l"` | — | Restrict display to the large breakpoint and up. Omit to show at all viewport sizes |
|
|
113
|
+
| `className` | `string` | — | Extra class on the root |
|
|
114
|
+
| `onVisibleChange` | `(isVisible: boolean) => void` | — | Called whenever the visible state changes. React analogue of `back-to-top-toggle` |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Modifiers
|
|
119
|
+
|
|
120
|
+
| CSS class | React `breakpoint` | Behaviour |
|
|
121
|
+
| ------------------- | ------------------ | ----------------------------------------------------------- |
|
|
122
|
+
| `mx-back-to-top` | `undefined` | Visible (once scrolled) at all viewport sizes |
|
|
123
|
+
| `mx-back-to-top--l` | `"l"` | Visible (once scrolled) only at the large breakpoint and up |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### React ⇄ Twig parity
|
|
128
|
+
|
|
129
|
+
`onVisibleChange` has no Twig equivalent — Twig/HTML has no concept of a function prop. The
|
|
130
|
+
`<mx-back-to-top>` Web Component dispatches a `back-to-top-toggle` `CustomEvent` instead; listen
|
|
131
|
+
for it if you need the same notification outside React.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Accessibility
|
|
136
|
+
|
|
137
|
+
- The trigger is a real `<a href>` (via the `Button` atom, `as: 'link'`) and is always visible,
|
|
138
|
+
in the tab order, and keyboard-activatable **without JavaScript**. The scroll-gated show/hide
|
|
139
|
+
behaviour is only switched on once JS has confirmed it can drive it — the Web Component adds
|
|
140
|
+
an `is-enhanced` class in `connectedCallback`, and React (which always has JS) adds it
|
|
141
|
+
unconditionally — so a blocked or failed script load can never leave the trigger permanently
|
|
142
|
+
hidden or unreachable.
|
|
143
|
+
- On activation, focus is moved to the scroll destination. If the destination isn't natively
|
|
144
|
+
focusable, a temporary `tabindex="-1"` is added and removed again once it loses focus — this
|
|
145
|
+
fixes a common gap where a plain anchor jump scrolls the page but leaves screen reader focus
|
|
146
|
+
behind.
|
|
147
|
+
- Smooth scrolling respects `prefers-reduced-motion: reduce` (falls back to an instant jump).
|
|
148
|
+
- Hidden from print via `@media print`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Customisation via CSS custom properties
|
|
153
|
+
|
|
154
|
+
```css
|
|
155
|
+
.mx-back-to-top {
|
|
156
|
+
--back-to-top-gap: 64px; /* distance from the bottom of the viewport */
|
|
157
|
+
--back-to-top-offset: 0; /* additional offset, e.g. to clear a sticky CTA bar */
|
|
158
|
+
--back-to-top-inline-end: var(--spacing-m); /* distance from the side of the viewport */
|
|
159
|
+
--back-to-top-z-index: 10;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The React component writes `--back-to-top-offset` as an inline style from the `offset` prop. In
|
|
164
|
+
HTML/Twig, pass `offset` (which sets it inline) or override any of these in your own CSS.
|