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;
}
}