diff --git a/.gitignore b/.gitignore index 04c8b78..8bcfcf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +.DS_Store +.env +.env.* +.npm/ +.cache/ +coverage/ node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.log *.zip artefact.xml diff --git a/.pressshipignore b/.pressshipignore new file mode 100644 index 0000000..1f5be0e --- /dev/null +++ b/.pressshipignore @@ -0,0 +1,7 @@ +.pressshipignore +assets/ +src/ +.wp-playground/ +README.md +package.json +package-lock.json diff --git a/.wp-playground/blueprint.json b/.wp-playground/blueprint.json new file mode 100644 index 0000000..4e551bb --- /dev/null +++ b/.wp-playground/blueprint.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "preferredVersions": { + "php": "8.3", + "wp": "7.0" + }, + "landingPage": "/wp-admin/post-new.php?post_type=page", + "login": true, + "steps": [ + { + "step": "installPlugin", + "pluginZipFile": { + "resource": "url", + "url": "https://raw.githubusercontent.com/dhanson-wp/scroll-indicator/codex/submission-readiness/.wp-playground/scroll-indicator.zip" + }, + "options": { + "activate": true + } + } + ] +} diff --git a/.wp-playground/scroll-indicator.zip b/.wp-playground/scroll-indicator.zip new file mode 100644 index 0000000..27fbdbe Binary files /dev/null and b/.wp-playground/scroll-indicator.zip differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9c84063 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +Scroll Indicator is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the Free +Software Foundation, either version 2 of the License, or any later version. + +Scroll Indicator is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. + +You should have received a copy of the GNU General Public License along with +Scroll Indicator. If not, see https://www.gnu.org/licenses/gpl-2.0.html. diff --git a/README.md b/README.md index abee0e5..4df1627 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,19 @@ # Scroll Indicator -Scroll Indicator is a lightweight WordPress block for adding a polished “keep scrolling” cue to landing pages, editorial layouts, hero sections, and long-form content. +Scroll Indicator is a lightweight WordPress block for adding a “keep scrolling” cue to landing pages, editorial layouts, hero sections, and long-form content. -It gives site builders a small, expressive block that feels designed: choose an icon style, tune the size and color, add optional text, and let visitors click or keyboard-activate the indicator to move one viewport down the page. +Site builders can choose an icon style, tune the size and color, add optional text, and let visitors click or keyboard-activate the indicator to move one viewport down the page. ## Highlights - Five animated icon styles: mouse, arrow, chevron, dots, and hand. - Preset sizes plus a custom CSS size value. - Native block editor support for text color, spacing, typography, and alignment. +- Fixed screen positioning and draggable absolute positioning for hero-style sections. - CSS-only animation that respects `prefers-reduced-motion`. - Optional helper text beneath the icon. -- Optional hide-after-scrolling behavior. +- Automatic hide-on-scroll behavior. - Keyboard-accessible click-to-scroll behavior on the front end. - No animation libraries or heavy runtime dependencies. @@ -44,12 +45,19 @@ Create a production build: npm run build ``` -Create a distributable plugin ZIP: +Create a local plugin ZIP: ```bash npm run plugin-zip ``` +PressShip is the preferred WordPress.org submission path for this repo: + +```bash +npx pressship pack . +npx pressship publish . --dry-run +``` + ## Quality Checks ```bash @@ -70,6 +78,8 @@ The `assets/` directory contains WordPress.org plugin directory artwork: These are intended for the WordPress.org SVN `assets` directory after the plugin is approved. They are not required inside the installable plugin ZIP. +The compiled block files live in `compiled/` so PressShip includes them in the installable ZIP by default. + ## License GPLv2 or later. diff --git a/build/index-rtl.css b/build/index-rtl.css deleted file mode 100644 index 445b9ad..0000000 --- a/build/index-rtl.css +++ /dev/null @@ -1 +0,0 @@ -.scroll-indicator-icon-picker{border:0;margin:0 0 16px;padding:0}.scroll-indicator-icon-picker legend{font-weight:500;margin-bottom:8px}.scroll-indicator-icon-buttons{display:flex;flex-wrap:wrap;gap:4px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button{align-items:center;display:flex;height:44px;justify-content:center;padding:8px;width:44px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button svg{height:20px;width:20px}.scroll-indicator-size-picker{border:0;margin:0;padding:0}.scroll-indicator-size-picker legend{font-weight:500;margin-bottom:8px}.scroll-indicator-size-buttons{display:flex;margin-bottom:12px} diff --git a/build/index.asset.php b/build/index.asset.php deleted file mode 100644 index 36dc98f..0000000 --- a/build/index.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n'), 'version' => '5bf8d8c47e5abc153b6f'); diff --git a/build/index.css b/build/index.css deleted file mode 100644 index 445b9ad..0000000 --- a/build/index.css +++ /dev/null @@ -1 +0,0 @@ -.scroll-indicator-icon-picker{border:0;margin:0 0 16px;padding:0}.scroll-indicator-icon-picker legend{font-weight:500;margin-bottom:8px}.scroll-indicator-icon-buttons{display:flex;flex-wrap:wrap;gap:4px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button{align-items:center;display:flex;height:44px;justify-content:center;padding:8px;width:44px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button svg{height:20px;width:20px}.scroll-indicator-size-picker{border:0;margin:0;padding:0}.scroll-indicator-size-picker legend{font-weight:500;margin-bottom:8px}.scroll-indicator-size-buttons{display:flex;margin-bottom:12px} diff --git a/build/index.js b/build/index.js deleted file mode 100644 index d8c2ab4..0000000 --- a/build/index.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{"use strict";var o,e={280(){const o=window.wp.blocks,e=window.wp.i18n,r=window.wp.blockEditor,l=window.wp.components,i=window.ReactJSXRuntime,s={S:"18px",M:"24px",L:"32px",XL:"48px"};function n(o,e){return"custom"===o?e||"24px":s[o]||"24px"}function t(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 38",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",focusable:"false",children:[(0,i.jsx)("rect",{x:"1",y:"1",width:"22",height:"36",rx:"11",ry:"11"}),(0,i.jsx)("line",{x1:"12",y1:"8",x2:"12",y2:"16",strokeLinecap:"round",className:"scroll-wheel"})]})}const c={mouse:t,"arrow-down":function(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:[(0,i.jsx)("path",{d:"M7 13l5 5 5-5"}),(0,i.jsx)("path",{d:"M7 7l5 5 5-5",opacity:"0.4"})]})},"chevron-bounce":function(){return(0,i.jsx)("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:(0,i.jsx)("path",{d:"M6 9l6 6 6-6"})})},"scroll-dots":function(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 36",fill:"currentColor","aria-hidden":"true",focusable:"false",children:[(0,i.jsx)("circle",{cx:"12",cy:"6",r:"3",className:"dot dot-1"}),(0,i.jsx)("circle",{cx:"12",cy:"18",r:"3",className:"dot dot-2"}),(0,i.jsx)("circle",{cx:"12",cy:"30",r:"3",className:"dot dot-3"})]})},"hand-point":function(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:[(0,i.jsx)("path",{d:"M9 11V6a2 2 0 0 1 4 0v5"}),(0,i.jsx)("path",{d:"M13 11V8a2 2 0 0 1 4 0v5l1 3a4 4 0 0 1-4 4H9a4 4 0 0 1-4-4v-2a2 2 0 0 1 2-2h1"})]})}};function a({iconType:o}){const e=c[o]||t;return(0,i.jsx)(e,{})}const d=[{value:"mouse",label:(0,e.__)("Mouse","scroll-indicator")},{value:"arrow-down",label:(0,e.__)("Arrow","scroll-indicator")},{value:"chevron-bounce",label:(0,e.__)("Chevron","scroll-indicator")},{value:"scroll-dots",label:(0,e.__)("Dots","scroll-indicator")},{value:"hand-point",label:(0,e.__)("Hand","scroll-indicator")}],u=["S","M","L","XL","custom"],h=JSON.parse('{"UU":"scroll-indicator/scroll-indicator"}');(0,o.registerBlockType)(h.UU,{edit:function({attributes:o,setAttributes:s}){const{iconType:t="mouse",iconSize:h="M",customSizeValue:x="24px",hideAfterScrolling:p=!1,showText:v=!0,customText:j="Scroll down"}=o,f=n(h,x),m=(0,r.useBlockProps)({style:{"--scroll-indicator-size":f}});return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(r.InspectorControls,{children:[(0,i.jsxs)(l.PanelBody,{title:(0,e.__)("Settings","scroll-indicator"),initialOpen:!0,children:[(0,i.jsxs)("fieldset",{className:"scroll-indicator-icon-picker",children:[(0,i.jsx)("legend",{children:(0,e.__)("Icon Type","scroll-indicator")}),(0,i.jsx)(l.ButtonGroup,{className:"scroll-indicator-icon-buttons",children:d.map(o=>{const e=c[o.value];return(0,i.jsx)(l.Button,{className:"scroll-indicator-icon-button",isPressed:t===o.value,onClick:()=>s({iconType:o.value}),label:o.label,showTooltip:!0,children:(0,i.jsx)(e,{})},o.value)})})]}),(0,i.jsxs)("fieldset",{className:"scroll-indicator-size-picker",children:[(0,i.jsx)("legend",{children:(0,e.__)("Size","scroll-indicator")}),(0,i.jsx)(l.ButtonGroup,{className:"scroll-indicator-size-buttons",children:u.map(o=>(0,i.jsx)(l.Button,{isPressed:h===o,onClick:()=>s({iconSize:o}),children:"custom"===o?(0,e.__)("Custom","scroll-indicator"):o},o))}),"custom"===h&&(0,i.jsx)(l.TextControl,{label:(0,e.__)("Custom Size","scroll-indicator"),value:x,onChange:o=>s({customSizeValue:o}),help:(0,e.__)("Use a CSS size such as 24px, 2rem, or 3em.","scroll-indicator")})]})]}),(0,i.jsxs)(l.PanelBody,{title:(0,e.__)("Text","scroll-indicator"),initialOpen:!1,children:[(0,i.jsx)(l.ToggleControl,{label:(0,e.__)("Show text","scroll-indicator"),checked:v,onChange:o=>s({showText:o})}),v&&(0,i.jsx)(l.TextControl,{label:(0,e.__)("Custom Text","scroll-indicator"),value:j,onChange:o=>s({customText:o}),placeholder:(0,e.__)("Scroll down","scroll-indicator")})]}),(0,i.jsx)(l.PanelBody,{title:(0,e.__)("Behavior","scroll-indicator"),initialOpen:!1,children:(0,i.jsx)(l.ToggleControl,{label:(0,e.__)("Hide after scrolling","scroll-indicator"),help:(0,e.__)("Hide the indicator once user starts scrolling","scroll-indicator"),checked:p,onChange:o=>s({hideAfterScrolling:o})})})]}),(0,i.jsx)("div",{...m,children:(0,i.jsxs)("div",{className:`scroll-indicator icon-${t}`,children:[(0,i.jsx)(a,{iconType:t}),v&&(0,i.jsx)("div",{className:"scroll-text",children:j||(0,e.__)("Scroll down","scroll-indicator")})]})})]})},save:function({attributes:o}){const{iconType:e="mouse",iconSize:l="M",customSizeValue:s="24px",hideAfterScrolling:t=!1,showText:c=!0,customText:d="Scroll down"}=o,u=n(l,s),h=r.useBlockProps.save({style:{"--scroll-indicator-size":u}});return(0,i.jsx)("div",{...h,children:(0,i.jsxs)("div",{className:`scroll-indicator icon-${e}`,role:"button",tabIndex:"0","aria-label":d||"Scroll down","data-hide-after-scrolling":t?"true":"false",children:[(0,i.jsx)(a,{iconType:e}),c&&(0,i.jsx)("div",{className:"scroll-text",children:d||"Scroll down"})]})})}})}},r={};function l(o){var i=r[o];if(void 0!==i)return i.exports;var s=r[o]={exports:{}};return e[o](s,s.exports,l),s.exports}l.m=e,o=[],l.O=(e,r,i,s)=>{if(!r){var n=1/0;for(d=0;d=s)&&Object.keys(l.O).every(o=>l.O[o](r[c]))?r.splice(c--,1):(t=!1,s0&&o[d-1][2]>s;d--)o[d]=o[d-1];o[d]=[r,i,s]},l.o=(o,e)=>Object.prototype.hasOwnProperty.call(o,e),(()=>{var o={57:0,350:0};l.O.j=e=>0===o[e];var e=(e,r)=>{var i,s,[n,t,c]=r,a=0;if(n.some(e=>0!==o[e])){for(i in t)l.o(t,i)&&(l.m[i]=t[i]);if(c)var d=c(l)}for(e&&e(r);al(280));i=l.O(i)})(); \ No newline at end of file diff --git a/build/style-index-rtl.css b/build/style-index-rtl.css deleted file mode 100644 index aa1eabc..0000000 --- a/build/style-index-rtl.css +++ /dev/null @@ -1 +0,0 @@ -.wp-block-scroll-indicator-scroll-indicator .scroll-indicator{align-items:center;cursor:pointer;display:flex;flex-direction:column;gap:10px;padding:20px}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator svg{height:var(--scroll-indicator-size,24px);width:var(--scroll-indicator-size,24px)}.wp-block-scroll-indicator-scroll-indicator .scroll-text{color:currentcolor;font-size:14px;font-weight:500;opacity:.8}@media(prefers-reduced-motion:no-preference){.wp-block-scroll-indicator-scroll-indicator .icon-mouse svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-mouse .scroll-wheel{animation:scroll-indicator-wheel 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-chevron-bounce svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-arrow-down svg path:last-child{animation:scroll-indicator-fade-pulse 2s ease-in-out infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-1{animation:scroll-indicator-dot-pulse 1.4s ease-in-out 0s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-2{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-3{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .4s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-hand-point svg{animation:scroll-indicator-bounce 2s infinite}@keyframes scroll-indicator-bounce{0%,20%,50%,80%,to{transform:translateY(0)}40%{transform:translateY(-8px)}60%{transform:translateY(-4px)}}@keyframes scroll-indicator-wheel{0%,50%,to{opacity:1;transform:translateY(0)}25%{opacity:.5;transform:translateY(8px)}}@keyframes scroll-indicator-fade-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes scroll-indicator-dot-pulse{0%,to{opacity:.2}50%{opacity:1}}} diff --git a/build/style-index.css b/build/style-index.css deleted file mode 100644 index aa1eabc..0000000 --- a/build/style-index.css +++ /dev/null @@ -1 +0,0 @@ -.wp-block-scroll-indicator-scroll-indicator .scroll-indicator{align-items:center;cursor:pointer;display:flex;flex-direction:column;gap:10px;padding:20px}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator svg{height:var(--scroll-indicator-size,24px);width:var(--scroll-indicator-size,24px)}.wp-block-scroll-indicator-scroll-indicator .scroll-text{color:currentcolor;font-size:14px;font-weight:500;opacity:.8}@media(prefers-reduced-motion:no-preference){.wp-block-scroll-indicator-scroll-indicator .icon-mouse svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-mouse .scroll-wheel{animation:scroll-indicator-wheel 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-chevron-bounce svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-arrow-down svg path:last-child{animation:scroll-indicator-fade-pulse 2s ease-in-out infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-1{animation:scroll-indicator-dot-pulse 1.4s ease-in-out 0s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-2{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-3{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .4s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-hand-point svg{animation:scroll-indicator-bounce 2s infinite}@keyframes scroll-indicator-bounce{0%,20%,50%,80%,to{transform:translateY(0)}40%{transform:translateY(-8px)}60%{transform:translateY(-4px)}}@keyframes scroll-indicator-wheel{0%,50%,to{opacity:1;transform:translateY(0)}25%{opacity:.5;transform:translateY(8px)}}@keyframes scroll-indicator-fade-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes scroll-indicator-dot-pulse{0%,to{opacity:.2}50%{opacity:1}}} diff --git a/build/view.asset.php b/build/view.asset.php deleted file mode 100644 index caa3e8d..0000000 --- a/build/view.asset.php +++ /dev/null @@ -1 +0,0 @@ - array(), 'version' => '2b3837a3c2a852532a1c'); diff --git a/build/view.js b/build/view.js deleted file mode 100644 index 2ad1101..0000000 --- a/build/view.js +++ /dev/null @@ -1 +0,0 @@ -document.addEventListener("DOMContentLoaded",function(){const t=document.querySelectorAll(".scroll-indicator");0!==t.length&&t.forEach(function(t){const e="true"===t.getAttribute("data-hide-after-scrolling"),o=t.closest(".wp-block-scroll-indicator-scroll-indicator");function n(){const t=window.innerHeight,e=window.scrollY+t;window.scrollTo({top:e,behavior:"smooth"})}if(t.addEventListener("click",n),t.addEventListener("keydown",function(t){"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),n())}),t.style.cursor="pointer",e&&o){let s=o.offsetTop,c=s+o.offsetHeight;function i(){const e=window.scrollY,o=window.innerHeight;e<=s||se?(t.style.transition="opacity 0.3s ease-in",t.style.opacity="1"):(t.style.transition="opacity 0.3s ease-out",t.style.opacity="0")}window.addEventListener("resize",function(){s=o.offsetTop,c=s+o.offsetHeight}),i(),window.addEventListener("scroll",i,{passive:!0})}})}); \ No newline at end of file diff --git a/build/block.json b/compiled/block.json similarity index 72% rename from build/block.json rename to compiled/block.json index bb345c0..a6d3701 100644 --- a/build/block.json +++ b/compiled/block.json @@ -21,9 +21,12 @@ "type": "string", "default": "24px" }, + "align": { + "type": "string" + }, "hideAfterScrolling": { "type": "boolean", - "default": false + "default": true }, "showText": { "type": "boolean", @@ -32,22 +35,34 @@ "customText": { "type": "string", "default": "Scroll down" + }, + "positionMode": { + "type": "string", + "default": "flow" + }, + "screenPosition": { + "type": "string", + "default": "bottom-center" + }, + "absoluteX": { + "type": "number", + "default": 50 + }, + "absoluteY": { + "type": "number", + "default": 85 + }, + "flowAlign": { + "type": "string", + "default": "center" } }, "supports": { "html": false, - "align": [ - "left", - "center", - "right" - ], "color": { "text": true, "background": false }, - "typography": { - "fontSize": true - }, "spacing": { "margin": true, "padding": true diff --git a/compiled/index-rtl.css b/compiled/index-rtl.css new file mode 100644 index 0000000..4049b21 --- /dev/null +++ b/compiled/index-rtl.css @@ -0,0 +1 @@ +.scroll-indicator-icon-picker,.scroll-indicator-position-picker,.scroll-indicator-size-picker{border:0;margin:0 0 16px;padding:0}.scroll-indicator-icon-picker legend,.scroll-indicator-position-picker legend,.scroll-indicator-size-picker legend{font-weight:500;margin-bottom:8px}.scroll-indicator-icon-buttons{display:flex;flex-wrap:wrap;gap:4px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button{align-items:center;display:flex;height:44px;justify-content:center;padding:8px;width:44px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button svg{height:20px;width:20px}.scroll-indicator-icon-preview{align-items:center;display:inline-flex;height:20px;justify-content:center;width:20px}.scroll-indicator-icon-preview.icon-arrow-down line,.scroll-indicator-icon-preview.icon-arrow-down path,.scroll-indicator-icon-preview.icon-arrow-down rect,.scroll-indicator-icon-preview.icon-arrow-down svg,.scroll-indicator-icon-preview.icon-chevron-bounce line,.scroll-indicator-icon-preview.icon-chevron-bounce path,.scroll-indicator-icon-preview.icon-chevron-bounce rect,.scroll-indicator-icon-preview.icon-chevron-bounce svg,.scroll-indicator-icon-preview.icon-hand-point line,.scroll-indicator-icon-preview.icon-hand-point path,.scroll-indicator-icon-preview.icon-hand-point rect,.scroll-indicator-icon-preview.icon-hand-point svg,.scroll-indicator-icon-preview.icon-mouse line,.scroll-indicator-icon-preview.icon-mouse path,.scroll-indicator-icon-preview.icon-mouse rect,.scroll-indicator-icon-preview.icon-mouse svg{fill:none}.scroll-indicator-mode-buttons{display:grid;gap:4px;grid-template-columns:repeat(3,minmax(0,1fr));margin-bottom:12px}.scroll-indicator-position-buttons{display:flex;gap:4px}.scroll-indicator-position-buttons .components-button{justify-content:center;min-width:44px}.scroll-indicator-absolute-controls,.scroll-indicator-position-note{color:#1e1e1e;font-size:12px;line-height:1.4;margin:0 0 12px}.scroll-indicator-control-icon{display:inline-flex;height:18px;margin-left:6px;vertical-align:middle;width:18px}.editor-styles-wrapper .wp-block-scroll-indicator-scroll-indicator.is-position-absolute{cursor:grab}.editor-styles-wrapper .wp-block-scroll-indicator-scroll-indicator.is-position-absolute:active{cursor:grabbing} diff --git a/compiled/index.asset.php b/compiled/index.asset.php new file mode 100644 index 0000000..ef8cd77 --- /dev/null +++ b/compiled/index.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '3e0f1d3a7adc1dd50ccc'); diff --git a/compiled/index.css b/compiled/index.css new file mode 100644 index 0000000..38265f0 --- /dev/null +++ b/compiled/index.css @@ -0,0 +1 @@ +.scroll-indicator-icon-picker,.scroll-indicator-position-picker,.scroll-indicator-size-picker{border:0;margin:0 0 16px;padding:0}.scroll-indicator-icon-picker legend,.scroll-indicator-position-picker legend,.scroll-indicator-size-picker legend{font-weight:500;margin-bottom:8px}.scroll-indicator-icon-buttons{display:flex;flex-wrap:wrap;gap:4px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button{align-items:center;display:flex;height:44px;justify-content:center;padding:8px;width:44px}.scroll-indicator-icon-buttons .scroll-indicator-icon-button svg{height:20px;width:20px}.scroll-indicator-icon-preview{align-items:center;display:inline-flex;height:20px;justify-content:center;width:20px}.scroll-indicator-icon-preview.icon-arrow-down line,.scroll-indicator-icon-preview.icon-arrow-down path,.scroll-indicator-icon-preview.icon-arrow-down rect,.scroll-indicator-icon-preview.icon-arrow-down svg,.scroll-indicator-icon-preview.icon-chevron-bounce line,.scroll-indicator-icon-preview.icon-chevron-bounce path,.scroll-indicator-icon-preview.icon-chevron-bounce rect,.scroll-indicator-icon-preview.icon-chevron-bounce svg,.scroll-indicator-icon-preview.icon-hand-point line,.scroll-indicator-icon-preview.icon-hand-point path,.scroll-indicator-icon-preview.icon-hand-point rect,.scroll-indicator-icon-preview.icon-hand-point svg,.scroll-indicator-icon-preview.icon-mouse line,.scroll-indicator-icon-preview.icon-mouse path,.scroll-indicator-icon-preview.icon-mouse rect,.scroll-indicator-icon-preview.icon-mouse svg{fill:none}.scroll-indicator-mode-buttons{display:grid;gap:4px;grid-template-columns:repeat(3,minmax(0,1fr));margin-bottom:12px}.scroll-indicator-position-buttons{display:flex;gap:4px}.scroll-indicator-position-buttons .components-button{justify-content:center;min-width:44px}.scroll-indicator-absolute-controls,.scroll-indicator-position-note{color:#1e1e1e;font-size:12px;line-height:1.4;margin:0 0 12px}.scroll-indicator-control-icon{display:inline-flex;height:18px;margin-right:6px;vertical-align:middle;width:18px}.editor-styles-wrapper .wp-block-scroll-indicator-scroll-indicator.is-position-absolute{cursor:grab}.editor-styles-wrapper .wp-block-scroll-indicator-scroll-indicator.is-position-absolute:active{cursor:grabbing} diff --git a/compiled/index.js b/compiled/index.js new file mode 100644 index 0000000..3d3a92e --- /dev/null +++ b/compiled/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var o,e={83(){const o=window.wp.blocks,e=window.wp.primitives,t=window.ReactJSXRuntime;var l=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"m16.5 13.5-3.7 3.7V4h-1.5v13.2l-3.8-3.7-1 1 5.5 5.6 5.5-5.6z"})});const n=window.wp.i18n,i=window.wp.blockEditor,r=window.wp.element;var s=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"M5 5.5h8V4H5v1.5ZM5 20h8v-1.5H5V20ZM19 9H5v6h14V9Z"})}),c=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"M19 5.5H5V4h14v1.5ZM19 20H5v-1.5h14V20ZM7 9h10v6H7V9Z"})}),a=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"M19 5.5h-8V4h8v1.5ZM19 20h-8v-1.5h8V20ZM5 9h14v6H5V9Z"})}),d=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"M19.75 9c0-1.257-.565-2.197-1.39-2.858-.797-.64-1.827-1.017-2.815-1.247-1.802-.42-3.703-.403-4.383-.396L11 4.5V6l.177-.001c.696-.006 2.416-.02 4.028.356.887.207 1.67.518 2.216.957.52.416.829.945.829 1.688 0 .592-.167.966-.407 1.23-.255.281-.656.508-1.236.674-1.19.34-2.82.346-4.607.346h-.077c-1.692 0-3.527 0-4.942.404-.732.209-1.424.545-1.935 1.108-.526.579-.796 1.33-.796 2.238 0 1.257.565 2.197 1.39 2.858.797.64 1.827 1.017 2.815 1.247 1.802.42 3.703.403 4.383.396L13 19.5h.714V22L18 18.5 13.714 15v3H13l-.177.001c-.696.006-2.416.02-4.028-.356-.887-.207-1.67-.518-2.216-.957-.52-.416-.829-.945-.829-1.688 0-.592.167-.966.407-1.23.255-.281.656-.508 1.237-.674 1.189-.34 2.819-.346 4.606-.346h.077c1.692 0 3.527 0 4.941-.404.732-.209 1.425-.545 1.936-1.108.526-.579.796-1.33.796-2.238z"})}),u=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"M8 7h2V5H8v2zm0 6h2v-2H8v2zm0 6h2v-2H8v2zm6-14v2h2V5h-2zm0 8h2v-2h-2v2zm0 6h2v-2h-2v2z"})}),h=(0,t.jsx)(e.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,t.jsx)(e.Path,{d:"M20.5 16h-.7V8c0-1.1-.9-2-2-2H6.2c-1.1 0-2 .9-2 2v8h-.7c-.8 0-1.5.7-1.5 1.5h20c0-.8-.7-1.5-1.5-1.5zM5.7 8c0-.3.2-.5.5-.5h11.6c.3 0 .5.2.5.5v7.6H5.7V8z"})});const x=window.wp.components,v=window.wp.data,p={S:"18px",M:"24px",L:"32px",XL:"48px"},m=/^(?:\d+|\d*\.\d+)(?:px|em|rem|vh|vw|vmin|vmax|%)$/;function b(o,e){if("custom"===o){const o="string"==typeof e?e.trim():"";return m.test(o)?o:"24px"}return p[o]||"24px"}const w={mouse:function(){return(0,t.jsxs)("svg",{viewBox:"0 0 24 38",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",focusable:"false",children:[(0,t.jsx)("rect",{x:"1",y:"1",width:"22",height:"36",rx:"11",ry:"11"}),(0,t.jsx)("line",{x1:"12",y1:"8",x2:"12",y2:"16",strokeLinecap:"round",className:"scroll-wheel"})]})},"arrow-down":function(){return(0,t.jsxs)("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:[(0,t.jsx)("path",{d:"M7 13l5 5 5-5"}),(0,t.jsx)("path",{d:"M7 7l5 5 5-5",opacity:"0.4"})]})},"chevron-bounce":function(){return(0,t.jsx)("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:(0,t.jsx)("path",{d:"M6 9l6 6 6-6"})})},"scroll-dots":function(){return(0,t.jsxs)("svg",{viewBox:"0 0 24 36",fill:"currentColor","aria-hidden":"true",focusable:"false",children:[(0,t.jsx)("circle",{cx:"12",cy:"6",r:"3",className:"dot dot-1"}),(0,t.jsx)("circle",{cx:"12",cy:"18",r:"3",className:"dot dot-2"}),(0,t.jsx)("circle",{cx:"12",cy:"30",r:"3",className:"dot dot-3"})]})},"hand-point":function(){return(0,t.jsxs)("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:[(0,t.jsx)("path",{d:"M9 11V6a2 2 0 0 1 4 0v5"}),(0,t.jsx)("path",{d:"M13 11V8a2 2 0 0 1 4 0v5l1 3a4 4 0 0 1-4 4H9a4 4 0 0 1-4-4v-2a2 2 0 0 1 2-2h1"})]})}};function f(o){return Object.prototype.hasOwnProperty.call(w,o)?o:"mouse"}function g({iconType:o}){const e=w[f(o)];return(0,t.jsx)(e,{})}const j={flow:"flow",fixed:"fixed",absolute:"absolute"},_={"bottom-left":"bottom-left","bottom-center":"bottom-center","bottom-right":"bottom-right"},k={left:"left",center:"center",right:"right"},B={left:"bottom-left",center:"bottom-center",right:"bottom-right"},M={"bottom-left":"left","bottom-center":"center","bottom-right":"right"};function C(o){return j[o]||j.flow}function y(o){return _[o]||_["bottom-center"]}function N(o){return k[o]||k.center}function S(o){return M[y(o)]||k.center}function V(o){return B[N(o)]||_["bottom-center"]}function T(o){const e=k[o];return e?`align${e}`:""}function P(o,e){const t="number"==typeof o?o:Number.parseFloat(o);return Number.isFinite(t)?Math.min(100,Math.max(0,t)):e}function z(o,e,t="center"){const l=C(o);if("fixed"===l)return`is-position-fixed is-screen-position-${y(e)}`;if("absolute"===l)return"is-position-absolute";const n=N(t);return"center"!==n?`is-flow-align-${n}`:""}function L(o,e,t){return"absolute"!==C(o)?{}:{"--scroll-indicator-x":`${P(e,50)}%`,"--scroll-indicator-y":`${P(t,85)}%`}}const H=[{value:"mouse",label:(0,n.__)("Mouse","scroll-indicator")},{value:"arrow-down",label:(0,n.__)("Arrow","scroll-indicator")},{value:"chevron-bounce",label:(0,n.__)("Chevron","scroll-indicator")},{value:"scroll-dots",label:(0,n.__)("Dots","scroll-indicator")},{value:"hand-point",label:(0,n.__)("Hand","scroll-indicator")}],O=[{value:"S",label:(0,n.__)("Small","scroll-indicator"),text:"S"},{value:"M",label:(0,n.__)("Medium","scroll-indicator"),text:"M"},{value:"L",label:(0,n.__)("Large","scroll-indicator"),text:"L"},{value:"XL",label:(0,n.__)("Extra Large","scroll-indicator"),text:"XL"},{value:"custom",label:(0,n.__)("Custom","scroll-indicator"),text:(0,n.__)("Custom","scroll-indicator")}],G=[{value:"flow",label:(0,n.__)("Flow","scroll-indicator")},{value:"fixed",label:(0,n.__)("Fixed","scroll-indicator")},{value:"absolute",label:(0,n.__)("Absolute","scroll-indicator")}],A=[{value:"left",label:(0,n.__)("Align left","scroll-indicator"),icon:s},{value:"center",label:(0,n.__)("Align center","scroll-indicator"),icon:c},{value:"right",label:(0,n.__)("Align right","scroll-indicator"),icon:a}],$=[{value:"bottom-left",label:(0,n.__)("Bottom left","scroll-indicator"),icon:s},{value:"bottom-center",label:(0,n.__)("Bottom center","scroll-indicator"),icon:c},{value:"bottom-right",label:(0,n.__)("Bottom right","scroll-indicator"),icon:a}],X=JSON.parse('{"UU":"scroll-indicator/scroll-indicator"}');(0,o.registerBlockType)(X.UU,{icon:l,edit:function({attributes:o,setAttributes:e,clientId:l}){const{iconType:s="mouse",iconSize:c="M",customSizeValue:a="24px",align:p,showText:m=!0,customText:j="Scroll down",positionMode:_="flow",screenPosition:k="bottom-center",absoluteX:B=50,absoluteY:M=85,flowAlign:X="center"}=o,Z=(0,r.useRef)(),{selectBlock:D}=(0,v.useDispatch)("core/block-editor"),E=f(s),F=b(c,a),R=C(_),Y=["left","center","right"].includes(p),I=Y?V(p):y(k),U=P(B,50),W=P(M,85),J=N(Y?p:X),q=Y?p:J,K=function(o){const e="number"==typeof o?o:Number.parseFloat(o);return Number.isFinite(e)?Math.min(96,Math.max(12,e)):24}(a);function Q(o){const t=Z.current,l=t?.closest(".wp-block-cover")||t?.parentElement;if(!l)return;const n=l.getBoundingClientRect();0!==n.width&&0!==n.height&&e({absoluteX:P((o.clientX-n.left)/n.width*100,50),absoluteY:P((o.clientY-n.top)/n.height*100,85)})}const oo=(0,i.useBlockProps)({ref:Z,className:z(R,I,J).concat(" ",T(p)).trim(),style:{"--scroll-indicator-size":F,...L(R,U,W)}});return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.BlockControls,{group:"block",children:"absolute"===R?(0,t.jsx)(x.ToolbarGroup,{children:(0,t.jsx)(x.ToolbarButton,{icon:d,label:(0,n.__)("Drag on canvas to position","scroll-indicator"),onClick:()=>D(l)})}):(0,t.jsx)(i.AlignmentToolbar,{value:q,onChange:function(o){const t=N(o||"center");e("fixed"!==R?{align:t,flowAlign:t}:{align:t,screenPosition:V(t)})}})}),(0,t.jsxs)(i.InspectorControls,{children:[(0,t.jsxs)(x.PanelBody,{title:(0,n.__)("Settings","scroll-indicator"),initialOpen:!0,children:[(0,t.jsxs)("fieldset",{className:"scroll-indicator-icon-picker",children:[(0,t.jsx)("legend",{children:(0,n.__)("Icon Type","scroll-indicator")}),(0,t.jsx)(x.ButtonGroup,{className:"scroll-indicator-icon-buttons",children:H.map(o=>{const l=w[o.value];return(0,t.jsx)(x.Button,{className:"scroll-indicator-icon-button",isPressed:s===o.value,onClick:()=>e({iconType:o.value}),label:o.label,showTooltip:!0,children:(0,t.jsx)("span",{className:`scroll-indicator-icon-preview icon-${o.value}`,children:(0,t.jsx)(l,{})})},o.value)})})]}),(0,t.jsxs)("fieldset",{className:"scroll-indicator-size-picker",children:[(0,t.jsx)("legend",{children:(0,n.__)("Icon Size","scroll-indicator")}),(0,t.jsx)(x.ButtonGroup,{className:"scroll-indicator-size-buttons",children:O.map(o=>(0,t.jsx)(x.Button,{isPressed:c===o.value,label:o.label,showTooltip:!0,onClick:()=>function(o){const t={iconSize:o};"custom"===o&&(t.customSizeValue=`${K}px`),e(t)}(o.value),children:o.text},o.value))}),"custom"===c&&(0,t.jsx)(x.RangeControl,{label:(0,n.__)("Custom size","scroll-indicator"),value:K,min:12,max:96,step:1,renderTooltipContent:o=>`${o}px`,onChange:o=>e({customSizeValue:`${o||24}px`})})]}),(0,t.jsx)(x.ToggleControl,{label:(0,n.__)("Show text","scroll-indicator"),checked:m,onChange:o=>e({showText:o})}),m&&(0,t.jsx)(x.TextControl,{__next40pxDefaultSize:!0,label:(0,n.__)("Text","scroll-indicator"),value:j,onChange:o=>e({customText:o}),placeholder:(0,n.__)("Scroll down","scroll-indicator")})]}),(0,t.jsxs)(x.PanelBody,{title:(0,n.__)("Position","scroll-indicator"),initialOpen:!1,children:[(0,t.jsxs)("fieldset",{className:"scroll-indicator-position-picker",children:[(0,t.jsx)("legend",{children:(0,n.__)("Position Mode","scroll-indicator")}),(0,t.jsx)(x.ButtonGroup,{className:"scroll-indicator-mode-buttons",children:G.map(o=>(0,t.jsx)(x.Button,{isPressed:R===o.value,onClick:()=>e({positionMode:o.value}),children:o.label},o.value))})]}),"flow"===R&&(0,t.jsxs)("fieldset",{className:"scroll-indicator-position-picker",children:[(0,t.jsx)("legend",{children:(0,n.__)("Alignment","scroll-indicator")}),(0,t.jsx)(x.ButtonGroup,{className:"scroll-indicator-position-buttons",children:A.map(o=>{const l=o.icon;return(0,t.jsx)(x.Button,{icon:l,label:o.label,showTooltip:!0,isPressed:J===o.value,onClick:()=>e({flowAlign:o.value,align:o.value})},o.value)})})]}),"fixed"===R&&(0,t.jsxs)("fieldset",{className:"scroll-indicator-position-picker",children:[(0,t.jsx)("legend",{children:(0,n.__)("Screen Position","scroll-indicator")}),(0,t.jsx)(x.ButtonGroup,{className:"scroll-indicator-position-buttons",children:$.map(o=>{const l=o.icon;return(0,t.jsx)(x.Button,{icon:l,label:o.label,showTooltip:!0,isPressed:I===o.value,onClick:()=>e({screenPosition:o.value,align:S(o.value)})},o.value)})})]}),"absolute"===R&&(0,t.jsxs)("div",{className:"scroll-indicator-absolute-controls",children:[(0,t.jsxs)("p",{children:[(0,t.jsx)("span",{className:"scroll-indicator-control-icon",children:u}),(0,n.__)("Drag the indicator on the canvas, or fine-tune its position below.","scroll-indicator")]}),(0,t.jsx)(x.RangeControl,{label:(0,n.__)("Horizontal","scroll-indicator"),value:U,min:0,max:100,onChange:o=>e({absoluteX:o})}),(0,t.jsx)(x.RangeControl,{label:(0,n.__)("Vertical","scroll-indicator"),value:W,min:0,max:100,onChange:o=>e({absoluteY:o})})]}),"fixed"===R&&(0,t.jsxs)("p",{className:"scroll-indicator-position-note",children:[(0,t.jsx)("span",{className:"scroll-indicator-control-icon",children:h}),(0,n.__)("Fixed positioning pins the indicator to the visitor's screen.","scroll-indicator")]})]})]}),(0,t.jsx)("div",{...oo,children:(0,t.jsxs)("div",{className:`scroll-indicator icon-${E}`,onPointerDown:function(o){if("absolute"!==R||0!==o.button)return;o.preventDefault(),o.stopPropagation(),D(l),Q(o);const e=o.currentTarget.ownerDocument,t=o=>{Q(o)},n=()=>{e.removeEventListener("pointermove",t),e.removeEventListener("pointerup",n)};e.addEventListener("pointermove",t),e.addEventListener("pointerup",n)},children:[(0,t.jsx)(g,{iconType:E}),m&&(0,t.jsx)("div",{className:"scroll-text",children:j||(0,n.__)("Scroll down","scroll-indicator")})]})})]})},save:function({attributes:o}){const{iconType:e="mouse",iconSize:l="M",customSizeValue:n="24px",align:r,showText:s=!0,customText:c="Scroll down",positionMode:a="flow",screenPosition:d="bottom-center",absoluteX:u=50,absoluteY:h=85,flowAlign:x="center"}=o,v=f(e),p=b(l,n),m=c||"Scroll down",w=["left","center","right"].includes(r),j=w?V(r):y(d),_=N(w?r:x),k=i.useBlockProps.save({className:z(a,j,_).concat(" ",T(r)).trim(),style:{"--scroll-indicator-size":p,...L(a,u,h)}});return(0,t.jsx)("div",{...k,children:(0,t.jsxs)("button",{type:"button",className:`scroll-indicator icon-${v}`,"aria-label":m,"data-hide-after-scrolling":"true",children:[(0,t.jsx)(g,{iconType:v}),s&&(0,t.jsx)("div",{className:"scroll-text",children:m})]})})}})}},t={};function l(o){var n=t[o];if(void 0!==n)return n.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,l),i.exports}l.m=e,o=[],l.O=(e,t,n,i)=>{if(!t){var r=1/0;for(d=0;d=i)&&Object.keys(l.O).every(o=>l.O[o](t[c]))?t.splice(c--,1):(s=!1,i0&&o[d-1][2]>i;d--)o[d]=o[d-1];o[d]=[t,n,i]},l.o=(o,e)=>Object.prototype.hasOwnProperty.call(o,e),(()=>{var o={57:0,350:0};l.O.j=e=>0===o[e];var e=(e,t)=>{var n,i,[r,s,c]=t,a=0;if(r.some(e=>0!==o[e])){for(n in s)l.o(s,n)&&(l.m[n]=s[n]);if(c)var d=c(l)}for(e&&e(t);al(83));n=l.O(n)})(); \ No newline at end of file diff --git a/compiled/style-index-rtl.css b/compiled/style-index-rtl.css new file mode 100644 index 0000000..ae584fd --- /dev/null +++ b/compiled/style-index-rtl.css @@ -0,0 +1 @@ +.wp-block-scroll-indicator-scroll-indicator{box-sizing:border-box;display:flex;justify-content:center;max-width:100%;width:100%}.wp-block-scroll-indicator-scroll-indicator.aligncenter,.wp-block-scroll-indicator-scroll-indicator.alignleft,.wp-block-scroll-indicator-scroll-indicator.alignright{float:none}.wp-block-scroll-indicator-scroll-indicator.alignleft{justify-content:flex-start}.wp-block-scroll-indicator-scroll-indicator.aligncenter{justify-content:center}.wp-block-scroll-indicator-scroll-indicator.alignright{justify-content:flex-end}.wp-block-scroll-indicator-scroll-indicator.is-flow-align-left{justify-content:flex-start}.wp-block-scroll-indicator-scroll-indicator.is-flow-align-right{justify-content:flex-end}.is-layout-constrained>.wp-block-scroll-indicator-scroll-indicator:not(.is-position-fixed):not(.is-position-absolute){max-width:var(--wp--style--global--content-size);width:100%}.editor-styles-wrapper .is-layout-constrained>.wp-block-scroll-indicator-scroll-indicator:not(.is-position-fixed):not(.is-position-absolute),.is-layout-constrained>.wp-block-scroll-indicator-scroll-indicator:not(.is-position-fixed):not(.is-position-absolute){float:none!important;margin-inline-end:auto!important;margin-inline-start:auto!important;margin-right:auto!important;margin-left:auto!important}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed{bottom:clamp(16px,4vw,32px);right:50%;margin:0;position:fixed;left:auto;transform:translateX(50%);width:auto;z-index:1000}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed.is-screen-position-bottom-left{right:clamp(16px,4vw,32px);left:auto;transform:none}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed.is-screen-position-bottom-center{right:50%;left:auto;transform:translateX(50%)}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed.is-screen-position-bottom-right{right:auto;left:clamp(16px,4vw,32px);transform:none}.wp-block-scroll-indicator-scroll-indicator.is-position-absolute{right:var(--scroll-indicator-x,50%);margin:0;position:absolute;top:var(--scroll-indicator-y,85%);transform:translate(50%,-50%);width:auto;z-index:2}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator{align-items:center;background:transparent;border:0;color:inherit;cursor:pointer;display:inline-flex;flex-direction:column;font:inherit;gap:10px;max-width:100%;padding:0;text-align:center}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator:focus-visible{border-radius:8px;box-shadow:0 0 0 2px currentcolor;outline:2px solid transparent;outline-offset:4px}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator svg{height:var(--scroll-indicator-size,24px);width:var(--scroll-indicator-size,24px)}.wp-block-scroll-indicator-scroll-indicator.is-position-absolute .scroll-indicator{cursor:grab;touch-action:none}.wp-block-scroll-indicator-scroll-indicator.is-position-absolute .scroll-indicator:active{cursor:grabbing}.wp-block-scroll-indicator-scroll-indicator .scroll-text{color:currentcolor;font-size:14px;font-weight:500;opacity:.8}.wp-block-cover:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute),.wp-block-group:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute){position:relative}.wp-block-cover:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute)>.wp-block-cover__inner-container{position:static}.wp-block-cover:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute)>.wp-block-cover__inner-container>:not(.wp-block-scroll-indicator-scroll-indicator){position:relative;z-index:1}@media(prefers-reduced-motion:no-preference){.wp-block-scroll-indicator-scroll-indicator .icon-mouse svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-mouse .scroll-wheel{animation:scroll-indicator-wheel 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-chevron-bounce svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-arrow-down svg path:last-child{animation:scroll-indicator-fade-pulse 2s ease-in-out infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-1{animation:scroll-indicator-dot-pulse 1.4s ease-in-out 0s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-2{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-3{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .4s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-hand-point svg{animation:scroll-indicator-bounce 2s infinite}@keyframes scroll-indicator-bounce{0%,20%,50%,80%,to{transform:translateY(0)}40%{transform:translateY(-8px)}60%{transform:translateY(-4px)}}@keyframes scroll-indicator-wheel{0%,50%,to{opacity:1;transform:translateY(0)}25%{opacity:.5;transform:translateY(8px)}}@keyframes scroll-indicator-fade-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes scroll-indicator-dot-pulse{0%,to{opacity:.2}50%{opacity:1}}} diff --git a/compiled/style-index.css b/compiled/style-index.css new file mode 100644 index 0000000..c5a1a6a --- /dev/null +++ b/compiled/style-index.css @@ -0,0 +1 @@ +.wp-block-scroll-indicator-scroll-indicator{box-sizing:border-box;display:flex;justify-content:center;max-width:100%;width:100%}.wp-block-scroll-indicator-scroll-indicator.aligncenter,.wp-block-scroll-indicator-scroll-indicator.alignleft,.wp-block-scroll-indicator-scroll-indicator.alignright{float:none}.wp-block-scroll-indicator-scroll-indicator.alignleft{justify-content:flex-start}.wp-block-scroll-indicator-scroll-indicator.aligncenter{justify-content:center}.wp-block-scroll-indicator-scroll-indicator.alignright{justify-content:flex-end}.wp-block-scroll-indicator-scroll-indicator.is-flow-align-left{justify-content:flex-start}.wp-block-scroll-indicator-scroll-indicator.is-flow-align-right{justify-content:flex-end}.is-layout-constrained>.wp-block-scroll-indicator-scroll-indicator:not(.is-position-fixed):not(.is-position-absolute){max-width:var(--wp--style--global--content-size);width:100%}.editor-styles-wrapper .is-layout-constrained>.wp-block-scroll-indicator-scroll-indicator:not(.is-position-fixed):not(.is-position-absolute),.is-layout-constrained>.wp-block-scroll-indicator-scroll-indicator:not(.is-position-fixed):not(.is-position-absolute){float:none!important;margin-inline-end:auto!important;margin-inline-start:auto!important;margin-left:auto!important;margin-right:auto!important}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed{bottom:clamp(16px,4vw,32px);left:50%;margin:0;position:fixed;right:auto;transform:translateX(-50%);width:auto;z-index:1000}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed.is-screen-position-bottom-left{left:clamp(16px,4vw,32px);right:auto;transform:none}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed.is-screen-position-bottom-center{left:50%;right:auto;transform:translateX(-50%)}.wp-block-scroll-indicator-scroll-indicator.is-position-fixed.is-screen-position-bottom-right{left:auto;right:clamp(16px,4vw,32px);transform:none}.wp-block-scroll-indicator-scroll-indicator.is-position-absolute{left:var(--scroll-indicator-x,50%);margin:0;position:absolute;top:var(--scroll-indicator-y,85%);transform:translate(-50%,-50%);width:auto;z-index:2}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator{align-items:center;background:transparent;border:0;color:inherit;cursor:pointer;display:inline-flex;flex-direction:column;font:inherit;gap:10px;max-width:100%;padding:0;text-align:center}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator:focus-visible{border-radius:8px;box-shadow:0 0 0 2px currentcolor;outline:2px solid transparent;outline-offset:4px}.wp-block-scroll-indicator-scroll-indicator .scroll-indicator svg{height:var(--scroll-indicator-size,24px);width:var(--scroll-indicator-size,24px)}.wp-block-scroll-indicator-scroll-indicator.is-position-absolute .scroll-indicator{cursor:grab;touch-action:none}.wp-block-scroll-indicator-scroll-indicator.is-position-absolute .scroll-indicator:active{cursor:grabbing}.wp-block-scroll-indicator-scroll-indicator .scroll-text{color:currentcolor;font-size:14px;font-weight:500;opacity:.8}.wp-block-cover:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute),.wp-block-group:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute){position:relative}.wp-block-cover:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute)>.wp-block-cover__inner-container{position:static}.wp-block-cover:has(.wp-block-scroll-indicator-scroll-indicator.is-position-absolute)>.wp-block-cover__inner-container>:not(.wp-block-scroll-indicator-scroll-indicator){position:relative;z-index:1}@media(prefers-reduced-motion:no-preference){.wp-block-scroll-indicator-scroll-indicator .icon-mouse svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-mouse .scroll-wheel{animation:scroll-indicator-wheel 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-chevron-bounce svg{animation:scroll-indicator-bounce 2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-arrow-down svg path:last-child{animation:scroll-indicator-fade-pulse 2s ease-in-out infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-1{animation:scroll-indicator-dot-pulse 1.4s ease-in-out 0s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-2{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .2s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-scroll-dots .dot-3{animation:scroll-indicator-dot-pulse 1.4s ease-in-out .4s infinite}.wp-block-scroll-indicator-scroll-indicator .icon-hand-point svg{animation:scroll-indicator-bounce 2s infinite}@keyframes scroll-indicator-bounce{0%,20%,50%,80%,to{transform:translateY(0)}40%{transform:translateY(-8px)}60%{transform:translateY(-4px)}}@keyframes scroll-indicator-wheel{0%,50%,to{opacity:1;transform:translateY(0)}25%{opacity:.5;transform:translateY(8px)}}@keyframes scroll-indicator-fade-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes scroll-indicator-dot-pulse{0%,to{opacity:.2}50%{opacity:1}}} diff --git a/compiled/view.asset.php b/compiled/view.asset.php new file mode 100644 index 0000000..faf9a1b --- /dev/null +++ b/compiled/view.asset.php @@ -0,0 +1 @@ + array(), 'version' => '2a6542ac716471a64c5c'); diff --git a/compiled/view.js b/compiled/view.js new file mode 100644 index 0000000..7e60c65 --- /dev/null +++ b/compiled/view.js @@ -0,0 +1 @@ +document.addEventListener("DOMContentLoaded",function(){const t=document.querySelectorAll(".scroll-indicator");if(0===t.length)return;const e=window.matchMedia("(prefers-reduced-motion: reduce)");t.forEach(function(t){const n=t.getAttribute("tabindex");let i=!1;t.addEventListener("click",function(){const t=window.innerHeight,n=window.scrollY+t;window.scrollTo({top:n,behavior:e.matches?"auto":"smooth"})}),t.style.cursor="pointer";const o=e.matches?"none":"opacity 0.45s ease, transform 0.45s ease, visibility 0s linear 0.45s",s=e.matches?"none":"opacity 0.35s ease, transform 0.35s ease, visibility 0s";function r(){var e;e=window.scrollY<=16,t.style.transition=e?s:o,t.style.opacity=e?"1":"0",t.style.visibility=e?"visible":"hidden",t.style.pointerEvents=e?"auto":"none",t.style.transform=e?"translateY(0)":"translateY(10px)",e?(t.removeAttribute("aria-hidden"),null===n?t.removeAttribute("tabindex"):t.setAttribute("tabindex",n)):(t.setAttribute("aria-hidden","true"),t.setAttribute("tabindex","-1"),t.ownerDocument.activeElement===t&&t.blur())}r(),window.addEventListener("scroll",function(){i||(i=!0,window.requestAnimationFrame(function(){r(),i=!1}))},{passive:!0}),"function"==typeof e.addEventListener&&e.addEventListener("change",function(){r()})})}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8f58469..78282ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "GPL-2.0-or-later", "devDependencies": { + "@wordpress/icons": "^13.1.0", "@wordpress/scripts": "^30.15.0" } }, @@ -6018,15 +6019,15 @@ } }, "node_modules/@wordpress/element": { - "version": "6.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.40.0.tgz", - "integrity": "sha512-OhU8B2xEGg7c41rh/VRiJLOz6TnM/r5r8sraAg5ISc2bF7s2oAFqLwvlR0/U6ervyYwbK644osWZGQxFyL3huA==", + "version": "6.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.46.0.tgz", + "integrity": "sha512-hjnrqZi0cZVdkmN0xQavKfSQJYAkb9pVSnDPpuX65OLxeD9/EWkIXvFzBb+nH8c4NzKKSqQU96XCTQrH37OCIA==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@types/react": "^18.3.27", "@types/react-dom": "^18.3.1", - "@wordpress/escape-html": "^3.40.0", + "@wordpress/escape-html": "^3.46.0", "change-case": "^4.1.2", "is-plain-object": "^5.0.0", "react": "^18.3.0", @@ -6038,9 +6039,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.40.0.tgz", - "integrity": "sha512-DD6xWVbnw4fGGgO6DFDTJiLj52om0OG4cYHLz7ZhuipmOlEUGljPYOcrj8uxtlh5EFrqHCIPkOya+qQXUHUSBw==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.46.0.tgz", + "integrity": "sha512-SzrVQwLQBZdaSStYVpTKeYqp97NABz1w551T8me3msDDsfhWWPhSZiZTNaGZ6iqUNfOX2uKyZsqXedvkqwLHqA==", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -6110,6 +6111,25 @@ "node": ">=10" } }, + "node_modules/@wordpress/icons": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-13.1.0.tgz", + "integrity": "sha512-KMZAeYghsLs6e5wKMZ3/Ynrsuu5yZt2gAlMHmZSkWJKQFld++Pz/pEj8nDCJ79z/zx9FO7q4teG49vHHvVosjQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/element": "^6.46.0", + "@wordpress/primitives": "^4.46.0", + "change-case": "4.1.2" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@wordpress/jest-console": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.40.0.tgz", @@ -6194,6 +6214,24 @@ "prettier": ">=3" } }, + "node_modules/@wordpress/primitives": { + "version": "4.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.46.0.tgz", + "integrity": "sha512-x1IhEVa/aGDe6otGJ4VIqEioQGfIeK5B1VQm32+ycqinJRbtbw9F5bgx4ARIdnm5M1Lg63oV9Bhmg/XMyGSTZA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/element": "^6.46.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@wordpress/private-apis": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.40.0.tgz", @@ -7978,6 +8016,16 @@ "node": ">=0.10.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", diff --git a/package.json b/package.json index 403a98e..6e43339 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,18 @@ "description": "An animated scroll indicator with multiple icon styles that encourages users to scroll down the page.", "author": "Derek Hanson", "license": "GPL-2.0-or-later", - "main": "build/index.js", + "main": "compiled/index.js", "scripts": { - "build": "wp-scripts build --webpack-copy-php", + "build": "wp-scripts build --webpack-copy-php --output-path=compiled", "format": "wp-scripts format", - "lint:css": "wp-scripts lint-style", - "lint:js": "wp-scripts lint-js", + "lint:css": "wp-scripts lint-style \"src/**/*.scss\"", + "lint:js": "wp-scripts lint-js \"src/**/*.js\"", "packages-update": "wp-scripts packages-update", - "plugin-zip": "wp-scripts plugin-zip", - "start": "wp-scripts start --blocks-manifest" + "plugin-zip": "npx pressship pack . --no-validate", + "start": "wp-scripts start --blocks-manifest --output-path=compiled" }, "devDependencies": { + "@wordpress/icons": "^13.1.0", "@wordpress/scripts": "^30.15.0" } } diff --git a/readme.txt b/readme.txt index a0672e7..b80426c 100644 --- a/readme.txt +++ b/readme.txt @@ -2,33 +2,38 @@ Contributors: derekhanson Tags: block, scroll, animation, indicator, gutenberg Requires at least: 6.4 -Tested up to: 6.9 +Tested up to: 7.0 Requires PHP: 7.4 Stable tag: 1.0.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html -Add a polished, animated scroll indicator block that encourages visitors to keep moving through your page. +Add an animated scroll indicator block that encourages visitors to keep moving through your page. == Description == -Scroll Indicator adds a small, expressive Gutenberg block for landing pages, hero sections, editorial layouts, and long-form content. It gives visitors a clear visual cue that there is more to explore below the fold. +Scroll Indicator adds a focused block for landing pages, hero sections, editorial layouts, and long-form content. It gives visitors a clear visual cue that there is more to explore below the fold. Choose from multiple icon styles, adjust the size, use native WordPress text color controls, add optional helper text, and let visitors click or keyboard-activate the indicator to move smoothly down the page. -The block is intentionally lightweight. Animations are handled with CSS, respect reduced-motion preferences, and the front-end script is limited to click-to-scroll and optional hide-after-scrolling behavior. +The block is intentionally lightweight. Animations are handled with CSS, respect reduced-motion preferences, and the front-end script is limited to click-to-scroll and automatic hide-on-scroll behavior. = Key Features = * Five icon styles: mouse, arrow, chevron, dots, and hand. * Preset sizes plus a custom CSS size value. * Native block editor controls for text color, spacing, typography, and alignment. +* Fixed screen positioning and draggable absolute positioning for hero-style sections. * CSS-only animation that respects the visitor's reduced-motion preference. * Optional text label beneath the icon. -* Optional hide-after-scrolling behavior. +* Automatic hide-on-scroll behavior. * Keyboard-accessible click-to-scroll interaction. * No animation libraries or heavy runtime dependencies. += Development = + +Source code and build tooling are maintained at https://github.com/dhanson-wp/scroll-indicator. + = Good For = * Landing page hero sections. @@ -48,7 +53,7 @@ The block is intentionally lightweight. Animations are handled with CSS, respect = Does this plugin require a JavaScript animation library? = -No. Animations are handled with CSS. The front-end script only powers click-to-scroll and the optional hide-after-scrolling behavior. +No. Animations are handled with CSS. The front-end script only powers click-to-scroll and automatic hide-on-scroll behavior. = Can I change the icon color? = @@ -64,7 +69,7 @@ Yes. The front-end indicator is saved as a keyboard-focusable button-like contro = Will the block work without JavaScript? = -The icon and optional text still render without JavaScript. JavaScript is only needed for click-to-scroll and hide-after-scrolling behavior. +The icon and optional text still render without JavaScript. JavaScript is only needed for click-to-scroll and automatic hide-on-scroll behavior. == Changelog == diff --git a/scroll-indicator.php b/scroll-indicator.php index b58a921..281cff1 100644 --- a/scroll-indicator.php +++ b/scroll-indicator.php @@ -19,7 +19,7 @@ if ( ! function_exists( 'scroll_indicator_init' ) ) { function scroll_indicator_init() { - register_block_type( __DIR__ . '/build/' ); + register_block_type( __DIR__ . '/compiled/' ); } } add_action( 'init', 'scroll_indicator_init' ); diff --git a/src/block.json b/src/block.json index 1696f72..ddde4f2 100644 --- a/src/block.json +++ b/src/block.json @@ -21,9 +21,12 @@ "type": "string", "default": "24px" }, + "align": { + "type": "string" + }, "hideAfterScrolling": { "type": "boolean", - "default": false + "default": true }, "showText": { "type": "boolean", @@ -32,13 +35,31 @@ "customText": { "type": "string", "default": "Scroll down" + }, + "positionMode": { + "type": "string", + "default": "flow" + }, + "screenPosition": { + "type": "string", + "default": "bottom-center" + }, + "absoluteX": { + "type": "number", + "default": 50 + }, + "absoluteY": { + "type": "number", + "default": 85 + }, + "flowAlign": { + "type": "string", + "default": "center" } }, "supports": { "html": false, - "align": [ "left", "center", "right" ], "color": { "text": true, "background": false }, - "typography": { "fontSize": true }, "spacing": { "margin": true, "padding": true } }, "textdomain": "scroll-indicator", diff --git a/src/edit.js b/src/edit.js index 396045a..0f5065f 100644 --- a/src/edit.js +++ b/src/edit.js @@ -1,15 +1,49 @@ import { __ } from '@wordpress/i18n'; -import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + useBlockProps, + InspectorControls, + BlockControls, + AlignmentToolbar, +} from '@wordpress/block-editor'; +import { useRef } from '@wordpress/element'; +import { + desktop, + dragHandle, + moveTo, + positionCenter, + positionLeft, + positionRight, +} from '@wordpress/icons'; import { PanelBody, - ToggleControl, TextControl, + ToggleControl, ButtonGroup, Button, + RangeControl, + ToolbarButton, + ToolbarGroup, } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; import './editor.scss'; -import { ICON_COMPONENTS, IconRenderer, getSizeValue } from './icons'; +import { + ICON_COMPONENTS, + IconRenderer, + getIconType, + getSizeValue, +} from './icons'; +import { + getPositionClassNames, + getPositionMode, + getPositionStyle, + getAbsoluteCoordinate, + getFlowAlignment, + getScreenPosition, + getAlignmentFromScreenPosition, + getScreenPositionFromAlignment, + getAlignmentClassName, +} from './position'; const ICON_OPTIONS = [ { value: 'mouse', label: __( 'Mouse', 'scroll-indicator' ) }, @@ -22,28 +56,234 @@ const ICON_OPTIONS = [ { value: 'hand-point', label: __( 'Hand', 'scroll-indicator' ) }, ]; -const SIZE_OPTIONS = [ 'S', 'M', 'L', 'XL', 'custom' ]; +const ICON_SIZE_OPTIONS = [ + { value: 'S', label: __( 'Small', 'scroll-indicator' ), text: 'S' }, + { value: 'M', label: __( 'Medium', 'scroll-indicator' ), text: 'M' }, + { value: 'L', label: __( 'Large', 'scroll-indicator' ), text: 'L' }, + { value: 'XL', label: __( 'Extra Large', 'scroll-indicator' ), text: 'XL' }, + { + value: 'custom', + label: __( 'Custom', 'scroll-indicator' ), + text: __( 'Custom', 'scroll-indicator' ), + }, +]; + +const CUSTOM_SIZE_MIN = 12; +const CUSTOM_SIZE_MAX = 96; +const CUSTOM_SIZE_DEFAULT = 24; + +const POSITION_MODE_OPTIONS = [ + { value: 'flow', label: __( 'Flow', 'scroll-indicator' ) }, + { value: 'fixed', label: __( 'Fixed', 'scroll-indicator' ) }, + { value: 'absolute', label: __( 'Absolute', 'scroll-indicator' ) }, +]; + +const FLOW_ALIGNMENT_OPTIONS = [ + { + value: 'left', + label: __( 'Align left', 'scroll-indicator' ), + icon: positionLeft, + }, + { + value: 'center', + label: __( 'Align center', 'scroll-indicator' ), + icon: positionCenter, + }, + { + value: 'right', + label: __( 'Align right', 'scroll-indicator' ), + icon: positionRight, + }, +]; + +const SCREEN_POSITION_OPTIONS = [ + { + value: 'bottom-left', + label: __( 'Bottom left', 'scroll-indicator' ), + icon: positionLeft, + }, + { + value: 'bottom-center', + label: __( 'Bottom center', 'scroll-indicator' ), + icon: positionCenter, + }, + { + value: 'bottom-right', + label: __( 'Bottom right', 'scroll-indicator' ), + icon: positionRight, + }, +]; -export default function Edit( { attributes, setAttributes } ) { +export default function Edit( { attributes, setAttributes, clientId } ) { const { iconType = 'mouse', iconSize = 'M', customSizeValue = '24px', - hideAfterScrolling = false, + align, showText = true, customText = 'Scroll down', + positionMode = 'flow', + screenPosition = 'bottom-center', + absoluteX = 50, + absoluteY = 85, + flowAlign = 'center', } = attributes; + const blockRef = useRef(); + const { selectBlock } = useDispatch( 'core/block-editor' ); + const normalizedIconType = getIconType( iconType ); const sizeValue = getSizeValue( iconSize, customSizeValue ); + const normalizedPositionMode = getPositionMode( positionMode ); + const hasCoreAlignment = [ 'left', 'center', 'right' ].includes( align ); + const normalizedScreenPosition = hasCoreAlignment + ? getScreenPositionFromAlignment( align ) + : getScreenPosition( screenPosition ); + const normalizedAbsoluteX = getAbsoluteCoordinate( absoluteX, 50 ); + const normalizedAbsoluteY = getAbsoluteCoordinate( absoluteY, 85 ); + const normalizedFlowAlign = getFlowAlignment( + hasCoreAlignment ? align : flowAlign + ); + const toolbarAlignment = hasCoreAlignment ? align : normalizedFlowAlign; + const customSizeNumber = getCustomSizeNumber( customSizeValue ); + + function getCustomSizeNumber( value ) { + const numericValue = + typeof value === 'number' ? value : Number.parseFloat( value ); + + if ( ! Number.isFinite( numericValue ) ) { + return CUSTOM_SIZE_DEFAULT; + } + + return Math.min( + CUSTOM_SIZE_MAX, + Math.max( CUSTOM_SIZE_MIN, numericValue ) + ); + } + + function setIconSize( nextIconSize ) { + const nextAttributes = { + iconSize: nextIconSize, + }; + + if ( nextIconSize === 'custom' ) { + nextAttributes.customSizeValue = `${ customSizeNumber }px`; + } + + setAttributes( nextAttributes ); + } + + function setAlignment( nextAlignment ) { + const alignment = getFlowAlignment( nextAlignment || 'center' ); + + if ( normalizedPositionMode === 'fixed' ) { + setAttributes( { + align: alignment, + screenPosition: getScreenPositionFromAlignment( alignment ), + } ); + return; + } + + setAttributes( { + align: alignment, + flowAlign: alignment, + } ); + } + + function updateAbsolutePosition( event ) { + const wrapper = blockRef.current; + const canvas = + wrapper?.closest( '.wp-block-cover' ) || wrapper?.parentElement; + + if ( ! canvas ) { + return; + } + + const canvasRect = canvas.getBoundingClientRect(); + + if ( canvasRect.width === 0 || canvasRect.height === 0 ) { + return; + } + + setAttributes( { + absoluteX: getAbsoluteCoordinate( + ( ( event.clientX - canvasRect.left ) / canvasRect.width ) * + 100, + 50 + ), + absoluteY: getAbsoluteCoordinate( + ( ( event.clientY - canvasRect.top ) / canvasRect.height ) * + 100, + 85 + ), + } ); + } + + function startAbsoluteDrag( event ) { + if ( normalizedPositionMode !== 'absolute' || event.button !== 0 ) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + selectBlock( clientId ); + updateAbsolutePosition( event ); + + const ownerDocument = event.currentTarget.ownerDocument; + const handlePointerMove = ( moveEvent ) => { + updateAbsolutePosition( moveEvent ); + }; + const handlePointerUp = () => { + ownerDocument.removeEventListener( + 'pointermove', + handlePointerMove + ); + ownerDocument.removeEventListener( 'pointerup', handlePointerUp ); + }; + + ownerDocument.addEventListener( 'pointermove', handlePointerMove ); + ownerDocument.addEventListener( 'pointerup', handlePointerUp ); + } const blockProps = useBlockProps( { + ref: blockRef, + className: getPositionClassNames( + normalizedPositionMode, + normalizedScreenPosition, + normalizedFlowAlign + ) + .concat( ' ', getAlignmentClassName( align ) ) + .trim(), style: { '--scroll-indicator-size': sizeValue, + ...getPositionStyle( + normalizedPositionMode, + normalizedAbsoluteX, + normalizedAbsoluteY + ), }, } ); return ( <> + + { normalizedPositionMode === 'absolute' ? ( + + selectBlock( clientId ) } + /> + + ) : ( + + ) } + - + + + ); } ) }
- { __( 'Size', 'scroll-indicator' ) } + + { __( 'Icon Size', 'scroll-indicator' ) } + - { SIZE_OPTIONS.map( ( size ) => ( + { ICON_SIZE_OPTIONS.map( ( option ) => ( ) ) } { iconSize === 'custom' && ( - + `${ value }px` + } onChange={ ( value ) => setAttributes( { - customSizeValue: value, + customSizeValue: `${ + value || CUSTOM_SIZE_DEFAULT + }px`, } ) } - help={ __( - 'Use a CSS size such as 24px, 2rem, or 3em.', - 'scroll-indicator' - ) } /> ) }
-
- { showText && ( setAttributes( { customText: value } ) @@ -139,28 +385,146 @@ export default function Edit( { attributes, setAttributes } ) { ) } - - setAttributes( { hideAfterScrolling: value } ) - } - /> +
+ + { __( 'Position Mode', 'scroll-indicator' ) } + + + { POSITION_MODE_OPTIONS.map( ( option ) => ( + + ) ) } + +
+ { normalizedPositionMode === 'flow' && ( +
+ + { __( 'Alignment', 'scroll-indicator' ) } + + + { FLOW_ALIGNMENT_OPTIONS.map( ( option ) => { + const AlignmentIcon = option.icon; + + return ( +
+ ) } + { normalizedPositionMode === 'fixed' && ( +
+ + { __( 'Screen Position', 'scroll-indicator' ) } + + + { SCREEN_POSITION_OPTIONS.map( ( option ) => { + const PositionIcon = option.icon; + + return ( +
+ ) } + { normalizedPositionMode === 'absolute' && ( +
+

+ + { dragHandle } + + { __( + 'Drag the indicator on the canvas, or fine-tune its position below.', + 'scroll-indicator' + ) } +

+ + setAttributes( { absoluteX: value } ) + } + /> + + setAttributes( { absoluteY: value } ) + } + /> +
+ ) } + { normalizedPositionMode === 'fixed' && ( +

+ + { desktop } + + { __( + "Fixed positioning pins the indicator to the visitor's screen.", + 'scroll-indicator' + ) } +

+ ) }
-
- +
+ { showText && (
{ customText || diff --git a/src/editor.scss b/src/editor.scss index b6b44c0..8fe6df8 100644 --- a/src/editor.scss +++ b/src/editor.scss @@ -1,4 +1,6 @@ -.scroll-indicator-icon-picker { +.scroll-indicator-icon-picker, +.scroll-indicator-size-picker, +.scroll-indicator-position-picker { border: 0; padding: 0; margin: 0 0 16px; @@ -29,18 +31,67 @@ } } -.scroll-indicator-size-picker { - border: 0; - padding: 0; - margin: 0; +.scroll-indicator-icon-preview { + align-items: center; + display: inline-flex; + height: 20px; + justify-content: center; + width: 20px; - legend { - font-weight: 500; - margin-bottom: 8px; + &.icon-mouse, + &.icon-arrow-down, + &.icon-chevron-bounce, + &.icon-hand-point { + + svg, + path, + rect, + line { + fill: none; + } } } -.scroll-indicator-size-buttons { - display: flex; +.scroll-indicator-mode-buttons { + display: grid; + gap: 4px; + grid-template-columns: repeat(3, minmax(0, 1fr)); margin-bottom: 12px; } + +.scroll-indicator-position-buttons { + display: flex; + gap: 4px; + + .components-button { + justify-content: center; + min-width: 44px; + } +} + +.scroll-indicator-absolute-controls, +.scroll-indicator-position-note { + color: #1e1e1e; + font-size: 12px; + line-height: 1.4; + margin: 0 0 12px; +} + +.scroll-indicator-control-icon { + display: inline-flex; + height: 18px; + margin-right: 6px; + vertical-align: middle; + width: 18px; +} + +.editor-styles-wrapper { + + .wp-block-scroll-indicator-scroll-indicator.is-position-absolute { + cursor: grab; + + &:active { + cursor: grabbing; + } + } +} diff --git a/src/icons.js b/src/icons.js index 12d2990..72fc2b6 100644 --- a/src/icons.js +++ b/src/icons.js @@ -1,8 +1,3 @@ -/** - * SVG icon components for the Scroll Indicator block. - * Shared between edit.js and save.js. - */ - export const SIZE_MAP = { S: '18px', M: '24px', @@ -10,9 +5,14 @@ export const SIZE_MAP = { XL: '48px', }; +const CSS_SIZE_PATTERN = /^(?:\d+|\d*\.\d+)(?:px|em|rem|vh|vw|vmin|vmax|%)$/; + export function getSizeValue( iconSize, customSizeValue ) { if ( iconSize === 'custom' ) { - return customSizeValue || '24px'; + const sizeValue = + typeof customSizeValue === 'string' ? customSizeValue.trim() : ''; + + return CSS_SIZE_PATTERN.test( sizeValue ) ? sizeValue : '24px'; } return SIZE_MAP[ iconSize ] || '24px'; } @@ -116,7 +116,13 @@ export const ICON_COMPONENTS = { 'hand-point': HandPointIcon, }; +export function getIconType( iconType ) { + return Object.prototype.hasOwnProperty.call( ICON_COMPONENTS, iconType ) + ? iconType + : 'mouse'; +} + export function IconRenderer( { iconType } ) { - const Component = ICON_COMPONENTS[ iconType ] || MouseIcon; + const Component = ICON_COMPONENTS[ getIconType( iconType ) ]; return ; } diff --git a/src/index.js b/src/index.js index ade1e47..dc3fa61 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,13 @@ -/** - * Registers a new block provided a unique name and an object defining its behavior. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ - */ import { registerBlockType } from '@wordpress/blocks'; - -/** - * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. - * All files containing `style` keyword are bundled together. The code used - * gets applied both to the front of your site and to the editor. - * - * @see https://www.npmjs.com/package/@wordpress/scripts#using-css - */ +import { arrowDown } from '@wordpress/icons'; import './style.scss'; -/** - * Internal dependencies - */ import Edit from './edit'; import save from './save'; import metadata from './block.json'; -/** - * Every block starts by registering a new block type definition. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ - */ registerBlockType( metadata.name, { - /** - * @see ./edit.js - */ + icon: arrowDown, edit: Edit, - - /** - * @see ./save.js - */ save, } ); diff --git a/src/position.js b/src/position.js new file mode 100644 index 0000000..0f5184c --- /dev/null +++ b/src/position.js @@ -0,0 +1,114 @@ +const POSITION_MODE_MAP = { + flow: 'flow', + fixed: 'fixed', + absolute: 'absolute', +}; + +const SCREEN_POSITION_MAP = { + 'bottom-left': 'bottom-left', + 'bottom-center': 'bottom-center', + 'bottom-right': 'bottom-right', +}; + +const FLOW_ALIGNMENT_MAP = { + left: 'left', + center: 'center', + right: 'right', +}; + +const ALIGNMENT_TO_SCREEN_POSITION = { + left: 'bottom-left', + center: 'bottom-center', + right: 'bottom-right', +}; + +const SCREEN_POSITION_TO_ALIGNMENT = { + 'bottom-left': 'left', + 'bottom-center': 'center', + 'bottom-right': 'right', +}; + +export function getPositionMode( positionMode ) { + return POSITION_MODE_MAP[ positionMode ] || POSITION_MODE_MAP.flow; +} + +export function getScreenPosition( screenPosition ) { + return ( + SCREEN_POSITION_MAP[ screenPosition ] || + SCREEN_POSITION_MAP[ 'bottom-center' ] + ); +} + +export function getFlowAlignment( flowAlign ) { + return FLOW_ALIGNMENT_MAP[ flowAlign ] || FLOW_ALIGNMENT_MAP.center; +} + +export function getAlignmentFromScreenPosition( screenPosition ) { + return ( + SCREEN_POSITION_TO_ALIGNMENT[ getScreenPosition( screenPosition ) ] || + FLOW_ALIGNMENT_MAP.center + ); +} + +export function getScreenPositionFromAlignment( alignment ) { + return ( + ALIGNMENT_TO_SCREEN_POSITION[ getFlowAlignment( alignment ) ] || + SCREEN_POSITION_MAP[ 'bottom-center' ] + ); +} + +export function getAlignmentClassName( alignment ) { + const normalizedAlignment = FLOW_ALIGNMENT_MAP[ alignment ]; + + return normalizedAlignment ? `align${ normalizedAlignment }` : ''; +} + +export function getAbsoluteCoordinate( coordinate, fallback ) { + const numericCoordinate = + typeof coordinate === 'number' + ? coordinate + : Number.parseFloat( coordinate ); + + if ( ! Number.isFinite( numericCoordinate ) ) { + return fallback; + } + + return Math.min( 100, Math.max( 0, numericCoordinate ) ); +} + +export function getPositionClassNames( + positionMode, + screenPosition, + flowAlign = 'center' +) { + const normalizedPositionMode = getPositionMode( positionMode ); + + if ( normalizedPositionMode === 'fixed' ) { + return `is-position-fixed is-screen-position-${ getScreenPosition( + screenPosition + ) }`; + } + + if ( normalizedPositionMode === 'absolute' ) { + return 'is-position-absolute'; + } + + const normalizedFlowAlign = getFlowAlignment( flowAlign ); + + if ( normalizedFlowAlign !== 'center' ) { + return `is-flow-align-${ normalizedFlowAlign }`; + } + + return ''; +} + +export function getPositionStyle( positionMode, absoluteX, absoluteY ) { + if ( getPositionMode( positionMode ) !== 'absolute' ) { + return {}; + } + + return { + '--scroll-indicator-x': `${ getAbsoluteCoordinate( absoluteX, 50 ) }%`, + '--scroll-indicator-y': `${ getAbsoluteCoordinate( absoluteY, 85 ) }%`, + }; +} diff --git a/src/save.js b/src/save.js index 6536f56..dc42eaa 100644 --- a/src/save.js +++ b/src/save.js @@ -1,42 +1,65 @@ import { useBlockProps } from '@wordpress/block-editor'; -import { IconRenderer, getSizeValue } from './icons'; +import { IconRenderer, getIconType, getSizeValue } from './icons'; +import { + getPositionClassNames, + getPositionStyle, + getFlowAlignment, + getScreenPosition, + getScreenPositionFromAlignment, + getAlignmentClassName, +} from './position'; export default function save( { attributes } ) { const { iconType = 'mouse', iconSize = 'M', customSizeValue = '24px', - hideAfterScrolling = false, + align, showText = true, customText = 'Scroll down', + positionMode = 'flow', + screenPosition = 'bottom-center', + absoluteX = 50, + absoluteY = 85, + flowAlign = 'center', } = attributes; + const normalizedIconType = getIconType( iconType ); const sizeValue = getSizeValue( iconSize, customSizeValue ); + const label = customText || 'Scroll down'; + const hasCoreAlignment = [ 'left', 'center', 'right' ].includes( align ); + const normalizedScreenPosition = hasCoreAlignment + ? getScreenPositionFromAlignment( align ) + : getScreenPosition( screenPosition ); + const normalizedFlowAlign = getFlowAlignment( + hasCoreAlignment ? align : flowAlign + ); const blockProps = useBlockProps.save( { + className: getPositionClassNames( + positionMode, + normalizedScreenPosition, + normalizedFlowAlign + ) + .concat( ' ', getAlignmentClassName( align ) ) + .trim(), style: { '--scroll-indicator-size': sizeValue, + ...getPositionStyle( positionMode, absoluteX, absoluteY ), }, } ); return (
-
- - { showText && ( -
- { customText || 'Scroll down' } -
- ) } -
+ + { showText &&
{ label }
} +
); } diff --git a/src/style.scss b/src/style.scss index cf51547..00b9259 100644 --- a/src/style.scss +++ b/src/style.scss @@ -1,12 +1,113 @@ .wp-block-scroll-indicator-scroll-indicator { + box-sizing: border-box; + display: flex; + justify-content: center; + max-width: 100%; + width: 100%; + + &.alignleft, + &.aligncenter, + &.alignright { + float: none; + } + + &.alignleft { + justify-content: flex-start; + } + + &.aligncenter { + justify-content: center; + } + + &.alignright { + justify-content: flex-end; + } + + &.is-flow-align-left { + justify-content: flex-start; + } + + &.is-flow-align-right { + justify-content: flex-end; + } + + .is-layout-constrained > &:not(.is-position-fixed):not(.is-position-absolute) { + float: none !important; + margin-inline-start: auto !important; + margin-inline-end: auto !important; + margin-left: auto !important; + margin-right: auto !important; + max-width: var(--wp--style--global--content-size); + width: 100%; + } + + .editor-styles-wrapper .is-layout-constrained > &:not(.is-position-fixed):not(.is-position-absolute) { + float: none !important; + margin-inline-start: auto !important; + margin-inline-end: auto !important; + margin-left: auto !important; + margin-right: auto !important; + } + + &.is-position-fixed { + bottom: clamp(16px, 4vw, 32px); + left: 50%; + margin: 0; + position: fixed; + right: auto; + transform: translateX(-50%); + width: auto; + z-index: 1000; + } + + &.is-position-fixed.is-screen-position-bottom-left { + left: clamp(16px, 4vw, 32px); + right: auto; + transform: none; + } + + &.is-position-fixed.is-screen-position-bottom-center { + left: 50%; + right: auto; + transform: translateX(-50%); + } + + &.is-position-fixed.is-screen-position-bottom-right { + left: auto; + right: clamp(16px, 4vw, 32px); + transform: none; + } + + &.is-position-absolute { + left: var(--scroll-indicator-x, 50%); + margin: 0; + position: absolute; + top: var(--scroll-indicator-y, 85%); + transform: translate(-50%, -50%); + width: auto; + z-index: 2; + } .scroll-indicator { - display: flex; - flex-direction: column; align-items: center; - gap: 10px; - padding: 20px; + background: transparent; + border: 0; + color: inherit; cursor: pointer; + display: inline-flex; + flex-direction: column; + font: inherit; + gap: 10px; + max-width: 100%; + padding: 0; + text-align: center; + + &:focus-visible { + border-radius: 8px; + box-shadow: 0 0 0 2px currentcolor; + outline: 2px solid transparent; + outline-offset: 4px; + } svg { width: var(--scroll-indicator-size, 24px); @@ -14,6 +115,15 @@ } } + &.is-position-absolute .scroll-indicator { + cursor: grab; + touch-action: none; + } + + &.is-position-absolute .scroll-indicator:active { + cursor: grabbing; + } + .scroll-text { font-size: 14px; color: currentcolor; @@ -22,6 +132,29 @@ } } +.wp-block-cover:has( +.wp-block-scroll-indicator-scroll-indicator.is-position-absolute +), +.wp-block-group:has( +.wp-block-scroll-indicator-scroll-indicator.is-position-absolute +) { + position: relative; +} + +.wp-block-cover:has( +.wp-block-scroll-indicator-scroll-indicator.is-position-absolute +) { + + > .wp-block-cover__inner-container { + position: static; + + > :not(.wp-block-scroll-indicator-scroll-indicator) { + position: relative; + z-index: 1; + } + } +} + // Animations — only when user hasn't requested reduced motion @media (prefers-reduced-motion: no-preference) { diff --git a/src/view.js b/src/view.js index 5ec9514..26adfca 100644 --- a/src/view.js +++ b/src/view.js @@ -5,12 +5,14 @@ document.addEventListener( 'DOMContentLoaded', function () { return; } + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ); + scrollIndicators.forEach( function ( indicator ) { - const hideAfterScrolling = - indicator.getAttribute( 'data-hide-after-scrolling' ) === 'true'; - const blockElement = indicator.closest( - '.wp-block-scroll-indicator-scroll-indicator' - ); + const originalTabIndex = indicator.getAttribute( 'tabindex' ); + const hideThreshold = 16; + let ticking = false; function scrollOneViewport() { const windowHeight = window.innerHeight; @@ -18,56 +20,81 @@ document.addEventListener( 'DOMContentLoaded', function () { window.scrollTo( { top: targetY, - behavior: 'smooth', + behavior: prefersReducedMotion.matches ? 'auto' : 'smooth', } ); } indicator.addEventListener( 'click', scrollOneViewport ); - indicator.addEventListener( 'keydown', function ( event ) { - if ( event.key !== 'Enter' && event.key !== ' ' ) { - return; - } - - event.preventDefault(); - scrollOneViewport(); - } ); - indicator.style.cursor = 'pointer'; - if ( hideAfterScrolling && blockElement ) { - let blockTop = blockElement.offsetTop; - let blockBottom = blockTop + blockElement.offsetHeight; + const hiddenTransition = prefersReducedMotion.matches + ? 'none' + : 'opacity 0.45s ease, transform 0.45s ease, visibility 0s linear 0.45s'; + const visibleTransition = prefersReducedMotion.matches + ? 'none' + : 'opacity 0.35s ease, transform 0.35s ease, visibility 0s'; - function updateBlockPosition() { - blockTop = blockElement.offsetTop; - blockBottom = blockTop + blockElement.offsetHeight; - } - - window.addEventListener( 'resize', updateBlockPosition ); + function setIndicatorVisibility( isVisible ) { + indicator.style.transition = isVisible + ? visibleTransition + : hiddenTransition; + indicator.style.opacity = isVisible ? '1' : '0'; + indicator.style.visibility = isVisible ? 'visible' : 'hidden'; + indicator.style.pointerEvents = isVisible ? 'auto' : 'none'; + indicator.style.transform = isVisible + ? 'translateY(0)' + : 'translateY(10px)'; - function handleScroll() { - const scrollTop = window.scrollY; - const viewportHeight = window.innerHeight; - const viewportBottom = scrollTop + viewportHeight; + if ( isVisible ) { + indicator.removeAttribute( 'aria-hidden' ); - if ( - scrollTop <= blockTop || - ( blockTop < viewportBottom && blockBottom > scrollTop ) - ) { - indicator.style.transition = 'opacity 0.3s ease-in'; - indicator.style.opacity = '1'; + if ( originalTabIndex === null ) { + indicator.removeAttribute( 'tabindex' ); } else { - indicator.style.transition = 'opacity 0.3s ease-out'; - indicator.style.opacity = '0'; + indicator.setAttribute( 'tabindex', originalTabIndex ); + } + } else { + indicator.setAttribute( 'aria-hidden', 'true' ); + indicator.setAttribute( 'tabindex', '-1' ); + + if ( indicator.ownerDocument.activeElement === indicator ) { + indicator.blur(); } } + } - handleScroll(); + function handleScroll() { + setIndicatorVisibility( window.scrollY <= hideThreshold ); + } + + function requestVisibilityUpdate() { + if ( ticking ) { + return; + } - window.addEventListener( 'scroll', handleScroll, { - passive: true, + ticking = true; + window.requestAnimationFrame( function () { + handleScroll(); + ticking = false; } ); } + + function handleReducedMotionChange() { + handleScroll(); + } + + handleScroll(); + + window.addEventListener( 'scroll', requestVisibilityUpdate, { + passive: true, + } ); + + if ( typeof prefersReducedMotion.addEventListener === 'function' ) { + prefersReducedMotion.addEventListener( + 'change', + handleReducedMotionChange + ); + } } ); } );