From 5fb3d3f279caeff9ef5b769ed0c631a41360b4a0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 21 Oct 2016 06:36:57 +0000 Subject: [PATCH] Customize: Add sticky headers for panels and sections. Includes autoprefixing of CSS. Props delawski, celloexpressions. See #35186. Fixes #34343. git-svn-id: https://develop.svn.wordpress.org/trunk@38853 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/customize-controls.css | 63 ++++++++- src/wp-admin/css/customize-nav-menus.css | 5 + src/wp-admin/js/customize-controls.js | 171 +++++++++++++++++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index 96aba5455c..39dbb94316 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -57,6 +57,29 @@ body { margin-bottom: 15px; } +#customize-controls .customize-info.is-in-view, +#customize-controls .customize-section-title.is-in-view { + position: absolute; + z-index: 9; + width: 100%; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, .1); + box-shadow: 0 1px 0 rgba(0, 0, 0, .1); +} + +#customize-controls .customize-section-title.is-in-view { + margin-top: 0; +} + +#customize-controls .customize-info.is-in-view + .accordion-section { + margin-top: 15px; +} + +#customize-controls .customize-info.is-sticky, +#customize-controls .customize-section-title.is-sticky { + position: fixed; + top: 46px; +} + #customize-controls .customize-info .accordion-section-title { background: #fff; color: #555; @@ -325,6 +348,10 @@ body { padding: 12px; } +#customize-theme-controls .customize-pane-child.menu li { + position: static; +} + .customize-section-description-container { margin-bottom: 15px; } @@ -409,7 +436,7 @@ h3.customize-section-title { display: block; float: left; width: 48px; - height: 70px; + height: 71px; padding: 0 24px 0 0; margin: 0; background: #fff; @@ -423,7 +450,7 @@ h3.customize-section-title { } .customize-section-back { - height: 73px; + height: 74px; } .ios .customize-panel-back { @@ -484,6 +511,7 @@ h3.customize-section-title { } .wp-full-overlay-sidebar .wp-full-overlay-header { + background-color: #eee; -webkit-transition: padding ease-in-out .18s; transition: padding ease-in-out .18s; } @@ -1088,11 +1116,14 @@ p.customize-section-description { position: fixed; top: 0; left: 0; + -webkit-transition: .18s left ease-in-out; transition: .18s left ease-in-out; margin: 0 0 0 300px; padding:25px; overflow-y: scroll; + width: -webkit-calc(100% - 350px); width: calc(100% - 350px); + height: -webkit-calc(100% - 50px); height: calc(100% - 50px); background: #eee; z-index: 20; @@ -1104,12 +1135,14 @@ p.customize-section-description { #customize-header-actions .customize-controls-preview-toggle { position: relative; top: 0; + -webkit-transition: .18s top ease-in-out; transition: .18s top ease-in-out; } #customize-footer-actions, #customize-footer-actions .collapse-sidebar { bottom: 0; + -webkit-transition: .18s bottom ease-in-out; transition: .18s bottom ease-in-out; } @@ -1146,6 +1179,17 @@ p.customize-section-description { } /* Adds a delay before fading in to avoid it "jumping" */ +@-webkit-keyframes themes-fade-in { + 0% { + opacity: 0; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } +} @keyframes themes-fade-in { 0% { opacity: 0; @@ -1159,10 +1203,12 @@ p.customize-section-description { } .control-panel-themes .customize-themes-full-container.animate { + -webkit-animation: .6s themes-fade-in 1; animation: .6s themes-fade-in 1; } .in-themes-panel:not(.animating) .control-panel-themes .filter-themes-count { + -webkit-animation: .6s themes-fade-in 1; animation: .6s themes-fade-in 1; } @@ -1205,6 +1251,7 @@ p.customize-section-description { } .control-panel-themes .filter-themes-count { + width: -webkit-calc(100% - 93px); width: calc(100% - 93px); } @@ -1236,9 +1283,11 @@ p.customize-section-description { } .control-panel-themes .customize-themes-full-container { + width: -webkit-calc(100% - 50px); width: calc(100% - 50px); margin: 0; top: 46px; + height: -webkit-calc(100% - 96px); height: calc(100% - 96px); z-index: 1; display: none; @@ -1273,6 +1322,7 @@ p.customize-section-description { .control-panel-themes .customize-themes-section-title { width: 100%; background: #fff; + -webkit-box-shadow: none; box-shadow: none; outline: none; border-top: none; @@ -1304,10 +1354,13 @@ p.customize-section-description { .control-panel-themes .theme-section .customize-themes-section-title.selected:after { content: "\f147"; font: 16px/1 dashicons; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; box-sizing: border-box; width: 20px; height: 20px; padding: 3px 3px 1px 1px; /* Re-align the icon to the smaller grid */ + -webkit-border-radius: 100%; border-radius: 100%; position: absolute; top: 9px; @@ -1376,6 +1429,7 @@ p.customize-section-description { background: transparent; border: none; padding: 0; + -webkit-box-shadow: none; box-shadow: none; } @@ -1406,6 +1460,7 @@ p.customize-section-description { color: #0073aa; border-bottom-color: #0073aa; /* Color change for focus style should be acceptable because border-bottom is barely visible previously. */ outline: none; + -webkit-box-shadow: none; box-shadow: none; } @@ -1425,6 +1480,7 @@ p.customize-section-description { color: #555d66; margin: 0; padding: 12px 10px 12px 34px; + width: -webkit-calc(100% - 46px); width: calc(100% - 46px); line-height: 16px; font-weight: 600; @@ -1453,6 +1509,7 @@ p.customize-section-description { clear: both; width: 20px; height: 20px; + left: -webkit-calc(50% - 10px); left: calc(50% - 10px); float: none; margin-top: 50px; @@ -1488,6 +1545,8 @@ p.customize-section-description { } .customize-control.customize-control-theme { /* override most properties on .customize-control */ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; box-sizing: border-box; width: 18.4%; margin: 0 2% 2% 0; diff --git a/src/wp-admin/css/customize-nav-menus.css b/src/wp-admin/css/customize-nav-menus.css index c6dd7244c1..1386fe0940 100644 --- a/src/wp-admin/css/customize-nav-menus.css +++ b/src/wp-admin/css/customize-nav-menus.css @@ -65,6 +65,10 @@ text-align: right; } +.wp-customizer .menu-item-handle:hover { + z-index: 8; +} + .customize-control-nav_menu_item.has-notifications .menu-item-handle { border-left: 4px solid #00a0d2; } @@ -72,6 +76,7 @@ .wp-customizer .menu-item-settings { max-width: 100%; overflow: hidden; + z-index: 8; padding: 10px; background: #eee; border: 1px solid #999; diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index e354d0decd..3508bf5652 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -1004,6 +1004,7 @@ content.addClass( 'open' ); overlay.addClass( 'section-open' ); + api.state( 'expandedSection' ).set( section ); }, this ); } @@ -1042,6 +1043,9 @@ content.removeClass( 'open' ); overlay.removeClass( 'section-open' ); + if ( section === api.state( 'expandedSection' ).get() ) { + api.state( 'expandedSection' ).set( false ); + } } else { if ( args.completeCallback ) { @@ -1995,6 +1999,7 @@ overlay.addClass( 'in-sub-panel' ); accordionSection.addClass( 'current-panel' ); + api.state( 'expandedPanel' ).set( panel ); } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) { panel._animateChangeExpanded( function() { @@ -2011,6 +2016,9 @@ overlay.removeClass( 'in-sub-panel' ); accordionSection.removeClass( 'current-panel' ); + if ( panel === api.state( 'expandedPanel' ).get() ) { + api.state( 'expandedPanel' ).set( false ); + } } }, @@ -4968,6 +4976,8 @@ activated = state.create( 'activated' ), processing = state.create( 'processing' ), paneVisible = state.create( 'paneVisible' ), + expandedPanel = state.create( 'expandedPanel' ), + expandedSection = state.create( 'expandedSection' ), changesetStatus = state.create( 'changesetStatus' ), previewerAlive = state.create( 'previewerAlive' ), populateChangesetUuidParam; @@ -5003,6 +5013,8 @@ activated( api.settings.theme.active ); processing( 0 ); paneVisible( true ); + expandedPanel( false ); + expandedSection( false ); previewerAlive( true ); changesetStatus( api.settings.changeset.status ); @@ -5156,6 +5168,165 @@ overlay.toggleClass( 'preview-only' ); }); + /* + * Sticky header feature. + */ + (function initStickyHeaders() { + var parentContainer = $( '.wp-full-overlay-sidebar-content' ), + changeContainer, getHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader, + activeHeader, lastScrollTop; + + // Determine which panel or section is currently expanded. + changeContainer = function( container ) { + var newInstance = container, + expandedSection = api.state( 'expandedSection' ).get(), + expandedPanel = api.state( 'expandedPanel' ).get(), + headerElement; + + // Release previously active header element. + if ( activeHeader && activeHeader.element ) { + releaseStickyHeader( activeHeader.element ); + } + + if ( ! newInstance ) { + if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) { + newInstance = expandedPanel; + } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) { + newInstance = expandedSection; + } else { + activeHeader = false; + return; + } + } + + headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first(); + if ( headerElement.length ) { + activeHeader = { + instance: newInstance, + element: headerElement, + parent: headerElement.closest( '.customize-pane-child' ), + height: getHeaderHeight( headerElement ) + }; + if ( expandedSection ) { + resetStickyHeader( activeHeader.element, activeHeader.parent ); + } + } else { + activeHeader = false; + } + }; + api.state( 'expandedSection' ).bind( changeContainer ); + api.state( 'expandedPanel' ).bind( changeContainer ); + + // Throttled scroll event handler. + parentContainer.on( 'scroll', _.throttle( function() { + if ( ! activeHeader ) { + return; + } + + var scrollTop = parentContainer.scrollTop(), + isScrollingUp = ( lastScrollTop ) ? scrollTop <= lastScrollTop : true; + + lastScrollTop = scrollTop; + positionStickyHeader( activeHeader, scrollTop, isScrollingUp ); + }, 8 ) ); + + // Release header element if it is sticky. + releaseStickyHeader = function( headerElement ) { + if ( ! headerElement.hasClass( 'is-sticky' ) ) { + return; + } + headerElement + .removeClass( 'is-sticky' ) + .addClass( 'maybe-sticky is-in-view' ) + .css( 'top', parentContainer.scrollTop() + 'px' ); + }; + + // Reset position of the sticky header. + resetStickyHeader = function( headerElement, headerParent ) { + headerElement + .removeClass( 'maybe-sticky is-in-view' ) + .css( { + width: '', + top: '' + } ); + headerParent.css( 'padding-top', '' ); + }; + + // Get header height. + getHeaderHeight = function( headerElement ) { + var height = headerElement.data( 'height' ); + if ( ! height ) { + height = headerElement.outerHeight(); + headerElement.data( 'height', height ); + } + return height; + }; + + // Reposition header on throttled `scroll` event. + positionStickyHeader = function( header, scrollTop, isScrollingUp ) { + var headerElement = header.element, + headerParent = header.parent, + headerHeight = header.height, + headerTop = parseInt( headerElement.css( 'top' ), 10 ), + maybeSticky = headerElement.hasClass( 'maybe-sticky' ), + isSticky = headerElement.hasClass( 'is-sticky' ), + isInView = headerElement.hasClass( 'is-in-view' ); + + // When scrolling down, gradually hide sticky header. + if ( ! isScrollingUp ) { + if ( isSticky ) { + headerTop = scrollTop; + headerElement + .removeClass( 'is-sticky' ) + .css( { + top: headerTop + 'px', + width: '' + } ); + } + if ( isInView && scrollTop > headerTop + headerHeight ) { + headerElement.removeClass( 'is-in-view' ); + headerParent.css( 'padding-top', '' ); + } + return; + } + + // Scrolling up. + if ( ! maybeSticky && scrollTop >= headerHeight ) { + maybeSticky = true; + headerElement.addClass( 'maybe-sticky' ); + } else if ( 0 === scrollTop ) { + // Reset header in base position. + headerElement + .removeClass( 'maybe-sticky is-in-view is-sticky' ) + .css( { + top: '', + width: '' + } ); + headerParent.css( 'padding-top', '' ); + return; + } + + if ( isInView && ! isSticky ) { + // Header is in the view but is not yet sticky. + if ( headerTop >= scrollTop ) { + // Header is fully visible. + headerElement + .addClass( 'is-sticky' ) + .css( { + top: '', + width: headerParent.outerWidth() + 'px' + } ); + } + } else if ( maybeSticky && ! isInView ) { + // Header is out of the view. + headerElement + .addClass( 'is-in-view' ) + .css( 'top', ( scrollTop - headerHeight ) + 'px' ); + headerParent.css( 'padding-top', headerHeight + 'px' ); + } + }; + }()); + // Previewed device bindings. api.previewedDevice = new api.Value();