From 9b2bc9fe291149d633feac18f3c8e188d41e32d1 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 27 Sep 2024 11:16:47 +0100 Subject: [PATCH 01/49] Edit Mode: Prevent editable text selection on first click --- packages/block-editor/src/store/selectors.js | 58 +++----------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 7bb002661565b..fdadea8c7b716 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2801,35 +2801,18 @@ export const __unstableGetVisibleBlocks = createSelector( ); export function __unstableHasActiveBlockOverlayActive( state, clientId ) { - // Prevent overlay on blocks with a non-default editing mode. If the mdoe is - // 'disabled' then the overlay is redundant since the block can't be - // selected. If the mode is 'contentOnly' then the overlay is redundant - // since there will be no controls to interact with once selected. - if ( getBlockEditingMode( state, clientId ) !== 'default' ) { - return false; - } - // If the block editing is locked, the block overlay is always active. if ( ! canEditBlock( state, clientId ) ) { return true; } - const editorMode = __unstableGetEditorMode( state ); - - // In zoom-out mode, the block overlay is always active for section level blocks. - if ( editorMode === 'zoom-out' ) { - const sectionRootClientId = getSectionRootClientId( state ); - if ( sectionRootClientId ) { - const sectionClientIds = getBlockOrder( - state, - sectionRootClientId - ); - if ( sectionClientIds?.includes( clientId ) ) { - return true; - } - } else if ( clientId && ! getBlockRootClientId( state, clientId ) ) { - return true; - } + // Section blocks need to be selected first before being able to select their children. + if ( + isSectionBlock( state, clientId ) && + ! isBlockSelected( state, clientId ) && + ! hasSelectedInnerBlock( state, clientId, true ) + ) { + return true; } // In navigation mode, the block overlay is active when the block is not @@ -2913,33 +2896,8 @@ export const getBlockEditingMode = createRegistrySelector( clientId = ''; } - // In zoom-out mode, override the behavior set by - // __unstableSetBlockEditingMode to only allow editing the top-level - // sections. const editorMode = __unstableGetEditorMode( state ); - if ( editorMode === 'zoom-out' ) { - const sectionRootClientId = getSectionRootClientId( state ); - - if ( clientId === '' /* ROOT_CONTAINER_CLIENT_ID */ ) { - return sectionRootClientId ? 'disabled' : 'contentOnly'; - } - if ( clientId === sectionRootClientId ) { - return 'contentOnly'; - } - const sectionsClientIds = getBlockOrder( - state, - sectionRootClientId - ); - - // Sections are always contentOnly. - if ( sectionsClientIds?.includes( clientId ) ) { - return 'contentOnly'; - } - - return 'disabled'; - } - - if ( editorMode === 'navigation' ) { + if ( [ 'navigation', 'zoom-out' ].includes( editorMode ) ) { const sectionRootClientId = getSectionRootClientId( state ); // The root section is "default mode" From a9c5e10cbd72ef0830a7cee04b7b1229a73a9d84 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 27 Sep 2024 12:27:56 +0100 Subject: [PATCH 02/49] Show zoom-out toolbar even when child blocks are selected --- .../src/components/block-tools/index.js | 29 ++++++++++++++----- .../block-tools/use-show-block-tools.js | 12 ++++---- .../src/store/private-selectors.js | 7 +++-- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 099323925384b..d038662455dea 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -34,18 +34,28 @@ function selector( select ) { getSettings, __unstableGetEditorMode, isTyping, - } = select( blockEditorStore ); + isSectionBlock, + getParentSectionBlock, + } = unlock( select( blockEditorStore ) ); const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); const editorMode = __unstableGetEditorMode(); + const isZoomOut = editorMode === 'zoom-out'; + let zoomOutToolbarClientId; + if ( isZoomOut ) { + zoomOutToolbarClientId = isSectionBlock( clientId ) + ? clientId + : getParentSectionBlock( clientId ); + } return { clientId, + zoomOutToolbarClientId, hasFixedToolbar: getSettings().hasFixedToolbar, isTyping: isTyping(), - isZoomOutMode: editorMode === 'zoom-out', + isZoomOutMode: isZoomOut, }; } @@ -63,10 +73,13 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const { clientId, hasFixedToolbar, isTyping, isZoomOutMode } = useSelect( - selector, - [] - ); + const { + clientId, + zoomOutToolbarClientId, + hasFixedToolbar, + isTyping, + isZoomOutMode, + } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getBlocksByClientId, @@ -221,10 +234,10 @@ export default function BlockTools( { /> ) } - { showZoomOutToolbar && ( + { showZoomOutToolbar && !! zoomOutToolbarClientId && ( ) } diff --git a/packages/block-editor/src/components/block-tools/use-show-block-tools.js b/packages/block-editor/src/components/block-tools/use-show-block-tools.js index 02a8f0583bcdd..abc70bb146987 100644 --- a/packages/block-editor/src/components/block-tools/use-show-block-tools.js +++ b/packages/block-editor/src/components/block-tools/use-show-block-tools.js @@ -8,6 +8,8 @@ import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; +import { isZoomOutMode } from '../../store/private-selectors'; /** * Source of truth for which block tools are showing in the block editor. @@ -24,7 +26,8 @@ export function useShowBlockTools() { getSettings, __unstableGetEditorMode, isTyping, - } = select( blockEditorStore ); + isSectionBlock, + } = unlock( select( blockEditorStore ) ); const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); @@ -42,12 +45,9 @@ export function useShowBlockTools() { editorMode === 'edit' && isEmptyDefaultBlock; const isZoomOut = editorMode === 'zoom-out'; - const _showZoomOutToolbar = - isZoomOut && - block?.attributes?.align === 'full' && - ! _showEmptyBlockSideInserter; + const _showZoomOutToolbar = isZoomOut; const _showBlockToolbarPopover = - ! _showZoomOutToolbar && + ( ! isZoomOutMode || ! isSectionBlock( clientId ) ) && ! getSettings().hasFixedToolbar && ! _showEmptyBlockSideInserter && hasSelectedBlock && diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index c322898031065..993b9bfd90291 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -15,8 +15,8 @@ import { getBlockName, getTemplateLock, getClientIdsWithDescendants, - isNavigationMode, getBlockRootClientId, + __unstableGetEditorMode, } from './selectors'; import { checkAllowListRecursive, @@ -522,7 +522,10 @@ export function isSectionBlock( state, clientId ) { return ( getBlockName( state, clientId ) === 'core/block' || getTemplateLock( state, clientId ) === 'contentOnly' || - ( isNavigationMode( state ) && sectionClientIds.includes( clientId ) ) + ( [ 'navigation', 'zoom-out' ].includes( + __unstableGetEditorMode( state ) + ) && + sectionClientIds.includes( clientId ) ) ); } From 8635e98652c30c93efdd401e0f3f7890967617a0 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 30 Sep 2024 22:52:22 +0900 Subject: [PATCH 03/49] ToggleGroupControl: Fix arrow key navigation in RTL (#65735) * ToggleGroupControl: Fix arrow key navigation in RTL * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + .../toggle-group-control/as-radio-group.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8ef1c7c3bd200..09f46b43a623f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,7 @@ - `ToolsPanel`: atomic one-step state update when (un)registering panels ([#65564](https://github.com/WordPress/gutenberg/pull/65564)). - `Navigator`: fix `isInitial` logic ([#65527](https://github.com/WordPress/gutenberg/pull/65527)). +- `ToggleGroupControl`: Fix arrow key navigation in RTL ([#65735](https://github.com/WordPress/gutenberg/pull/65735)). ### Deprecations diff --git a/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx b/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx index 6baadd65dc5ff..c062e35cb2b72 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx @@ -10,6 +10,7 @@ import { useStoreState } from '@ariakit/react'; */ import { useInstanceId } from '@wordpress/compose'; import { forwardRef, useMemo } from '@wordpress/element'; +import { isRTL } from '@wordpress/i18n'; /** * Internal dependencies @@ -65,6 +66,7 @@ function UnforwardedToggleGroupControlAsRadioGroup( defaultValue, value, setValue: wrappedOnChangeProp, + rtl: isRTL(), } ); const selectedValue = useStoreState( radio, 'value' ); From bb29dbd27a4ef2dc468de1bd0dcdd252cba7d364 Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Mon, 30 Sep 2024 21:25:58 +0200 Subject: [PATCH 04/49] `Tabs`: tweak sizing and overflow behavior of TabList (#64371) * Tweak sizing and overflow behavior of TabList. * Add size and overflow playground story. * Add "scroll into view" behavior to selected tabs. * fit-content only on horizontal orientation * Reduce specificity of `fit-content` to make it easier to override. * centered label only when orientation is horizontal * Remove unused file. * Fix inspector controls tabs. * Fix font library modal tabs. * Fix style-book tabs. * fix typo * Add changelog entry. * fix emotion being weird ugh * Prevent unwanted focusable container in Firefox. * Add fade effect. * Fix IntersectionObserver logic. * Feature detect IntersectionObserver to prevent tests from failing. * Add a bit of tolerance for scroll state detection. * Fix vertical indicator. * Better handling of vertical overflow. * Add a bit of scroll margin for better "scroll into view" experience. * Horizontal fade should only happen on horizontal direction. * Adjust for offset parent scroll state in `getElementOffsetRect`. * Better "scroll into view" positioning heuristics ("nearest"). * Invert use of before and after to remove z-index and fix related issues. * Make vertical indicator light blue as discussed. * Undo most overrides in pattern/media vertical tabs. * Clean up outdated styles previously needed for label wrapping. * Revert vertical indicator changes and some indicator patterns/media tabs styles * Revert vertical indicator bug fix * Add changelog entry * Remove outdated style. * Address feedback * Fix scroll bug * Improve automatic tab scrolling behavior. * Tweaks to prevent unit test failure and minor cleanup. * Undo unnecessary changes. * Improved story * Fix scroll jumping bug. * Scroll to active tab instead of selected (support `selectOnMove=false`). * Fix minor visual glitch with overflow fade out indicators. * Misc tweaks * Fix. * Fix changelog * Fix changelog but it's actually true * Fix changelog * Make Story Book tabs nicer. * Temp fix for scrollbar issue in Style Book tabs. * Fix scroll bug and clean up a little. * Simplify and clean up a bit more. * Fix merge issues. * Fix merge issues again. * Make inserter patterns/media changes more minimal * Fix outdated comment * Fix another typo in comment. * Minor cleanup. * Fix bad automatic merge. * ugh, fix again Co-authored-by: DaniGuardiola Co-authored-by: tyxla Co-authored-by: ciampo Co-authored-by: jasmussen Co-authored-by: jameskoster Co-authored-by: afercia --- .../src/components/inserter/style.scss | 28 ----- .../src/components/inserter/tabs.js | 78 ------------- .../inspector-controls-tabs/style.scss | 8 +- packages/components/CHANGELOG.md | 1 + .../src/tabs/stories/index.story.tsx | 106 ++++++++++++++++++ packages/components/src/tabs/styles.ts | 74 ++++++++---- packages/components/src/tabs/tablist.tsx | 86 +++++++++----- .../components/src/tabs/use-track-overflow.ts | 76 +++++++++++++ packages/components/src/utils/element-rect.ts | 22 +++- .../editor-canvas-container/style.scss | 2 +- .../global-styles/font-library-modal/index.js | 2 +- .../font-library-modal/style.scss | 6 +- .../src/components/style-book/index.js | 22 ++-- .../src/components/style-book/style.scss | 14 ++- .../src/components/sidebar/index.js | 6 +- 15 files changed, 346 insertions(+), 185 deletions(-) delete mode 100644 packages/block-editor/src/components/inserter/tabs.js create mode 100644 packages/components/src/tabs/use-track-overflow.ts diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 3ce088901bce5..f3fa8d1e7df04 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -257,39 +257,11 @@ $block-inserter-tabs-height: 44px; svg { fill: var(--wp-admin-theme-color); } - - &::after { - content: ""; - display: block; - outline: none; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - border-radius: $radius-small; - opacity: 0.04; - background: var(--wp-admin-theme-color); - height: 100%; - } - } - - &:focus-visible, - &:focus:not(:disabled) { - border-radius: $radius-small; - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - // Windows high contrast mode. - outline: 2px solid transparent; - outline-offset: 0; } &::before { display: none; } - - &::after { - display: none; - } } } diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js deleted file mode 100644 index b46e4bfdaf014..0000000000000 --- a/packages/block-editor/src/components/inserter/tabs.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * WordPress dependencies - */ -import { - Button, - privateApis as componentsPrivateApis, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { forwardRef } from '@wordpress/element'; -import { closeSmall } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -const { Tabs } = unlock( componentsPrivateApis ); - -const blocksTab = { - name: 'blocks', - /* translators: Blocks tab title in the block inserter. */ - title: __( 'Blocks' ), -}; -const patternsTab = { - name: 'patterns', - /* translators: Theme and Directory Patterns tab title in the block inserter. */ - title: __( 'Patterns' ), -}; - -const mediaTab = { - name: 'media', - /* translators: Media tab title in the block inserter. */ - title: __( 'Media' ), -}; - -function InserterTabs( { onSelect, children, onClose, selectedTab }, ref ) { - const tabs = [ blocksTab, patternsTab, mediaTab ]; - - return ( -
- -
-
- { tabs.map( ( tab ) => ( - - { children } - - ) ) } -
-
- ); -} - -export default forwardRef( InserterTabs ); diff --git a/packages/block-editor/src/components/inspector-controls-tabs/style.scss b/packages/block-editor/src/components/inspector-controls-tabs/style.scss index 863ac3d9bed03..9c9b04f7b8473 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/style.scss +++ b/packages/block-editor/src/components/inspector-controls-tabs/style.scss @@ -1,7 +1,3 @@ -.show-icon-labels { - .block-editor-block-inspector__tabs [role="tablist"] { - .components-button { - justify-content: center; - } - } +.block-editor-block-inspector__tabs [role="tablist"] { + width: 100%; } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 09f46b43a623f..f98f878080aa2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ ### Enhancements +- `Tabs`: handle horizontal overflow and large tab lists gracefully ([#64371](https://github.com/WordPress/gutenberg/pull/64371)). - `BorderBoxControl`: promote to stable ([#65586](https://github.com/WordPress/gutenberg/pull/65586)). - `MenuGroup`: Simplify the MenuGroup styles within dropdown menus. ([#65561](https://github.com/WordPress/gutenberg/pull/65561)). - `DatePicker`: Use compact button size. ([#65653](https://github.com/WordPress/gutenberg/pull/65653)). diff --git a/packages/components/src/tabs/stories/index.story.tsx b/packages/components/src/tabs/stories/index.story.tsx index e5f113d93b7d0..0f7e0d2c6ac75 100644 --- a/packages/components/src/tabs/stories/index.story.tsx +++ b/packages/components/src/tabs/stories/index.story.tsx @@ -70,6 +70,112 @@ const Template: StoryFn< typeof Tabs > = ( props ) => { export const Default = Template.bind( {} ); +export const SizeAndOverflowPlayground: StoryFn< typeof Tabs > = ( props ) => { + const [ fullWidth, setFullWidth ] = useState( false ); + return ( +
+
+

+ This story helps understand how the TabList component + behaves under different conditions. The container below + (with the dotted red border) can be horizontally resized, + and it has a bit of padding to be out of the way of the + TabList. +

+

+ The button will toggle between full width (adding{ ' ' } + width: 100%) and the default width. +

+

Try the following:

+
    +
  • + Small container that causes tabs to + overflow with scroll. +
  • +
  • + Large container that exceeds the normal + width of the tabs. +
      +
    • + + With width: 100% + { ' ' } + set on the TabList (tabs fill up the space). +
    • +
    • + + Without width: 100% + { ' ' } + (defaults to auto) set on the + TabList (tabs take up space proportional to + their content). +
    • +
    +
  • +
+
+ + +
+ + + Label with multiple words + + Short + + Hippopotomonstrosesquippedaliophobia + + Tab 4 + Tab 5 + +
+ +

Selected tab: Tab 1

+

(Label with multiple words)

+
+ +

Selected tab: Tab 2

+

(Short)

+
+ +

Selected tab: Tab 3

+

(Hippopotomonstrosesquippedaliophobia)

+
+ +

Selected tab: Tab 4

+
+ +

Selected tab: Tab 5

+
+
+
+ ); +}; +SizeAndOverflowPlayground.args = { + defaultTabId: 'tab4', +}; + const VerticalTemplate: StoryFn< typeof Tabs > = ( props ) => { return ( diff --git a/packages/components/src/tabs/styles.ts b/packages/components/src/tabs/styles.ts index c00943b180f63..283d6421f5b76 100644 --- a/packages/components/src/tabs/styles.ts +++ b/packages/components/src/tabs/styles.ts @@ -16,32 +16,40 @@ export const TabListWrapper = styled.div` align-items: stretch; flex-direction: row; text-align: center; + overflow-x: auto; &[aria-orientation='vertical'] { flex-direction: column; text-align: start; } - @media not ( prefers-reduced-motion ) { - &.is-animation-enabled::after { - transition-property: transform; - transition-duration: 0.2s; - transition-timing-function: ease-out; - } + :where( [aria-orientation='horizontal'] ) { + width: fit-content; } + --direction-factor: 1; - --direction-origin-x: left; + --direction-start: left; + --direction-end: right; --indicator-start: var( --indicator-left ); &:dir( rtl ) { --direction-factor: -1; - --direction-origin-x: right; + --direction-start: right; + --direction-end: left; --indicator-start: var( --indicator-right ); } - &::after { + + @media not ( prefers-reduced-motion ) { + &.is-animation-enabled::before { + transition-property: transform; + transition-duration: 0.2s; + transition-timing-function: ease-out; + } + } + &::before { content: ''; position: absolute; pointer-events: none; - transform-origin: var( --direction-origin-x ) top; + transform-origin: var( --direction-start ) top; // Windows high contrast mode. outline: 2px solid transparent; @@ -52,7 +60,31 @@ export const TabListWrapper = styled.div` when scaling in the transform, see: https://stackoverflow.com/a/52159123 */ --antialiasing-factor: 100; &:not( [aria-orientation='vertical'] ) { - &::after { + --fade-width: 4rem; + --fade-gradient-base: transparent 0%, black var( --fade-width ); + --fade-gradient-composed: var( --fade-gradient-base ), black 60%, + transparent 50%; + &.is-overflowing-first { + mask-image: linear-gradient( + to var( --direction-end ), + var( --fade-gradient-base ) + ); + } + &.is-overflowing-last { + mask-image: linear-gradient( + to var( --direction-start ), + var( --fade-gradient-base ) + ); + } + &.is-overflowing-first.is-overflowing-last { + mask-image: linear-gradient( + to right, + var( --fade-gradient-composed ) + ), + linear-gradient( to left, var( --fade-gradient-composed ) ); + } + + &::before { bottom: 0; height: 0; width: calc( var( --antialiasing-factor ) * 1px ); @@ -71,8 +103,7 @@ export const TabListWrapper = styled.div` ${ COLORS.theme.accent }; } } - &[aria-orientation='vertical']::after { - z-index: -1; + &[aria-orientation='vertical']::before { top: 0; left: 0; width: 100%; @@ -87,14 +118,14 @@ export const TabListWrapper = styled.div` export const Tab = styled( Ariakit.Tab )` & { + scroll-margin: 24px; + flex-grow: 1; + flex-shrink: 0; display: inline-flex; align-items: center; position: relative; border-radius: 0; - min-height: ${ space( - 12 - ) }; // Avoid fixed height to allow for long strings that go in multiple lines. - height: auto; + height: ${ space( 12 ) }; background: transparent; border: none; box-shadow: none; @@ -104,7 +135,6 @@ export const Tab = styled( Ariakit.Tab )` margin-left: 0; font-weight: 500; text-align: inherit; - hyphens: auto; color: ${ COLORS.theme.foreground }; &[aria-disabled='true'] { @@ -123,7 +153,7 @@ export const Tab = styled( Ariakit.Tab )` } // Focus. - &::before { + &::after { content: ''; position: absolute; top: ${ space( 3 ) }; @@ -146,7 +176,7 @@ export const Tab = styled( Ariakit.Tab )` } } - &:focus-visible::before { + &:focus-visible::after { opacity: 1; } } @@ -156,6 +186,10 @@ export const Tab = styled( Ariakit.Tab )` 10 ) }; // Avoid fixed height to allow for long strings that go in multiple lines. } + + [aria-orientation='horizontal'] & { + justify-content: center; + } `; export const TabPanel = styled( Ariakit.TabPanel )` diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx index 2977d6a628370..ae8daf60fc237 100644 --- a/packages/components/src/tabs/tablist.tsx +++ b/packages/components/src/tabs/tablist.tsx @@ -8,7 +8,8 @@ import { useStoreState } from '@ariakit/react'; * WordPress dependencies */ import warning from '@wordpress/warning'; -import { forwardRef, useState } from '@wordpress/element'; +import { forwardRef, useLayoutEffect, useState } from '@wordpress/element'; +import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies @@ -20,33 +21,58 @@ import type { WordPressComponentProps } from '../context'; import clsx from 'clsx'; import { useTrackElementOffsetRect } from '../utils/element-rect'; import { useOnValueUpdate } from '../utils/hooks/use-on-value-update'; +import { useTrackOverflow } from './use-track-overflow'; + +const SCROLL_MARGIN = 24; export const TabList = forwardRef< HTMLDivElement, WordPressComponentProps< TabListProps, 'div', false > >( function TabList( { children, ...otherProps }, ref ) { - const context = useTabsContext(); + const { store } = useTabsContext() ?? {}; + + const selectedId = useStoreState( store, 'selectedId' ); + const activeId = useStoreState( store, 'activeId' ); + const selectOnMove = useStoreState( store, 'selectOnMove' ); + const items = useStoreState( store, 'items' ); + const [ parent, setParent ] = useState< HTMLElement | null >(); + const refs = useMergeRefs( [ ref, setParent ] ); + const overflow = useTrackOverflow( parent, { + first: items?.at( 0 )?.element, + last: items?.at( -1 )?.element, + } ); - const tabStoreState = useStoreState( context?.store ); - const selectedId = tabStoreState?.selectedId; - const indicatorPosition = useTrackElementOffsetRect( - context?.store.item( selectedId )?.element + const selectedTabPosition = useTrackElementOffsetRect( + store?.item( selectedId )?.element ); const [ animationEnabled, setAnimationEnabled ] = useState( false ); - useOnValueUpdate( - selectedId, - ( { previousValue } ) => previousValue && setAnimationEnabled( true ) - ); + useOnValueUpdate( selectedId, ( { previousValue } ) => { + if ( previousValue ) { + setAnimationEnabled( true ); + } + } ); - if ( ! context || ! tabStoreState ) { - warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' ); - return null; - } + // Make sure selected tab is scrolled into view. + useLayoutEffect( () => { + if ( ! parent || ! selectedTabPosition ) { + return; + } + + const { scrollLeft: parentScroll } = parent; + const parentWidth = parent.getBoundingClientRect().width; + const { left: childLeft, width: childWidth } = selectedTabPosition; - const { store } = context; - const { activeId, selectOnMove } = tabStoreState; - const { setActiveId } = store; + const parentRightEdge = parentScroll + parentWidth; + const childRightEdge = childLeft + childWidth; + const rightOverflow = childRightEdge + SCROLL_MARGIN - parentRightEdge; + const leftOverflow = parentScroll - ( childLeft - SCROLL_MARGIN ); + if ( leftOverflow > 0 ) { + parent.scrollLeft = parentScroll - leftOverflow; + } else if ( rightOverflow > 0 ) { + parent.scrollLeft = parentScroll + rightOverflow; + } + }, [ parent, selectedTabPosition ] ); const onBlur = () => { if ( ! selectOnMove ) { @@ -58,35 +84,43 @@ export const TabList = forwardRef< // that the selected tab will receive keyboard focus when tabbing back into // the tablist. if ( selectedId !== activeId ) { - setActiveId( selectedId ); + store?.setActiveId( selectedId ); } }; + if ( ! store ) { + warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' ); + return null; + } + return ( { - if ( event.pseudoElement === '::after' ) { + if ( event.pseudoElement === '::before' ) { setAnimationEnabled( false ); } } } /> } onBlur={ onBlur } + tabIndex={ -1 } { ...otherProps } style={ { - '--indicator-top': indicatorPosition.top, - '--indicator-right': indicatorPosition.right, - '--indicator-left': indicatorPosition.left, - '--indicator-width': indicatorPosition.width, - '--indicator-height': indicatorPosition.height, + '--indicator-top': selectedTabPosition.top, + '--indicator-right': selectedTabPosition.right, + '--indicator-left': selectedTabPosition.left, + '--indicator-width': selectedTabPosition.width, + '--indicator-height': selectedTabPosition.height, ...otherProps.style, } } className={ clsx( - animationEnabled ? 'is-animation-enabled' : '', + overflow.first && 'is-overflowing-first', + overflow.last && 'is-overflowing-last', + animationEnabled && 'is-animation-enabled', otherProps.className ) } > diff --git a/packages/components/src/tabs/use-track-overflow.ts b/packages/components/src/tabs/use-track-overflow.ts new file mode 100644 index 0000000000000..5f6504e687521 --- /dev/null +++ b/packages/components/src/tabs/use-track-overflow.ts @@ -0,0 +1,76 @@ +/* eslint-disable jsdoc/require-param */ +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { useEvent } from '@wordpress/compose'; + +/** + * Tracks if an element contains overflow and on which end by tracking the + * first and last child elements with an `IntersectionObserver` in relation + * to the parent element. + * + * Note that the returned value will only indicate whether the first or last + * element is currently "going out of bounds" but not whether it happens on + * the X or Y axis. + */ +export function useTrackOverflow( + parent: HTMLElement | undefined | null, + children: { + first: HTMLElement | undefined | null; + last: HTMLElement | undefined | null; + } +) { + const [ first, setFirst ] = useState( false ); + const [ last, setLast ] = useState( false ); + const [ observer, setObserver ] = useState< IntersectionObserver >(); + + const callback: IntersectionObserverCallback = useEvent( ( entries ) => { + for ( const entry of entries ) { + if ( entry.target === children.first ) { + setFirst( ! entry.isIntersecting ); + } + if ( entry.target === children.last ) { + setLast( ! entry.isIntersecting ); + } + } + } ); + + useEffect( () => { + if ( ! parent || ! window.IntersectionObserver ) { + return; + } + const newObserver = new IntersectionObserver( callback, { + root: parent, + threshold: 0.9, + } ); + setObserver( newObserver ); + + return () => newObserver.disconnect(); + }, [ callback, parent ] ); + + useEffect( () => { + if ( ! observer ) { + return; + } + + if ( children.first ) { + observer.observe( children.first ); + } + if ( children.last ) { + observer.observe( children.last ); + } + + return () => { + if ( children.first ) { + observer.unobserve( children.first ); + } + if ( children.last ) { + observer.unobserve( children.last ); + } + }; + }, [ children.first, children.last, observer ] ); + + return { first, last }; +} +/* eslint-enable jsdoc/require-param */ diff --git a/packages/components/src/utils/element-rect.ts b/packages/components/src/utils/element-rect.ts index a96c25ecfac94..4c60e4ba51c48 100644 --- a/packages/components/src/utils/element-rect.ts +++ b/packages/components/src/utils/element-rect.ts @@ -75,9 +75,11 @@ export function getElementOffsetRect( if ( rect.width === 0 || rect.height === 0 ) { return; } + const offsetParent = element.offsetParent; const offsetParentRect = - element.offsetParent?.getBoundingClientRect() ?? - NULL_ELEMENT_OFFSET_RECT; + offsetParent?.getBoundingClientRect() ?? NULL_ELEMENT_OFFSET_RECT; + const offsetParentScrollX = offsetParent?.scrollLeft ?? 0; + const offsetParentScrollY = offsetParent?.scrollTop ?? 0; // Computed widths and heights have subpixel precision, and are not affected // by distortions. @@ -93,10 +95,18 @@ export function getElementOffsetRect( // To obtain the adjusted values for the position: // 1. Compute the element's position relative to the offset parent. // 2. Correct for the scale factor. - top: ( rect.top - offsetParentRect?.top ) * scaleY, - right: ( offsetParentRect?.right - rect.right ) * scaleX, - bottom: ( offsetParentRect?.bottom - rect.bottom ) * scaleY, - left: ( rect.left - offsetParentRect?.left ) * scaleX, + // 3. Adjust for the scroll position of the offset parent. + top: + ( rect.top - offsetParentRect?.top ) * scaleY + offsetParentScrollY, + right: + ( offsetParentRect?.right - rect.right ) * scaleX - + offsetParentScrollX, + bottom: + ( offsetParentRect?.bottom - rect.bottom ) * scaleY - + offsetParentScrollY, + left: + ( rect.left - offsetParentRect?.left ) * scaleX + + offsetParentScrollX, // Computed dimensions don't need any adjustments. width: computedWidth, height: computedHeight, diff --git a/packages/edit-site/src/components/editor-canvas-container/style.scss b/packages/edit-site/src/components/editor-canvas-container/style.scss index 80d6a909d0c95..0bdbc2bbe3235 100644 --- a/packages/edit-site/src/components/editor-canvas-container/style.scss +++ b/packages/edit-site/src/components/editor-canvas-container/style.scss @@ -30,6 +30,6 @@ position: absolute; right: $grid-unit-10; top: $grid-unit-10; - z-index: 1; + z-index: 2; background: $white; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index 495652f144275..27093e0ef1cbb 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -67,7 +67,7 @@ function FontLibraryModal( { className="font-library-modal" > -
+
{ tabs.map( ( { id, title } ) => ( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index e8c48ca2c30bf..7d94376ac8d94 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -133,7 +133,7 @@ $footer-height: 70px; padding-bottom: $grid-unit-20; } -.font-library-modal__tablist { +.font-library-modal__tablist-container { position: sticky; top: 0; border-bottom: 1px solid $gray-300; @@ -141,6 +141,10 @@ $footer-height: 70px; margin: 0 #{$grid-unit-40 * -1}; padding: 0 $grid-unit-20; z-index: 1; + + [role="tablist"] { + margin-bottom: -1px; + } } diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index e68474e19f407..7b85c320e20c9 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -122,16 +122,18 @@ function StyleBook( { { showTabs ? (
- - { tabs.map( ( tab ) => ( - - { tab.title } - - ) ) } - +
+ + { tabs.map( ( tab ) => ( + + { tab.title } + + ) ) } + +
{ tabs.map( ( tab ) => ( { if ( !! newSelectedTabId ) { From 39f2aa713b247aa7e2bf08f9ec00e3e9ca8fb92f Mon Sep 17 00:00:00 2001 From: Matias Ventura Date: Mon, 30 Sep 2024 22:01:41 +0200 Subject: [PATCH 05/49] Update tools menus with Write / Design order (#65721) * Update tools menus: - Write / Design order - Change description copy * update tools help text --------- Co-authored-by: mtias Co-authored-by: richtabor Co-authored-by: provenself --- .../src/components/tool-selector/index.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/tool-selector/index.js b/packages/block-editor/src/components/tool-selector/index.js index 53fab037ba368..fbaf8af5fac68 100644 --- a/packages/block-editor/src/components/tool-selector/index.js +++ b/packages/block-editor/src/components/tool-selector/index.js @@ -71,33 +71,31 @@ function ToolSelector( props, ref ) { onSelect={ __unstableSetEditorMode } choices={ [ { - value: 'edit', + value: 'navigation', label: ( <> - { selectIcon } - { __( 'Design' ) } + + { __( 'Write' ) } ), - info: __( - 'Full control over layout and styling.' - ), + info: __( 'Focus on content.' ), }, { - value: 'navigation', + value: 'edit', label: ( <> - - { __( 'Edit' ) } + { selectIcon } + { __( 'Design' ) } ), - info: __( 'Focus on content.' ), + info: __( 'Edit layout and styles.' ), }, ] } />
{ __( - 'Tools provide different interactions for selecting, navigating, and editing blocks. Toggle between select and edit by pressing Escape and Enter.' + 'Tools provide different sets of interactions for blocks. Toggle between simplified content tools (Write) and advanced visual editing tools (Design).' ) }
From 444347456139c4bc841d58e2a55dad12fa8c094f Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 30 Sep 2024 15:06:49 -0500 Subject: [PATCH 06/49] Only show zoom out inserters on block selection (#65759) Removes code related to showing/hiding inserters on hover. --- .../src/components/block-tools/style.scss | 8 -------- .../zoom-out-mode-inserter-button.js | 19 ++----------------- .../block-tools/zoom-out-mode-inserters.js | 17 ++++------------- 3 files changed, 6 insertions(+), 38 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index a3d9153273e98..b7586a6715dcc 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -202,11 +202,3 @@ border: none; } } - -.block-editor-block-tools__zoom-out-mode-inserter-button { - visibility: hidden; - - &.is-visible { - visibility: visible; - } -} diff --git a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserter-button.js b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserter-button.js index 8ea80a5383013..961552caa66e0 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserter-button.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserter-button.js @@ -6,17 +6,11 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; import { Button } from '@wordpress/components'; import { plus } from '@wordpress/icons'; import { _x } from '@wordpress/i18n'; -function ZoomOutModeInserterButton( { isVisible, onClick } ) { - const [ - zoomOutModeInserterButtonHovered, - setZoomOutModeInserterButtonHovered, - ] = useState( false ); - +function ZoomOutModeInserterButton( { onClick } ) { return (
diff --git a/packages/block-editor/src/components/url-input/index.js b/packages/block-editor/src/components/url-input/index.js index 25c033e88749b..cb058be5c932f 100644 --- a/packages/block-editor/src/components/url-input/index.js +++ b/packages/block-editor/src/components/url-input/index.js @@ -540,8 +540,7 @@ class URLInput extends Component { > { suggestions.map( ( suggestion, index ) => (
- + ); } From d637d8afa0fe10943bd8dc652ac1676b784bbfd5 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 1 Oct 2024 14:47:37 +0200 Subject: [PATCH 16/49] Navigator: use stable export instead of experimental export (#65753) * Navigator: use stable export instead of experimental export * More clear export of useNavigator --- Co-authored-by: ciampo Co-authored-by: mirka <0mirka00@git.wordpress.org> --- .../inserter/mobile-tab-navigation.js | 23 +++++++--------- packages/components/src/index.ts | 7 +++-- .../components/global-styles-sidebar/index.js | 2 +- .../font-library-modal/font-card.js | 2 +- .../font-library-modal/font-collection.js | 18 ++++++------- .../font-library-modal/installed-fonts.js | 20 +++++++------- .../global-styles/font-sizes/font-size.js | 2 +- .../src/components/global-styles/header.js | 4 +-- .../global-styles/navigation-button.js | 7 +++-- .../global-styles/screen-revisions/index.js | 2 +- .../global-styles/shadows-edit-panel.js | 2 +- .../src/components/global-styles/ui.js | 11 ++++---- .../preferences-modal-tabs/index.js | 26 +++++++------------ 13 files changed, 57 insertions(+), 69 deletions(-) diff --git a/packages/block-editor/src/components/inserter/mobile-tab-navigation.js b/packages/block-editor/src/components/inserter/mobile-tab-navigation.js index fa8191cc5eaaa..5f34c3c21d832 100644 --- a/packages/block-editor/src/components/inserter/mobile-tab-navigation.js +++ b/packages/block-editor/src/components/inserter/mobile-tab-navigation.js @@ -10,10 +10,7 @@ import { __experimentalSpacer as Spacer, __experimentalHeading as Heading, __experimentalView as View, - __experimentalNavigatorProvider as NavigatorProvider, - __experimentalNavigatorScreen as NavigatorScreen, - __experimentalNavigatorButton as NavigatorButton, - __experimentalNavigatorBackButton as NavigatorBackButton, + Navigator, FlexBlock, } from '@wordpress/components'; import { Icon, chevronRight, chevronLeft } from '@wordpress/icons'; @@ -24,7 +21,7 @@ function ScreenHeader( { title } ) { - - + { categories.map( ( category ) => ( - - + ) ) } - + { categories.map( ( category ) => ( - { children( category ) } - + ) ) } - + ); } diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index cc3c0265c4220..fe3642f770077 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -130,9 +130,12 @@ export { NavigatorButton as __experimentalNavigatorButton, NavigatorBackButton as __experimentalNavigatorBackButton, NavigatorToParentButton as __experimentalNavigatorToParentButton, - useNavigator as __experimentalUseNavigator, } from './navigator/legacy'; -export { Navigator, useNavigator } from './navigator'; +export { + Navigator, + useNavigator, + useNavigator as __experimentalUseNavigator, +} from './navigator'; export { default as Notice } from './notice'; export { default as __experimentalNumberControl } from './number-control'; export { default as NoticeList } from './notice/list'; diff --git a/packages/edit-site/src/components/global-styles-sidebar/index.js b/packages/edit-site/src/components/global-styles-sidebar/index.js index b314b5d7e7524..966005907cda4 100644 --- a/packages/edit-site/src/components/global-styles-sidebar/index.js +++ b/packages/edit-site/src/components/global-styles-sidebar/index.js @@ -6,7 +6,7 @@ import { FlexBlock, Flex, Button, - __experimentalUseNavigator as useNavigator, + useNavigator, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { styles, seen, backup } from '@wordpress/icons'; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js index 579c6564fdf3e..61f8c28c77144 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js @@ -3,7 +3,7 @@ */ import { _n, sprintf, isRTL } from '@wordpress/i18n'; import { - __experimentalUseNavigator as useNavigator, + useNavigator, __experimentalText as Text, Button, Flex, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index ce7b4c1766c64..caf339091de75 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -13,9 +13,7 @@ import { __experimentalText as Text, __experimentalHStack as HStack, __experimentalVStack as VStack, - __experimentalNavigatorProvider as NavigatorProvider, - __experimentalNavigatorScreen as NavigatorScreen, - __experimentalNavigatorBackButton as NavigatorBackButton, + Navigator, __experimentalHeading as Heading, Notice, SelectControl, @@ -284,11 +282,11 @@ function FontCollection( { slug } ) { { ! isLoading && ( <> - - + @@ -378,11 +376,11 @@ function FontCollection( { slug } ) { { /* eslint-enable jsx-a11y/no-redundant-roles */ }
- + - + - - - + + { selectedFont && ( - - + { notice && ( ) } - + - + - { /* eslint-enable jsx-a11y/no-redundant-roles */ } - - + + - ; + return ; } function NavigationBackButtonAsItem( props ) { - return ; + return ; } export { NavigationButtonAsItem, NavigationBackButtonAsItem }; diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index de27e92113b55..89af705cd9250 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -3,7 +3,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { - __experimentalUseNavigator as useNavigator, + useNavigator, __experimentalConfirmDialog as ConfirmDialog, Spinner, } from '@wordpress/components'; diff --git a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js b/packages/edit-site/src/components/global-styles/shadows-edit-panel.js index ec1dd1a900c3b..f12f960f6a36b 100644 --- a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js +++ b/packages/edit-site/src/components/global-styles/shadows-edit-panel.js @@ -15,7 +15,7 @@ import { __experimentalUnitControl as UnitControl, __experimentalGrid as Grid, __experimentalDropdownContentWrapper as DropdownContentWrapper, - __experimentalUseNavigator as useNavigator, + useNavigator, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalConfirmDialog as ConfirmDialog, diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index b1550d2a24513..fbc3e461e6abb 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -2,9 +2,8 @@ * WordPress dependencies */ import { - __experimentalNavigatorProvider as NavigatorProvider, - __experimentalNavigatorScreen as NavigatorScreen, - __experimentalUseNavigator as useNavigator, + Navigator, + useNavigator, createSlotFill, DropdownMenu, MenuGroup, @@ -124,7 +123,7 @@ function GlobalStylesActionMenu() { function GlobalStylesNavigationScreen( { className, ...props } ) { return ( - @@ -385,7 +384,7 @@ function GlobalStylesUI() { - + ); } export { GlobalStylesMenuSlot }; diff --git a/packages/preferences/src/components/preferences-modal-tabs/index.js b/packages/preferences/src/components/preferences-modal-tabs/index.js index d87e565f5e336..f73bdd7f029dc 100644 --- a/packages/preferences/src/components/preferences-modal-tabs/index.js +++ b/packages/preferences/src/components/preferences-modal-tabs/index.js @@ -3,10 +3,7 @@ */ import { useViewportMatch } from '@wordpress/compose'; import { - __experimentalNavigatorProvider as NavigatorProvider, - __experimentalNavigatorScreen as NavigatorScreen, - __experimentalNavigatorButton as NavigatorButton, - __experimentalNavigatorBackButton as NavigatorBackButton, + Navigator, __experimentalItemGroup as ItemGroup, __experimentalItem as Item, __experimentalHStack as HStack, @@ -98,17 +95,14 @@ export default function PreferencesModalTabs( { sections } ) { ); } else { modalContent = ( - - + + { tabs.map( ( tab ) => { return ( - - + ); } ) } - + { sections.length && sections.map( ( section ) => { return ( - @@ -151,7 +145,7 @@ export default function PreferencesModalTabs( { sections } ) { size="small" gap="6" > - { section.content } - + ); } ) } - + ); } From 3b10909170796eb97eea5f8d5091661e366473a0 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 1 Oct 2024 14:52:11 +0200 Subject: [PATCH 17/49] Navigator: fix README heading hierarchy (#65763) Co-authored-by: ciampo Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/src/navigator/README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/components/src/navigator/README.md b/packages/components/src/navigator/README.md index 00b1cfaeebe0f..b56a82e0524ee 100644 --- a/packages/components/src/navigator/README.md +++ b/packages/components/src/navigator/README.md @@ -70,6 +70,8 @@ The children elements. #### `Navigator.Screen` +##### Props + ###### `path`: `string` The screen's path, matched against the current path stored in the navigator. @@ -94,7 +96,9 @@ The children elements. - Required: Yes -##### `Navigator.Button` +#### `Navigator.Button` + +##### Props ###### `path`: `string` @@ -119,7 +123,9 @@ The children elements. `Navigator.Button` also inherits all of the [`Button` props](/packages/components/src/button/README.md#props), except for `href` and `target`. -##### `Navigator.BackButton` +#### `Navigator.BackButton` + +##### Props ###### `children`: `string` @@ -131,10 +137,12 @@ The children elements. `Navigator.BackButton` also inherits all of the [`Button` props](/packages/components/src/button/README.md#props), except for `href` and `target`. -###### `useNavigator` +#### `useNavigator` You can retrieve a `navigator` instance by using the `useNavigator` hook. +##### Props + The `navigator` instance has a few properties: ###### `goTo`: `( path: string, options: NavigateOptions ) => void` From fba175d5d336634eeca3594afaba5990dea0b1b7 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 1 Oct 2024 09:17:03 -0500 Subject: [PATCH 18/49] Remove editorMode from blockProps (#65326) --- .../src/components/block-list/block.js | 6 ---- .../block-list/use-block-props/index.js | 3 +- .../use-block-props/use-zoom-out-mode-exit.js | 30 ++++++++++++------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 2cecd941dfa3b..783b45da932a3 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -609,7 +609,6 @@ function BlockListBlockProvider( props ) { isBlockBeingDragged, isDragging, __unstableHasActiveBlockOverlayActive, - __unstableGetEditorMode, getSelectedBlocksInitialCaretPosition, } = unlock( select( blockEditorStore ) ); const blockWithoutAttributes = @@ -680,8 +679,6 @@ function BlockListBlockProvider( props ) { blocksWithSameName.length && blocksWithSameName[ 0 ] !== clientId; - const editorMode = __unstableGetEditorMode(); - return { ...previewContext, mode: getBlockMode( clientId ), @@ -708,7 +705,6 @@ function BlockListBlockProvider( props ) { ) && hasSelectedInnerBlock( clientId ), blockApiVersion: blockType?.apiVersion || 1, blockTitle: match?.title || blockType?.title, - editorMode, isSubtreeDisabled: blockEditingMode === 'disabled' && isBlockSubtreeDisabled( clientId ), @@ -755,7 +751,6 @@ function BlockListBlockProvider( props ) { themeSupportsLayout, isTemporarilyEditingAsBlocks, blockEditingMode, - editorMode, mayDisplayControls, mayDisplayParentControls, index, @@ -808,7 +803,6 @@ function BlockListBlockProvider( props ) { hasOverlay, initialPosition, blockEditingMode, - editorMode, isHighlighted, isMultiSelected, isPartiallySelected, diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 1cf1d4908b076..45fc1d9eb5ea1 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -85,7 +85,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { name, blockApiVersion, blockTitle, - editorMode, isSelected, isSubtreeDisabled, hasOverlay, @@ -113,7 +112,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { useBlockRefProvider( clientId ), useFocusHandler( clientId ), useEventHandlers( { clientId, isSelected } ), - useZoomOutModeExit( { editorMode } ), + useZoomOutModeExit(), useIsHovered( { clientId } ), useIntersectionObserver(), useMovingAnimation( { triggerAnimationOnChange: index, clientId } ), diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js index 92c54bac9b806..494694952110b 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js @@ -12,25 +12,27 @@ import { unlock } from '../../../lock-unlock'; /** * Allows Zoom Out mode to be exited by double clicking in the selected block. - * - * @param {string} clientId Block client ID. */ -export function useZoomOutModeExit( { editorMode } ) { - const { getSettings, isZoomOut } = unlock( useSelect( blockEditorStore ) ); +export function useZoomOutModeExit() { + const { getSettings, isZoomOut, __unstableGetEditorMode } = unlock( + useSelect( blockEditorStore ) + ); + const { __unstableSetEditorMode, resetZoomLevel } = unlock( useDispatch( blockEditorStore ) ); return useRefEffect( ( node ) => { - // In "compose" mode. - const composeMode = editorMode === 'zoom-out' && isZoomOut(); + function onDoubleClick( event ) { + // In "compose" mode. + const composeMode = + __unstableGetEditorMode() === 'zoom-out' && isZoomOut(); - if ( ! composeMode ) { - return; - } + if ( ! composeMode ) { + return; + } - function onDoubleClick( event ) { if ( ! event.defaultPrevented ) { event.preventDefault(); @@ -52,6 +54,12 @@ export function useZoomOutModeExit( { editorMode } ) { node.removeEventListener( 'dblclick', onDoubleClick ); }; }, - [ editorMode, getSettings, __unstableSetEditorMode ] + [ + getSettings, + __unstableSetEditorMode, + __unstableGetEditorMode, + isZoomOut, + resetZoomLevel, + ] ); } From bc2eb38a505f2eaba1cec99b8a4a984024c49a5a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 1 Oct 2024 16:28:08 +0100 Subject: [PATCH 19/49] Temp disable test for Classic Block Media issue (#65793) * Temp disable test * Sounds good. Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --------- Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- test/e2e/specs/editor/blocks/classic.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/specs/editor/blocks/classic.spec.js b/test/e2e/specs/editor/blocks/classic.spec.js index 95d39906b0d8b..6896433a3575e 100644 --- a/test/e2e/specs/editor/blocks/classic.spec.js +++ b/test/e2e/specs/editor/blocks/classic.spec.js @@ -39,7 +39,10 @@ test.describe( 'Classic', () => { await expect.poll( editor.getEditedPostContent ).toBe( 'test' ); } ); - test( 'should insert media, convert to blocks, and undo in one step', async ( { + // Reinitiate once this ticket is fixed: + // https://core.trac.wordpress.org/ticket/60666 + // eslint-disable-next-line playwright/no-skipped-test + test.skip( 'should insert media, convert to blocks, and undo in one step', async ( { editor, mediaUtils, page, From 5b6b849a2f1f0cebd46f0e121f46a9bedb32d6c5 Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Tue, 1 Oct 2024 18:10:02 +0200 Subject: [PATCH 20/49] BorderControl: Promote to stable (#65475) * Export without experimental prefix * Update README * Move Storybook stories and add redirect * Add changelog entries * Fix changelog. * Fix changelog (for real?) * Fix changelog * Apply feedback. * Fix changelog * Remove alpha story. * README fix. * Fix default in README * Fix changelog * Remove `showDropdownHeader` prop and the header itself. * Added `showDropdownHeader` as deprecated. * Remove deprecation warning * Add ignores to jsdocs. Co-authored-by: DaniGuardiola Co-authored-by: youknowriad Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: aaronrobertshaw Co-authored-by: tyxla Co-authored-by: jameskoster Co-authored-by: ciampo --- packages/components/CHANGELOG.md | 5 +- .../border-control-dropdown/component.tsx | 15 ----- .../border-control/border-control/README.md | 62 ++++++++----------- .../border-control/component.tsx | 3 +- .../border-control/stories/index.story.tsx | 14 ++--- .../src/border-control/test/index.js | 17 +---- .../components/src/border-control/types.ts | 19 +++--- packages/components/src/index.ts | 6 +- storybook/manager-head.html | 1 + 9 files changed, 53 insertions(+), 89 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f98f878080aa2..3baddb5cccadd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,22 +6,21 @@ - `ToolsPanel`: atomic one-step state update when (un)registering panels ([#65564](https://github.com/WordPress/gutenberg/pull/65564)). - `Navigator`: fix `isInitial` logic ([#65527](https://github.com/WordPress/gutenberg/pull/65527)). -- `ToggleGroupControl`: Fix arrow key navigation in RTL ([#65735](https://github.com/WordPress/gutenberg/pull/65735)). ### Deprecations +- `__experimentalBorderControl` can now be imported as a stable `BorderControl` ([#65475](https://github.com/WordPress/gutenberg/pull/65475)). - `__experimentalBorderBoxControl` can now be imported as a stable `BorderBoxControl` ([#65586](https://github.com/WordPress/gutenberg/pull/65586)). ### Enhancements -- `Tabs`: handle horizontal overflow and large tab lists gracefully ([#64371](https://github.com/WordPress/gutenberg/pull/64371)). +- `BorderControl`: promote to stable ([#65475](https://github.com/WordPress/gutenberg/pull/65475)). - `BorderBoxControl`: promote to stable ([#65586](https://github.com/WordPress/gutenberg/pull/65586)). - `MenuGroup`: Simplify the MenuGroup styles within dropdown menus. ([#65561](https://github.com/WordPress/gutenberg/pull/65561)). - `DatePicker`: Use compact button size. ([#65653](https://github.com/WordPress/gutenberg/pull/65653)). - `Navigator`: add support for exit animation ([#64777](https://github.com/WordPress/gutenberg/pull/64777)). - `Guide`: Update finish button to use the new default size ([#65680](https://github.com/WordPress/gutenberg/pull/65680)). - `BorderControl`: Use `__next40pxDefaultSize` prop for Reset button ([#65682](https://github.com/WordPress/gutenberg/pull/65682)). -- `Navigator`: stabilize APIs ([#64613](https://github.com/WordPress/gutenberg/pull/64613)). ## 28.8.0 (2024-09-19) diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx index d84d6bea77335..0223de66a4c78 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -7,7 +7,6 @@ import type { CSSProperties } from 'react'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies @@ -17,12 +16,10 @@ import Button from '../../button'; import ColorIndicator from '../../color-indicator'; import ColorPalette from '../../color-palette'; import Dropdown from '../../dropdown'; -import { HStack } from '../../h-stack'; import { VStack } from '../../v-stack'; import type { WordPressComponentProps } from '../../context'; import { contextConnect } from '../../context'; import { useBorderControlDropdown } from './hook'; -import { StyledLabel } from '../../base-control/styles/base-control-styles'; import DropdownContentWrapper from '../../dropdown/dropdown-content-wrapper'; import type { ColorObject } from '../../color-palette/types'; @@ -149,7 +146,6 @@ const BorderControlDropdown = ( popoverContentClassName, popoverControlsClassName, resetButtonClassName, - showDropdownHeader, size, __unstablePopoverProps, ...otherProps @@ -197,17 +193,6 @@ const BorderControlDropdown = ( <> - { showDropdownHeader ? ( - - { __( 'Border color' ) } - -
-
-
-
{ if ( showTooltip && text ) { return ( @@ -58,7 +51,6 @@ function ToggleGroupControlOptionBase( >, forwardedRef: ForwardedRef< any > ) { - const shouldReduceMotion = useReducedMotion(); const toggleGroupControlContext = useToggleGroupControlContext(); const id = useInstanceId( @@ -107,7 +99,6 @@ function ToggleGroupControlOptionBase( ), [ cx, isDeselectable, isIcon, isPressed, size, className ] ); - const backdropClasses = useMemo( () => cx( styles.backdropView ), [ cx ] ); const buttonOnClick = () => { if ( isDeselectable && isPressed ) { @@ -124,8 +115,15 @@ function ToggleGroupControlOptionBase( ref: forwardedRef, }; + const labelRef = useRef< HTMLDivElement | null >( null ); + useLayoutEffect( () => { + if ( isPressed && labelRef.current ) { + toggleGroupControlContext.setSelectedElement( labelRef.current ); + } + }, [ isPressed, toggleGroupControlContext ] ); + return ( - + ) } - { /* Animated backdrop using framer motion's shared layout animation */ } - { isPressed ? ( - - - - ) : null } ); } diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts index 020468991225c..c0248f9b3f7f2 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts @@ -119,14 +119,3 @@ const isIconStyles = ( { padding-right: 0; `; }; - -export const backdropView = css` - background: ${ COLORS.gray[ 900 ] }; - border-radius: ${ CONFIG.radiusXSmall }; - position: absolute; - inset: 0; - z-index: 1; - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - outline-offset: -3px; -`; diff --git a/packages/components/src/toggle-group-control/toggle-group-control/as-button-group.tsx b/packages/components/src/toggle-group-control/toggle-group-control/as-button-group.tsx index b3f56bccd07c5..7ce762b6e71df 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/as-button-group.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/as-button-group.tsx @@ -26,6 +26,7 @@ function UnforwardedToggleGroupControlAsButtonGroup( size, value: valueProp, id: idProp, + setSelectedElement, ...otherProps }: WordPressComponentProps< ToggleGroupControlMainControlProps, @@ -54,16 +55,23 @@ function UnforwardedToggleGroupControlAsButtonGroup( } ); const groupContextValue = useMemo( - () => - ( { - baseId, - value: selectedValue, - setValue: setSelectedValue, - isBlock: ! isAdaptiveWidth, - isDeselectable: true, - size, - } ) as ToggleGroupControlContextProps, - [ baseId, selectedValue, setSelectedValue, isAdaptiveWidth, size ] + (): ToggleGroupControlContextProps => ( { + baseId, + value: selectedValue, + setValue: setSelectedValue, + isBlock: ! isAdaptiveWidth, + isDeselectable: true, + size, + setSelectedElement, + } ), + [ + baseId, + selectedValue, + setSelectedValue, + isAdaptiveWidth, + size, + setSelectedElement, + ] ); return ( diff --git a/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx b/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx index c062e35cb2b72..342f9f128defd 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx @@ -33,6 +33,7 @@ function UnforwardedToggleGroupControlAsRadioGroup( size, value: valueProp, id: idProp, + setSelectedElement, ...otherProps }: WordPressComponentProps< ToggleGroupControlMainControlProps, @@ -73,15 +74,24 @@ function UnforwardedToggleGroupControlAsRadioGroup( const setValue = radio.setValue; const groupContextValue = useMemo( - () => - ( { - baseId, - isBlock: ! isAdaptiveWidth, - size, - value: selectedValue, - setValue, - } ) as ToggleGroupControlContextProps, - [ baseId, isAdaptiveWidth, size, selectedValue, setValue ] + (): ToggleGroupControlContextProps => ( { + baseId, + isBlock: ! isAdaptiveWidth, + size, + // @ts-expect-error - This is wrong and we should fix it. + value: selectedValue, + // @ts-expect-error - This is wrong and we should fix it. + setValue, + setSelectedElement, + } ), + [ + baseId, + isAdaptiveWidth, + selectedValue, + setSelectedElement, + setValue, + size, + ] ); return ( diff --git a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx index 1c86c93548f6d..5f8da76676293 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx @@ -2,13 +2,11 @@ * External dependencies */ import type { ForwardedRef } from 'react'; -import { LayoutGroup } from 'framer-motion'; /** * WordPress dependencies */ -import { useInstanceId } from '@wordpress/compose'; -import { useMemo } from '@wordpress/element'; +import { useLayoutEffect, useMemo, useState } from '@wordpress/element'; /** * Internal dependencies @@ -22,6 +20,68 @@ import { VisualLabelWrapper } from './styles'; import * as styles from './styles'; import { ToggleGroupControlAsRadioGroup } from './as-radio-group'; import { ToggleGroupControlAsButtonGroup } from './as-button-group'; +import { useTrackElementOffsetRect } from '../../utils/element-rect'; +import { useOnValueUpdate } from '../../utils/hooks/use-on-value-update'; +import { useEvent, useMergeRefs } from '@wordpress/compose'; + +/** + * A utility used to animate something (e.g. an indicator for the selected option + * of a component). + * + * It works by tracking the position and size (i.e., the "rect") of a given subelement, + * typically the one that corresponds to the selected option, relative to its offset + * parent. Then it: + * + * - Keeps CSS variables with that information in the parent, so that the animation + * can be implemented with them. + * - Adds a `is-animation-enabled` CSS class when the element changes, so that the + * target (e.g. the indicator) can be animated to its new position. + * - Removes the `is-animation-enabled` class when the animation is done. + */ +function useSubelementAnimation( + subelement?: HTMLElement | null, + { + parent = subelement?.offsetParent as HTMLElement | null | undefined, + prefix = 'subelement', + transitionEndFilter, + }: { + parent?: HTMLElement | null | undefined; + prefix?: string; + transitionEndFilter?: ( event: TransitionEvent ) => boolean; + } = {} +) { + const rect = useTrackElementOffsetRect( subelement ); + + const setProperties = useEvent( () => { + ( Object.keys( rect ) as Array< keyof typeof rect > ).forEach( + ( property ) => + property !== 'element' && + parent?.style.setProperty( + `--${ prefix }-${ property }`, + String( rect[ property ] ) + ) + ); + } ); + useLayoutEffect( () => { + setProperties(); + }, [ rect, setProperties ] ); + useOnValueUpdate( rect.element, ( { previousValue } ) => { + // Only enable the animation when moving from one element to another. + if ( rect.element && previousValue ) { + parent?.classList.add( 'is-animation-enabled' ); + } + } ); + useLayoutEffect( () => { + function onTransitionEnd( event: TransitionEvent ) { + if ( transitionEndFilter?.( event ) ?? true ) { + parent?.classList.remove( 'is-animation-enabled' ); + } + } + parent?.addEventListener( 'transitionend', onTransitionEnd ); + return () => + parent?.removeEventListener( 'transitionend', onTransitionEnd ); + }, [ parent, transitionEndFilter ] ); +} function UnconnectedToggleGroupControl( props: WordPressComponentProps< ToggleGroupControlProps, 'div', false >, @@ -44,10 +104,18 @@ function UnconnectedToggleGroupControl( ...otherProps } = useContextSystem( props, 'ToggleGroupControl' ); - const baseId = useInstanceId( ToggleGroupControl, 'toggle-group-control' ); const normalizedSize = __next40pxDefaultSize && size === 'default' ? '__unstable-large' : size; + const [ selectedElement, setSelectedElement ] = useState< HTMLElement >(); + const [ controlElement, setControlElement ] = useState< HTMLElement >(); + const refs = useMergeRefs( [ setControlElement, forwardedRef ] ); + useSubelementAnimation( value ? selectedElement : undefined, { + parent: controlElement, + prefix: 'selected', + transitionEndFilter: ( event ) => event.pseudoElement === '::before', + } ); + const cx = useCx(); const classes = useMemo( @@ -81,15 +149,16 @@ function UnconnectedToggleGroupControl( ) } - { children } + { children } ); diff --git a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts index 8d01c150a45ea..ee6122126f557 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts @@ -26,6 +26,47 @@ export const toggleGroupControl = ( { ${ toggleGroupControlSize( size ) } ${ ! isDeselectable && enclosingBorders( isBlock ) } + + @media not ( prefers-reduced-motion ) { + &.is-animation-enabled::before { + transition-property: transform, border-radius; + transition-duration: 0.2s; + transition-timing-function: ease-out; + } + } + + &::before { + content: ''; + position: absolute; + pointer-events: none; + background: ${ COLORS.gray[ 900 ] }; + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + outline-offset: -3px; + + /* Using a large value to avoid antialiasing rounding issues + when scaling in the transform, see: https://stackoverflow.com/a/52159123 */ + --antialiasing-factor: 100; + /* Adjusting the border radius to match the scaling in the x axis. */ + border-radius: calc( + ${ CONFIG.radiusXSmall } / + ( + var( --selected-width, 0 ) / + var( --antialiasing-factor ) + ) + ) / ${ CONFIG.radiusXSmall }; + left: -1px; // Correcting for border. + width: calc( var( --antialiasing-factor ) * 1px ); + height: calc( var( --selected-height, 0 ) * 1px ); + transform-origin: left top; + transform: translateX( calc( var( --selected-left, 0 ) * 1px ) ) + scaleX( + calc( + var( --selected-width, 0 ) / var( --antialiasing-factor ) + ) + ); + } `; const enclosingBorders = ( isBlock: ToggleGroupControlProps[ 'isBlock' ] ) => { diff --git a/packages/components/src/toggle-group-control/types.ts b/packages/components/src/toggle-group-control/types.ts index d49ef3cbb77cb..2a4af680263db 100644 --- a/packages/components/src/toggle-group-control/types.ts +++ b/packages/components/src/toggle-group-control/types.ts @@ -137,9 +137,11 @@ export type ToggleGroupControlContextProps = { size: ToggleGroupControlProps[ 'size' ]; value: ToggleGroupControlProps[ 'value' ]; setValue: ( newValue: string | number | undefined ) => void; + setSelectedElement: ( element: HTMLElement | undefined ) => void; }; export type ToggleGroupControlMainControlProps = Pick< ToggleGroupControlProps, 'children' | 'isAdaptiveWidth' | 'label' | 'size' | 'onChange' | 'value' ->; +> & + Pick< ToggleGroupControlContextProps, 'setSelectedElement' >; diff --git a/packages/components/src/utils/element-rect.ts b/packages/components/src/utils/element-rect.ts index 4c60e4ba51c48..7c83db4428ca0 100644 --- a/packages/components/src/utils/element-rect.ts +++ b/packages/components/src/utils/element-rect.ts @@ -9,6 +9,10 @@ import { useEvent, useResizeObserver } from '@wordpress/compose'; * The position and dimensions of an element, relative to its offset parent. */ export type ElementOffsetRect = { + /** + * The element the rect belongs to. + */ + element: HTMLElement | undefined; /** * The distance from the top edge of the offset parent to the top edge of * the element. @@ -43,6 +47,7 @@ export type ElementOffsetRect = { * An `ElementOffsetRect` object with all values set to zero. */ export const NULL_ELEMENT_OFFSET_RECT = { + element: undefined, top: 0, right: 0, bottom: 0, @@ -92,6 +97,7 @@ export function getElementOffsetRect( const scaleY = computedHeight / rect.height; return { + element, // To obtain the adjusted values for the position: // 1. Compute the element's position relative to the offset parent. // 2. Correct for the scale factor. @@ -119,6 +125,9 @@ const POLL_RATE = 100; * Tracks the position and dimensions of an element, relative to its offset * parent. The element can be changed dynamically. * + * When no element is provided (`null` or `undefined`), the hook will return + * a "null" rect, in which all values are `0` and `element` is `undefined`. + * * **Note:** sometimes, the measurement will fail (see `getElementOffsetRect`'s * documentation for more details). When that happens, this hook will attempt * to measure again after a frame, and if that fails, it will poll every 100 @@ -155,10 +164,12 @@ export function useTrackElementOffsetRect( } } ); - useLayoutEffect( - () => setElement( targetElement ), - [ setElement, targetElement ] - ); + useLayoutEffect( () => { + setElement( targetElement ); + if ( ! targetElement ) { + setIndicatorPosition( NULL_ELEMENT_OFFSET_RECT ); + } + }, [ setElement, targetElement ] ); return indicatorPosition; } diff --git a/packages/components/src/utils/hooks/use-on-value-update.ts b/packages/components/src/utils/hooks/use-on-value-update.ts index 05c7173d092fa..15cfc321359e7 100644 --- a/packages/components/src/utils/hooks/use-on-value-update.ts +++ b/packages/components/src/utils/hooks/use-on-value-update.ts @@ -3,7 +3,7 @@ * WordPress dependencies */ import { useEvent } from '@wordpress/compose'; -import { useRef, useEffect } from '@wordpress/element'; +import { useRef, useLayoutEffect } from '@wordpress/element'; /** * Context object for the `onUpdate` callback of `useOnValueUpdate`. @@ -27,7 +27,7 @@ export function useOnValueUpdate< T >( ) { const previousValueRef = useRef( value ); const updateCallbackEvent = useEvent( onUpdate ); - useEffect( () => { + useLayoutEffect( () => { if ( previousValueRef.current !== value ) { updateCallbackEvent( { previousValue: previousValueRef.current, From 493cfe68ef2aff6a43960d011fadf5a61f67b8f0 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Tue, 1 Oct 2024 14:02:51 -0500 Subject: [PATCH 22/49] Fix focus loss when deleting selected block in zoom out mode (#65761) --- .../block-editor/src/components/block-tools/zoom-out-toolbar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js index b8736de11481a..f2c073117d2ce 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js @@ -147,6 +147,7 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { label={ __( 'Delete' ) } onClick={ () => { removeBlock( clientId ); + __unstableContentRef.current?.focus(); } } /> ) } From 44aaefbc7b22c474ade9788e8fab3265b7988c2d Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:45:57 +0900 Subject: [PATCH 23/49] Social Links: Fix block appender size (#65769) Co-authored-by: t-hamano Co-authored-by: jeryj --- .../block-library/src/social-links/editor.scss | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/block-library/src/social-links/editor.scss b/packages/block-library/src/social-links/editor.scss index f9491cc068f15..11f1ed86d1122 100644 --- a/packages/block-library/src/social-links/editor.scss +++ b/packages/block-library/src/social-links/editor.scss @@ -101,19 +101,10 @@ .wp-block-social-links .block-list-appender { position: static; // display inline. - .block-editor-button-block-appender.components-button.components-button { - padding: $grid-unit-10 - 2px; - } -} - -.wp-block-social-links { - &.has-small-icon-size .block-editor-button-block-appender.components-button.components-button { + .block-editor-button-block-appender { + height: 1.5em; + width: 1.5em; + font-size: inherit; padding: 0; } - &.has-large-icon-size .block-editor-button-block-appender.components-button.components-button { - padding: $grid-unit-20 - 2px; - } - &.has-huge-icon-size .block-editor-button-block-appender.components-button.components-button { - padding: $grid-unit-30 - 1px; - } } From 56542d6c2bffa6e360176fec350ce03987bd1bed Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:36:27 +1000 Subject: [PATCH 24/49] Revert "Temp disable test for Classic Block Media issue (#65793)" (#65809) This reverts commit 8477a6184055c3c8a62c77af354826645dcaf351. Co-authored-by: peterwilsoncc Co-authored-by: Mamaduka --- test/e2e/specs/editor/blocks/classic.spec.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/e2e/specs/editor/blocks/classic.spec.js b/test/e2e/specs/editor/blocks/classic.spec.js index 6896433a3575e..95d39906b0d8b 100644 --- a/test/e2e/specs/editor/blocks/classic.spec.js +++ b/test/e2e/specs/editor/blocks/classic.spec.js @@ -39,10 +39,7 @@ test.describe( 'Classic', () => { await expect.poll( editor.getEditedPostContent ).toBe( 'test' ); } ); - // Reinitiate once this ticket is fixed: - // https://core.trac.wordpress.org/ticket/60666 - // eslint-disable-next-line playwright/no-skipped-test - test.skip( 'should insert media, convert to blocks, and undo in one step', async ( { + test( 'should insert media, convert to blocks, and undo in one step', async ( { editor, mediaUtils, page, From 06297caeb174b9151e870c8d8f10587a897b1a8a Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Wed, 2 Oct 2024 02:26:46 +0200 Subject: [PATCH 25/49] Restore accidentally removed entries in changelog (components package) (#65804) Co-authored-by: DaniGuardiola Co-authored-by: andrewserong Co-authored-by: ramonjd --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2d859184ae381..4289866ce8b46 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -16,6 +16,7 @@ ### Enhancements +- `Tabs`: handle horizontal overflow and large tab lists gracefully ([#64371](https://github.com/WordPress/gutenberg/pull/64371)). - `BorderControl`: promote to stable ([#65475](https://github.com/WordPress/gutenberg/pull/65475)). - `BorderBoxControl`: promote to stable ([#65586](https://github.com/WordPress/gutenberg/pull/65586)). - `MenuGroup`: Simplify the MenuGroup styles within dropdown menus. ([#65561](https://github.com/WordPress/gutenberg/pull/65561)). From fc6cd26eea98146682457c78df94b7c7b77a94ce Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 2 Oct 2024 14:38:06 +1000 Subject: [PATCH 26/49] Global style revisions: remove unnecessary `goTo` navigation call (#65810) Co-authored-by: ramonjd Co-authored-by: Mamaduka --- .../src/components/global-styles/screen-revisions/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 89af705cd9250..b980d199e7be3 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -72,7 +72,6 @@ function ScreenRevisions() { ); const onCloseRevisions = () => { - goTo( '/' ); // Return to global styles main panel. const canvasContainerView = editorCanvasContainerView === 'global-styles-revisions:style-book' ? 'style-book' From d727b8a35c50ab00bec6e383298bb90c68d1301b Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:41:44 +0200 Subject: [PATCH 27/49] Block Bindings: Add `@since` tag in bindings apis JSDocs (#65796) * Add tag to `useBlockBindingsUtils` * Add since tag to the registration functions Co-authored-by: SantosGuillamot Co-authored-by: gziolo Co-authored-by: colorful-tones --- packages/block-editor/README.md | 4 ++++ .../block-editor/src/utils/block-bindings.js | 2 ++ packages/blocks/README.md | 16 ++++++++++++++++ packages/blocks/src/api/registration.js | 8 ++++++++ 4 files changed, 30 insertions(+) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 32ba4598c4f92..a6f3b8980cfd7 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -881,6 +881,10 @@ _Returns_ - `?WPBlockBindingsUtils`: Object containing the block bindings utils. +_Changelog_ + +`6.7.0` Introduced in WordPress core. + ### useBlockCommands Undocumented declaration. diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js index db4de9d39cb25..2deeb95937174 100644 --- a/packages/block-editor/src/utils/block-bindings.js +++ b/packages/block-editor/src/utils/block-bindings.js @@ -30,6 +30,8 @@ function isObjectEmpty( object ) { * - `updateBlockBindings`: Updates the value of the bindings connected to block attributes. It can be used to remove a specific binding by setting the value to `undefined`. * - `removeAllBlockBindings`: Removes the bindings property of the `metadata` attribute. * + * @since 6.7.0 Introduced in WordPress core. + * * @return {?WPBlockBindingsUtils} Object containing the block bindings utils. * * @example diff --git a/packages/blocks/README.md b/packages/blocks/README.md index c5a754f88d492..f4805e1c60b38 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -127,6 +127,10 @@ _Returns_ - `?Object`: Block bindings source. +_Changelog_ + +`6.7.0` Introduced in WordPress core. + ### getBlockBindingsSources Returns all registered block bindings sources. @@ -135,6 +139,10 @@ _Returns_ - `Array`: Block bindings sources. +_Changelog_ + +`6.7.0` Introduced in WordPress core. + ### getBlockContent Given a block object, returns the Block's Inner HTML markup. @@ -542,6 +550,10 @@ _Parameters_ - _source.setValues_ `[Function]`: Optional function to update multiple values connected to the source. - _source.canUserEditValue_ `[Function]`: Optional function to determine if the user can edit the value. +_Changelog_ + +`6.7.0` Introduced in WordPress core. + ### registerBlockCollection Registers a new block collection to group blocks in the same namespace in the inserter. @@ -859,6 +871,10 @@ _Parameters_ - _name_ `string`: The name of the block bindings source to unregister. +_Changelog_ + +`6.7.0` Introduced in WordPress core. + ### unregisterBlockStyle Unregisters a block style for the given block. diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index f7d85cd816c9d..31be38b861c28 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -767,6 +767,8 @@ export const unregisterBlockVariation = ( blockName, variationName ) => { * behavior. Once registered, the source is available to be connected * to the supported block attributes. * + * @since 6.7.0 Introduced in WordPress core. + * * @param {Object} source Properties of the source to be registered. * @param {string} source.name The unique and machine-readable name. * @param {string} [source.label] Human-readable label. Optional when it is defined in the server. @@ -905,6 +907,8 @@ export const registerBlockBindingsSource = ( source ) => { /** * Unregisters a block bindings source by providing its name. * + * @since 6.7.0 Introduced in WordPress core. + * * @param {string} name The name of the block bindings source to unregister. * * @example @@ -926,6 +930,8 @@ export function unregisterBlockBindingsSource( name ) { /** * Returns a registered block bindings source by its name. * + * @since 6.7.0 Introduced in WordPress core. + * * @param {string} name Block bindings source name. * * @return {?Object} Block bindings source. @@ -937,6 +943,8 @@ export function getBlockBindingsSource( name ) { /** * Returns all registered block bindings sources. * + * @since 6.7.0 Introduced in WordPress core. + * * @return {Array} Block bindings sources. */ export function getBlockBindingsSources() { From 24e5a7c24850c86c97840a37df79e4a042b621b5 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Wed, 2 Oct 2024 09:45:01 +0300 Subject: [PATCH 28/49] Resize cover block only in normal mode (#65731) * only show resize controls in normal rendering mode * get block editing mode from hook not prop - misread code elsewhere Co-authored-by: draganescu Co-authored-by: getdave Co-authored-by: MaggieCabrera Co-authored-by: scruffian Co-authored-by: ellatrix --- packages/block-library/src/cover/edit/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index ec62bd58a2c33..804027708881b 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -18,6 +18,7 @@ import { useInnerBlocksProps, __experimentalUseGradient, store as blockEditorStore, + useBlockEditingMode, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -278,6 +279,9 @@ function CoverEdit( { const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType; const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType; + const blockEditingMode = useBlockEditingMode(); + const hasNonContentControls = blockEditingMode === 'default'; + const [ resizeListener, { height, width } ] = useResizeObserver(); const resizableBoxDimensions = useMemo( () => { return { @@ -447,7 +451,7 @@ function CoverEdit( { <> { blockControls } { inspectorControls } - { isSelected && ( + { hasNonContentControls && isSelected && ( ) }
- { isSelected && ( + { hasNonContentControls && isSelected && ( ) } From 6da8fab9a6055be1b2cacfeb6f0897db5abdf114 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 2 Oct 2024 08:21:51 +0100 Subject: [PATCH 29/49] Inserter: Fix Block visibility manager (#65700) Co-authored-by: youknowriad Co-authored-by: t-hamano --- .../inserter/hooks/use-patterns-state.js | 42 ++--- packages/block-editor/src/store/selectors.js | 155 ++++++++++++------ .../editor/various/allowed-patterns.spec.js | 8 +- 3 files changed, 123 insertions(+), 82 deletions(-) diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js index 13dae7f2ed7c0..91b34c0ec72c3 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -13,7 +13,7 @@ import { store as noticesStore } from '@wordpress/notices'; import { store as blockEditorStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; import { INSERTER_PATTERN_TYPES } from '../block-patterns-tab/utils'; -import { getParsedPattern } from '../../../store/utils'; +import { isFiltered } from '../../../store/utils'; /** * Retrieves the block patterns inserter state. @@ -31,48 +31,34 @@ const usePatternsState = ( selectedCategory, isQuick ) => { - const { patternCategories, allPatterns, userPatternCategories } = useSelect( + const options = useMemo( + () => ( { [ isFiltered ]: !! isQuick } ), + [ isQuick ] + ); + const { patternCategories, patterns, userPatternCategories } = useSelect( ( select ) => { - const { - getAllPatterns, - getSettings, - __experimentalGetAllowedPatterns, - } = unlock( select( blockEditorStore ) ); + const { getSettings, __experimentalGetAllowedPatterns } = unlock( + select( blockEditorStore ) + ); const { __experimentalUserPatternCategories, __experimentalBlockPatternCategories, } = getSettings(); return { - allPatterns: isQuick - ? __experimentalGetAllowedPatterns() - : getAllPatterns(), + patterns: __experimentalGetAllowedPatterns( + rootClientId, + options + ), userPatternCategories: __experimentalUserPatternCategories, patternCategories: __experimentalBlockPatternCategories, }; }, - [ isQuick ] + [ rootClientId, options ] ); const { getClosestAllowedInsertionPointForPattern } = unlock( useSelect( blockEditorStore ) ); - const patterns = useMemo( - () => - isQuick - ? allPatterns - : allPatterns - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( pattern ) => { - return { - ...pattern, - get blocks() { - return getParsedPattern( pattern ).blocks; - }, - }; - } ), - [ isQuick, allPatterns ] - ); - const allCategories = useMemo( () => { const categories = [ ...patternCategories ]; userPatternCategories?.forEach( ( userCategory ) => { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index fdadea8c7b716..4a754dd2d9b0a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1540,6 +1540,59 @@ export function getTemplateLock( state, rootClientId ) { return getBlockListSettings( state, rootClientId )?.templateLock ?? false; } +/** + * Determines if the given block type is visible in the inserter. + * Note that this is different than whether a block is allowed to be inserted. + * In some cases, the block is not allowed in a given position but + * it should still be visible in the inserter to be able to add it + * to a different position. + * + * @param {Object} state Editor state. + * @param {string|Object} blockNameOrType The block type object, e.g., the response + * from the block directory; or a string name of + * an installed block type, e.g.' core/paragraph'. + * + * @return {boolean} Whether the given block type is allowed to be inserted. + */ +const isBlockVisibleInTheInserter = ( state, blockNameOrType ) => { + let blockType; + let blockName; + if ( blockNameOrType && 'object' === typeof blockNameOrType ) { + blockType = blockNameOrType; + blockName = blockNameOrType.name; + } else { + blockType = getBlockType( blockNameOrType ); + blockName = blockNameOrType; + } + if ( ! blockType ) { + return false; + } + + const { allowedBlockTypes } = getSettings( state ); + + const isBlockAllowedInEditor = checkAllowList( + allowedBlockTypes, + blockName, + true + ); + if ( ! isBlockAllowedInEditor ) { + return false; + } + + // If parent blocks are not visible, child blocks should be hidden too. + if ( !! blockType.parent?.length ) { + return blockType.parent.some( + ( name ) => + isBlockVisibleInTheInserter( state, name ) || + // Exception for blocks with post-content parent, + // the root level is often consider as "core/post-content". + // This exception should only apply to the post editor ideally though. + name === 'core/post-content' + ); + } + return true; +}; + /** * Determines if the given block type is allowed to be inserted into the block list. * This function is not exported and not memoized because using a memoized selector @@ -1558,6 +1611,10 @@ const canInsertBlockTypeUnmemoized = ( blockName, rootClientId = null ) => { + if ( ! isBlockVisibleInTheInserter( state, blockName ) ) { + return false; + } + let blockType; if ( blockName && 'object' === typeof blockName ) { blockType = blockName; @@ -1565,20 +1622,6 @@ const canInsertBlockTypeUnmemoized = ( } else { blockType = getBlockType( blockName ); } - if ( ! blockType ) { - return false; - } - - const { allowedBlockTypes } = getSettings( state ); - - const isBlockAllowedInEditor = checkAllowList( - allowedBlockTypes, - blockName, - true - ); - if ( ! isBlockAllowedInEditor ) { - return false; - } const isLocked = !! getTemplateLock( state, rootClientId ); if ( isLocked ) { @@ -1972,6 +2015,7 @@ const buildBlockTypeItem = description: blockType.description, category: blockType.category, keywords: blockType.keywords, + parent: blockType.parent, variations: inserterVariations, example: blockType.example, utility: 1, // Deprecated. @@ -2067,16 +2111,18 @@ export const getInserterItems = createRegistrySelector( ( select ) => ) ); } else { - blockTypeInserterItems = blockTypeInserterItems.map( - ( blockType ) => ( { + blockTypeInserterItems = blockTypeInserterItems + .filter( ( blockType ) => + isBlockVisibleInTheInserter( state, blockType ) + ) + .map( ( blockType ) => ( { ...blockType, isAllowedInCurrentRoot: canIncludeBlockTypeInInserter( state, blockType, rootClientId ), - } ) - ); + } ) ); } const items = blockTypeInserterItems.reduce( @@ -2348,37 +2394,50 @@ const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => [ */ export const __experimentalGetAllowedPatterns = createRegistrySelector( ( select ) => { - return createSelector( ( state, rootClientId = null ) => { - const { getAllPatterns } = unlock( select( STORE_NAME ) ); - const patterns = getAllPatterns(); - const { allowedBlockTypes } = getSettings( state ); - const parsedPatterns = patterns - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( pattern ) => { - return { - ...pattern, - get blocks() { - return getParsedPattern( pattern ).blocks; - }, - }; - } ); - - const availableParsedPatterns = parsedPatterns.filter( - ( pattern ) => - checkAllowListRecursive( - getGrammar( pattern ), - allowedBlockTypes - ) - ); - const patternsAllowed = availableParsedPatterns.filter( - ( pattern ) => - getGrammar( pattern ).every( ( { blockName: name } ) => - canInsertBlockType( state, name, rootClientId ) - ) - ); + return createSelector( + ( + state, + rootClientId = null, + options = DEFAULT_INSERTER_OPTIONS + ) => { + const { getAllPatterns } = unlock( select( STORE_NAME ) ); + const patterns = getAllPatterns(); + const { allowedBlockTypes } = getSettings( state ); + const parsedPatterns = patterns + .filter( ( { inserter = true } ) => !! inserter ) + .map( ( pattern ) => { + return { + ...pattern, + get blocks() { + return getParsedPattern( pattern ).blocks; + }, + }; + } ); + + const availableParsedPatterns = parsedPatterns.filter( + ( pattern ) => + checkAllowListRecursive( + getGrammar( pattern ), + allowedBlockTypes + ) + ); + const patternsAllowed = availableParsedPatterns.filter( + ( pattern ) => + getGrammar( pattern ).every( ( { blockName: name } ) => + options[ isFiltered ] !== false + ? canInsertBlockType( + state, + name, + rootClientId + ) + : isBlockVisibleInTheInserter( state, name ) + ) + ); - return patternsAllowed; - }, getAllowedPatternsDependants( select ) ); + return patternsAllowed; + }, + getAllowedPatternsDependants( select ) + ); } ); diff --git a/test/e2e/specs/editor/various/allowed-patterns.spec.js b/test/e2e/specs/editor/various/allowed-patterns.spec.js index 894f143d19bb8..83d44403d60ee 100644 --- a/test/e2e/specs/editor/various/allowed-patterns.spec.js +++ b/test/e2e/specs/editor/various/allowed-patterns.spec.js @@ -54,7 +54,7 @@ test.describe( 'Allowed Patterns', () => { ); } ); - test( 'should show all patterns even if not allowed', async ( { + test( 'should hide patterns with only hidden blocks', async ( { admin, page, } ) => { @@ -77,11 +77,7 @@ test.describe( 'Allowed Patterns', () => { page .getByRole( 'listbox', { name: 'Block patterns' } ) .getByRole( 'option' ) - ).toHaveText( [ - 'Test: Single heading', - 'Test: Single paragraph', - 'Test: Paragraph inside group', - ] ); + ).toHaveText( [ 'Test: Single heading' ] ); } ); } ); } ); From a5027917a11d3ba921af616b9595b44c00ddb63e Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 2 Oct 2024 05:23:16 -0300 Subject: [PATCH 30/49] Avoid errors when a fontSize preset is not available (#65791) * Avoid errors when the fontSize is not available * remove goBack * Update packages/edit-site/src/components/global-styles/font-sizes/font-size.js --- Co-authored-by: matiasbenedetto Co-authored-by: t-hamano Co-authored-by: ciampo Co-authored-by: ramonjd Co-authored-by: firoz2456 Co-authored-by: nith53 Co-authored-by: benniledl --- .../global-styles/font-sizes/font-size.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js index 80b1ea55c405b..ef520a0163c03 100644 --- a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js +++ b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js @@ -15,7 +15,7 @@ import { ToggleControl, } from '@wordpress/components'; import { moreVertical } from '@wordpress/icons'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -36,7 +36,6 @@ function FontSize() { const { params: { origin, slug }, - goBack, goTo, } = useNavigator(); @@ -54,10 +53,10 @@ function FontSize() { // Whether the font size is fluid. If not defined, use the global fluid value of the theme. const isFluid = - fontSize.fluid !== undefined ? !! fontSize.fluid : !! globalFluid; + fontSize?.fluid !== undefined ? !! fontSize.fluid : !! globalFluid; // Whether custom fluid values are used. - const isCustomFluid = typeof fontSize.fluid === 'object'; + const isCustomFluid = typeof fontSize?.fluid === 'object'; const handleNameChange = ( value ) => { updateFontSize( 'name', value ); @@ -107,9 +106,6 @@ function FontSize() { }; const handleRemoveFontSize = () => { - // Navigate to the font sizes list. - goBack(); - const newFontSizes = sizes.filter( ( size ) => size.slug !== slug ); setFontSizes( { ...fontSizes, @@ -125,6 +121,18 @@ function FontSize() { setIsRenameDialogOpen( ! isRenameDialogOpen ); }; + // Navigate to the font sizes list if the font size is not available. + useEffect( () => { + if ( ! fontSize ) { + goTo( '/typography/font-sizes/', { isBack: true } ); + } + }, [ fontSize, goTo ] ); + + // Avoid rendering if the font size is not available. + if ( ! fontSize ) { + return null; + } + return ( <> Date: Wed, 2 Oct 2024 09:48:59 +0100 Subject: [PATCH 31/49] Hide Zoom Out inserter buttons when dragging (#65789) Co-authored-by: getdave Co-authored-by: talldan Co-authored-by: richtabor --- packages/block-editor/src/components/block-tools/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index d038662455dea..f720b871dbced 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -36,6 +36,7 @@ function selector( select ) { isTyping, isSectionBlock, getParentSectionBlock, + isDragging, } = unlock( select( blockEditorStore ) ); const clientId = @@ -56,6 +57,7 @@ function selector( select ) { hasFixedToolbar: getSettings().hasFixedToolbar, isTyping: isTyping(), isZoomOutMode: isZoomOut, + isDragging: isDragging(), }; } @@ -254,7 +256,7 @@ export default function BlockTools( { name="__unstable-block-tools-after" ref={ blockToolbarAfterRef } /> - { isZoomOutMode && ( + { isZoomOutMode && ! isDragging && ( From 5ab5789fc1efdcf770d1dd298d46c422a3a107c5 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:56:00 +0900 Subject: [PATCH 32/49] Fix: Shadow/Font size preset panel crashes the editor (#65765) * Fix: Shadow/Font size preset panel crashes the editor * Refactor logic Co-authored-by: t-hamano Co-authored-by: tyxla Co-authored-by: ciampo Co-authored-by: ramonjd --- .../global-styles/font-sizes/font-size.js | 23 +++++++++---------- .../global-styles/shadows-edit-panel.js | 4 ++++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js index ef520a0163c03..63310203ef05b 100644 --- a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js +++ b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js @@ -51,6 +51,17 @@ function FontSize() { // Get the font size by slug. const fontSize = sizes.find( ( size ) => size.slug === slug ); + // Navigate to the font sizes list if the font size is not available. + useEffect( () => { + if ( ! fontSize ) { + goTo( '/typography/font-sizes/', { isBack: true } ); + } + }, [ fontSize, goTo ] ); + + if ( ! origin || ! slug || ! fontSize ) { + return null; + } + // Whether the font size is fluid. If not defined, use the global fluid value of the theme. const isFluid = fontSize?.fluid !== undefined ? !! fontSize.fluid : !! globalFluid; @@ -121,18 +132,6 @@ function FontSize() { setIsRenameDialogOpen( ! isRenameDialogOpen ); }; - // Navigate to the font sizes list if the font size is not available. - useEffect( () => { - if ( ! fontSize ) { - goTo( '/typography/font-sizes/', { isBack: true } ); - } - }, [ fontSize, goTo ] ); - - // Avoid rendering if the font size is not available. - if ( ! fontSize ) { - return null; - } - return ( <> { setSelectedShadow( { ...selectedShadow, shadow } ); const updatedShadows = shadows.map( ( s ) => From 377316a211fd659caddf92f1d3dde16c761fd856 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 2 Oct 2024 14:22:10 +0200 Subject: [PATCH 33/49] Composite: always await initial render setup in unit tests (#65823) Co-authored-by: ciampo Co-authored-by: tyxla --- .../src/composite/legacy/test/index.tsx | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/components/src/composite/legacy/test/index.tsx b/packages/components/src/composite/legacy/test/index.tsx index c034d31442ca8..a118dbcfbadbb 100644 --- a/packages/components/src/composite/legacy/test/index.tsx +++ b/packages/components/src/composite/legacy/test/index.tsx @@ -232,7 +232,7 @@ describe.each( [ ); - renderAndValidate( ); + await renderAndValidate( ); await press.Tab(); expect( screen.getByText( 'Before' ) ).toHaveFocus(); @@ -260,7 +260,7 @@ describe.each( [ ); }; - renderAndValidate( ); + await renderAndValidate( ); const { item1, item2, item3 } = getOneDimensionalItems(); @@ -289,7 +289,7 @@ describe.each( [ ); }; - renderAndValidate( ); + await renderAndValidate( ); const { item1, item2, item3 } = getOneDimensionalItems(); expect( item2 ).toBeEnabled(); @@ -310,7 +310,7 @@ describe.each( [ } ) } /> ); - renderAndValidate( ); + await renderAndValidate( ); const { item1, item2, item3 } = getOneDimensionalItems(); expect( item1.id ).toMatch( 'test-id-1' ); @@ -327,7 +327,7 @@ describe.each( [ } ) } /> ); - renderAndValidate( ); + await renderAndValidate( ); const { item2 } = getOneDimensionalItems(); await press.Tab(); @@ -341,37 +341,37 @@ describe.each( [ ] )( '%s', ( _when, rtl ) => { const { previous, next, first, last } = getKeys( rtl ); - function useOneDimensionalTest( initialState?: InitialState ) { + async function useOneDimensionalTest( initialState?: InitialState ) { const Test = () => ( ); - renderAndValidate( ); + await renderAndValidate( ); return getOneDimensionalItems(); } - function useTwoDimensionalTest( initialState?: InitialState ) { + async function useTwoDimensionalTest( initialState?: InitialState ) { const Test = () => ( ); - renderAndValidate( ); + await renderAndValidate( ); return getTwoDimensionalItems(); } - function useShiftTest( shift: boolean ) { + async function useShiftTest( shift: boolean ) { const Test = () => ( ); - renderAndValidate( ); + await renderAndValidate( ); return getShiftTestItems(); } describe( 'In one dimension', () => { test( 'All directions work with no orientation', async () => { - const { item1, item2, item3 } = useOneDimensionalTest(); + const { item1, item2, item3 } = await useOneDimensionalTest(); await press.Tab(); expect( item1 ).toHaveFocus(); @@ -406,7 +406,7 @@ describe.each( [ } ); test( 'Only left/right work with horizontal orientation', async () => { - const { item1, item2, item3 } = useOneDimensionalTest( { + const { item1, item2, item3 } = await useOneDimensionalTest( { orientation: 'horizontal', } ); @@ -435,7 +435,7 @@ describe.each( [ } ); test( 'Only up/down work with vertical orientation', async () => { - const { item1, item2, item3 } = useOneDimensionalTest( { + const { item1, item2, item3 } = await useOneDimensionalTest( { orientation: 'vertical', } ); @@ -464,7 +464,7 @@ describe.each( [ } ); test( 'Focus wraps with loop enabled', async () => { - const { item1, item2, item3 } = useOneDimensionalTest( { + const { item1, item2, item3 } = await useOneDimensionalTest( { loop: true, } ); @@ -488,7 +488,7 @@ describe.each( [ describe( 'In two dimensions', () => { test( 'All directions work as standard', async () => { const { itemA1, itemA2, itemA3, itemB1, itemB2, itemC1, itemC3 } = - useTwoDimensionalTest(); + await useTwoDimensionalTest(); await press.Tab(); expect( itemA1 ).toHaveFocus(); @@ -524,7 +524,7 @@ describe.each( [ test( 'Focus wraps around rows/columns with loop enabled', async () => { const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = - useTwoDimensionalTest( { loop: true } ); + await useTwoDimensionalTest( { loop: true } ); await press.Tab(); expect( itemA1 ).toHaveFocus(); @@ -548,7 +548,7 @@ describe.each( [ test( 'Focus moves between rows/columns with wrap enabled', async () => { const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = - useTwoDimensionalTest( { wrap: true } ); + await useTwoDimensionalTest( { wrap: true } ); await press.Tab(); expect( itemA1 ).toHaveFocus(); @@ -577,7 +577,7 @@ describe.each( [ } ); test( 'Focus wraps around start/end with loop and wrap enabled', async () => { - const { itemA1, itemC3 } = useTwoDimensionalTest( { + const { itemA1, itemC3 } = await useTwoDimensionalTest( { loop: true, wrap: true, } ); @@ -595,7 +595,8 @@ describe.each( [ } ); test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { - const { itemA1, itemB1, itemB2, itemC1 } = useShiftTest( true ); + const { itemA1, itemB1, itemB2, itemC1 } = + await useShiftTest( true ); await press.Tab(); expect( itemA1 ).toHaveFocus(); @@ -616,7 +617,7 @@ describe.each( [ } ); test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { - const { itemA1, itemB1, itemB2 } = useShiftTest( false ); + const { itemA1, itemB1, itemB2 } = await useShiftTest( false ); await press.Tab(); expect( itemA1 ).toHaveFocus(); From 7c339b1193dec9abb60e9e201f27bf62a447c0b1 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 2 Oct 2024 14:22:34 +0200 Subject: [PATCH 34/49] Navigator: mark experimental exports as deprecated (#65802) * Navigator: mark experimental exports as deprecated * CHANGELOG * Update JSDocs deprecation alternatives suggestions --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/index.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4289866ce8b46..2cb9849819bef 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,6 +13,7 @@ - `__experimentalBorderControl` can now be imported as a stable `BorderControl` ([#65475](https://github.com/WordPress/gutenberg/pull/65475)). - `__experimentalBorderBoxControl` can now be imported as a stable `BorderBoxControl` ([#65586](https://github.com/WordPress/gutenberg/pull/65586)). +- `__experimentalNavigator*` components can now be imported as a stable `Navigator`. Similarly, the `__experimentalUseNavigator` hook can be imported as a stable `useNavigator` ([#65802](https://github.com/WordPress/gutenberg/pull/65802)). ### Enhancements diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 69e76b0b91f43..e82d6da70279e 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -129,15 +129,21 @@ export { default as __experimentalNavigationGroup } from './navigation/group'; export { default as __experimentalNavigationItem } from './navigation/item'; export { default as __experimentalNavigationMenu } from './navigation/menu'; export { + /** @deprecated Import `Navigator` instead. */ NavigatorProvider as __experimentalNavigatorProvider, + /** @deprecated Import `Navigator` and use `Navigator.Screen` instead. */ NavigatorScreen as __experimentalNavigatorScreen, + /** @deprecated Import `Navigator` and use `Navigator.Button` instead. */ NavigatorButton as __experimentalNavigatorButton, + /** @deprecated Import `Navigator` and use `Navigator.BackButton` instead. */ NavigatorBackButton as __experimentalNavigatorBackButton, + /** @deprecated Import `Navigator` and use `Navigator.BackButton` instead. */ NavigatorToParentButton as __experimentalNavigatorToParentButton, } from './navigator/legacy'; export { Navigator, useNavigator, + /** @deprecated Import `useNavigator` instead. */ useNavigator as __experimentalUseNavigator, } from './navigator'; export { default as Notice } from './notice'; From 78c0be87d3cc1d871a83b0f4fd8f719429df38fc Mon Sep 17 00:00:00 2001 From: JuanMa Date: Wed, 2 Oct 2024 14:48:52 +0100 Subject: [PATCH 35/49] Docs/interactivity api router package readme (#62062) * Updated intro to the package * removed examples and demos * update README.md * Query Loop reference * fixed typo * Wording improved * Interactivity API: Update and expand on the interactivity-router docs * Update the inline HTML in the callout --------- Co-authored-by: Michal Czaplinski --- packages/interactivity-router/README.md | 139 ++++++++++++++++++------ 1 file changed, 107 insertions(+), 32 deletions(-) diff --git a/packages/interactivity-router/README.md b/packages/interactivity-router/README.md index 94b88e80886c9..efb52e59be2b5 100644 --- a/packages/interactivity-router/README.md +++ b/packages/interactivity-router/README.md @@ -1,21 +1,32 @@ -# Interactivity Router +# `@wordpress/interactivity-router` -> **Note** -> This package is a extension of the API shared at [Proposal: The Interactivity API – A better developer experience in building interactive blocks](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/). As part of an [Open Source project](https://developer.wordpress.org/block-editor/getting-started/faq/#the-gutenberg-project) we encourage participation in helping shape this API and the [discussions in GitHub](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api) is the best place to engage. +The package `@wordpress/interactivity-router` enables loading content from other pages without a full page reload. Currently, the only supported mode is "region-based". Full "client-side navigation" is still in experimental phase. -This package defines an Interactivity API store with the `core/router` namespace, exposing state and actions like `navigate` and `prefetch` to handle client-side navigations. +The package defines an Interactivity API store with the `core/router` namespace, exposing state and 2 actions: `navigate` and `prefetch` to handle client-side navigation. + +The `@wordpress/interactivity-router` package was [introduced in WordPress Core in v6.5](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/). This means this package is already bundled in Core in any version of WordPress higher than v6.5. + +
+ Check the Interactivity API Reference docs in the Block Editor handbook to learn more about the Interactivity API. +
## Usage -The package is intended to be imported dynamically in the `view.js` files of interactive blocks. +The package is intended to be imported dynamically in the `view.js` files of interactive blocks. This is done in in order to reduce the JS bundle size on the initial page load. ```js +/* view.js */ + import { store } from '@wordpress/interactivity'; -store( 'myblock', { +// This is how you would typically use the navigate() action in your block. +store( 'my-namespace/myblock', { actions: { - *navigate( e ) { + *goToPage( e ) { e.preventDefault(); + + // We import the package dynamically to reduce the initial JS bundle size. + // Async actions are defined as generators so the import() must be called with `yield`. const { actions } = yield import( '@wordpress/interactivity-router' ); @@ -25,52 +36,116 @@ store( 'myblock', { } ); ``` -## Frequently Asked Questions +Now, you can call `actions.navigate()` in your block's `view.js` file to navigate to a different page or e.g. pass it to a `data-wp-on--click` attribute. + +When loaded, this package [adds the following state and actions](https://github.com/WordPress/gutenberg/blob/ed7d78652526270b63976d7a970dba46a2bfcbb0/packages/interactivity-router/src/index.ts#L212) to the `core/router` store: + +```js +const { state, actions } = store( 'core/router', { + state: { + url: window.location.href, + navigation: { + hasStarted: false, + hasFinished: false, + texts: { + loading: '', + loaded: '', + }, + message: '', + }, + }, + actions: { + *navigate(href, options) {...}, + prefetch(url, options) {...}, + } +}) +``` + +
+ The core "Query Loop" block is using this package to provide the region-based navigation. +
+ +### Directives + +#### `data-wp-router-region` + +It defines a region that is updated on navigation. It requires a unique ID as the value and can only be used in root interactive elements, i.e., elements with `data-wp-interactive` that are not nested inside other elements with `data-wp-interactive`. + +Example: + +```html +
+ + +
+``` + +### Actions + +#### `navigate` + +Navigates to the specified page. -At this point, some of the questions you have about the Interactivity API may be: +This function normalizes the passed `href`, fetches the page HTML if needed, and updates any interactive regions whose contents have changed in the new page. It also creates a new entry in the browser session history. -### What is this? +**Params** -This is the base of a new standard to create interactive blocks. Read [the proposal](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/) to learn more about this. +```js +navigate( href: string, options: NavigateOptions = {} ) +``` -### Can I use it? +- `href`: The page `href`. +- `options`: Options object. + - `force`: If `true`, it forces re-fetching the URL. `navigate()` always caches the page, so if the page has been navigated to before, it will be used. Default is `false`. + - `html`: HTML string to be used instead of fetching the requested URL. + - `replace`: If `true`, it replaces the current entry in the browser session history. Default is `false`. + - `timeout`: Time until the navigation is aborted, in milliseconds. Default is `10000`. + - `loadingAnimation`: Whether an animation should be shown while navigating. Default to `true`. + - `screenReaderAnnouncement`: Whether a message for screen readers should be announced while navigating. Default to `true`. -You can test it, but it's still very experimental. +#### `prefetch` -### How do I get started? +Prefetches the page for the passed URL. The page is cached and can be used for navigation. -The best place to start with the Interactivity API is this [**Getting started guide**](https://github.com/WordPress/gutenberg/blob/trunk/packages/interactivity/docs/1-getting-started.md). There you'll will find a very quick start guide and the current requirements of the Interactivity API. +The function normalizes the URL and stores internally the fetch promise, to avoid triggering a second fetch for an ongoing request. -### Where can I ask questions? +**Params** -The [“Interactivity API” category](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api) in Gutenberg repo discussions is the best place to ask questions about the Interactivity API. +```js +prefetch( url: string, options: PrefetchOptions = {} ) +``` -### Where can I share my feedback about the API? +- `url`: The page `url`. +- `options`: Options object. -The [“Interactivity API” category](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api) in Gutenberg repo discussions is also the best place to share your feedback about the Interactivity API. + - `force`: If `true`, forces fetching the URL again. + - `html`: HTML string to be used instead of fetching the requested URL. + +### State + +`state.url` is a reactive property synchronized with the current URL. +Properties under `state.navigation` are meant for loading bar animations. ## Installation Install the module: ```bash -npm install @wordpress/interactivity --save +npm install @wordpress/interactivity-router --save ``` -_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ - -## Docs & Examples +This step is only required if you use the Interactivity API outside WordPress. -**[Interactivity API Documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/interactivity/docs)** is the best place to learn about this proposal. Although it's still in progress, some key pages are already available: +Within WordPress, the package is already bundled in Core. To ensure it's enqueued, add `@wordpress/interactivity-router` to the dependency array of the script module. This process is often done automatically with tools like [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/). -- **[Getting Started Guide](https://github.com/WordPress/gutenberg/blob/trunk/packages/interactivity/docs/1-getting-started.md)**: Follow this Getting Started guide to learn how to scaffold a new project and create your first interactive blocks. -- **[API Reference](https://github.com/WordPress/gutenberg/blob/trunk/packages/interactivity/docs/2-api-reference.md)**: Check this page for technical detailed explanations and examples of the directives and the store. +Furthermore, this package assumes your code will run in an **ES2015+** environment. If you're using an environment with limited or no support for such language features and APIs, you should include the polyfill shipped in [`@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code. -Here you have some more resources to learn/read more about the Interactivity API: +## License -- **[Interactivity API Discussions](https://github.com/WordPress/gutenberg/discussions/52882)** -- [Proposal: The Interactivity API – A better developer experience in building interactive blocks](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/) -- Developer Hours sessions ([Americas](https://www.youtube.com/watch?v=RXNoyP2ZiS8&t=664s) & [APAC/EMEA](https://www.youtube.com/watch?v=6ghbrhyAcvA)) -- [wpmovies.dev](http://wpmovies.dev/) demo and its [wp-movies-demo](https://github.com/WordPress/wp-movies-demo) repo +Interactivity API proposal, as part of Gutenberg and the WordPress project is free software, and is released under the terms of the GNU General Public License version 2 or (at your option) any later version. See [LICENSE.md](https://github.com/WordPress/gutenberg/blob/trunk/LICENSE.md) for complete license. -

Code is Poetry.

+

Code is Poetry.

From 2b6b567dda16b42fcb36b14abf995e6baa235303 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 2 Oct 2024 15:49:19 +0200 Subject: [PATCH 36/49] Composite: fix legacy implementation passing store prop (#65821) * Composite: fix legacy implementation passing store prop * Simplify code * Improve comment * Apply same fix to the top level Composite too * CHANGELOG --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/composite/group-label.tsx | 12 +++++++----- packages/components/src/composite/group.tsx | 14 +++++++------- packages/components/src/composite/hover.tsx | 14 +++++++------- packages/components/src/composite/index.tsx | 7 ++++++- packages/components/src/composite/item.tsx | 14 +++++++------- packages/components/src/composite/row.tsx | 14 +++++++------- packages/components/src/composite/typeahead.tsx | 14 +++++++------- 8 files changed, 49 insertions(+), 41 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2cb9849819bef..4c00ea32bae2c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,6 +8,7 @@ - `Navigator`: fix `isInitial` logic ([#65527](https://github.com/WordPress/gutenberg/pull/65527)). - `ToggleGroupControl`: Fix arrow key navigation in RTL ([#65735](https://github.com/WordPress/gutenberg/pull/65735)). - `ToggleGroupControl`: indicator doesn't jump around when the layout around it changes ([#65175](https://github.com/WordPress/gutenberg/pull/65175)). +- `Composite`: fix legacy support for the store prop ([#65821](https://github.com/WordPress/gutenberg/pull/65821)). ### Deprecations diff --git a/packages/components/src/composite/group-label.tsx b/packages/components/src/composite/group-label.tsx index 17070dbb86bf8..7e3c6ffdc7759 100644 --- a/packages/components/src/composite/group-label.tsx +++ b/packages/components/src/composite/group-label.tsx @@ -20,11 +20,13 @@ export const CompositeGroupLabel = forwardRef< WordPressComponentProps< CompositeGroupLabelProps, 'div', false > >( function CompositeGroupLabel( props, ref ) { const context = useCompositeContext(); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + return ( - + ); } ); diff --git a/packages/components/src/composite/group.tsx b/packages/components/src/composite/group.tsx index ae21ca6f11dd9..bcfb47e684613 100644 --- a/packages/components/src/composite/group.tsx +++ b/packages/components/src/composite/group.tsx @@ -20,11 +20,11 @@ export const CompositeGroup = forwardRef< WordPressComponentProps< CompositeGroupProps, 'div', false > >( function CompositeGroup( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/hover.tsx b/packages/components/src/composite/hover.tsx index ca0bd9d8f6aa1..1507a1879cc19 100644 --- a/packages/components/src/composite/hover.tsx +++ b/packages/components/src/composite/hover.tsx @@ -20,11 +20,11 @@ export const CompositeHover = forwardRef< WordPressComponentProps< CompositeHoverProps, 'div', false > >( function CompositeHover( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx index e9e97072261fb..8eb562f5bdab3 100644 --- a/packages/components/src/composite/index.tsx +++ b/packages/components/src/composite/index.tsx @@ -73,7 +73,10 @@ export const Composite = Object.assign( }, ref ) { - const store = Ariakit.useCompositeStore( { + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. + const storeProp = props.store as Ariakit.CompositeStore; + const internalStore = Ariakit.useCompositeStore( { activeId, defaultActiveId, setActiveId, @@ -85,6 +88,8 @@ export const Composite = Object.assign( rtl, } ); + const store = storeProp ?? internalStore; + const contextValue = useMemo( () => ( { store, diff --git a/packages/components/src/composite/item.tsx b/packages/components/src/composite/item.tsx index 6d75b90f0baaa..4a02f76039a5c 100644 --- a/packages/components/src/composite/item.tsx +++ b/packages/components/src/composite/item.tsx @@ -20,11 +20,11 @@ export const CompositeItem = forwardRef< WordPressComponentProps< CompositeItemProps, 'button', false > >( function CompositeItem( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/row.tsx b/packages/components/src/composite/row.tsx index a082af03ad678..1a88da557785e 100644 --- a/packages/components/src/composite/row.tsx +++ b/packages/components/src/composite/row.tsx @@ -20,11 +20,11 @@ export const CompositeRow = forwardRef< WordPressComponentProps< CompositeRowProps, 'div', false > >( function CompositeRow( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/typeahead.tsx b/packages/components/src/composite/typeahead.tsx index 771d58bcb6c25..519c59ea374e5 100644 --- a/packages/components/src/composite/typeahead.tsx +++ b/packages/components/src/composite/typeahead.tsx @@ -20,11 +20,11 @@ export const CompositeTypeahead = forwardRef< WordPressComponentProps< CompositeTypeaheadProps, 'div', false > >( function CompositeTypeahead( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); From e0a8d291c2f6ff369b17a458cb7ea41811817e35 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Wed, 2 Oct 2024 10:55:56 -0300 Subject: [PATCH 37/49] DataForm - Add combined fields support (#65399) Co-authored-by: louwie17 Co-authored-by: youknowriad Co-authored-by: gigitux Co-authored-by: oandregal Co-authored-by: jameskoster --- .../dataform-combined-edit/index.tsx | 66 +++++++++++++++++++ .../dataform-combined-edit/style.scss | 12 ++++ .../dataform/stories/index.story.tsx | 65 ++++++++++++++++++ .../dataforms-layouts/get-visible-fields.ts | 29 ++++++++ .../src/dataforms-layouts/panel/index.tsx | 15 +++-- .../src/dataforms-layouts/regular/index.tsx | 15 +++-- packages/dataviews/src/normalize-fields.ts | 34 +++++++++- packages/dataviews/src/style.scss | 1 + packages/dataviews/src/types.ts | 38 ++++++++--- packages/dataviews/src/validation.ts | 2 +- 10 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 packages/dataviews/src/components/dataform-combined-edit/index.tsx create mode 100644 packages/dataviews/src/components/dataform-combined-edit/style.scss create mode 100644 packages/dataviews/src/dataforms-layouts/get-visible-fields.ts diff --git a/packages/dataviews/src/components/dataform-combined-edit/index.tsx b/packages/dataviews/src/components/dataform-combined-edit/index.tsx new file mode 100644 index 0000000000000..6b2a752fa8de5 --- /dev/null +++ b/packages/dataviews/src/components/dataform-combined-edit/index.tsx @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalHeading as Heading, + __experimentalSpacer as Spacer, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import type { DataFormCombinedEditProps, NormalizedField } from '../../types'; + +function Header( { title }: { title: string } ) { + return ( + + + + { title } + + + + + ); +} + +function DataFormCombinedEdit< Item >( { + field, + data, + onChange, + hideLabelFromVision, +}: DataFormCombinedEditProps< Item > ) { + const className = 'dataforms-combined-edit'; + const visibleChildren = ( field.children ?? [] ) + .map( ( fieldId ) => field.fields.find( ( { id } ) => id === fieldId ) ) + .filter( + ( childField ): childField is NormalizedField< Item > => + !! childField + ); + const children = visibleChildren.map( ( child ) => { + return ( +
+ +
+ ); + } ); + + const Stack = field.direction === 'horizontal' ? HStack : VStack; + + return ( + <> + { ! hideLabelFromVision &&
} + + { children } + + + ); +} + +export default DataFormCombinedEdit; diff --git a/packages/dataviews/src/components/dataform-combined-edit/style.scss b/packages/dataviews/src/components/dataform-combined-edit/style.scss new file mode 100644 index 0000000000000..0b59cbc9a4776 --- /dev/null +++ b/packages/dataviews/src/components/dataform-combined-edit/style.scss @@ -0,0 +1,12 @@ +.dataforms-layouts-panel__field-dropdown { + .dataforms-combined-edit { + border: none; + padding: 0; + } +} + +.dataforms-combined-edit { + &__field { + flex: 1 1 auto; + } +} diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 7147b9c234263..c929c21f1c21a 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -7,6 +7,7 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import DataForm from '../index'; +import type { CombinedFormField } from '../../../types'; const meta = { title: 'DataViews/DataForm', @@ -76,6 +77,11 @@ const fields = [ { value: 'published', label: 'Published' }, ], }, + { + id: 'password', + label: 'Password', + type: 'text' as const, + }, ]; export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { @@ -118,3 +124,62 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { /> ); }; + +const CombinedFieldsComponent = ( { + type = 'regular', + combinedFieldDirection = 'vertical', +}: { + type: 'panel' | 'regular'; + combinedFieldDirection: 'vertical' | 'horizontal'; +} ) => { + const [ post, setPost ] = useState( { + title: 'Hello, World!', + order: 2, + author: 1, + status: 'draft', + } ); + + const form = { + fields: [ 'title', 'status_and_visibility', 'order', 'author' ], + combinedFields: [ + { + id: 'status_and_visibility', + label: 'Status & Visibility', + children: [ 'status', 'password' ], + direction: combinedFieldDirection, + render: ( { item } ) => item.status, + }, + ] as CombinedFormField< any >[], + }; + + return ( + + setPost( ( prev ) => ( { + ...prev, + ...edits, + } ) ) + } + /> + ); +}; + +export const CombinedFields = { + title: 'DataViews/CombinedFields', + render: CombinedFieldsComponent, + argTypes: { + ...meta.argTypes, + combinedFieldDirection: { + control: { type: 'select' }, + description: + 'Chooses the direction of the combined field. "vertical" is the default layout.', + options: [ 'vertical', 'horizontal' ], + }, + }, +}; diff --git a/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts b/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts new file mode 100644 index 0000000000000..d95d59a88394e --- /dev/null +++ b/packages/dataviews/src/dataforms-layouts/get-visible-fields.ts @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import { normalizeCombinedFields } from '../normalize-fields'; +import type { + Field, + CombinedFormField, + NormalizedCombinedFormField, +} from '../types'; + +export function getVisibleFields< Item >( + fields: Field< Item >[], + formFields: string[] = [], + combinedFields?: CombinedFormField< Item >[] +): Field< Item >[] { + const visibleFields: Array< + Field< Item > | NormalizedCombinedFormField< Item > + > = [ ...fields ]; + if ( combinedFields ) { + visibleFields.push( + ...normalizeCombinedFields( combinedFields, fields ) + ); + } + return formFields + .map( ( fieldId ) => + visibleFields.find( ( { id } ) => id === fieldId ) + ) + .filter( ( field ): field is Field< Item > => !! field ); +} diff --git a/packages/dataviews/src/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx index 94e107dc20665..5d3bbc532ad45 100644 --- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx @@ -17,7 +17,8 @@ import { closeSmall } from '@wordpress/icons'; * Internal dependencies */ import { normalizeFields } from '../../normalize-fields'; -import type { DataFormProps, NormalizedField, Field } from '../../types'; +import { getVisibleFields } from '../get-visible-fields'; +import type { DataFormProps, NormalizedField } from '../../types'; interface FormFieldProps< Item > { data: Item; @@ -142,13 +143,13 @@ export default function FormPanel< Item >( { const visibleFields = useMemo( () => normalizeFields( - ( form.fields ?? [] ) - .map( ( fieldId ) => - fields.find( ( { id } ) => id === fieldId ) - ) - .filter( ( field ): field is Field< Item > => !! field ) + getVisibleFields< Item >( + fields, + form.fields, + form.combinedFields + ) ), - [ fields, form.fields ] + [ fields, form.fields, form.combinedFields ] ); return ( diff --git a/packages/dataviews/src/dataforms-layouts/regular/index.tsx b/packages/dataviews/src/dataforms-layouts/regular/index.tsx index 0ec427ae01003..57aa163b890e5 100644 --- a/packages/dataviews/src/dataforms-layouts/regular/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/regular/index.tsx @@ -8,7 +8,8 @@ import { useMemo } from '@wordpress/element'; * Internal dependencies */ import { normalizeFields } from '../../normalize-fields'; -import type { DataFormProps, Field } from '../../types'; +import { getVisibleFields } from '../get-visible-fields'; +import type { DataFormProps } from '../../types'; export default function FormRegular< Item >( { data, @@ -19,13 +20,13 @@ export default function FormRegular< Item >( { const visibleFields = useMemo( () => normalizeFields( - ( form.fields ?? [] ) - .map( ( fieldId ) => - fields.find( ( { id } ) => id === fieldId ) - ) - .filter( ( field ): field is Field< Item > => !! field ) + getVisibleFields< Item >( + fields, + form.fields, + form.combinedFields + ) ), - [ fields, form.fields ] + [ fields, form.fields, form.combinedFields ] ); return ( diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 2d1cc0402bc20..5ef219e45a478 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -2,8 +2,14 @@ * Internal dependencies */ import getFieldTypeDefinition from './field-types'; -import type { Field, NormalizedField } from './types'; +import type { + CombinedFormField, + Field, + NormalizedField, + NormalizedCombinedFormField, +} from './types'; import { getControl } from './dataform-controls'; +import DataFormCombinedEdit from './components/dataform-combined-edit'; /** * Apply default values and normalize the fields config. @@ -66,3 +72,29 @@ export function normalizeFields< Item >( }; } ); } + +/** + * Apply default values and normalize the fields config. + * + * @param combinedFields combined field list. + * @param fields Fields config. + * @return Normalized fields config. + */ +export function normalizeCombinedFields< Item >( + combinedFields: CombinedFormField< Item >[], + fields: Field< Item >[] +): NormalizedCombinedFormField< Item >[] { + return combinedFields.map( ( combinedField ) => { + return { + ...combinedField, + Edit: DataFormCombinedEdit, + fields: normalizeFields( + combinedField.children + .map( ( fieldId ) => + fields.find( ( { id } ) => id === fieldId ) + ) + .filter( ( field ): field is Field< Item > => !! field ) + ), + }; + } ); +} diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 087e812fffa19..26c6ecea645f4 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -6,6 +6,7 @@ @import "./components/dataviews-item-actions/style.scss"; @import "./components/dataviews-selection-checkbox/style.scss"; @import "./components/dataviews-view-config/style.scss"; +@import "./components/dataform-combined-edit/style.scss"; @import "./dataviews-layouts/grid/style.scss"; @import "./dataviews-layouts/list/style.scss"; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index e95a43994cd63..bc44b57eaaecc 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -174,14 +174,6 @@ export type Fields< Item > = Field< Item >[]; export type Data< Item > = Item[]; -/** - * The form configuration. - */ -export type Form = { - type?: 'regular' | 'panel'; - fields?: string[]; -}; - export type DataFormControlProps< Item > = { data: Item; field: NormalizedField< Item >; @@ -524,9 +516,37 @@ export interface SupportedLayouts { table?: Omit< ViewTable, 'type' >; } +export interface CombinedFormField< Item > extends CombinedField { + render?: ComponentType< { item: Item } >; +} + +export interface DataFormCombinedEditProps< Item > { + field: NormalizedCombinedFormField< Item >; + data: Item; + onChange: ( value: Record< string, any > ) => void; + hideLabelFromVision?: boolean; +} + +export type NormalizedCombinedFormField< Item > = CombinedFormField< Item > & { + fields: NormalizedField< Item >[]; + Edit?: ComponentType< DataFormCombinedEditProps< Item > >; +}; + +/** + * The form configuration. + */ +export type Form< Item > = { + type?: 'regular' | 'panel'; + fields?: string[]; + /** + * The fields to combine. + */ + combinedFields?: CombinedFormField< Item >[]; +}; + export interface DataFormProps< Item > { data: Item; fields: Field< Item >[]; - form: Form; + form: Form< Item >; onChange: ( value: Record< string, any > ) => void; } diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index cc0b031f6c96c..41969a7960af6 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -7,7 +7,7 @@ import type { Field, Form } from './types'; export function isItemValid< Item >( item: Item, fields: Field< Item >[], - form: Form + form: Form< Item > ): boolean { const _fields = normalizeFields( fields.filter( ( { id } ) => !! form.fields?.includes( id ) ) From 6aa8828b98936e3a07c292bd3b5d09c6309e853a Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 2 Oct 2024 16:17:50 +0200 Subject: [PATCH 38/49] Customize widgets, edit post: refactor Button to new sizes (#65807) * Customize widgets, edit post: refactor Button usages to new component sizing * Update snapshots * Switch to compact size in the customize widgets welcome guide * Remove unused classnames, update snapshots --- Co-authored-by: ciampo Co-authored-by: tyxla --- .../src/components/error-boundary/index.js | 7 +------ .../customize-widgets/src/components/inserter/index.js | 4 +--- .../src/components/welcome-guide/index.js | 4 +--- .../src/components/back-button/fullscreen-mode-close.js | 3 +-- .../test/__snapshots__/fullscreen-mode-close.js.snap | 2 +- .../edit-post/src/components/init-pattern-modal/index.js | 3 +-- .../components/preferences-modal/enable-custom-fields.js | 4 +--- .../test/__snapshots__/enable-custom-fields.js.snap | 4 ++-- 8 files changed, 9 insertions(+), 22 deletions(-) diff --git a/packages/customize-widgets/src/components/error-boundary/index.js b/packages/customize-widgets/src/components/error-boundary/index.js index 49867787afd05..0fff18a616d11 100644 --- a/packages/customize-widgets/src/components/error-boundary/index.js +++ b/packages/customize-widgets/src/components/error-boundary/index.js @@ -11,12 +11,7 @@ import { doAction } from '@wordpress/hooks'; function CopyButton( { text, children } ) { const ref = useCopyToClipboard( text ); return ( - ); diff --git a/packages/customize-widgets/src/components/inserter/index.js b/packages/customize-widgets/src/components/inserter/index.js index 41fc037cf673c..4f271bef9e9a3 100644 --- a/packages/customize-widgets/src/components/inserter/index.js +++ b/packages/customize-widgets/src/components/inserter/index.js @@ -37,9 +37,7 @@ function Inserter( { setIsOpened } ) { { __( 'Add a block' ) }