diff --git a/assets/src/js/frontend/services/site-shell.ts b/assets/src/js/frontend/services/site-shell.ts index cae2e7160f..452b6a9e34 100644 --- a/assets/src/js/frontend/services/site-shell.ts +++ b/assets/src/js/frontend/services/site-shell.ts @@ -2,6 +2,7 @@ const CSS_VARIABLE_HEADER_OFFSET = '--tutor-site-header-offset'; const CSS_VARIABLE_HEADER_OVERLAY_OFFSET = '--tutor-site-header-overlay-offset'; const WP_ADMIN_BAR_ID = 'wpadminbar'; +const MAX_HEADER_HEIGHT = 250; const SITE_SHELL_ROOT_SELECTOR = '[data-tutor-learning-site-shell], [data-tutor-dashboard-site-shell]'; @@ -9,9 +10,21 @@ const SITE_SHELL_ROOT_SELECTOR = '[data-tutor-learning-site-shell], [data-tutor- * Returns the lowest visible bottom edge among the site header and admin bar. */ const getVisibleHeaderBoundary = (elements: HTMLElement[]): number => { - return elements.reduce((offset, element) => { - return Math.max(offset, element.getBoundingClientRect().bottom, 0); + const boundary = elements.reduce((offset, element) => { + const style = window.getComputedStyle(element); + if (style.display === 'none' || style.visibility === 'hidden') { + return offset; + } + + const rect = element.getBoundingClientRect(); + if (rect.bottom <= 0 || rect.top > MAX_HEADER_HEIGHT) { + return offset; + } + + return Math.max(offset, rect.bottom); }, 0); + + return Math.min(Math.round(boundary), MAX_HEADER_HEIGHT); }; class SiteShellController { @@ -29,6 +42,7 @@ class SiteShellController { this.updateOffset(); const isSticky = this.isStickyHeader(this.themeHeader); + const hasAdminBar = Boolean(document.getElementById(WP_ADMIN_BAR_ID)); if (isSticky && this.themeHeader && typeof ResizeObserver !== 'undefined') { this.resizeObserver = new ResizeObserver(() => this.scheduleOffsetUpdate()); @@ -37,7 +51,7 @@ class SiteShellController { window.addEventListener('resize', this.scheduleOffsetUpdate); - if (isSticky) { + if (isSticky || hasAdminBar) { window.addEventListener('scroll', this.scheduleOffsetUpdate, { passive: true }); } } @@ -76,12 +90,12 @@ class SiteShellController { } const position = window.getComputedStyle(header).position; - if (position === 'fixed' || position === 'sticky') { + if (position === 'fixed' || position === 'sticky' || position === 'absolute') { return true; } const className = (header.className || '').toString(); - return /sticky/i.test(className) || header.hasAttribute('data-sticky'); + return /sticky|transparent/i.test(className) || header.hasAttribute('data-sticky'); } private getStickyHeader(header: HTMLElement | null): HTMLElement | null { @@ -89,27 +103,28 @@ class SiteShellController { return null; } - const position = window.getComputedStyle(header).position; - if (position === 'fixed' || position === 'sticky') { + const isHeaderBar = (el: HTMLElement): boolean => { + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden') { + return false; + } + const rect = el.getBoundingClientRect(); + // Must have realistic header bar dimensions (reject full-screen mobile drawers/modals > 250px) + return rect.height > 0 && rect.height <= MAX_HEADER_HEIGHT && rect.width >= window.innerWidth * 0.4; + }; + + if (this.isStickyHeader(header) && isHeaderBar(header)) { return header; } // Check if an inner row is fixed or sticky (e.g. Sydney, Astra) - const children = header.querySelectorAll('*'); + const children = header.querySelectorAll( + 'div, nav, header, [class*="header"], [class*="sticky"], [class*="navbar"], [class*="shfb"]', + ); for (let i = 0; i < children.length; i++) { - const pos = window.getComputedStyle(children[i]).position; - if (pos === 'fixed' || pos === 'sticky') { - return children[i]; - } - } - - // If the header has sticky classes (e.g. TutorStarter .header-sticky, Sydney .has-sticky-header) - // and is active in the viewport - const className = (header.className || '').toString(); - if (/sticky/i.test(className) || header.hasAttribute('data-sticky')) { - const rect = header.getBoundingClientRect(); - if (rect.top <= 50 && rect.bottom > 0) { - return header; + const child = children[i]; + if (this.isStickyHeader(child) && isHeaderBar(child)) { + return child; } } diff --git a/assets/src/scss/frontend/dashboard/layout/_account.scss b/assets/src/scss/frontend/dashboard/layout/_account.scss index e125499e75..dad64d9b5a 100644 --- a/assets/src/scss/frontend/dashboard/layout/_account.scss +++ b/assets/src/scss/frontend/dashboard/layout/_account.scss @@ -5,6 +5,8 @@ --tutor-site-header-offset: 0px; --tutor-site-header-overlay-offset: 0px; + width: 100%; + .tutor-account-header { position: sticky; top: 0; @@ -36,13 +38,10 @@ &.tutor-has-site-shell { padding-top: var(--tutor-site-header-overlay-offset); - - .tutor-account-header { - top: var(--tutor-site-header-offset); - } } } +.tutor-account-page-wrapper.tutor-has-site-shell .tutor-account-header, body:has(#wpadminbar) .tutor-account-page-wrapper.tutor-has-site-shell .tutor-account-header { top: var(--tutor-site-header-offset); } diff --git a/assets/src/scss/frontend/dashboard/layout/_header.scss b/assets/src/scss/frontend/dashboard/layout/_header.scss index 15691ea745..bfebf2e43b 100644 --- a/assets/src/scss/frontend/dashboard/layout/_header.scss +++ b/assets/src/scss/frontend/dashboard/layout/_header.scss @@ -281,10 +281,7 @@ } } -.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-header { - top: var(--tutor-site-header-offset); -} - +.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-header, body:has(#wpadminbar) .tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-header { top: var(--tutor-site-header-offset); } diff --git a/assets/src/scss/frontend/dashboard/layout/_layout.scss b/assets/src/scss/frontend/dashboard/layout/_layout.scss index b594b2084f..a20ab02264 100644 --- a/assets/src/scss/frontend/dashboard/layout/_layout.scss +++ b/assets/src/scss/frontend/dashboard/layout/_layout.scss @@ -8,6 +8,7 @@ --tutor-site-header-offset: 0px; --tutor-site-header-overlay-offset: 0px; + width: 100%; display: grid; grid-template-columns: 274px 1fr; @@ -42,6 +43,8 @@ --tutor-site-header-offset: 0px; --tutor-site-header-overlay-offset: 0px; + width: 100%; + &.tutor-has-site-shell { padding-top: var(--tutor-site-header-overlay-offset); } diff --git a/assets/src/scss/frontend/dashboard/layout/_profile-header.scss b/assets/src/scss/frontend/dashboard/layout/_profile-header.scss index 10aca1623a..24927020cc 100644 --- a/assets/src/scss/frontend/dashboard/layout/_profile-header.scss +++ b/assets/src/scss/frontend/dashboard/layout/_profile-header.scss @@ -55,10 +55,7 @@ } } -.tutor-has-site-shell .tutor-profile-header { - top: var(--tutor-site-header-offset); -} - +.tutor-has-site-shell .tutor-profile-header, body:has(#wpadminbar) .tutor-has-site-shell .tutor-profile-header { top: var(--tutor-site-header-offset); } diff --git a/assets/src/scss/frontend/dashboard/layout/_sidebar.scss b/assets/src/scss/frontend/dashboard/layout/_sidebar.scss index 51f61a71f4..49bed3bab5 100644 --- a/assets/src/scss/frontend/dashboard/layout/_sidebar.scss +++ b/assets/src/scss/frontend/dashboard/layout/_sidebar.scss @@ -120,11 +120,7 @@ } } -.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-sidebar { - top: var(--tutor-site-header-offset); - min-height: calc(100dvh - var(--tutor-site-header-offset)); -} - +.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-sidebar, body:has(#wpadminbar) .tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-sidebar { top: var(--tutor-site-header-offset); min-height: calc(100dvh - var(--tutor-site-header-offset)); diff --git a/assets/src/scss/frontend/learning-area/_quiz.scss b/assets/src/scss/frontend/learning-area/_quiz.scss index 2ac0d11033..eae1d08bc2 100644 --- a/assets/src/scss/frontend/learning-area/_quiz.scss +++ b/assets/src/scss/frontend/learning-area/_quiz.scss @@ -555,18 +555,8 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8} } &[data-has-scroll-left='true'][data-has-scroll-right='false'] { - -webkit-mask-image: linear-gradient( - to right, - transparent 0, - black #{$tutor-spacing-10}, - black 100% - ); - mask-image: linear-gradient( - to right, - transparent 0, - black #{$tutor-spacing-10}, - black 100% - ); + -webkit-mask-image: linear-gradient(to right, transparent 0, black #{$tutor-spacing-10}, black 100%); + mask-image: linear-gradient(to right, transparent 0, black #{$tutor-spacing-10}, black 100%); } &[data-has-scroll-left='false'][data-has-scroll-right='true'] { @@ -576,12 +566,7 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8} black calc(100% - #{$tutor-spacing-10}), transparent 100% ); - mask-image: linear-gradient( - to right, - black 0, - black calc(100% - #{$tutor-spacing-10}), - transparent 100% - ); + mask-image: linear-gradient(to right, black 0, black calc(100% - #{$tutor-spacing-10}), transparent 100%); } &::-webkit-scrollbar { @@ -1299,7 +1284,7 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8} display: block; // Cap against the questions slot (size container), not a stretched card. // Interpolate the whole value so Sass does not strip the outer calc(). - max-height: #{"min(744px, calc(100cqh - (#{$tutor-quiz-draw-pin-chrome})))"}; + max-height: #{'min(744px, calc(100cqh - (#{$tutor-quiz-draw-pin-chrome})))'}; max-width: 100%; width: auto; height: auto; @@ -1471,13 +1456,16 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8} // An active quiz is normally an isolated, viewport-fixed screen. When the // Learning Area renders the theme shell, keep its controls inside the quiz // instead so the theme footer remains in document flow after the attempt. -.tutor-learning-area.tutor-has-site-shell { +.tutor-learning-area.tutor-has-site-shell, +body:has(#wpadminbar) .tutor-learning-area.tutor-has-site-shell { .tutor-quiz-header { position: sticky; top: var(--tutor-site-header-offset); } .tutor-quiz-submission { + padding-top: 0; + &[data-question-layout-view='single_question'], &[data-question-layout-view='question_pagination'] { height: calc(100dvh - var(--tutor-site-header-overlay-offset)); @@ -1505,30 +1493,3 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8} position: absolute; } } - -// Keep the site-shell geometry authoritative when the WordPress admin bar is -// present; the isolated-mode admin-bar selectors above intentionally win in -// every other context. -body:has(#wpadminbar) .tutor-learning-area.tutor-has-site-shell { - .tutor-quiz-header { - position: sticky; - top: var(--tutor-site-header-offset); - } - - .tutor-quiz-submission { - &[data-question-layout-view='single_question'], - &[data-question-layout-view='question_pagination'] { - height: calc(100dvh - var(--tutor-site-header-overlay-offset)); - } - } - - .tutor-quiz-questions { - &[data-question-layout-view='single_question'], - &[data-question-layout-view='question_pagination'] { - top: calc(#{$tutor-quiz-header-offset} + var(--tutor-site-header-overlay-offset)); - height: calc( - 100% - #{$tutor-quiz-header-offset} - #{$tutor-quiz-footer-offset} - var(--tutor-site-header-overlay-offset) - ); - } - } -} diff --git a/assets/src/scss/frontend/learning-area/layout/_layout.scss b/assets/src/scss/frontend/learning-area/layout/_layout.scss index b84d264b44..b96c6a24e1 100644 --- a/assets/src/scss/frontend/learning-area/layout/_layout.scss +++ b/assets/src/scss/frontend/learning-area/layout/_layout.scss @@ -6,6 +6,10 @@ $tutor-fixed-footer-height: 80px; $tutor-fixed-footer-offset: $tutor-spacing-6; +.tutor-learning-page { + width: 100%; +} + .tutor-learning-area { --tutor-site-header-offset: 0px; --tutor-site-header-overlay-offset: 0px; @@ -27,6 +31,14 @@ $tutor-fixed-footer-offset: $tutor-spacing-6; padding-inline-start: 0; } + .tutor-learning-area-content { + padding-inline-end: $tutor-spacing-4; + } + + &.is-fullscreen .tutor-learning-area-content { + padding-inline: $tutor-spacing-4; + } + &.tutor-has-site-shell { // Sticky positioning offsets the header visually without reserving that // offset in normal flow. Reserve only the theme-header portion that @@ -35,6 +47,7 @@ $tutor-fixed-footer-offset: $tutor-spacing-6; .tutor-learning-area-content { padding-top: 0; + padding-inline-end: $tutor-spacing-4; } @include tutor-breakpoint-up(lg) { @@ -57,6 +70,7 @@ $tutor-fixed-footer-offset: $tutor-spacing-6; .tutor-learning-area-content { grid-column: 2; min-width: 0; + padding-inline-end: $tutor-spacing-4; } } @@ -67,6 +81,10 @@ $tutor-fixed-footer-offset: $tutor-spacing-6; top: var(--tutor-site-header-offset); height: calc(100dvh - var(--tutor-site-header-offset)); } + + .tutor-learning-area-content { + padding-inline-end: 0; + } } } @@ -82,6 +100,7 @@ $tutor-fixed-footer-offset: $tutor-spacing-6; .tutor-learning-area-content { grid-column: 2; + padding-inline: $tutor-spacing-4; } } } diff --git a/classes/Utils.php b/classes/Utils.php index 0cc13bc25e..1b19b74648 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -9067,6 +9067,9 @@ public function tutor_custom_header( bool $show = true ) { if ( $show ) { if ( $is_block_theme ) { + $theme = wp_get_theme(); + $theme_slug = $theme->get( 'TextDomain' ); + $header_content = do_blocks( '' ); ?> > @@ -9078,9 +9081,7 @@ public function tutor_custom_header( bool $show = true ) {
get( 'TextDomain' ); - echo do_blocks( '' ); + echo $header_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { get_header(); } @@ -9127,8 +9128,16 @@ public function tutor_custom_footer( bool $show = true ) { if ( $is_block_theme ) { $theme = wp_get_theme(); $theme_slug = $theme->get( 'TextDomain' ); - echo do_blocks( '' ); + echo do_blocks( '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
'; + + if ( function_exists( 'wp_style_engine_get_stylesheet_from_context' ) ) { + $late_styles = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); + if ( ! empty( $late_styles ) ) { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + wp_footer(); echo ''; echo '';