From b191b83c01ef8e2298cc36674af99532f3202b44 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 29 Oct 2025 16:58:35 +0530 Subject: [PATCH 1/5] feat: move scroll to top to free --- .../apps/customizer-controls/src/controls.js | 45 ++ assets/js/src/customizer-preview/app.js | 20 + assets/js/src/scroll-to-top.js | 97 +++ inc/admin/dashboard/main.php | 4 - inc/core/core_loader.php | 1 + inc/core/front_end.php | 86 ++ inc/core/styles/frontend.php | 107 +++ inc/customizer/loader.php | 1 + inc/customizer/options/scroll_to_top.php | 742 ++++++++++++++++++ inc/customizer/options/upsells.php | 38 - inc/views/scroll_to_top.php | 185 +++++ rollup.config.js | 75 +- 12 files changed, 1320 insertions(+), 81 deletions(-) create mode 100644 assets/js/src/scroll-to-top.js create mode 100644 inc/customizer/options/scroll_to_top.php create mode 100644 inc/views/scroll_to_top.php diff --git a/assets/apps/customizer-controls/src/controls.js b/assets/apps/customizer-controls/src/controls.js index 83b4a5e6f8..d2b6b79f81 100644 --- a/assets/apps/customizer-controls/src/controls.js +++ b/assets/apps/customizer-controls/src/controls.js @@ -297,6 +297,50 @@ const checkHasElementorTemplates = () => { } }; +/** + * Find the Scroll to top button within the customizer preview. + */ +function findScrollToTopBtn() { + const iframeElement = document.querySelector('#customize-preview iframe'); + + if (!iframeElement) { + return; + } + + const scrollToTopBtn = + iframeElement.contentWindow.document.querySelector('#scroll-to-top'); + + return scrollToTopBtn; +} + +/** + * Show the Scroll to Top button as soon as the user enters the section in Customizer. + */ +function previewScrollToTopChanges() { + wp.customize.section('neve_scroll_to_top', (section) => { + section.expanded.bind((isExpanded) => { + const scrollToTopBtn = findScrollToTopBtn(); + + if (!scrollToTopBtn) { + return; + } + + // If Scroll to top customizer section is expanded + if (isExpanded) { + wp.customize.previewer.bind('ready', () => { + wp.customize.previewer.send('nv-opened-stt', true); + }); + scrollToTopBtn.style.visibility = 'visible'; + scrollToTopBtn.style.opacity = '1'; + } else { + // Hide the button when we leave the section + scrollToTopBtn.style.visibility = 'hidden'; + scrollToTopBtn.style.opacity = '0'; + } + }); + }); +} + window.wp.customize.bind('ready', () => { initStarterContentNotice(); initDocSection(); @@ -311,6 +355,7 @@ window.wp.customize.bind('ready', () => { initBlogPageFocus(); initSearchCustomizer(); initLocalGoogleFonts(); + previewScrollToTopChanges(); }); window.HFG = { diff --git a/assets/js/src/customizer-preview/app.js b/assets/js/src/customizer-preview/app.js index 15c683cfcf..d2592b71f2 100644 --- a/assets/js/src/customizer-preview/app.js +++ b/assets/js/src/customizer-preview/app.js @@ -25,6 +25,26 @@ function handleResponsiveRadioButtons(args, nextValue) { }); } +window.wp.customize.bind('ready', () => { + previewScrollToTopChanges(); +}); + +/** + * Preview scroll to top changes made in customizer. + */ +function previewScrollToTopChanges() { + wp.customize.preview.bind('nv-opened-stt', (show) => { + if (show) { + const scrollToTopBtn = document.querySelector('#scroll-to-top'); + if (!scrollToTopBtn) { + return; + } + scrollToTopBtn.style.visibility = 'visible'; + scrollToTopBtn.style.opacity = '1'; + } + }); +} + /** * Run JS on preview-ready. */ diff --git a/assets/js/src/scroll-to-top.js b/assets/js/src/scroll-to-top.js new file mode 100644 index 0000000000..3672abe1bd --- /dev/null +++ b/assets/js/src/scroll-to-top.js @@ -0,0 +1,97 @@ +/*global neveScrollOffset*/ + +function scrollTopSafe(to) { + let i = window.scrollY; + to = parseInt(to); + const scrollInterval = setInterval(function () { + if (i < to + 20) i -= 1; + else if (i < to + 40) i -= 6; + else if (i < to + 80) i -= 16; + else if (i < to + 160) i -= 36; + else if (i < to + 200) i -= 48; + else if (i < to + 300) i -= 80; + else i -= 120; + window.scroll(0, i); + if (i <= to) clearInterval(scrollInterval); + }, 15); +} + +function runScroll() { + const smoothScrollFeature = + 'scrollBehavior' in document.documentElement.style; + if (!smoothScrollFeature) { + scrollTopSafe(0); + } else { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } + const content = document.getElementById('content'); + const scrollButton = document.getElementById('scroll-to-top'); + if (content) { + scrollButton.blur(); + content.focus(); + } +} +function scrollToTop() { + const element = document.getElementById('scroll-to-top'); + if (!element) { + return false; + } + + element.addEventListener('click', function () { + runScroll(); + }); + + element.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + runScroll(); + } + }); + + window.addEventListener('scroll', function () { + const yScrollPos = window.scrollY; + const offset = neveScrollOffset.offset; + + if (yScrollPos > offset) { + element.style.visibility = 'visible'; + element.style.opacity = '1'; + } + if (yScrollPos <= offset) { + element.style.opacity = '0'; + element.style.visibility = 'hidden'; + } + + // Change scroll to top position if there is a sticky add to cart in place. + const stickyAddToCart = document.querySelector( + '.sticky-add-to-cart-bottom' + ); + if (stickyAddToCart) { + element.style.bottom = '30px'; + + // Try to get Neve's sticky footer. If it doesn't exist try to get Elementor's. + let stickyFooter = document.querySelector('.hfg_footer'); + if ( + !stickyFooter || + !stickyFooter.classList.contains('has-sticky-rows') + ) { + stickyFooter = document.querySelector( + '.elementor-location-footer .elementor-sticky' + ); + } + const footerHeight = stickyFooter ? stickyFooter.offsetHeight : 0; + + if ( + stickyAddToCart.classList.contains('sticky-add-to-cart--active') + ) { + element.style.bottom = + stickyAddToCart.offsetHeight + footerHeight + 10 + 'px'; + } + } + }); +} + +window.addEventListener('load', function () { + scrollToTop(); +}); diff --git a/inc/admin/dashboard/main.php b/inc/admin/dashboard/main.php index 2924fc5e30..49a6f049e6 100755 --- a/inc/admin/dashboard/main.php +++ b/inc/admin/dashboard/main.php @@ -667,10 +667,6 @@ private function get_modules() { 'nicename' => __( 'Post types enhancements', 'neve' ), 'description' => __( 'Extend Neve\'s powerful features to custom post types. Create unique layouts for portfolios, testimonials, and more.', 'neve' ), ), - 'scroll_to_top' => array( - 'nicename' => __( 'Scroll To Top', 'neve' ), - 'description' => __( 'Add a customizable scroll-to-top button that appears exactly when needed. Style it to match your brand.', 'neve' ), - ), 'performance_features' => array( 'nicename' => __( 'Performance', 'neve' ), 'description' => __( 'Optimize core vitals, enable lazy loading, and minify resources for lightning-fast load times.', 'neve' ), diff --git a/inc/core/core_loader.php b/inc/core/core_loader.php index 3279533399..f82bb0c665 100644 --- a/inc/core/core_loader.php +++ b/inc/core/core_loader.php @@ -97,6 +97,7 @@ private function define_modules() { 'Views\Content_None', 'Views\Content_404', 'Views\Breadcrumbs', + 'Views\Scroll_To_Top', 'Views\Layouts\Layout_Container', 'Views\Layouts\Layout_Sidebar', diff --git a/inc/core/front_end.php b/inc/core/front_end.php index aac92b9835..39f0e6104e 100644 --- a/inc/core/front_end.php +++ b/inc/core/front_end.php @@ -16,6 +16,7 @@ use Neve\Core\Settings\Mods; use Neve\Core\Dynamic_Css; use Neve\Core\Traits\Theme_Mods; +use Neve\Customizer\Options\Scroll_To_Top; /** * Front end handler class. @@ -86,6 +87,8 @@ public function setup_theme() { add_filter( 'theme_mod_background_color', '__return_empty_string' ); $this->add_woo_support(); add_filter( 'neve_dynamic_style_output', array( $this, 'css_global_custom_colors' ), PHP_INT_MAX, 2 ); + + add_filter( 'neve_dynamic_style_output', array( $this, 'css_scroll_to_top' ), 99, 2 ); } /** @@ -610,6 +613,89 @@ public function css_global_custom_colors( $current_styles, $context ) { return $current_styles; } + /** + * Add module css. + * + * @param string $css Current CSS style. + * @param string $context Current context. + * + * @return string Altered CSS. + */ + public function css_scroll_to_top( $css, $context = 'frontend' ) { + if ( ! Scroll_To_Top::is_enabled() ) { + return $css; + } + + if ( $context !== 'frontend' ) { + return $css; + } + + $scroll_to_top_css = '.scroll-to-top {' . ( is_rtl() ? 'left: 20px;' : 'right: 20px;' ) . ' + border: none; + position: fixed; + bottom: 30px; + display: none; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; + align-items: center; + justify-content: center; + z-index: 999; + } + @supports (-webkit-overflow-scrolling: touch) { + .scroll-to-top { + bottom: 74px; + } + } + .scroll-to-top.image { + background-position: center; + } + .scroll-to-top .scroll-to-top-image { + width: 100%; + height: 100%; + } + .scroll-to-top .scroll-to-top-label { + margin: 0; + padding: 5px; + } + .scroll-to-top:hover { + text-decoration: none; + } + .scroll-to-top.scroll-to-top-left {' . ( is_rtl() ? 'right: 20px; left: unset;' : 'left: 20px; right: unset;' ) . '} + .scroll-to-top.scroll-show-mobile { + display: flex; + } + @media (min-width: 960px) { + .scroll-to-top { + display: flex; + } + }'; + + $scroll_to_top_css .= '.scroll-to-top { + color: var(--color); + padding: var(--padding); + border-radius: var(--borderradius); + background: var(--bgcolor); + } + + .scroll-to-top:hover, .scroll-to-top:focus { + color: var(--hovercolor); + background: var(--hoverbgcolor); + } + + .scroll-to-top-icon, .scroll-to-top.image .scroll-to-top-image { + width: var(--size); + height: var(--size); + } + + .scroll-to-top-image { + background-image: var(--bgimage); + background-size: cover; + }'; + + return $css . $scroll_to_top_css; + } + /** * Fix script translations language directory. * diff --git a/inc/core/styles/frontend.php b/inc/core/styles/frontend.php index 84a56509dd..6705378be0 100644 --- a/inc/core/styles/frontend.php +++ b/inc/core/styles/frontend.php @@ -9,8 +9,10 @@ use Neve\Core\Settings\Config; use Neve\Core\Settings\Mods; +use Neve\Core\Styles\Css_Prop; use Neve\Customizer\Defaults\Layout; use Neve\Customizer\Defaults\Single_Post; +use Neve\Customizer\Options\Scroll_To_Top; /** * Class Generator for Frontend. @@ -54,6 +56,7 @@ public function __construct() { $this->setup_header_style(); $this->setup_single_post_style(); $this->setup_content_vspacing(); + $this->setup_scroll_to_top(); } /** @@ -1058,4 +1061,108 @@ private function setup_content_vspacing() { ]; } } + + /** + * Setup scroll to top styles. + * + * @return void + */ + private function setup_scroll_to_top() { + if ( ! Scroll_To_Top::is_enabled() ) { + return; + } + + // Add CSS variables to root + $rules = $this->get_scroll_to_top_rules(); + $this->_subscribers[] = [ + Dynamic_Selector::KEY_SELECTOR => '.scroll-to-top', + Dynamic_Selector::KEY_RULES => $rules, + ]; + } + + /** + * Get scroll to top CSS variables rules. + * + * @return array + */ + private function get_scroll_to_top_rules() { + $rules = [ + '--color' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_icon_color', + Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_icon_color', 'var(--nv-text-dark-bg)' ) ) ? 'transparent' : 'var(--nv-text-dark-bg)', + ], + '--padding' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_padding', + Dynamic_Selector::META_IS_RESPONSIVE => true, + Dynamic_Selector::META_SUFFIX => 'responsive_unit', + 'directional-prop' => Config::CSS_PROP_PADDING, + Dynamic_Selector::META_DEFAULT => array( + 'desktop' => array( + 'top' => 8, + 'right' => 10, + 'bottom' => 8, + 'left' => 10, + ), + 'tablet' => array( + 'top' => 8, + 'right' => 10, + 'bottom' => 8, + 'left' => 10, + ), + 'mobile' => array( + 'top' => 8, + 'right' => 10, + 'bottom' => 8, + 'left' => 10, + ), + 'desktop-unit' => 'px', + 'tablet-unit' => 'px', + 'mobile-unit' => 'px', + ), + ], + '--borderradius' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_border_radius', + Dynamic_Selector::META_DEFAULT => 3, + Dynamic_Selector::META_SUFFIX => 'px', + ], + '--bgcolor' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_background_color', + Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_background_color', 'var(--nv-primary-accent)' ) ) ? 'transparent' : 'var(--nv-primary-accent)', + ], + '--hovercolor' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_icon_hover_color', + Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_icon_hover_color', 'var(--nv-text-dark-bg)' ) ) ? 'transparent' : 'var(--nv-text-dark-bg)', + ], + '--hoverbgcolor' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_background_hover_color', + Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_background_hover_color', 'var(--nv-primary-accent)' ) ) ? 'transparent' : 'var(--nv-primary-accent)', + ], + '--size' => [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_icon_size', + Dynamic_Selector::META_DEFAULT => '{ "mobile": "16", "tablet": "16", "desktop": "16" }', + Dynamic_Selector::META_IS_RESPONSIVE => true, + Dynamic_Selector::META_FILTER => function ( $css_prop, $value, $meta, $device ) { + $value = (int) $value; + if ( $value > 0 ) { + $unit_suffix = Css_Prop::get_suffix_responsive( $meta, $device ); + return sprintf( '%s:%s;', $css_prop, $value . $unit_suffix ); + } + return ''; + }, + ], + ]; + + $type = Mods::get( 'neve_scroll_to_top_type', 'icon' ); + + if ( $type === 'image' ) { + $rules['--bgimage'] = [ + Dynamic_Selector::META_KEY => 'neve_scroll_to_top_image', + Dynamic_Selector::META_FILTER => function ( $css_prop, $value, $meta, $device ) { + return sprintf( '--bgimage:url(%s);', wp_get_attachment_url( $value ) ); + }, + ]; + } + + return $rules; + } } diff --git a/inc/customizer/loader.php b/inc/customizer/loader.php index 605ad804d2..dd5232a140 100644 --- a/inc/customizer/loader.php +++ b/inc/customizer/loader.php @@ -83,6 +83,7 @@ private function define_modules() { 'Customizer\Options\Layout_Single_Page', 'Customizer\Options\Layout_Single_Product', 'Customizer\Options\Layout_Sidebar', + 'Customizer\Options\Scroll_To_Top', 'Customizer\Options\Typography', 'Customizer\Options\Colors_Background', 'Customizer\Options\Checkout', diff --git a/inc/customizer/options/scroll_to_top.php b/inc/customizer/options/scroll_to_top.php new file mode 100644 index 0000000000..092b8f74b3 --- /dev/null +++ b/inc/customizer/options/scroll_to_top.php @@ -0,0 +1,742 @@ + + * Created on: 2019-02-06 + * + * @package Neve Pro Addon + */ + +namespace Neve\Customizer\Options; + +use Neve\Customizer\Base_Customizer; +use Neve\Customizer\Types\Control; +use Neve\Customizer\Types\Section; + +/** + * Class Scroll_To_Top + * + * @package Neve_Pro\Customizer\Options + */ +class Scroll_To_Top extends Base_Customizer { + /** + * The minimum value of some customizer controls is 0 to able to allow usability relative to CSS units. + * That can be removed after the https://github.com/Codeinwp/neve/issues/3609 issue is handled. + * + * That is defined here against the usage of old Neve versions, Base_Customizer class of the stable Neve version already has the RELATIVE_CSS_UNIT_SUPPORTED_MIN_VALUE constant. + */ + const RELATIVE_CSS_UNIT_SUPPORTED_MIN_VALUE = 0; + + /** + * Base initialization. + */ + public function init() { + parent::init(); + add_action( 'wp_head', array( $this, 'live_refresh_scripts' ) ); + } + + /** + * Live refresh for scroll to top controls. + */ + public function live_refresh_scripts() { + if ( ! is_customize_preview() ) { + return; + } + ?> + + scroll_to_top_section(); + $this->scroll_to_top_options(); + $this->scroll_to_top_style_controls(); + } + + /** + * Register customizer section for the module + */ + private function scroll_to_top_section() { + + $this->add_section( + new Section( + 'neve_scroll_to_top', + array( + 'priority' => 80, + 'title' => esc_html__( 'Scroll To Top', 'neve' ), + 'panel' => 'neve_layout', + ) + ) + ); + + } + + /** + * Register option toggle in customizer + */ + private function scroll_to_top_options() { + + $this->add_control( + new Control( + 'neve_scroll_to_top_status', + array( + 'sanitize_callback' => array( $this, 'sanitize_module_status' ), + 'default' => '1', + ), + array( + 'label' => esc_html__( 'Enable Scroll to Top', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'type' => 'neve_toggle_control', + 'priority' => 5, + ) + ) + ); + + $this->add_control( + new Control( + 'neve_scroll_to_top_general', + array( + 'sanitize_callback' => 'sanitize_text_field', + ), + array( + 'label' => esc_html__( 'General', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 10, + 'class' => 'scroll-to-top-general', + 'accordion' => true, + 'expanded' => true, + 'controls_to_wrap' => 6, + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + 'Neve\Customizer\Controls\Heading' + ) + ); + + /** + * Button side + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_side', + array( + 'default' => 'right', + 'sanitize_callback' => array( $this, 'sanitize_scroll_to_top_side' ), + 'transport' => $this->selective_refresh, + ), + array( + 'label' => esc_html__( 'Choose Side', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 20, + 'type' => 'select', + 'choices' => array( + 'left' => esc_html__( 'Left', 'neve' ), + 'right' => esc_html__( 'Right', 'neve' ), + ), + 'active_callback' => array( $this, 'is_module_enabled' ), + ) + ) + ); + + /** + * Scroll to top type + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_type', + array( + 'default' => 'icon', + 'sanitize_callback' => array( $this, 'sanitize_scroll_to_top_type' ), + ), + array( + 'label' => esc_html__( 'Type', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 30, + 'type' => 'select', + 'choices' => array( + 'icon' => esc_html__( 'Icon', 'neve' ), + 'image' => esc_html__( 'Image', 'neve' ), + ), + 'active_callback' => array( $this, 'is_module_enabled' ), + ) + ) + ); + + /** + * Scroll to top icon + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_icon', + array( + 'sanitize_callback' => 'wp_filter_nohtml_kses', + 'default' => 'stt-icon-style-1', + ), + array( + 'label' => esc_html__( 'Scroll to Top Icon', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 35, + 'active_callback' => array( $this, 'is_icon_type_control' ), + 'is_for' => 'scroll_to_top', + 'large_buttons' => false, + 'type' => 'neve_radio_buttons_control', + ), + '\Neve\Customizer\Controls\React\Radio_Buttons' + ) + ); + + /** + * Image button + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_image', + array( + 'sanitize_callback' => 'absint', + ), + array( + 'label' => esc_html__( 'Image', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 40, + 'active_callback' => array( $this, 'is_image_type_control' ), + 'flex_height' => true, + 'flex_width' => true, + ), + '\WP_Customize_Cropped_Image_Control' + ) + ); + + /* + * Label + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_label', + array( + 'sanitize_callback' => 'sanitize_text_field', + 'transport' => $this->selective_refresh, + ), + array( + 'priority' => 50, + 'section' => 'neve_scroll_to_top', + 'label' => esc_html__( 'Label', 'neve' ), + 'type' => 'text', + 'active_callback' => array( $this, 'is_module_enabled' ), + ) + ) + ); + + /** + * Offset + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_offset', + array( + 'sanitize_callback' => 'absint', + 'default' => 0, + ), + array( + 'label' => esc_html__( 'Offset (px)', 'neve' ), + 'description' => esc_html__( 'Show button when page is scrolled x pixels.', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'step' => 1, + 'input_attr' => array( + 'min' => 0, + 'max' => 1000, + 'default' => 0, + ), + 'input_attrs' => array( + 'min' => 0, + 'max' => 1000, + 'defaultVal' => 0, + ), + 'priority' => 60, + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + class_exists( 'Neve\Customizer\Controls\React\Range' ) ? 'Neve\Customizer\Controls\React\Range' : 'Neve\Customizer\Controls\Range' + ) + ); + + /** + * Hide on mobile + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_on_mobile', + array( + 'sanitize_callback' => 'neve_sanitize_checkbox', + 'default' => false, + 'transport' => $this->selective_refresh, + ), + array( + 'label' => esc_html__( 'Hide on mobile', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'type' => 'neve_toggle_control', + 'priority' => 70, + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + 'Neve\Customizer\Controls\Checkbox' + ) + ); + } + + /** + * Add style controls for Scroll to top module. + */ + private function scroll_to_top_style_controls() { + + $this->add_control( + new Control( + 'neve_scroll_to_top_style', + array( + 'sanitize_callback' => 'sanitize_text_field', + ), + array( + 'label' => esc_html__( 'Style', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 80, + 'class' => 'scroll-to-top-accordion', + 'accordion' => true, + 'expanded' => false, + 'controls_to_wrap' => 3, + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + 'Neve\Customizer\Controls\Heading' + ) + ); + + $default_padding_values = array( + 'desktop' => array( + 'top' => 8, + 'right' => 10, + 'bottom' => 8, + 'left' => 10, + ), + 'tablet' => array( + 'top' => 8, + 'right' => 10, + 'bottom' => 8, + 'left' => 10, + ), + 'mobile' => array( + 'top' => 8, + 'right' => 10, + 'bottom' => 8, + 'left' => 10, + ), + 'desktop-unit' => 'px', + 'tablet-unit' => 'px', + 'mobile-unit' => 'px', + ); + $this->add_control( + new Control( + 'neve_scroll_to_top_padding', + array( + 'default' => $default_padding_values, + 'transport' => $this->selective_refresh, + ), + array( + 'label' => __( 'Padding', 'neve' ), + 'sanitize_callback' => array( $this, 'sanitize_spacing_array' ), + 'section' => 'neve_scroll_to_top', + 'input_attrs' => array( + 'units' => array( 'px', 'em', 'rem' ), + ), + 'default' => $default_padding_values, + 'priority' => 90, + 'live_refresh_selector' => true, + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'vars' => '--padding', + 'selector' => '#scroll-to-top', + 'responsive' => true, + ), + 'responsive' => true, + 'directional' => true, + 'template' => + '#scroll-to-top { + padding-top: {{value.top}}; + padding-right: {{value.right}}; + padding-bottom: {{value.bottom}}; + padding-left: {{value.left}}; + }', + ), + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + '\Neve\Customizer\Controls\React\Spacing' + ) + ); + + /** + * Icon size + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_icon_size', + array( + 'sanitize_callback' => 'neve_sanitize_range_value', + 'default' => '{ "mobile": "16", "tablet": "16", "desktop": "16" }', + 'transport' => $this->selective_refresh, + ), + array( + 'label' => esc_html__( 'Icon Size', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'media_query' => true, + 'step' => 1, + 'input_attr' => array( + 'mobile' => array( + 'min' => 10, + 'max' => 100, + 'default' => 16, + ), + 'tablet' => array( + 'min' => 10, + 'max' => 100, + 'default' => 16, + ), + 'desktop' => array( + 'min' => 10, + 'max' => 100, + 'default' => 16, + ), + ), + 'input_attrs' => array( + 'step' => 1, + 'min' => self::RELATIVE_CSS_UNIT_SUPPORTED_MIN_VALUE, + 'max' => 100, + 'defaultVal' => array( + 'mobile' => 16, + 'tablet' => 16, + 'desktop' => 16, + 'suffix' => [ + 'mobile' => 'px', + 'tablet' => 'px', + 'desktop' => 'px', + ], + ), + 'units' => array( 'px', 'em', 'rem' ), + ), + 'priority' => 100, + 'live_refresh_selector' => true, + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'vars' => '--size', + 'selector' => '.scroll-to-top-icon, .scroll-to-top-image', + 'responsive' => true, + 'suffix' => 'px', + ), + 'responsive' => true, + 'template' => 'body .scroll-to-top.icon .scroll-to-top-icon, body .scroll-to-top.image .scroll-to-top-image { + width: {{value}}px; + height: {{value}}px; + }', + ), + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + class_exists( 'Neve\Customizer\Controls\React\Responsive_Range', false ) ? 'Neve\Customizer\Controls\React\Responsive_Range' : 'Neve\Customizer\Controls\Range' + ) + ); + + /** + * Button border radius + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_border_radius', + array( + 'sanitize_callback' => 'absint', + 'default' => 3, + 'transport' => $this->selective_refresh, + ), + array( + 'label' => esc_html__( 'Border Radius', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'step' => 1, + 'input_attr' => array( + 'min' => 0, + 'max' => 200, + 'default' => 3, + ), + 'input_attrs' => array( + 'min' => 0, + 'max' => 200, + 'defaultVal' => 3, + ), + 'priority' => 110, + 'live_refresh_selector' => true, + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'fallback' => '0', + 'vars' => '--borderradius', + 'selector' => '.scroll-to-top', + 'suffix' => 'px', + ), + 'template' => 'body .scroll-to-top { + border-radius: {{value}}px; + }', + 'fallback' => '0', + ), + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + class_exists( 'Neve\Customizer\Controls\React\Range' ) ? 'Neve\Customizer\Controls\React\Range' : 'Neve\Customizer\Controls\Range' + ) + ); + + /** + * Colors heading + */ + $this->add_control( + new Control( + 'neve_scroll_to_top_colors', + array( + 'sanitize_callback' => 'sanitize_text_field', + ), + array( + 'label' => esc_html__( 'Colors', 'neve' ), + 'section' => 'neve_scroll_to_top', + 'priority' => 110, + 'class' => 'scroll-top-colors-accordion', + 'accordion' => true, + 'expanded' => false, + 'controls_to_wrap' => 4, + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + 'Neve\Customizer\Controls\Heading' + ) + ); + + $color_controls = array( + 'neve_scroll_to_top_icon_color' => array( + 'default' => 'var(--nv-text-dark-bg)', + 'priority' => 120, + 'label' => esc_html__( 'Color', 'neve' ), + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'vars' => '--color', + 'selector' => '.scroll-to-top', + ), + 'template' => ' + body .scroll-to-top { + color: {{value}}; + }', + ), + ), + 'neve_scroll_to_top_icon_hover_color' => array( + 'default' => 'var(--nv-text-dark-bg)', + 'priority' => 130, + 'label' => esc_html__( 'Hover Color', 'neve' ), + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'vars' => '--hovercolor', + 'selector' => '.scroll-to-top:hover', + ), + 'template' => ' + body .scroll-to-top:hover { + color: {{value}}; + }', + ), + ), + 'neve_scroll_to_top_background_color' => array( + 'default' => 'var(--nv-primary-accent)', + 'priority' => 140, + 'label' => esc_html__( 'Background Color', 'neve' ), + 'input_attrs' => [ + 'allow_gradient' => true, + ], + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'vars' => '--bgcolor', + 'selector' => '.scroll-to-top', + ), + 'template' => ' + body .scroll-to-top { + background: {{value}}; + }', + ), + ), + 'neve_scroll_to_top_background_hover_color' => array( + 'default' => 'var(--nv-primary-accent)', + 'priority' => 150, + 'label' => esc_html__( 'Background Hover Color', 'neve' ), + 'input_attrs' => [ + 'allow_gradient' => true, + ], + 'live_refresh_css_prop' => array( + 'cssVar' => array( + 'vars' => '--hoverbgcolor', + 'selector' => '.scroll-to-top:hover', + ), + 'template' => ' + body .scroll-to-top:hover { + background: {{value}}; + }', + ), + ), + ); + + /** + * Color controls + */ + foreach ( $color_controls as $control_id => $control_properties ) { + $this->add_control( + new Control( + $control_id, + array( + 'sanitize_callback' => 'neve_sanitize_colors', + 'default' => $control_properties['default'], + 'transport' => $this->selective_refresh, + ), + array( + 'label' => $control_properties['label'], + 'section' => 'neve_scroll_to_top', + 'priority' => $control_properties['priority'], + 'input_attrs' => isset( $control_properties['input_attrs'] ) ? $control_properties['input_attrs'] : [], + 'live_refresh_selector' => true, + 'live_refresh_css_prop' => $control_properties['live_refresh_css_prop'], + 'active_callback' => array( $this, 'is_module_enabled' ), + ), + '\Neve\Customizer\Controls\React\Color' + ) + ); + } + } + + /** + * Active callback for controls that are available only if scroll to top is an image + */ + public function is_image_type_control() { + if ( ! $this->is_module_enabled() ) { + return false; + } + + return get_theme_mod( 'neve_scroll_to_top_type', 'icon' ) === 'image'; + } + + /** + * Active callback for controls that are available only if scroll to top is an icon + */ + public function is_icon_type_control() { + if ( ! $this->is_module_enabled() ) { + return false; + } + + return get_theme_mod( 'neve_scroll_to_top_type', 'icon' ) === 'icon'; + } + + /** + * Sanitize scroll to top type + * + * @param string $value - value of the control. + * + * @return string + */ + public function sanitize_scroll_to_top_type( $value ) { + $allowed_values = array( 'icon', 'image' ); + if ( ! in_array( $value, $allowed_values, true ) ) { + return 'icon'; + } + + return esc_html( $value ); + } + + /** + * Sanitize scroll to top side + * + * @param string $value - value of the control. + * + * @return string + */ + public function sanitize_scroll_to_top_side( $value ) { + $allowed_values = array( 'left', 'right' ); + if ( ! in_array( $value, $allowed_values, true ) ) { + return 'right'; + } + + return esc_html( $value ); + } + + /** + * Check if Scroll to Top is enabled. + */ + public static function is_enabled() { + // Check old option first for backward compatibility. + $old_value = get_option( 'nv_pro_scroll_to_top_status', null ); + + if ( null !== $old_value ) { + // Option exists — use it and migrate to the new theme_mod for future. + $value = $old_value === '1'; + + set_theme_mod( 'neve_scroll_to_top_status', $old_value ); + delete_option( 'nv_pro_scroll_to_top_status' ); + + return $value; + } + + // Otherwise, use the new theme_mod. + return get_theme_mod( 'neve_scroll_to_top_status', '1' ) === '1'; + } + + /** + * Active callback for scroll to top controls. + */ + public function is_module_enabled() { + return self::is_enabled(); + } + + /** + * Sanitize module status. The toggle in neve options returns '1' or '' so our control should return the same thing. + * + * @param bool|string $value Current value. + * + * @return string + */ + public function sanitize_module_status( $value ) { + if ( $value === true ) { + return '1'; + } + if ( $value === false ) { + return ''; + } + return $value; + } + +} diff --git a/inc/customizer/options/upsells.php b/inc/customizer/options/upsells.php index 768b876672..4287f78465 100644 --- a/inc/customizer/options/upsells.php +++ b/inc/customizer/options/upsells.php @@ -408,20 +408,6 @@ private function section_upsells() { ) ); } - - $this->add_section( - new Section( - 'neve_scroll_to_top_upsell', - array( - 'priority' => 80, - 'title' => esc_html__( 'Scroll To Top', 'neve' ), - 'cta' => esc_html__( 'PRO', 'neve' ), - 'url' => $this->stt_upsell_url, - 'panel' => 'neve_layout', - ), - 'Neve\Customizer\Controls\React\Upsell_Section' - ) - ); } /** @@ -443,30 +429,6 @@ private function control_upsells() { ) ); - - /* - * Deactivated. - * - * @since 4.1.0 - */ - $this->add_control( - new Control( - 'neve_scroll_to_top_cta_control', - [ 'sanitize_callback' => 'sanitize_text_field' ], - [ - /* translators: Module name for the upsell. */ - 'text' => sprintf( __( 'Unlock %s with the Pro version.', 'neve' ), __( 'Scroll To Top', 'neve' ) ), - 'button_text' => esc_html__( 'Get the PRO version!', 'neve' ), - 'section' => 'neve_scroll_to_top_upsell', - 'priority' => PHP_INT_MIN, - 'link' => $this->get_upgrade_url( 'scrolltotop' ), - 'class' => 'column-layout', - 'use_primary' => 'true', - ], - 'Neve\Customizer\Controls\Simple_Upsell' - ) - ); - $hfg_header = 'hfg_header'; $hfg_footer = 'hfg_footer'; diff --git a/inc/views/scroll_to_top.php b/inc/views/scroll_to_top.php new file mode 100644 index 0000000000..3faab20ca6 --- /dev/null +++ b/inc/views/scroll_to_top.php @@ -0,0 +1,185 @@ +'; + + // We use 2 `amp-animation` elements to trigger the visibility of the button. The first one is for making the button visible + echo ' + + + '; + + echo ' + + + + + '; + return true; + } + + /** + * Enqueue module scripts + */ + public function enqueue_scripts() { + if ( ! Scroll_To_Top_Options::is_enabled() ) { + return; + } + + if ( neve_is_amp() ) { + return; + } + + wp_register_script( + 'neve-scroll-to-top', + NEVE_ASSETS_URL . 'js/build/modern/scroll-to-top.js', + array(), + NEVE_VERSION, + true + ); + + wp_enqueue_script( 'neve-scroll-to-top' ); + + wp_script_add_data( 'neve-scroll-to-top', 'async', true ); + + wp_localize_script( 'neve-scroll-to-top', 'neveScrollOffset', $this->localize_scroll() ); + } + + /** + * Send offset to the JS object + * + * @return array + */ + private function localize_scroll() { + return array( + 'offset' => get_theme_mod( 'neve_scroll_to_top_offset', 0 ), + ); + } + + /** + * Display scroll to top button + */ + public function render_button() { + if ( ! Scroll_To_Top_Options::is_enabled() ) { + return false; + } + + $position = get_theme_mod( 'neve_scroll_to_top_side', 'right' ); + $hide_on_mobile = get_theme_mod( 'neve_scroll_to_top_on_mobile', false ); + $type = get_theme_mod( 'neve_scroll_to_top_type', 'icon' ); + $label = get_theme_mod( 'neve_scroll_to_top_label' ); + $image = get_theme_mod( 'neve_scroll_to_top_image' ); + $icon = get_theme_mod( 'neve_scroll_to_top_icon', 'stt-icon-style-1' ); + + $extra_class = sprintf( 'scroll-to-top-%s %s', $position, ( ( ! $hide_on_mobile ) ? ' scroll-show-mobile ' : '' ) ); + $extra_class .= $type; + + $amp = neve_is_amp() ? 'on="tap:neve_body.scrollTo(duration=200)"' : ''; + + echo ''; + + return true; + } + + /** + * Get SVG icon for scroll to top button. + * + * @param string $icon_style The icon style identifier. + * @return string SVG icon markup. + */ + private function get_icon_svg( $icon_style ) { + $icons = array( + 'stt-icon-style-1' => '', + 'stt-icon-style-2' => '', + 'stt-icon-style-3' => '', + 'stt-icon-style-4' => '', + 'stt-icon-style-5' => '', + ); + + return isset( $icons[ $icon_style ] ) ? $icons[ $icon_style ] : $icons['stt-icon-style-1']; + } +} diff --git a/rollup.config.js b/rollup.config.js index 6516f4101c..27df1369a8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,8 +1,8 @@ import babel from 'rollup-plugin-babel'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import {uglify} from 'rollup-plugin-uglify'; -import {terser} from 'rollup-plugin-terser'; +import { uglify } from 'rollup-plugin-uglify'; +import { terser } from 'rollup-plugin-terser'; import multi from '@rollup/plugin-multi-entry'; const ROLLUP_LEGACY = { @@ -10,55 +10,52 @@ const ROLLUP_LEGACY = { babelrc: false, presets: [ [ - "@babel/env", + '@babel/env', { - "targets": { - "browsers": [ - "> 0.5%, last 2 versions, Firefox ESR, not dead" - ] + targets: { + browsers: [ + '> 0.5%, last 2 versions, Firefox ESR, not dead', + ], }, - "useBuiltIns": "usage", - "corejs": 3, - "exclude": [ - 'es.regexp.exec', - 'es.string.split', - ] - } + useBuiltIns: 'usage', + corejs: 3, + exclude: ['es.regexp.exec', 'es.string.split'], + }, ], ], }; const ROLLUP_MODERN = { exclude: 'node_modules/**', babelrc: false, - presets: + presets: [ [ - [ - "@babel/env", - { - "targets": ["defaults", - "not ie >= 0"], - "debug": true, - "useBuiltIns": "usage", - "corejs": 3, - "exclude": [ - "es.string.split", - 'web.dom-collections.iterator' - ] - } - ] - + '@babel/env', + { + targets: ['defaults', 'not ie >= 0'], + debug: true, + useBuiltIns: 'usage', + corejs: 3, + exclude: ['es.string.split', 'web.dom-collections.iterator'], + }, + ], ], }; -let all_coverage = { +const all_coverage = { 'assets/js/build/all/metabox.js': 'assets/js/src/metabox.js', 'assets/js/build/all/gutenberg.js': 'assets/js/src/gutenberg.js', - 'assets/js/build/all/customizer-preview.js': ['assets/js/src/customizer-preview/app.js'], - 'assets/js/build/all/customizer-controls.js': ['./assets/customizer/js/*.js'] + 'assets/js/build/all/customizer-preview.js': [ + 'assets/js/src/customizer-preview/app.js', + ], + 'assets/js/build/all/customizer-controls.js': [ + './assets/customizer/js/*.js', + ], }, __export = [], modern = { 'assets/js/build/modern/shop.js': 'assets/js/src/shop/app.js', 'assets/js/build/modern/frontend.js': 'assets/js/src/frontend/app.js', + 'assets/js/build/modern/scroll-to-top.js': + 'assets/js/src/scroll-to-top.js', }; Object.keys(all_coverage).forEach(function (item) { @@ -67,15 +64,15 @@ Object.keys(all_coverage).forEach(function (item) { output: { file: item, format: 'iife', - sourceMap: 'inline' + sourceMap: 'inline', }, plugins: [ multi(), resolve(), commonjs(), babel(ROLLUP_LEGACY), - uglify() - ] + uglify(), + ], }); }); Object.keys(modern).forEach(function (item) { @@ -84,15 +81,15 @@ Object.keys(modern).forEach(function (item) { output: { file: item, format: 'iife', - sourceMap: 'inline' + sourceMap: 'inline', }, plugins: [ multi(), resolve(), commonjs(), babel(ROLLUP_MODERN), - terser() - ] + terser(), + ], }); }); From 490ead6ea0eee7d4030e50a5a5d5a00c9baf4e3d Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 29 Oct 2025 17:57:06 +0530 Subject: [PATCH 2/5] fix: phpstan errors --- inc/core/styles/frontend.php | 2 +- inc/customizer/options/scroll_to_top.php | 21 ++++++++++++++++++--- inc/customizer/options/upsells.php | 10 +--------- inc/views/scroll_to_top.php | 17 ++++++++++------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/inc/core/styles/frontend.php b/inc/core/styles/frontend.php index 6705378be0..f355571d94 100644 --- a/inc/core/styles/frontend.php +++ b/inc/core/styles/frontend.php @@ -1083,7 +1083,7 @@ private function setup_scroll_to_top() { /** * Get scroll to top CSS variables rules. * - * @return array + * @return array> */ private function get_scroll_to_top_rules() { $rules = [ diff --git a/inc/customizer/options/scroll_to_top.php b/inc/customizer/options/scroll_to_top.php index 092b8f74b3..c6c1042fca 100644 --- a/inc/customizer/options/scroll_to_top.php +++ b/inc/customizer/options/scroll_to_top.php @@ -28,6 +28,8 @@ class Scroll_To_Top extends Base_Customizer { /** * Base initialization. + * + * @return void */ public function init() { parent::init(); @@ -36,6 +38,8 @@ public function init() { /** * Live refresh for scroll to top controls. + * + * @return void */ public function live_refresh_scripts() { if ( ! is_customize_preview() ) { @@ -94,9 +98,10 @@ public function add_controls() { /** * Register customizer section for the module + * + * @return void */ private function scroll_to_top_section() { - $this->add_section( new Section( 'neve_scroll_to_top', @@ -112,9 +117,10 @@ private function scroll_to_top_section() { /** * Register option toggle in customizer + * + * @return void */ private function scroll_to_top_options() { - $this->add_control( new Control( 'neve_scroll_to_top_status', @@ -321,9 +327,10 @@ class_exists( 'Neve\Customizer\Controls\React\Range' ) ? 'Neve\Customizer\Contro /** * Add style controls for Scroll to top module. + * + * @return void */ private function scroll_to_top_style_controls() { - $this->add_control( new Control( 'neve_scroll_to_top_style', @@ -642,6 +649,8 @@ class_exists( 'Neve\Customizer\Controls\React\Range' ) ? 'Neve\Customizer\Contro /** * Active callback for controls that are available only if scroll to top is an image + * + * @return bool */ public function is_image_type_control() { if ( ! $this->is_module_enabled() ) { @@ -653,6 +662,8 @@ public function is_image_type_control() { /** * Active callback for controls that are available only if scroll to top is an icon + * + * @return bool */ public function is_icon_type_control() { if ( ! $this->is_module_enabled() ) { @@ -696,6 +707,8 @@ public function sanitize_scroll_to_top_side( $value ) { /** * Check if Scroll to Top is enabled. + * + * @return bool */ public static function is_enabled() { // Check old option first for backward compatibility. @@ -717,6 +730,8 @@ public static function is_enabled() { /** * Active callback for scroll to top controls. + * + * @return bool */ public function is_module_enabled() { return self::is_enabled(); diff --git a/inc/customizer/options/upsells.php b/inc/customizer/options/upsells.php index 4287f78465..668f5d3168 100644 --- a/inc/customizer/options/upsells.php +++ b/inc/customizer/options/upsells.php @@ -28,13 +28,6 @@ class Upsells extends Base_Customizer { */ private $upsell_url = ''; - /** - * Scroll to top upsell url - * - * @var string - */ - private $stt_upsell_url = ''; - /** * Init function * @@ -45,8 +38,7 @@ public function init() { return; } - $this->stt_upsell_url = esc_url_raw( apply_filters( 'neve_upgrade_link_from_child_theme_filter', $this->get_upgrade_url( 'scrolltotop' ) ) ); - $this->upsell_url = esc_url_raw( apply_filters( 'neve_upgrade_link_from_child_theme_filter', $this->get_upgrade_url( 'learnmorebtn' ) ) ); + $this->upsell_url = esc_url_raw( apply_filters( 'neve_upgrade_link_from_child_theme_filter', $this->get_upgrade_url( 'learnmorebtn' ) ) ); parent::init(); diff --git a/inc/views/scroll_to_top.php b/inc/views/scroll_to_top.php index 3faab20ca6..523a71b423 100644 --- a/inc/views/scroll_to_top.php +++ b/inc/views/scroll_to_top.php @@ -27,14 +27,16 @@ public function init() { /** * Scroll to top amp observer. + * + * @return void */ public function scroll_to_top_amp() { if ( ! Scroll_To_Top_Options::is_enabled() ) { - return false; + return; } if ( ! neve_is_amp() ) { - return false; + return; } echo ''; @@ -81,11 +83,12 @@ public function scroll_to_top_amp() { '; - return true; } /** * Enqueue module scripts + * + * @return void */ public function enqueue_scripts() { if ( ! Scroll_To_Top_Options::is_enabled() ) { @@ -114,7 +117,7 @@ public function enqueue_scripts() { /** * Send offset to the JS object * - * @return array + * @return array */ private function localize_scroll() { return array( @@ -124,10 +127,12 @@ private function localize_scroll() { /** * Display scroll to top button + * + * @return void */ public function render_button() { if ( ! Scroll_To_Top_Options::is_enabled() ) { - return false; + return; } $position = get_theme_mod( 'neve_scroll_to_top_side', 'right' ); @@ -161,8 +166,6 @@ public function render_button() { } echo ''; - - return true; } /** From b9876a320a8c5dbd1e38d09b035c3168d5779e76 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 29 Oct 2025 20:33:46 +0530 Subject: [PATCH 3/5] fix: e2e test failing --- e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts | 2 +- .../specs/customizer/hfg/hfg-palette-switch-component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts b/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts index 5466148015..ff8b6d5d29 100644 --- a/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts +++ b/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts @@ -128,7 +128,7 @@ test.describe('Logo Component palette', function () { await expect(await siteLogo.getAttribute('src')).toBe(logos[1]?.url); - await page.locator('.icon > svg > path').click(); + await page.getByRole('link', { name: 'Palette Switch' }).click(); await expect(await siteLogo.getAttribute('src')).toBe(logos[0]?.url); }); diff --git a/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts b/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts index 0983dc41c1..05e70c3c06 100644 --- a/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts +++ b/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts @@ -28,7 +28,7 @@ test.describe('Palette Switch component', function () { ); } - await page.locator('.icon > svg > path').click(); + await page.getByRole('link', { name: 'Palette Switch' }).click(); for (let i = 0; i < count; i++) { await expect(headerElements.nth(i)).toHaveCSS( From 2ffa67be5858cd40083b026b9f15a02bb343892d Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Thu, 30 Oct 2025 00:49:09 +0530 Subject: [PATCH 4/5] chore: move e2e tests --- .../scroll-to-top/scroll-to-top-setup.json | 28 +++ .../scroll-to-top/scroll-to-top.spec.ts | 190 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json create mode 100644 e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts diff --git a/e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json b/e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json new file mode 100644 index 0000000000..5df11f6b0e --- /dev/null +++ b/e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json @@ -0,0 +1,28 @@ +{ + "general": { + "neve_scroll_to_top_side": "left", + "neve_scroll_to_top_label": "Go up", + "neve_scroll_to_top_offset": 100, + "neve_scroll_to_top_padding": { + "desktop": { "top": 10, "right": 12, "bottom": 10, "left": 12 }, + "tablet": { "top": 6, "right": 8, "bottom": 6, "left": 8 }, + "mobile": { "top": 10, "right": 12, "bottom": 10, "left": 12 }, + "desktop-unit": "px", + "tablet-unit": "px", + "mobile-unit": "px" + }, + "neve_scroll_to_top_border_radius": 100, + "neve_scroll_to_top_icon_color": "#ff0000", + "neve_scroll_to_top_icon_hover_color": "#ff0000", + "neve_scroll_to_top_background_color": "#ffffff", + "neve_scroll_to_top_background_hover_color": "#ffffff", + "neve_scroll_to_top_type": "icon", + "neve_scroll_to_top_image": 0, + "neve_scroll_to_top_on_mobile": false + }, + "icon-check": { + "neve_scroll_to_top_side": "left", + "neve_scroll_to_top_icon_size": "{ \"mobile\": \"100\", \"tablet\": \"50\", \"desktop\": \"100\" }", + "neve_scroll_to_top_on_mobile": false + } +} \ No newline at end of file diff --git a/e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts b/e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts new file mode 100644 index 0000000000..fc301ff935 --- /dev/null +++ b/e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts @@ -0,0 +1,190 @@ +import { test, expect } from '@playwright/test'; +import { setCustomizeSettings, scrollTo, visitAdminPage } from '../../../utils'; +import data from '../../../fixtures/customizer/scroll-to-top/scroll-to-top-setup.json'; + +test.describe( 'Scroll to top', function () { + test( 'Checks the position', async function ( { page, request, baseURL } ) { + await setCustomizeSettings( 'stt-left', data.general, { + request, + baseURL, + } ); + await page.goto( '/hello-world/?test_name=stt-left' ); + await scrollTo( page, 'bottom' ); + const scrollToTopBtn = await page.locator( '#scroll-to-top' ); + await expect( scrollToTopBtn ).toHaveCSS( 'left', '20px' ); + + await setCustomizeSettings( + 'stt-right', + { neve_scroll_to_top_side: 'right' }, + { + request, + baseURL, + } + ); + await page.goto( '/hello-world/?test_name=stt-right' ); + await scrollTo( page, 'bottom' ); + await expect( scrollToTopBtn ).toHaveCSS( 'right', '20px' ); + await scrollToTopBtn.click(); + await page.waitForTimeout( 2000 ); + const isAtTop = await page.evaluate( () => { + return window.scrollY === 0; + } ); + await expect( isAtTop ).toBeTruthy(); + } ); + + test( 'Checks scroll to top general settings', async function ( { + page, + request, + baseURL, + } ) { + await setCustomizeSettings( 'stt-general', data.general, { + request, + baseURL, + } ); + + await page.goto( '/hello-world/?test_name=stt-general' ); + const sttButton = await page.locator( '#scroll-to-top' ); + + // Checks label + await page.evaluate( () => { + window.scrollTo( 0, document.body.scrollHeight ); + } ); + await expect( sttButton ).toBeVisible(); + await expect( + await sttButton.getByText( /Go up/ ).first() + ).toBeVisible(); + + // Checks offset + await scrollTo( page, 80 ); + await page.waitForTimeout( 500 ); + await expect( sttButton ).not.toBeVisible(); + await scrollTo( page, 110 ); + await page.waitForTimeout( 500 ); + await expect( sttButton ).toBeVisible(); + + // Checks button padding + await scrollTo( page, 'bottom' ); + await expect( sttButton ).toHaveCSS( 'padding', '10px 12px' ); + + await page.setViewportSize( { width: 768, height: 1024 } ); + await expect( sttButton ).toHaveCSS( 'padding', '6px 8px' ); + + await page.setViewportSize( { width: 375, height: 812 } ); + await expect( sttButton ).toHaveCSS( 'padding', '10px 12px' ); + + // Checks border radius + await scrollTo( page, 'bottom' ); + await expect( sttButton ).toHaveCSS( 'border-radius', '100px' ); + + // Checks colors + await scrollTo( page, 'bottom' ); + await expect( sttButton ).toHaveCSS( 'color', 'rgb(255, 0, 0)' ); + await expect( sttButton ).toHaveCSS( + 'background-color', + 'rgb(255, 255, 255)' + ); + + await sttButton.hover(); + await expect( sttButton ).toHaveCSS( + 'background-color', + 'rgb(255, 255, 255)' + ); + await expect( sttButton ).toHaveCSS( 'color', 'rgb(255, 0, 0)' ); + } ); + + test( 'Checks the icon type', async function ( { + page, + request, + baseURL, + } ) { + const iconTypeData = Object.assign( {}, data.general ); + + // Get the id of the first image to be able to apply it. + await visitAdminPage( page, 'upload.php', '' ); + await page.locator( '.attachment' ).first().click(); + const urlString = page.url(); + const url = new URL( urlString ); + const imageId = url.searchParams.get( 'item' ) || ''; + + iconTypeData.neve_scroll_to_top_type = 'image'; + iconTypeData.neve_scroll_to_top_image = parseInt( imageId ); + + await setCustomizeSettings( 'stt-icon-check', iconTypeData, { + request, + baseURL, + } ); + + await page.goto( '/hello-world/?test_name=stt-icon-check' ); + await scrollTo( page, 'bottom' ); + + const scrollToTopImage = await page.locator( + '#scroll-to-top .scroll-to-top-image' + ); + await expect( await scrollToTopImage.count() ).toBeGreaterThan( 0 ); + + await expect( scrollToTopImage ).toHaveCSS( + 'background-image', + /image.png/ + ); + + await setCustomizeSettings( 'stt-icon-check2', data.general, { + request, + baseURL, + } ); + await page.goto( '/hello-world/?test_name=stt-icon-check2' ); + await scrollTo( page, 'bottom' ); + await expect( + await page.locator( '#scroll-to-top svg' ).count() + ).toEqual( 1 ); + } ); + + test( 'Checks hiding on mobile', async function ( { + page, + request, + baseURL, + } ) { + const hidingData = Object.assign( {}, data.general ); + hidingData.neve_scroll_to_top_on_mobile = true; + + await setCustomizeSettings( 'stt-check-hiding', hidingData, { + request, + baseURL, + } ); + + await page.goto( '/hello-world/?test_name=stt-check-hiding' ); // iphone-x + + const sttButton = await page.locator( '#scroll-to-top' ); + + await page.setViewportSize( { width: 375, height: 812 } ); + await scrollTo( page, 'bottom' ); + await expect( sttButton ).not.toBeVisible(); + + await page.setViewportSize( { width: 1440, height: 900 } ); + await scrollTo( page, 'bottom' ); + await expect( sttButton ).toBeVisible(); + } ); + + test( 'Checks icon size', async function ( { page, request, baseURL } ) { + await setCustomizeSettings( 'stt-check-icon3', data[ 'icon-check' ], { + request, + baseURL, + } ); + + await page.goto( '/hello-world/?test_name=stt-check-icon3' ); + await scrollTo( page, 'bottom' ); + + const sttIcon = await page.locator( + '#scroll-to-top .scroll-to-top-icon' + ); + await expect( sttIcon ).toHaveCSS( 'width', '100px' ); + await expect( sttIcon ).toHaveCSS( 'height', '100px' ); + + await page.setViewportSize( { width: 768, height: 1024 } ); + await expect( sttIcon ).toHaveCSS( 'width', '50px' ); + await expect( sttIcon ).toHaveCSS( 'height', '50px' ); + + await page.setViewportSize( { width: 375, height: 812 } ); + await expect( sttIcon ).toHaveCSS( 'width', '100px' ); + await expect( sttIcon ).toHaveCSS( 'height', '100px' ); + } ); +} ); From 50c3040870adce1691619b9aa3e78e75126db850 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Thu, 6 Nov 2025 20:50:16 +0530 Subject: [PATCH 5/5] fix: wrong icons --- inc/views/scroll_to_top.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/inc/views/scroll_to_top.php b/inc/views/scroll_to_top.php index 523a71b423..75cca17e8b 100644 --- a/inc/views/scroll_to_top.php +++ b/inc/views/scroll_to_top.php @@ -176,11 +176,12 @@ public function render_button() { */ private function get_icon_svg( $icon_style ) { $icons = array( - 'stt-icon-style-1' => '', - 'stt-icon-style-2' => '', - 'stt-icon-style-3' => '', - 'stt-icon-style-4' => '', - 'stt-icon-style-5' => '', + 'stt-icon-style-1' => '', + 'stt-icon-style-2' => '', + 'stt-icon-style-3' => '', + 'stt-icon-style-4' => '', + 'stt-icon-style-5' => '', + 'stt-icon-style-6' => '', ); return isset( $icons[ $icon_style ] ) ? $icons[ $icon_style ] : $icons['stt-icon-style-1'];