From dfdb2e63cdb8d0cec097bb89315cc57df3e69b63 Mon Sep 17 00:00:00 2001 From: alistair3149 Date: Fri, 20 Dec 2024 18:31:03 -0500 Subject: [PATCH] =?UTF-8?q?feat(overflowElements):=20=E2=9C=A8=20add=20sti?= =?UTF-8?q?cky=20header=20for=20overflow=20elements=20(#989)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make a sticky header element that is within Citizen overflow content (e.g. table header), you can attach the `.citizen-overflow-sticky-header` class to the element (for table it would be the `` element). This is done through JS because `position: sticky` does not work with overflow. --- resources/mixins.less | 33 ++++++------ .../skins.citizen.scripts/overflowElements.js | 51 +++++++++++++++++++ .../components/OverflowElements.less | 9 ++++ .../components/StickyHeader.less | 12 +---- 4 files changed, 78 insertions(+), 27 deletions(-) diff --git a/resources/mixins.less b/resources/mixins.less index 238f2658e..19f6aedc8 100644 --- a/resources/mixins.less +++ b/resources/mixins.less @@ -52,10 +52,27 @@ top: var( --height-sticky-header ) !important; } +.citizen-sticky-header-background() { + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: @z-index-bottom; + height: 100%; + content: ''; + background-color: var( --color-surface-0 ); + filter: opacity( 0.9 ); + -webkit-backdrop-filter: saturate( 50% ) blur( 16px ); + backdrop-filter: saturate( 50% ) blur( 16px ); + } +} + .citizen-sticky-header(@bottomBorder: true, @zIndex: true) { position: -webkit-sticky; position: sticky; .sticky-header-element; + .citizen-sticky-header-background; & when (@bottomBorder ) { box-shadow: 0 1px 0 0 var( --border-color-base ); @@ -64,22 +81,6 @@ & when (@zIndex ) { z-index: @z-index-sticky; } - - // HACK: Hide overflow - // This has an issue if parent has overflow set - &::before { - position: absolute; - top: 0; - right: ~'calc( var(--padding-page ) * -1 )'; - left: ~'calc( var(--padding-page ) * -1 )'; - z-index: @z-index-bottom; - height: 100%; - content: ''; - background-color: var( --color-surface-0 ); - filter: opacity( 0.9 ); - -webkit-backdrop-filter: saturate( 50% ) blur( 16px ); - backdrop-filter: saturate( 50% ) blur( 16px ); - } } // To hide objects, but keep them accessible for screen-readers diff --git a/resources/skins.citizen.scripts/overflowElements.js b/resources/skins.citizen.scripts/overflowElements.js index ef40a3594..9d9eff150 100644 --- a/resources/skins.citizen.scripts/overflowElements.js +++ b/resources/skins.citizen.scripts/overflowElements.js @@ -14,6 +14,7 @@ class OverflowElement { this.contentWidth = 0; this.onScroll = mw.util.throttle( this.onScroll.bind( this ), 250 ); this.updateState = this.updateState.bind( this ); + this.headerToSticky = this.element.querySelector( '.citizen-overflow-sticky-header' ); } /** @@ -97,6 +98,9 @@ class OverflowElement { [ isRightOverflowing, 'citizen-overflow--right' ] ]; this.toggleClasses( updateClasses ); + if ( this.stickyHeader ) { + this.stickyHeader.style.setProperty( '--citizen-overflow-scroll-x', this.contentScrollLeft + 'px' ); + } } ); } @@ -182,6 +186,50 @@ class OverflowElement { } } + syncStickyHeaderColumns() { + const stickyCols = this.colgroup.querySelectorAll( 'col' ); + + this.originalTh.forEach( ( col, index ) => { + stickyCols[ index ].style.minWidth = col.getBoundingClientRect().width + 'px'; + } ); + } + + getStickyHeaderForTable() { + // If overflow content is a table, we need to create a proper table element + // for the sticky header, so that the styles are applied correctly + const stickyHeader = document.createElement( 'table' ); + const thead = document.createElement( 'thead' ); + + // Copy attributes from original table to sticky header + for ( const { name, value } of this.element.attributes ) { + stickyHeader.setAttribute( name, value ); + } + for ( const className of this.element.classList ) { + stickyHeader.classList.add( className ); + } + + // Create colgroup and columns so that we can align the column widths + this.colgroup = document.createElement( 'colgroup' ); + this.originalTh = this.headerToSticky.querySelectorAll( 'th' ); + for ( let i = 0; i < this.originalTh.length; i++ ) { + const colEl = document.createElement( 'col' ); + this.colgroup.append( colEl ); + } + this.syncStickyHeaderColumns(); + + thead.append( this.headerToSticky.cloneNode( true ) ); + stickyHeader.append( thead, this.colgroup ); + return stickyHeader; + } + + setupStickyHeader() { + const isTable = this.element instanceof HTMLTableElement; + this.stickyHeader = isTable ? this.getStickyHeaderForTable() : document.createElement( 'div' ); + this.stickyHeader.classList.add( 'citizen-overflow-content-sticky-header' ); + this.stickyHeader.setAttribute( 'aria-hidden', 'true' ); // this is not useful for screen reader + this.content.insertBefore( this.stickyHeader, this.element ); + } + /** * Scrolls the content element by the specified offset. * @@ -302,6 +350,9 @@ class OverflowElement { */ init() { this.wrap(); + if ( this.headerToSticky ) { + this.setupStickyHeader(); + } this.setupResizeObserver(); this.setupIntersectionObserver(); this.resume(); diff --git a/resources/skins.citizen.styles/components/OverflowElements.less b/resources/skins.citizen.styles/components/OverflowElements.less index 140747484..7aa9b6d8d 100644 --- a/resources/skins.citizen.styles/components/OverflowElements.less +++ b/resources/skins.citizen.styles/components/OverflowElements.less @@ -61,6 +61,15 @@ .citizen-overflow--left.citizen-overflow--right > & { .mask-gradient(90deg, transparent, #000 var( --overflow-gradient-size ), #000 ~'calc( 100% - var( --overflow-gradient-size ) )', transparent); } + + &-sticky-header { + --citizen-overflow-scroll-x: 0; // default value to be overriden + position: fixed; + top: 0; + border-bottom: var( --border-width-base ) solid var( --border-color-base ); + transform: ~'translate( calc( var( --citizen-overflow-scroll-x ) * -1 ), var( --height-sticky-header ) )'; + .citizen-sticky-header-background; + } } &-nav { diff --git a/resources/skins.citizen.styles/components/StickyHeader.less b/resources/skins.citizen.styles/components/StickyHeader.less index 50c14bf28..517102d82 100644 --- a/resources/skins.citizen.styles/components/StickyHeader.less +++ b/resources/skins.citizen.styles/components/StickyHeader.less @@ -16,19 +16,9 @@ .citizen-page-header { position: -webkit-sticky; position: sticky; + .citizen-sticky-header-background; &::before { - position: absolute; - top: 0; - right: 0; - left: 0; - z-index: @z-index-bottom; - height: 100%; - content: ''; - background-color: var( --color-surface-0 ); - filter: opacity( 0.9 ); - -webkit-backdrop-filter: saturate( 50% ) blur( 16px ); - backdrop-filter: saturate( 50% ) blur( 16px ); opacity: 0; transition: var( --transition-hover ); transition-property: opacity;