diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8689e20..9ebdd38 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a Button Icon selector panel for core Button blocks, including left/right positioning and up/down icon options.
- Added a Back to Top option as a `core/button` variation so users inherit native Button styling controls and icon compatibility.
- Added smooth scrolling support for Back to Top button clicks and internal anchor links using vanilla JavaScript.
+- Added sticky positioning mode for Back to Top button with scroll-based visibility control.
+- Added inspector controls for Back to Top button with position mode selection (Inline/Scroll, Sticky, Fixed) and configurable visibility threshold (0-100%).
- Linkable Group Blocks support for `core/group`, `core/column`, and `core/cover`, including custom URLs and current-post linking from the block toolbar.
- Added plugin-managed SCF Local JSON handling and validation utilities for field groups, post types, and taxonomies.
- Added an SCF field-group schema for repository validation workflows.
diff --git a/build/css/style-back-to-top-rtl.css b/build/css/style-back-to-top-rtl.css
index 694df9a..48e2e3f 100644
--- a/build/css/style-back-to-top-rtl.css
+++ b/build/css/style-back-to-top-rtl.css
@@ -1 +1 @@
-.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;position:fixed;left:2rem;z-index:40}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;left:1rem}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}}
+.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;opacity:0;position:fixed;left:2rem;transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;left:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{transition:none}}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:2rem;right:50%;opacity:0;position:fixed;transform:translateX(50%);transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{transition:none}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}}
diff --git a/build/css/style-back-to-top.css b/build/css/style-back-to-top.css
index 35ecbab..b40c754 100644
--- a/build/css/style-back-to-top.css
+++ b/build/css/style-back-to-top.css
@@ -1 +1 @@
-.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;position:fixed;right:2rem;z-index:40}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;right:1rem}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}}
+.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;opacity:0;position:fixed;right:2rem;transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;right:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{transition:none}}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:2rem;left:50%;opacity:0;position:fixed;transform:translateX(-50%);transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{transition:none}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}}
diff --git a/build/js/back-to-top-view.asset.php b/build/js/back-to-top-view.asset.php
index e436f46..6bf11ff 100644
--- a/build/js/back-to-top-view.asset.php
+++ b/build/js/back-to-top-view.asset.php
@@ -1 +1 @@
- array(), 'version' => '84a35a129b8cf8105805');
+ array(), 'version' => '2f878e4e9811d7262c21');
diff --git a/build/js/back-to-top-view.js b/build/js/back-to-top-view.js
index a5110dd..a340be3 100644
--- a/build/js/back-to-top-view.js
+++ b/build/js/back-to-top-view.js
@@ -1 +1 @@
-(()=>{"use strict";!function(){const t=window.matchMedia("(prefers-reduced-motion: reduce)").matches,e=(e,n=600)=>{if(t)return void("number"==typeof e?window.scrollTo(0,e):e instanceof HTMLElement&&e.scrollIntoView());const o=window.scrollY,r=("number"==typeof e?e:e.getBoundingClientRect().top+o-(()=>{const t=document.querySelector('header[sticky="true"], [data-sticky="true"], .is-sticky');if(!t)return 0;const e=t.getBoundingClientRect();return Math.max(0,e.height+20)})())-o,c=performance.now(),i=t=>{const e=t-c,a=Math.min(e/n,1),s=o+r*(t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(a);window.scrollTo(0,s),a<1&&requestAnimationFrame(i)};requestAnimationFrame(i)};var n;n=()=>{document.addEventListener("click",t=>{const n=t.target.closest('a[href*="#"]');if(!n||n.hostname!==window.location.hostname||n.pathname!==window.location.pathname)return;const o=n.getAttribute("href"),r=o.substring(o.indexOf("#"));if("#"===r||""===r)return;const c=document.querySelector(r);c&&(t.preventDefault(),e(c),window.history.pushState(null,"",r))}),document.querySelectorAll(".wp-block-button.is-back-to-top").forEach(n=>{const o=n.querySelector(".wp-block-button__link");o&&o.addEventListener("click",n=>{n.preventDefault(),e(0,t?0:600)})})},"loading"===document.readyState||"interactive"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}()})();
\ No newline at end of file
+(()=>{"use strict";!function(){const t=window.matchMedia("(prefers-reduced-motion: reduce)").matches,e=(e,n=600)=>{if(t)return void("number"==typeof e?window.scrollTo(0,e):e instanceof HTMLElement&&e.scrollIntoView());const o=window.scrollY,r=("number"==typeof e?e:e.getBoundingClientRect().top+o-(()=>{const t=document.querySelector('header[sticky="true"], [data-sticky="true"], .is-sticky');if(!t)return 0;const e=t.getBoundingClientRect();return Math.max(0,e.height+20)})())-o,i=performance.now(),c=t=>{const e=t-i,s=Math.min(e/n,1),a=o+r*(t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(s);window.scrollTo(0,a),s<1&&requestAnimationFrame(c)};requestAnimationFrame(c)};var n;n=()=>{document.addEventListener("click",t=>{const n=t.target.closest('a[href*="#"]');if(!n||n.hostname!==window.location.hostname||n.pathname!==window.location.pathname)return;const o=n.getAttribute("href"),r=o.substring(o.indexOf("#"));if("#"===r||""===r)return;const i=document.querySelector(r);i&&(t.preventDefault(),e(i),window.history.pushState(null,"",r))}),(()=>{const n=document.querySelectorAll(".wp-block-button.is-back-to-top"),o=[];if(n.forEach(n=>{const r=n.querySelector(".wp-block-button__link");if(!r)return;r.addEventListener("click",n=>{n.preventDefault(),e(0,t?0:600)});const i=n.getAttribute("data-back-to-top-mode");if("sticky"===i||"fixed"===i){const t=parseInt(n.getAttribute("data-scroll-threshold")||"75",10);o.push({wrapper:n,thresholdPercent:t})}}),o.length>0){const t=()=>{const t=window.scrollY,e=window.innerHeight,n=document.documentElement.scrollHeight-e;o.forEach(({wrapper:e,thresholdPercent:o})=>{t>=n*(o/100)?e.classList.add("is-visible"):e.classList.remove("is-visible")})};let e=!1;window.addEventListener("scroll",()=>{e||(window.requestAnimationFrame(()=>{t(),e=!1}),e=!0)}),t()}})()},"loading"===document.readyState||"interactive"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}()})();
\ No newline at end of file
diff --git a/build/js/back-to-top.asset.php b/build/js/back-to-top.asset.php
index e1920d0..2813d14 100644
--- a/build/js/back-to-top.asset.php
+++ b/build/js/back-to-top.asset.php
@@ -1 +1 @@
- array('wp-blocks', 'wp-hooks', 'wp-i18n'), 'version' => '9d676336fdab1bdceaa8');
+ array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-hooks', 'wp-i18n'), 'version' => 'fea66ba289b98b6e8f9d');
diff --git a/build/js/back-to-top.js b/build/js/back-to-top.js
index ebf3889..6f4ffa4 100644
--- a/build/js/back-to-top.js
+++ b/build/js/back-to-top.js
@@ -1 +1,2 @@
-(()=>{"use strict";const o=window.wp.hooks,t=window.wp.blocks,e=window.wp.i18n;(0,t.registerBlockVariation)("core/button",{name:"back-to-top",title:(0,e.__)("Back to Top","ls-plugin"),icon:"arrow-up",description:(0,e.__)("A button that scrolls to the top of the page with smooth animation.","ls-plugin"),attributes:{text:(0,e.__)("Back to Top","ls-plugin"),isBackToTop:!0,backToTopPositionMode:"scroll",backToTopScrollThreshold:50},isActive:o=>!0===o.isBackToTop}),(0,o.addFilter)("blocks.registerBlockType","ls-plugin/add-back-to-top-attributes",o=>"core/button"!==o.name?o:{...o,attributes:{...o.attributes,isBackToTop:{type:"boolean",default:!1},backToTopPositionMode:{type:"string",default:"scroll"},backToTopScrollThreshold:{type:"number",default:50}}}),(0,o.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/back-to-top-save-props",(o,t,e)=>{if("core/button"!==t.name||!e.isBackToTop)return o;const a=[o.className,"is-back-to-top"].filter(Boolean).join(" ");return{...o,className:a,"data-back-to-top-mode":e.backToTopPositionMode||"scroll"}})})();
\ No newline at end of file
+(()=>{"use strict";const o=window.wp.hooks,t=window.wp.blocks,e=window.wp.i18n,l=window.wp.blockEditor,i=window.wp.components,s=window.wp.compose,n=window.ReactJSXRuntime;(0,t.registerBlockVariation)("core/button",{name:"back-to-top",title:(0,e.__)("Back to Top","ls-plugin"),icon:"arrow-up",description:(0,e.__)("A button that scrolls to the top of the page with smooth animation.","ls-plugin"),attributes:{text:(0,e.__)("Back to Top","ls-plugin"),isBackToTop:!0,backToTopPositionMode:"scroll",backToTopScrollThreshold:50},isActive:o=>!0===o.isBackToTop}),(0,o.addFilter)("blocks.registerBlockType","ls-plugin/add-back-to-top-attributes",o=>"core/button"!==o.name?o:{...o,attributes:{...o.attributes,isBackToTop:{type:"boolean",default:!1},backToTopPositionMode:{type:"string",default:"scroll"},backToTopScrollThreshold:{type:"number",default:50}}});const a=(0,s.createHigherOrderComponent)(o=>t=>{const{attributes:s,setAttributes:a,name:c}=t;return"core/button"===c&&s.isBackToTop?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o,{...t}),(0,n.jsx)(l.InspectorControls,{children:(0,n.jsxs)(i.PanelBody,{title:(0,e.__)("Back to Top Settings","ls-plugin"),initialOpen:!0,children:[(0,n.jsx)(i.SelectControl,{label:(0,e.__)("Position Mode","ls-plugin"),value:s.backToTopPositionMode||"scroll",options:[{label:(0,e.__)("Inline (Scroll)","ls-plugin"),value:"scroll"},{label:(0,e.__)("Sticky (Fixed, Center)","ls-plugin"),value:"sticky"},{label:(0,e.__)("Fixed (Bottom Right)","ls-plugin"),value:"fixed"}],onChange:o=>a({backToTopPositionMode:o}),help:(0,e.__)("Choose how the button is positioned. Sticky and Fixed modes support scroll-based visibility.","ls-plugin")}),("sticky"===s.backToTopPositionMode||"fixed"===s.backToTopPositionMode)&&(0,n.jsx)(i.RangeControl,{label:(0,e.__)("Visibility Threshold (%)","ls-plugin"),value:s.backToTopScrollThreshold??75,onChange:o=>a({backToTopScrollThreshold:o}),min:0,max:100,step:5,help:(0,e.sprintf)(/* translators: %d: threshold percentage */ /* translators: %d: threshold percentage */
+(0,e.__)("Button appears after scrolling %d%% of the page.","ls-plugin"),s.backToTopScrollThreshold??75)})]})})]}):(0,n.jsx)(o,{...t})},"withBackToTopControls");(0,o.addFilter)("editor.BlockEdit","ls-plugin/with-back-to-top-controls",a),(0,o.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/back-to-top-save-props",(o,t,e)=>{if("core/button"!==t.name||!e.isBackToTop)return o;const l=[o.className,"is-back-to-top"].filter(Boolean).join(" "),i={...o,className:l,"data-back-to-top-mode":e.backToTopPositionMode||"scroll"};return"sticky"!==e.backToTopPositionMode&&"fixed"!==e.backToTopPositionMode||(i["data-scroll-threshold"]=e.backToTopScrollThreshold??75),i})})();
\ No newline at end of file
diff --git a/src/plugins/back-to-top/index.js b/src/plugins/back-to-top/index.js
index 53cdcef..c054f83 100644
--- a/src/plugins/back-to-top/index.js
+++ b/src/plugins/back-to-top/index.js
@@ -5,7 +5,10 @@
import { addFilter } from '@wordpress/hooks';
import { registerBlockVariation } from '@wordpress/blocks';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
+import { InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, SelectControl, RangeControl } from '@wordpress/components';
+import { createHigherOrderComponent } from '@wordpress/compose';
/**
* Register Back to Top as a core/button variation
@@ -60,9 +63,82 @@ addFilter(
);
/**
- * Add back-to-top inspector controls and editor classes
- * We don't need to override BlockEdit - the attributes filter handles everything
+ * Add back-to-top inspector controls
*/
+const withBackToTopControls = createHigherOrderComponent( ( BlockEdit ) => {
+ return ( props ) => {
+ const { attributes, setAttributes, name } = props;
+
+ // Only apply to core/button blocks with isBackToTop enabled
+ if ( name !== 'core/button' || ! attributes.isBackToTop ) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+ setAttributes( { backToTopPositionMode: value } )
+ }
+ help={ __(
+ 'Choose how the button is positioned. Sticky and Fixed modes support scroll-based visibility.',
+ 'ls-plugin'
+ ) }
+ />
+ { ( attributes.backToTopPositionMode === 'sticky' || attributes.backToTopPositionMode === 'fixed' ) && (
+
+ setAttributes( { backToTopScrollThreshold: value } )
+ }
+ min={ 0 }
+ max={ 100 }
+ step={ 5 }
+ help={ sprintf(
+ /* translators: %d: threshold percentage */
+ __(
+ 'Button appears after scrolling %d%% of the page.',
+ 'ls-plugin'
+ ),
+ attributes.backToTopScrollThreshold ?? 75
+ ) }
+ />
+ ) }
+
+
+ >
+ );
+ };
+}, 'withBackToTopControls' );
+
+addFilter(
+ 'editor.BlockEdit',
+ 'ls-plugin/with-back-to-top-controls',
+ withBackToTopControls
+);
/**
* Add back-to-top data attributes to saved button markup
@@ -79,10 +155,17 @@ addFilter(
.filter( Boolean )
.join( ' ' );
- return {
+ const props = {
...extraProps,
className: classes,
'data-back-to-top-mode': attributes.backToTopPositionMode || 'scroll',
};
+
+ // Add scroll threshold for sticky and fixed modes
+ if ( attributes.backToTopPositionMode === 'sticky' || attributes.backToTopPositionMode === 'fixed' ) {
+ props[ 'data-scroll-threshold' ] = attributes.backToTopScrollThreshold ?? 75;
+ }
+
+ return props;
}
);
diff --git a/src/plugins/back-to-top/style.css b/src/plugins/back-to-top/style.css
index 3cd0049..d6159ad 100644
--- a/src/plugins/back-to-top/style.css
+++ b/src/plugins/back-to-top/style.css
@@ -5,7 +5,8 @@
*/
/* Back to Top button specific positioning */
.wp-block-button.is-back-to-top {
- /* Fixed mode (sticky to footer) */
+ /* Fixed mode (bottom right, appears on scroll) */
+ /* Sticky mode (centered above footer, appears on scroll) */
/* Smooth transitions for hover state */
}
.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] {
@@ -13,6 +14,13 @@
bottom: 2rem;
right: 2rem;
z-index: 40;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
+}
+.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed].is-visible {
+ opacity: 1;
+ visibility: visible;
}
@media (max-width: 640px) {
.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] {
@@ -20,6 +28,35 @@
right: 1rem;
}
}
+@media (prefers-reduced-motion: reduce) {
+ .wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] {
+ transition: none;
+ }
+}
+.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky] {
+ position: fixed;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 40;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
+}
+.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky].is-visible {
+ opacity: 1;
+ visibility: visible;
+}
+@media (max-width: 640px) {
+ .wp-block-button.is-back-to-top[data-back-to-top-mode=sticky] {
+ bottom: 1rem;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .wp-block-button.is-back-to-top[data-back-to-top-mode=sticky] {
+ transition: none;
+ }
+}
.wp-block-button.is-back-to-top .wp-block-button__link {
transition: all 0.2s ease-in-out;
}
diff --git a/src/plugins/back-to-top/style.css.map b/src/plugins/back-to-top/style.css.map
index 9d3620c..24b282e 100644
--- a/src/plugins/back-to-top/style.css.map
+++ b/src/plugins/back-to-top/style.css.map
@@ -1 +1 @@
-{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA;;;;EAAA;AAMA,4CAAA;AACA;EACC,kCAAA;EAaA,uCAAA;ACZD;ADAC;EACC,eAAA;EACA,YAAA;EACA,WAAA;EACA,WAAA;ACEF;ADAE;EAND;IAOE,YAAA;IACA,WAAA;ECGD;AACF;ADCC;EACC,gCAAA;ACCF;ADCE;EACC,2BAAA;EACA,gFAAA;ACCH;ADGE;EACC,wBAAA;ACDH;ADIE;EAbD;IAcE,gBAAA;ECDD;EDGC;IACC,eAAA;ECDF;AACF","file":"style.css"}
\ No newline at end of file
+{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA;;;;EAAA;AAMA,4CAAA;AACA;EACC,iDAAA;EAyBA,2DAAA;EAyBA,uCAAA;AChDD;ADDC;EACC,eAAA;EACA,YAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;EACA,kBAAA;EACA,iEAAA;ACGF;ADDE;EACC,UAAA;EACA,mBAAA;ACGH;ADAE;EAdD;IAeE,YAAA;IACA,WAAA;ECGD;AACF;ADDE;EAnBD;IAoBE,gBAAA;ECID;AACF;ADAC;EACC,eAAA;EACA,YAAA;EACA,SAAA;EACA,2BAAA;EACA,WAAA;EACA,UAAA;EACA,kBAAA;EACA,iEAAA;ACEF;ADAE;EACC,UAAA;EACA,mBAAA;ACEH;ADCE;EAfD;IAgBE,YAAA;ECED;AACF;ADAE;EAnBD;IAoBE,gBAAA;ECGD;AACF;ADCC;EACC,gCAAA;ACCF;ADCE;EACC,2BAAA;EACA,gFAAA;ACCH;ADGE;EACC,wBAAA;ACDH;ADIE;EAbD;IAcE,gBAAA;ECDD;EDGC;IACC,eAAA;ECDF;AACF","file":"style.css"}
\ No newline at end of file
diff --git a/src/plugins/back-to-top/style.scss b/src/plugins/back-to-top/style.scss
index c751b07..8fbdaad 100644
--- a/src/plugins/back-to-top/style.scss
+++ b/src/plugins/back-to-top/style.scss
@@ -6,17 +6,54 @@
/* Back to Top button specific positioning */
.wp-block-button.is-back-to-top {
- /* Fixed mode (sticky to footer) */
+ /* Fixed mode (bottom right, appears on scroll) */
&[data-back-to-top-mode="fixed"] {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 40;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
+
+ &.is-visible {
+ opacity: 1;
+ visibility: visible;
+ }
@media ( max-width: 640px ) {
bottom: 1rem;
right: 1rem;
}
+
+ @media ( prefers-reduced-motion: reduce ) {
+ transition: none;
+ }
+ }
+
+ /* Sticky mode (centered above footer, appears on scroll) */
+ &[data-back-to-top-mode="sticky"] {
+ position: fixed;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX( -50% );
+ z-index: 40;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
+
+ &.is-visible {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ @media ( max-width: 640px ) {
+ bottom: 1rem;
+ }
+
+ @media ( prefers-reduced-motion: reduce ) {
+ transition: none;
+ }
}
/* Smooth transitions for hover state */
diff --git a/src/plugins/back-to-top/view.js b/src/plugins/back-to-top/view.js
index 17548c7..6ff9618 100644
--- a/src/plugins/back-to-top/view.js
+++ b/src/plugins/back-to-top/view.js
@@ -91,6 +91,9 @@
'.wp-block-button.is-back-to-top'
);
+ // Collect buttons that need scroll visibility handling
+ const scrollVisibilityButtons = [];
+
wrappers.forEach( ( wrapper ) => {
// Find the inner link or button
const link = wrapper.querySelector( '.wp-block-button__link' );
@@ -100,7 +103,49 @@
e.preventDefault();
smoothScrollTo( 0, prefersReducedMotion ? 0 : 600 );
} );
+
+ // Handle sticky/fixed mode visibility based on scroll
+ const mode = wrapper.getAttribute( 'data-back-to-top-mode' );
+ if ( mode === 'sticky' || mode === 'fixed' ) {
+ const thresholdPercent = parseInt( wrapper.getAttribute( 'data-scroll-threshold' ) || '75', 10 );
+ scrollVisibilityButtons.push( { wrapper, thresholdPercent } );
+ }
} );
+
+ // Set up a single shared scroll listener for all buttons
+ if ( scrollVisibilityButtons.length > 0 ) {
+ const handleScroll = () => {
+ const scrolled = window.scrollY;
+ const viewportHeight = window.innerHeight;
+ const documentHeight = document.documentElement.scrollHeight;
+ const scrollableDistance = documentHeight - viewportHeight;
+
+ scrollVisibilityButtons.forEach( ( { wrapper, thresholdPercent } ) => {
+ const threshold = scrollableDistance * ( thresholdPercent / 100 );
+
+ if ( scrolled >= threshold ) {
+ wrapper.classList.add( 'is-visible' );
+ } else {
+ wrapper.classList.remove( 'is-visible' );
+ }
+ } );
+ };
+
+ // Use throttle for performance
+ let ticking = false;
+ window.addEventListener( 'scroll', () => {
+ if ( ! ticking ) {
+ window.requestAnimationFrame( () => {
+ handleScroll();
+ ticking = false;
+ } );
+ ticking = true;
+ }
+ } );
+
+ // Check initial scroll position
+ handleScroll();
+ }
};
// DOM ready check