diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 80794990713ff..ed977deb42b1e 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -178,9 +178,6 @@ $z-layers: ( // Appear under the customizer heading UI, but over anything else. ".customize-widgets__topbar": 8, - - // Appear under the topbar. - ".customize-widgets__block-toolbar": 7, ); @function z-index( $key ) { diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index a8a19f6f83021..9a729dbef0ff0 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -244,6 +244,8 @@ _Parameters_ - _$0_ `Object`: Props. - _$0.children_ `Object`: The block content and style container. - _$0.\_\_unstableContentRef_ `Object`: Ref holding the content scroll container. +- _$0.\_\_experimentalStickyTop_ `number`: Top sticky position offset of floating and top toolbar. +- _$0.\_\_experimentalStickier_ `boolean`: Favor sticky position even if the block is out of view. # **BlockVerticalAlignmentControl** diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js index a8e55abe6f408..460d6fc5ded5c 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -17,7 +17,13 @@ import NavigableToolbar from '../navigable-toolbar'; import BlockToolbar from '../block-toolbar'; import { store as blockEditorStore } from '../../store'; -function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { +function BlockContextualToolbar( { + focusOnMount, + isFixed, + __experimentalStickyTop, + style = {}, + ...props +} ) { const { blockType, hasParents, showParentSelector } = useSelect( ( select ) => { const { @@ -59,12 +65,17 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { 'is-fixed': isFixed, } ); + if ( __experimentalStickyTop ) { + style.top = __experimentalStickyTop; + } + return ( diff --git a/packages/block-editor/src/components/block-tools/block-popover.js b/packages/block-editor/src/components/block-tools/block-popover.js index 8371cfff9bbba..e789c810cee58 100644 --- a/packages/block-editor/src/components/block-tools/block-popover.js +++ b/packages/block-editor/src/components/block-tools/block-popover.js @@ -54,6 +54,8 @@ function BlockPopover( { capturingClientId, __unstablePopoverSlot, __unstableContentRef, + __experimentalStickyTop, + __experimentalStickier, } ) { const { isNavigationMode, @@ -213,6 +215,8 @@ function BlockPopover( { // Observe movement for block animations (especially horizontal). __unstableObserveElement={ node } shouldAnchorIncludePadding + __experimentalStickyTop={ __experimentalStickyTop } + __experimentalStickier={ __experimentalStickier } > { ( shouldShowContextualToolbar || isToolbarForced ) && (
); } diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 04302c68e173f..1bc56f65e701b 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -19,11 +19,20 @@ import { usePopoverScroll } from './use-popover-scroll'; * insertion point and a slot for the inline rich text toolbar). Must be wrapped * around the block content and editor styles wrapper or iframe. * - * @param {Object} $0 Props. - * @param {Object} $0.children The block content and style container. - * @param {Object} $0.__unstableContentRef Ref holding the content scroll container. + * @param {Object} $0 Props. + * @param {Object} $0.children The block content and style container. + * @param {Object} $0.__unstableContentRef Ref holding the content scroll container. + * @param {number} $0.__experimentalStickyTop Top sticky position offset of floating and + * top toolbar. + * @param {boolean} $0.__experimentalStickier Favor sticky position even if the block is + out of view. */ -export default function BlockTools( { children, __unstableContentRef } ) { +export default function BlockTools( { + children, + __unstableContentRef, + __experimentalStickyTop, + __experimentalStickier, +} ) { const isLargeViewport = useViewportMatch( 'medium' ); const hasFixedToolbar = useSelect( ( select ) => select( blockEditorStore ).getSettings().hasFixedToolbar, @@ -33,11 +42,18 @@ export default function BlockTools( { children, __unstableContentRef } ) { return ( { ( hasFixedToolbar || ! isLargeViewport ) && ( - + ) } { /* Even if the toolbar is fixed, the block popover is still needed for navigation mode. */ } - + { /* Used for the inline rich text toolbar. */ } { + let isInView; + // If the anchor is singular, it is in view if the entry + // reports it so. Otherwise, there are two elements to + // consider and unless both are outside the same side of + // the bounds the anchor is still in view. + if ( ! isAnchorCompound ) { + isInView = entry.isIntersecting; + } else { + const rect = entry.boundingClientRect; + isInView = + entry.target === anchorRef.top + ? rect.top < entry.rootBounds.bottom + : rect.bottom > entry.rootBounds.top; + } + setAttribute( + containerRef.current, + 'data-away', + ! isInView ? 'true' : 'false' + ); + }, + { + root: __unstableStickyBoundaryElement, + rootMargin: `-${ __experimentalStickyTop }px 0px 0px 0px`, + } + ); + if ( isAnchorCompound ) { + inViewObserver.observe( anchorRef.top ); + inViewObserver.observe( anchorRef.bottom ); + } else { + inViewObserver.observe( anchorRef ); + } + } + return () => { defaultView.clearInterval( intervalHandle ); defaultView.removeEventListener( 'resize', refresh ); @@ -466,6 +511,10 @@ const Popover = ( if ( observer ) { observer.disconnect(); } + + // Stickier related cleanup + inViewObserver?.disconnect(); + setAttribute( containerRef.current, 'data-away' ); }; }, [ isExpanded, @@ -476,6 +525,8 @@ const Popover = ( position, contentSize, __unstableStickyBoundaryElement, + __experimentalStickyTop, + __experimentalStickier, __unstableObserveElement, __unstableBoundaryParent, ] ); diff --git a/packages/components/src/popover/utils.js b/packages/components/src/popover/utils.js index 25af0b8245507..3cce1c6a8157a 100644 --- a/packages/components/src/popover/utils.js +++ b/packages/components/src/popover/utils.js @@ -166,9 +166,13 @@ export function computePopoverXAxisPosition( * switching between sticky and normal * position. * @param {Element} anchorRef The anchor element. - * @param {Element} relativeOffsetTop If applicable, top offset of the + * @param {number} relativeOffsetTop If applicable, top offset of the * relative positioned parent container. * @param {boolean} forcePosition Don't adjust position based on anchor. + * @param {number} stickyTop Sticky top position offset from the + * boundaryElement. + * @param {boolean} stickier Favor sticky position even if the + * anchor is out of view. * * @return {Object} Popover xAxis position and constraints. */ @@ -180,18 +184,27 @@ export function computePopoverYAxisPosition( stickyBoundaryElement, anchorRef, relativeOffsetTop, - forcePosition + forcePosition, + stickyTop, + stickier ) { const { height } = contentSize; if ( stickyBoundaryElement ) { const stickyRect = stickyBoundaryElement.getBoundingClientRect(); - const stickyPosition = stickyRect.top + height - relativeOffsetTop; - - if ( anchorRect.top <= stickyPosition ) { + const top = stickyTop + stickyRect.top + height - relativeOffsetTop; + const bottom = stickyRect.bottom - relativeOffsetTop; + if ( anchorRect.top < top ) { + const end = stickier ? Infinity : anchorRect.bottom; return { yAxis, - popoverTop: Math.min( anchorRect.bottom, stickyPosition ), + popoverTop: Math.min( end, top ), + }; + } else if ( anchorRect.top > bottom ) { + const start = stickier ? -Infinity : anchorRect.top; + return { + yAxis, + popoverTop: Math.max( start, bottom ), }; } } @@ -287,7 +300,11 @@ export function computePopoverYAxisPosition( * relative positioned parent container. * @param {Element} boundaryElement Boundary element. * @param {boolean} forcePosition Don't adjust position based on anchor. - * @param {boolean} forceXAlignment Don't adjust alignment based on YAxis + * @param {boolean} forceXAlignment Don't adjust alignment based on YAxis. + * @param {number} stickyTop Sticky top position offset from the + * boundaryElement. + * @param {boolean} stickier Favor sticky position even if the + * anchor is out of view. * * @return {Object} Popover position and constraints. */ @@ -300,7 +317,9 @@ export function computePopoverPosition( relativeOffsetTop, boundaryElement, forcePosition, - forceXAlignment + forceXAlignment, + stickyTop = 0, + stickier = false ) { const [ yAxis, xAxis = 'center', corner ] = position.split( ' ' ); @@ -312,7 +331,9 @@ export function computePopoverPosition( stickyBoundaryElement, anchorRef, relativeOffsetTop, - forcePosition + forcePosition, + stickyTop, + stickier ); const xAxisPosition = computePopoverXAxisPosition( anchorRect, diff --git a/packages/customize-widgets/src/components/header/style.scss b/packages/customize-widgets/src/components/header/style.scss index 1faf5fbe5ae46..259f779930f41 100644 --- a/packages/customize-widgets/src/components/header/style.scss +++ b/packages/customize-widgets/src/components/header/style.scss @@ -1,4 +1,7 @@ .customize-widgets-header { + position: sticky; + top: 0; + @include break-medium() { // Make space for the floating toolbar. margin-bottom: $grid-unit-60 + $default-block-margin; diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js index 40592021000af..293526d42aa38 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/index.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -33,6 +33,9 @@ import { store as customizeWidgetsStore } from '../../store'; import WelcomeGuide from '../welcome-guide'; import KeyboardShortcuts from '../keyboard-shortcuts'; +// Used to offset the block toolbar’s sticky top +const headerHeight = 49; + export default function SidebarBlockEditor( { blockEditorSettings, sidebar, @@ -113,7 +116,10 @@ export default function SidebarBlockEditor( { isFixedToolbarActive={ isFixedToolbarActive } /> - + diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/style.scss b/packages/customize-widgets/src/components/sidebar-block-editor/style.scss index abbe6b8445410..b2accad8ae274 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/style.scss +++ b/packages/customize-widgets/src/components/sidebar-block-editor/style.scss @@ -9,10 +9,8 @@ // Scroll sideways. overflow-y: auto; - z-index: z-index(".customize-widgets__block-toolbar"); } .customize-control-sidebar_block_editor .block-editor-block-list__block-popover { position: fixed; - z-index: z-index(".customize-widgets__block-toolbar"); } diff --git a/packages/customize-widgets/src/controls/style.scss b/packages/customize-widgets/src/controls/style.scss index 57bc56d9fcbbf..de905da202a15 100644 --- a/packages/customize-widgets/src/controls/style.scss +++ b/packages/customize-widgets/src/controls/style.scss @@ -12,13 +12,13 @@ // Override the inline styles added via JS that make the section title // sticky feature work. The customize widget block-editor disables this // sticky title. - padding-top: 10px !important; + padding-top: 12px !important; .customize-section-title { // Disable the sticky title. `!important` as this overrides inline // styles added via JavaScript. position: static !important; - top: 0 !important; width: unset !important; + margin-top: -12px !important; } }