Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 55 additions & 22 deletions assets/src/js/frontend/services/site-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,13 @@ 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.
*
* A normal-flow header contributes while it is visible, then naturally drops
* out of the boundary as the document scrolls. Fixed and sticky headers keep
* contributing because their bottom edge remains in the viewport.
*/
const getVisibleHeaderBoundary = (elements: HTMLElement[]): number => {
return elements.reduce((offset, element) => {
return Math.max(offset, element.getBoundingClientRect().bottom, 0);
}, 0);
};

/**
* A static (normal-flow) header only needs its computed position checked,
* not its inline style, since positioning is almost always set via CSS.
*/
const isStaticPosition = (element: HTMLElement): boolean => {
return window.getComputedStyle(element).position === 'static';
};

class SiteShellController {
private animationFrame: number | null = null;
private resizeObserver: ResizeObserver | null = null;
Expand All @@ -40,20 +28,18 @@ class SiteShellController {
this.themeHeader = this.findThemeHeader();
this.updateOffset();

// A normal-flow (static) header naturally drops out of the visible
// boundary once scrolled past — its contribution to updateOffset()
// only matters on first paint, so skip the ongoing listeners/observer.
if (this.themeHeader && isStaticPosition(this.themeHeader)) {
return;
}
const isSticky = this.isStickyHeader(this.themeHeader);

if (this.themeHeader && typeof ResizeObserver !== 'undefined') {
if (isSticky && this.themeHeader && typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(() => this.scheduleOffsetUpdate());
this.resizeObserver.observe(this.themeHeader);
}

window.addEventListener('resize', this.scheduleOffsetUpdate);
window.addEventListener('scroll', this.scheduleOffsetUpdate, { passive: true });

if (isSticky) {
window.addEventListener('scroll', this.scheduleOffsetUpdate, { passive: true });
}
}

destroy(): void {
Expand All @@ -78,12 +64,58 @@ class SiteShellController {

try {
const header = document.querySelector(this.themeHeaderSelector);
return header instanceof HTMLElement ? header : null;
return header instanceof HTMLElement && !this.root.contains(header) ? header : null;
} catch {
return null;
}
}

private isStickyHeader(header: HTMLElement | null): boolean {
if (!header) {
return false;
}

const position = window.getComputedStyle(header).position;
if (position === 'fixed' || position === 'sticky') {
return true;
}

const className = (header.className || '').toString();
return /sticky/i.test(className) || header.hasAttribute('data-sticky');
}

private getStickyHeader(header: HTMLElement | null): HTMLElement | null {
if (!header) {
return null;
}

const position = window.getComputedStyle(header).position;
if (position === 'fixed' || position === 'sticky') {
return header;
}

// Check if an inner row is fixed or sticky (e.g. Sydney, Astra)
const children = header.querySelectorAll<HTMLElement>('*');
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;
}
}

return null;
}

private scheduleOffsetUpdate = (): void => {
if (this.animationFrame !== null) {
return;
Expand All @@ -97,7 +129,8 @@ class SiteShellController {

private updateOffset(): void {
const adminBar = document.getElementById(WP_ADMIN_BAR_ID);
const elements = [this.themeHeader, adminBar].filter(
const stickyHeader = this.getStickyHeader(this.themeHeader);
const elements = [stickyHeader, adminBar].filter(
(element): element is HTMLElement => element instanceof HTMLElement,
);
const offset = getVisibleHeaderBoundary(elements);
Expand Down
2 changes: 1 addition & 1 deletion classes/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
*
* @var string
*/
private const THEME_HEADER_SELECTOR = 'header[role="banner"], header[role="heading"], header.site-header, header#masthead, #masthead, .site-header, header.wp-block-template-part, header.header-sticky';
private const THEME_HEADER_SELECTOR = 'header[role="banner"], header[role="heading"], header.site-header, header#masthead, #masthead, .site-header, #site-header, header.wp-block-template-part, header.header-sticky, header.has-sticky-header, header.shfb, .shfb-header, .shfb-sticky-header, .ast-primary-header-bar, .main-header-bar, #main-header, .et-l--header, .ct-header, #rs-header, .rs-header, .full-width-header, .header-area, .navbar-area, .ecademy-nav, .edali-nav, .turitor-header, .edubin-header, .edumodo-header, .bb-header, #bb-header, .elementor-location-header, header';

/**
* Store Shortcode Object
Expand Down Expand Up @@ -466,7 +466,7 @@

if ( isset( $target_dashboard_page['login_require'] ) && false === $target_dashboard_page['login_require'] ) {
$template = tutor_load_template_part( "template-part.{$dashboard_page}" );
} else {

Check failure on line 469 in classes/Template.php

View workflow job for this annotation

GitHub Actions / WPCS

If control structure block found as the only statement within an &quot;else&quot; block. Use elseif instead.

/**
* Load view page based on dashboard Endpoint
Expand Down
Loading