diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index c58b8b3239f33e..768103bf39ebe1 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -72,7 +72,7 @@ Settings related to shadows. | Property | Type | Default | Props | | --- | --- | --- |--- | -| defaultPresets | boolean | true | | +| defaultPresets | boolean | false | | | presets | array | | name, shadow, slug | --- diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 0d2520ff9682a4..6bce853ba2ec2c 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -670,6 +670,7 @@ public static function get_element_class_name( $element ) { array( 'spacing', 'margin' ), array( 'spacing', 'padding' ), array( 'typography', 'lineHeight' ), + array( 'shadow', 'defaultPresets' ), ); /** diff --git a/lib/theme.json b/lib/theme.json index c2ed7fdca39ed5..5f261e2c120043 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -191,7 +191,7 @@ "text": true }, "shadow": { - "defaultPresets": true, + "defaultPresets": false, "presets": [ { "name": "Natural", diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index b988c0499f1fb8..41ef8c0e13cbc6 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -297,6 +297,16 @@ } } } + + &[aria-disabled="true"], + &:disabled { + background: $gray-100; + border-color: $gray-300; + cursor: default; + + // Override style inherited from wp-admin. Required to avoid degraded appearance on different backgrounds. + opacity: 1; + } } @mixin radio-control { diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index b280cc9c51f6b8..e8480912a87f03 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -6,6 +6,7 @@ import { createContext, useContext } from '@wordpress/element'; export const mayDisplayControlsKey = Symbol( 'mayDisplayControls' ); export const mayDisplayParentControlsKey = Symbol( 'mayDisplayParentControls' ); export const blockEditingModeKey = Symbol( 'blockEditingMode' ); +export const blockBindingsKey = Symbol( 'blockBindings' ); export const DEFAULT_BLOCK_EDIT_CONTEXT = { name: '', diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 457cd919f89381..4e94a8a427510d 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -14,6 +14,7 @@ import { mayDisplayControlsKey, mayDisplayParentControlsKey, blockEditingModeKey, + blockBindingsKey, } from './context'; /** @@ -41,7 +42,8 @@ export default function BlockEdit( { attributes = {}, __unstableLayoutClassNames, } = props; - const { layout = null } = attributes; + const { layout = null, metadata = {} } = attributes; + const { bindings } = metadata; const layoutSupport = hasBlockSupport( name, 'layout', false ) || hasBlockSupport( name, '__experimentalLayout', false ); @@ -62,6 +64,7 @@ export default function BlockEdit( { [ mayDisplayControlsKey ]: mayDisplayControls, [ mayDisplayParentControlsKey ]: mayDisplayParentControls, [ blockEditingModeKey ]: blockEditingMode, + [ blockBindingsKey ]: bindings, } ), [ name, @@ -73,6 +76,7 @@ export default function BlockEdit( { mayDisplayControls, mayDisplayParentControls, blockEditingMode, + bindings, ] ) } > diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index e8df38232306cf..43b3c4dc09327d 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -30,7 +30,7 @@ import PositionControls from '../inspector-controls-tabs/position-controls-panel import useBlockInspectorAnimationSettings from './useBlockInspectorAnimationSettings'; import BlockInfo from '../block-info-slot-fill'; import BlockQuickNavigation from '../block-quick-navigation'; -import { getBorderPanelLabel } from '../../hooks/border'; +import { useBorderPanelLabel } from '../../hooks/border'; function BlockInspectorLockedBlocks( { topLevelLockedBlock } ) { const contentClientIds = useSelect( @@ -116,6 +116,10 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { selectedBlockClientId ); + const borderPanelLabel = useBorderPanelLabel( { + blockName: selectedBlockName, + } ); + if ( count > 1 ) { return (
@@ -140,9 +144,7 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { /> @@ -251,7 +253,7 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => { [ blockName ] ); const blockInformation = useBlockDisplayInformation( clientId ); - const borderPanelLabel = getBorderPanelLabel( { blockName } ); + const borderPanelLabel = useBorderPanelLabel( { blockName } ); return (
diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js index 0fb7f55b9955d2..245d0ee4348b2e 100644 --- a/packages/block-editor/src/components/block-preview/index.js +++ b/packages/block-editor/src/components/block-preview/index.js @@ -54,7 +54,11 @@ export function BlockPreview( { [] ); const settings = useMemo( - () => ( { ...originalSettings, __unstableIsPreviewMode: true } ), + () => ( { + ...originalSettings, + focusMode: false, // Disable "Spotlight mode". + __unstableIsPreviewMode: true, + } ), [ originalSettings ] ); const renderedBlocks = useMemo( @@ -117,6 +121,7 @@ export function useBlockPreview( { blocks, props = {}, layout } ) { () => ( { ...originalSettings, styles: undefined, // Clear styles included by the parent settings, as they are already output by the parent's EditorStyles. + focusMode: false, // Disable "Spotlight mode". __unstableIsPreviewMode: true, } ), [ originalSettings ] diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 81dbb98528a84b..102594c004aff6 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -207,17 +207,18 @@ } } - .block-editor-block-mover .block-editor-block-mover__move-button-container { - width: auto; - - @include break-small() { + .block-editor-block-mover { + .block-editor-block-mover__move-button-container { + width: auto; position: relative; + } - &::before { + &:not(.is-horizontal) .block-editor-block-mover__move-button-container::before { + @include break-small() { content: ""; height: $border-width; width: 100%; - background: $gray-900; + background: $gray-200; position: absolute; top: 50%; left: 50%; @@ -226,6 +227,10 @@ transform: translate(-50%, 0); margin-top: -$border-width * 0.5; } + + @include break-medium { + background: $gray-900; + } } } diff --git a/packages/block-editor/src/components/global-styles/border-panel.js b/packages/block-editor/src/components/global-styles/border-panel.js index f8144f1545aebe..04bb5f9d15b1ad 100644 --- a/packages/block-editor/src/components/global-styles/border-panel.js +++ b/packages/block-editor/src/components/global-styles/border-panel.js @@ -21,23 +21,24 @@ import { useColorsPerOrigin } from './hooks'; import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { overrideOrigins } from '../../store/get-block-settings'; import { setImmutably } from '../../utils/object'; -import { getBorderPanelLabel } from '../../hooks/border'; -import { ShadowPopover } from './shadow-panel-components'; +import { useBorderPanelLabel } from '../../hooks/border'; +import { ShadowPopover, useShadowPresets } from './shadow-panel-components'; -function useHasShadowControl( settings ) { - return !! settings?.shadow; +export function useHasBorderPanel( settings ) { + const controls = Object.values( useHasBorderPanelControls( settings ) ); + return controls.some( Boolean ); } -export function useHasBorderPanel( settings ) { - const controls = [ - useHasBorderColorControl( settings ), - useHasBorderRadiusControl( settings ), - useHasBorderStyleControl( settings ), - useHasBorderWidthControl( settings ), - useHasShadowControl( settings ), - ]; +export function useHasBorderPanelControls( settings ) { + const controls = { + hasBorderColor: useHasBorderColorControl( settings ), + hasBorderRadius: useHasBorderRadiusControl( settings ), + hasBorderStyle: useHasBorderStyleControl( settings ), + hasBorderWidth: useHasBorderWidthControl( settings ), + hasShadow: useHasShadowControl( settings ), + }; - return controls.some( Boolean ); + return controls; } function useHasBorderColorControl( settings ) { @@ -56,6 +57,11 @@ function useHasBorderWidthControl( settings ) { return settings?.border?.width; } +function useHasShadowControl( settings ) { + const shadows = useShadowPresets( settings ); + return !! settings?.shadow && shadows.length > 0; +} + function BorderToolsPanel( { resetAllFilter, onChange, @@ -215,14 +221,16 @@ export default function BorderPanel( { const showBorderByDefault = defaultControls?.color || defaultControls?.width; - const label = getBorderPanelLabel( { + const hasBorderControl = + showBorderColor || + showBorderStyle || + showBorderWidth || + showBorderRadius; + + const label = useBorderPanelLabel( { blockName: name, hasShadowControl, - hasBorderControl: - showBorderColor || - showBorderStyle || - showBorderWidth || - showBorderRadius, + hasBorderControl, } ); return ( @@ -280,9 +288,12 @@ export default function BorderPanel( { isShownByDefault={ defaultControls.shadow } panelId={ panelId } > - - { __( 'Shadow' ) } - + { hasBorderControl ? ( + + { __( 'Shadow' ) } + + ) : null } + @@ -37,42 +45,76 @@ export function ShadowPopoverContainer( { shadow, onShadowChange, settings } ) { activeShadow={ shadow } onSelect={ onShadowChange } /> +
+ +
); } export function ShadowPresets( { presets, activeShadow, onSelect } ) { + const { CompositeV2: Composite, useCompositeStoreV2: useCompositeStore } = + unlock( componentsPrivateApis ); + const compositeStore = useCompositeStore(); return ! presets ? null : ( - + { presets.map( ( { name, slug, shadow } ) => ( onSelect( shadow === activeShadow ? undefined : shadow ) } shadow={ shadow } /> ) ) } - + ); } -export function ShadowIndicator( { label, isActive, onSelect, shadow } ) { +export function ShadowIndicator( { type, label, isActive, onSelect, shadow } ) { + const { CompositeItemV2: CompositeItem } = unlock( componentsPrivateApis ); return ( -
- -
+ + { isActive && } + + } + /> ); } @@ -123,3 +165,30 @@ function renderShadowToggle() { ); }; } + +export function useShadowPresets( settings ) { + return useMemo( () => { + if ( ! settings?.shadow ) { + return EMPTY_ARRAY; + } + + const defaultPresetsEnabled = settings?.shadow?.defaultPresets; + const { default: defaultShadows, theme: themeShadows } = + settings?.shadow?.presets ?? {}; + const unsetShadow = { + name: __( 'Unset' ), + slug: 'unset', + shadow: 'none', + }; + + const shadowPresets = [ + ...( ( defaultPresetsEnabled && defaultShadows ) || EMPTY_ARRAY ), + ...( themeShadows || EMPTY_ARRAY ), + ]; + if ( shadowPresets.length ) { + shadowPresets.unshift( unsetShadow ); + } + + return shadowPresets; + }, [ settings ] ); +} diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index d2ba88f9f31e00..d357d2e65ab72a 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -2,10 +2,24 @@ fill: currentColor; } +// @todo: Ideally, popover, swatch size, and gap values should be CSS variables +// to apply precise grid layouts. +// https://github.com/WordPress/gutenberg/blob/954ecae571abbddc113d3a4bd8e1a72230180554/packages/block-editor/src/components/duotone-control/style.scss#L3-L9 .block-editor-global-styles__shadow-popover-container { width: 230px; } +.block-editor-global-styles__shadow__list { + display: flex; + gap: 12px; + flex-wrap: wrap; + padding-bottom: $grid-unit-10; +} + +.block-editor-global-styles__clear-shadow { + text-align: right; +} + .block-editor-global-styles-filters-panel__dropdown, .block-editor-global-styles__shadow-dropdown { display: block; @@ -21,14 +35,6 @@ } } -// wrapper to clip the shadow beyond 6px -.block-editor-global-styles__shadow-indicator-wrapper { - padding: $grid-unit-15 * 0.5; - display: flex; - align-items: center; - justify-content: center; -} - // These styles are similar to the color palette. .block-editor-global-styles__shadow-indicator { color: $gray-800; @@ -37,8 +43,25 @@ cursor: pointer; padding: 0; - height: $button-size-small; - width: $button-size-small; + height: $button-size-small + 2 * $border-width; + width: $button-size-small + 2 * $border-width; + box-sizing: border-box; + + transform: scale(1); + transition: transform 0.1s ease; + will-change: transform; + + &:focus { + border: #{ $border-width * 2 } solid $gray-700; + } + + &:hover { + transform: scale(1.2); + } + + &.unset { + background: linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); + } } .block-editor-global-styles-advanced-panel__custom-css-input textarea { diff --git a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js index 6c2556f2378ff1..cdbd24e6eb144a 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js @@ -11,10 +11,10 @@ import { __ } from '@wordpress/i18n'; import BlockStyles from '../block-styles'; import DefaultStylePicker from '../default-style-picker'; import InspectorControls from '../inspector-controls'; -import { getBorderPanelLabel } from '../../hooks/border'; +import { useBorderPanelLabel } from '../../hooks/border'; const StylesTab = ( { blockName, clientId, hasBlockStyles } ) => { - const borderPanelLabel = getBorderPanelLabel( { blockName } ); + const borderPanelLabel = useBorderPanelLabel( { blockName } ); return ( <> diff --git a/packages/block-editor/src/components/link-control/link-preview.js b/packages/block-editor/src/components/link-control/link-preview.js index 88a19cc4c7af49..d922187ec3aa0d 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -16,8 +16,9 @@ import { useCopyToClipboard } from '@wordpress/compose'; import { filterURLForDisplay, safeDecodeURI } from '@wordpress/url'; import { Icon, globe, info, linkOff, edit, copySmall } from '@wordpress/icons'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -33,6 +34,12 @@ export default function LinkPreview( { hasUnlinkControl = false, onRemove, } ) { + const showIconLabels = useSelect( + ( select ) => + select( preferencesStore ).get( 'core', 'showIconLabels' ), + [] + ); + // Avoid fetching if rich previews are not desired. const showRichPreviews = hasRichPreviews ? value?.url : null; @@ -139,7 +146,7 @@ export default function LinkPreview( { label={ sprintf( // Translators: %s is a placeholder for the link URL and an optional colon, (if a Link URL is present). __( 'Copy link%s' ), // Ends up looking like "Copy link: https://example.com". - isEmptyURL ? '' : ': ' + value.url + isEmptyURL || showIconLabels ? '' : ': ' + value.url ) } ref={ ref } disabled={ isEmptyURL } diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index 425df96ab31fa6..9531ad6f0c7a09 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -34,6 +34,15 @@ $block-editor-link-control-number-of-actions: 1; content: attr(aria-label); } } + + .block-editor-link-control__search-item-top { + gap: $grid-unit-10; + + .components-button.has-icon { + min-width: inherit; + width: min-content; + } + } } } diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index bac2559ffbba9b..aa870dee073977 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -19,13 +19,14 @@ import { removeFormat, } from '@wordpress/rich-text'; import { Popover } from '@wordpress/components'; -import { getBlockType } from '@wordpress/blocks'; +import { getBlockType, store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies */ import { useBlockEditorAutocompleteProps } from '../autocomplete'; import { useBlockEditContext } from '../block-edit'; +import { blockBindingsKey } from '../block-edit/context'; import FormatToolbarContainer from './format-toolbar-container'; import { store as blockEditorStore } from '../../store'; import { useUndoAutomaticChange } from './use-undo-automatic-change'; @@ -109,6 +110,7 @@ export function RichTextWrapper( __unstableDisableFormats: disableFormats, disableLineBreaks, __unstableAllowPrefixTransformations, + disableEditing, ...props }, forwardedRef @@ -116,11 +118,9 @@ export function RichTextWrapper( props = removeNativeProps( props ); const anchorRef = useRef(); - const { - clientId, - isSelected: isBlockSelected, - name: blockName, - } = useBlockEditContext(); + const context = useBlockEditContext(); + const { clientId, isSelected: isBlockSelected, name: blockName } = context; + const blockBindings = context[ blockBindingsKey ]; const selector = ( select ) => { // Avoid subscribing to the block editor store if the block is not // selected. @@ -128,12 +128,10 @@ export function RichTextWrapper( return { isSelected: false }; } - const { getSelectionStart, getSelectionEnd, getBlockAttributes } = + const { getSelectionStart, getSelectionEnd } = select( blockEditorStore ); const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd(); - const blockBindings = - getBlockAttributes( clientId )?.metadata?.bindings; let isSelected; @@ -146,50 +144,60 @@ export function RichTextWrapper( isSelected = selectionStart.clientId === clientId; } - // Disable Rich Text editing if block bindings specify that. - let shouldDisableEditing = false; - if ( blockBindings && blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) { - const blockTypeAttributes = getBlockType( blockName ).attributes; - const { getBlockBindingsSource } = unlock( - select( blockEditorStore ) - ); - for ( const [ attribute, args ] of Object.entries( - blockBindings - ) ) { - if ( - blockTypeAttributes?.[ attribute ]?.source !== 'rich-text' - ) { - break; - } - - // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. - const blockBindingsSource = getBlockBindingsSource( - args.source - ); - if ( - ! blockBindingsSource || - blockBindingsSource.lockAttributesEditing - ) { - shouldDisableEditing = true; - break; - } - } - } - return { selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, - shouldDisableEditing, }; }; - const { selectionStart, selectionEnd, isSelected, shouldDisableEditing } = - useSelect( selector, [ - clientId, - identifier, - originalIsSelected, - isBlockSelected, - ] ); + const { selectionStart, selectionEnd, isSelected } = useSelect( selector, [ + clientId, + identifier, + originalIsSelected, + isBlockSelected, + ] ); + + const disableBoundBlocks = useSelect( + ( select ) => { + // Disable Rich Text editing if block bindings specify that. + let _disableBoundBlocks = false; + if ( blockBindings && blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) { + const blockTypeAttributes = + getBlockType( blockName ).attributes; + const { getBlockBindingsSource } = unlock( + select( blocksStore ) + ); + for ( const [ attribute, args ] of Object.entries( + blockBindings + ) ) { + if ( + blockTypeAttributes?.[ attribute ]?.source !== + 'rich-text' + ) { + break; + } + + // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. + const blockBindingsSource = getBlockBindingsSource( + args.source + ); + if ( + ! blockBindingsSource || + blockBindingsSource.lockAttributesEditing + ) { + _disableBoundBlocks = true; + break; + } + } + } + + return _disableBoundBlocks; + }, + [ blockBindings, blockName ] + ); + + const shouldDisableEditing = disableEditing || disableBoundBlocks; + const { getSelectionStart, getSelectionEnd, getBlockRootClientId } = useSelect( blockEditorStore ); const { selectionChange } = useDispatch( blockEditorStore ); @@ -442,19 +450,34 @@ export function RichTextWrapper( ); } -const ForwardedRichTextContainer = withDeprecations( +// This is the private API for the RichText component. +// It allows access to all props, not just the public ones. +export const PrivateRichText = withDeprecations( forwardRef( RichTextWrapper ) ); -ForwardedRichTextContainer.Content = Content; -ForwardedRichTextContainer.isEmpty = ( value ) => { +PrivateRichText.Content = Content; +PrivateRichText.isEmpty = ( value ) => { return ! value || value.length === 0; }; +// This is the public API for the RichText component. +// We wrap the PrivateRichText component to hide some props from the public API. /** * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md */ -export default ForwardedRichTextContainer; +const PublicForwardedRichTextContainer = forwardRef( ( props, ref ) => { + return ( + + ); +} ); + +PublicForwardedRichTextContainer.Content = Content; +PublicForwardedRichTextContainer.isEmpty = ( value ) => { + return ! value || value.length === 0; +}; + +export default PublicForwardedRichTextContainer; export { RichTextShortcut } from './shortcut'; export { RichTextToolbarButton } from './toolbar-button'; export { __unstableRichTextInputEvent } from './input-event'; diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js index 4daf70e7fa3c74..6b40a82d72d4b2 100644 --- a/packages/block-editor/src/components/rich-text/use-enter.js +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -21,6 +21,10 @@ export function useEnter( props ) { propsRef.current = props; return useRefEffect( ( element ) => { function onKeyDown( event ) { + if ( event.target.contentEditable !== 'true' ) { + return; + } + if ( event.defaultPrevented ) { return; } diff --git a/packages/block-editor/src/components/url-popover/style.scss b/packages/block-editor/src/components/url-popover/style.scss index df4e10fe13d534..324d82d4183aab 100644 --- a/packages/block-editor/src/components/url-popover/style.scss +++ b/packages/block-editor/src/components/url-popover/style.scss @@ -58,6 +58,7 @@ text-overflow: ellipsis; white-space: nowrap; margin-right: $grid-unit-10; + min-width: 150px; // Avoids the popover from growing too wide when the URL is long. // See https://github.com/WordPress/gutenberg/issues/58599 max-width: $modal-min-width; diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 1e0b8e894d2067..eb84352ab62f09 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -35,12 +35,12 @@ function BlockHooksControlPure( { name, clientId } ) { const { blockIndex, rootClientId, innerBlocksLength } = useSelect( ( select ) => { - const { getBlock, getBlockIndex, getBlockRootClientId } = + const { getBlocks, getBlockIndex, getBlockRootClientId } = select( blockEditorStore ); return { blockIndex: getBlockIndex( clientId ), - innerBlocksLength: getBlock( clientId )?.innerBlocks?.length, + innerBlocksLength: getBlocks( clientId )?.length, rootClientId: getBlockRootClientId( clientId ), }; }, @@ -49,7 +49,7 @@ function BlockHooksControlPure( { name, clientId } ) { const hookedBlockClientIds = useSelect( ( select ) => { - const { getBlock, getGlobalBlockCount } = + const { getBlocks, getGlobalBlockCount } = select( blockEditorStore ); const _hookedBlockClientIds = hookedBlocksForCurrentBlock.reduce( @@ -69,7 +69,7 @@ function BlockHooksControlPure( { name, clientId } ) { // Any of the current block's siblings (with the right block type) qualifies // as a hooked block (inserted `before` or `after` the current one), as the block // might've been automatically inserted and then moved around a bit by the user. - candidates = getBlock( rootClientId )?.innerBlocks; + candidates = getBlocks( rootClientId ); break; case 'first_child': @@ -77,7 +77,7 @@ function BlockHooksControlPure( { name, clientId } ) { // Any of the current block's child blocks (with the right block type) qualifies // as a hooked first or last child block, as the block might've been automatically // inserted and then moved around a bit by the user. - candidates = getBlock( clientId ).innerBlocks; + candidates = getBlocks( clientId ); break; } @@ -161,6 +161,11 @@ function BlockHooksControlPure( { name, clientId } ) { title={ __( 'Plugins' ) } initialOpen={ true } > +

+ { __( + 'Manage the inclusion of blocks added automatically by plugins.' + ) } +

{ Object.keys( groupedHookedBlocks ).map( ( vendor ) => { return ( diff --git a/packages/block-editor/src/hooks/block-hooks.scss b/packages/block-editor/src/hooks/block-hooks.scss index dc05d6e6947bb0..4d871233de482b 100644 --- a/packages/block-editor/src/hooks/block-hooks.scss +++ b/packages/block-editor/src/hooks/block-hooks.scss @@ -13,4 +13,10 @@ .components-toggle-control .components-h-stack .components-h-stack { flex-direction: row; } + + .block-editor-hooks__block-hooks-helptext { + color: $gray-700; + font-size: $helptext-font-size; + margin-bottom: $grid-unit-20; + } } diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index c743b457fc05cc..a8ed9bccaf7df9 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -18,9 +18,14 @@ import { useSelect } from '@wordpress/data'; import { getColorClassName } from '../components/colors'; import InspectorControls from '../components/inspector-controls'; import useMultipleOriginColorsAndGradients from '../components/colors-gradients/use-multiple-origin-colors-and-gradients'; -import { cleanEmptyObject, shouldSkipSerialization } from './utils'; +import { + cleanEmptyObject, + shouldSkipSerialization, + useBlockSettings, +} from './utils'; import { useHasBorderPanel, + useHasBorderPanelControls, BorderPanel as StylesBorderPanel, } from '../components/global-styles'; import { store as blockEditorStore } from '../store'; @@ -220,14 +225,21 @@ export function hasShadowSupport( blockName ) { return hasBlockSupport( blockName, SHADOW_SUPPORT_KEY ); } -export function getBorderPanelLabel( { +export function useBorderPanelLabel( { blockName, hasBorderControl, hasShadowControl, } = {} ) { + const settings = useBlockSettings( blockName ); + const controls = useHasBorderPanelControls( settings ); + if ( ! hasBorderControl && ! hasShadowControl && blockName ) { - hasBorderControl = hasBorderSupport( blockName ); - hasShadowControl = hasShadowSupport( blockName ); + hasBorderControl = + controls?.hasBorderColor || + controls?.hasBorderStyle || + controls?.hasBorderWidth || + controls?.hasBorderRadius; + hasShadowControl = controls?.hasShadow; } if ( hasBorderControl && hasShadowControl ) { diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 899faf0a8cbd5d..0e5b6614f07cbf 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { getBlockType } from '@wordpress/blocks'; +import { getBlockType, store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; @@ -33,18 +33,16 @@ const createEditFunctionWithBindingsAttribute = () => createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { clientId, name: blockName } = useBlockEditContext(); - const { getBlockBindingsSource } = unlock( - useSelect( blockEditorStore ) - ); + const blockBindingsSources = unlock( + useSelect( blocksStore ) + ).getAllBlockBindingsSources(); const { getBlockAttributes } = useSelect( blockEditorStore ); const updatedAttributes = getBlockAttributes( clientId ); if ( updatedAttributes?.metadata?.bindings ) { Object.entries( updatedAttributes.metadata.bindings ).forEach( ( [ attributeName, settings ] ) => { - const source = getBlockBindingsSource( - settings.source - ); + const source = blockBindingsSources[ settings.source ]; if ( source && source.useSource ) { // Second argument (`updateMetaValue`) will be used to update the value in the future. diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 220aa5f4127270..ec6843ead24895 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -27,6 +27,7 @@ import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; import { selectBlockPatternsKey } from './store/private-keys'; +import { PrivateRichText } from './components/rich-text/'; /** * Private @wordpress/block-editor APIs. @@ -58,4 +59,5 @@ lock( privateApis, { usesContextKey, useFlashEditableBlocks, selectBlockPatternsKey, + PrivateRichText, } ); diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index ae0a06152fb933..e32e604a02b1e1 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -390,16 +390,6 @@ export function stopEditingAsBlocks( clientId ) { }; } -export function registerBlockBindingsSource( source ) { - return { - type: 'REGISTER_BLOCK_BINDINGS_SOURCE', - sourceName: source.name, - sourceLabel: source.label, - useSource: source.useSource, - lockAttributesEditing: source.lockAttributesEditing, - }; -} - /** * Returns an action object used in signalling that the user has begun to drag. * diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index c885e43ba75208..e4885cbbd9e1e1 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -341,14 +341,6 @@ export function getLastFocus( state ) { return state.lastFocus; } -export function getAllBlockBindingsSources( state ) { - return state.blockBindingsSources; -} - -export function getBlockBindingsSource( state, sourceName ) { - return state.blockBindingsSources[ sourceName ]; -} - /** * Returns true if the user is dragging anything, or false otherwise. It is possible for a * user to be dragging data from outside of the editor, so this selector is separate from diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 1b535e51950164..12a2dc61d8269d 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2050,20 +2050,6 @@ export function lastFocus( state = false, action ) { return state; } -function blockBindingsSources( state = {}, action ) { - if ( action.type === 'REGISTER_BLOCK_BINDINGS_SOURCE' ) { - return { - ...state, - [ action.sourceName ]: { - label: action.sourceLabel, - useSource: action.useSource, - lockAttributesEditing: action.lockAttributesEditing ?? true, - }, - }; - } - return state; -} - const combinedReducers = combineReducers( { blocks, isDragging, @@ -2095,7 +2081,6 @@ const combinedReducers = combineReducers( { blockRemovalRules, openedBlockSettingsMenu, registeredInserterMediaCategories, - blockBindingsSources, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index ff90cdd1bf64c0..1a35cea9d8cc7d 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -45,6 +45,7 @@ import { createBlock, cloneBlock, getDefaultBlockName, + store as blocksStore, } from '@wordpress/blocks'; import { useMergeRefs, useRefEffect } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -239,7 +240,7 @@ function ButtonEdit( props ) { } const blockBindingsSource = unlock( - select( blockEditorStore ) + select( blocksStore ) ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { diff --git a/packages/block-library/src/buttons/transforms.js b/packages/block-library/src/buttons/transforms.js index 3e89b4973e3728..9848299f3a99f9 100644 --- a/packages/block-library/src/buttons/transforms.js +++ b/packages/block-library/src/buttons/transforms.js @@ -4,6 +4,11 @@ import { createBlock } from '@wordpress/blocks'; import { __unstableCreateElement as createElement } from '@wordpress/rich-text'; +/** + * Internal dependencies + */ +import { getTransformedMetadata } from '../utils/get-transformed-metadata'; + const transforms = { from: [ { @@ -33,10 +38,8 @@ const transforms = { {}, // Loop the selected buttons. buttons.map( ( attributes ) => { - const element = createElement( - document, - attributes.content - ); + const { content, metadata } = attributes; + const element = createElement( document, content ); // Remove any HTML tags. const text = element.innerText || ''; // Get first url. @@ -46,6 +49,13 @@ const transforms = { return createBlock( 'core/button', { text, url, + metadata: getTransformedMetadata( + metadata, + 'core/button', + ( { content: contentBinding } ) => ( { + text: contentBinding, + } ) + ), } ); } ) ), diff --git a/packages/block-library/src/code/transforms.js b/packages/block-library/src/code/transforms.js index af6d4686af8122..e537db342b8d5c 100644 --- a/packages/block-library/src/code/transforms.js +++ b/packages/block-library/src/code/transforms.js @@ -4,6 +4,11 @@ import { createBlock } from '@wordpress/blocks'; import { create, toHTMLString } from '@wordpress/rich-text'; +/** + * Internal dependencies + */ +import { getTransformedMetadata } from '../utils/get-transformed-metadata'; + const transforms = { from: [ { @@ -14,17 +19,21 @@ const transforms = { { type: 'block', blocks: [ 'core/paragraph' ], - transform: ( { content } ) => - createBlock( 'core/code', { content } ), + transform: ( { content, metadata } ) => + createBlock( 'core/code', { + content, + metadata: getTransformedMetadata( metadata, 'core/code' ), + } ), }, { type: 'block', blocks: [ 'core/html' ], - transform: ( { content: text } ) => { + transform: ( { content: text, metadata } ) => { return createBlock( 'core/code', { // The HTML is plain text (with plain line breaks), so // convert it to rich text. content: toHTMLString( { value: create( { text } ) } ), + metadata: getTransformedMetadata( metadata, 'core/code' ), } ); }, }, @@ -51,8 +60,14 @@ const transforms = { { type: 'block', blocks: [ 'core/paragraph' ], - transform: ( { content } ) => - createBlock( 'core/paragraph', { content } ), + transform: ( { content, metadata } ) => + createBlock( 'core/paragraph', { + content, + metadata: getTransformedMetadata( + metadata, + 'core/paragraph' + ), + } ), }, ], }; diff --git a/packages/block-library/src/cover/edit/block-controls.js b/packages/block-library/src/cover/edit/block-controls.js index 59aaaaffe77d75..c4137ad2a8409a 100644 --- a/packages/block-library/src/cover/edit/block-controls.js +++ b/packages/block-library/src/cover/edit/block-controls.js @@ -8,6 +8,7 @@ import { MediaReplaceFlow, __experimentalBlockAlignmentMatrixControl as BlockAlignmentMatrixControl, __experimentalBlockFullHeightAligmentControl as FullHeightAlignmentControl, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; @@ -15,6 +16,9 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { ALLOWED_MEDIA_TYPES } from '../shared'; +import { unlock } from '../../lock-unlock'; + +const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); export default function CoverBlockControls( { attributes, @@ -30,7 +34,10 @@ export default function CoverBlockControls( { const [ prevMinHeightValue, setPrevMinHeightValue ] = useState( minHeight ); const [ prevMinHeightUnit, setPrevMinHeightUnit ] = useState( minHeightUnit ); - const isMinFullHeight = minHeightUnit === 'vh' && minHeight === 100; + const isMinFullHeight = + minHeightUnit === 'vh' && + minHeight === 100 && + ! attributes?.style?.dimensions?.aspectRatio; const toggleMinFullHeight = () => { if ( isMinFullHeight ) { // If there aren't previous values, take the default ones. @@ -51,10 +58,17 @@ export default function CoverBlockControls( { setPrevMinHeightValue( minHeight ); setPrevMinHeightUnit( minHeightUnit ); - // Set full height. + // Set full height, and clear any aspect ratio value. return setAttributes( { minHeight: 100, minHeightUnit: 'vh', + style: cleanEmptyObject( { + ...attributes?.style, + dimensions: { + ...attributes?.style?.dimensions, + aspectRatio: undefined, // Reset aspect ratio when minHeight is set. + }, + } ), } ); }; diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index 889ce3716b9631..634e99dc2d98d6 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -308,7 +308,11 @@ export default function CoverInspectorControls( { panelId={ clientId } > setAttributes( { diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 3b4eac41a0d3b4..837e3834e2e1ba 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -8,9 +8,10 @@ align-items: center; padding: 1em; // Prevent the `wp-block-cover__background` span from overflowing the container when border-radius is applied. + // `overflow: hidden` is provided as a fallback for browsers that don't support `overflow: clip`. + overflow: hidden; // Use clip instead of overflow: hidden so that sticky position works on child elements. - // Use overflow-x instead of overflow so that aspect-ratio allows content to expand the area of the cover block. - overflow-x: clip; + overflow: clip; // This block has customizable padding, border-box makes that more predictable. box-sizing: border-box; // Keep the flex layout direction to the physical direction (LTR) in RTL languages. diff --git a/packages/block-library/src/footnotes/index.php b/packages/block-library/src/footnotes/index.php index 86eaf694add4ca..380c093c8117ed 100644 --- a/packages/block-library/src/footnotes/index.php +++ b/packages/block-library/src/footnotes/index.php @@ -105,7 +105,7 @@ function register_block_core_footnotes_post_meta() { } } } -/** +/* * Most post types are registered at priority 10, so use priority 20 here in * order to catch them. */ diff --git a/packages/block-library/src/heading/transforms.js b/packages/block-library/src/heading/transforms.js index a4db7884620963..f040ff06e37e86 100644 --- a/packages/block-library/src/heading/transforms.js +++ b/packages/block-library/src/heading/transforms.js @@ -7,6 +7,7 @@ import { createBlock, getBlockAttributes } from '@wordpress/blocks'; * Internal dependencies */ import { getLevelFromHeadingNodeName } from './shared'; +import { getTransformedMetadata } from '../utils/get-transformed-metadata'; const transforms = { from: [ @@ -15,12 +16,20 @@ const transforms = { isMultiBlock: true, blocks: [ 'core/paragraph' ], transform: ( attributes ) => - attributes.map( ( { content, anchor, align: textAlign } ) => - createBlock( 'core/heading', { - content, - anchor, - textAlign, - } ) + attributes.map( + ( { content, anchor, align: textAlign, metadata } ) => + createBlock( 'core/heading', { + content, + anchor, + textAlign, + metadata: getTransformedMetadata( + metadata, + 'core/heading', + ( { content: contentBinding } ) => ( { + content: contentBinding, + } ) + ), + } ) ), }, { @@ -82,8 +91,18 @@ const transforms = { isMultiBlock: true, blocks: [ 'core/paragraph' ], transform: ( attributes ) => - attributes.map( ( { content, textAlign: align } ) => - createBlock( 'core/paragraph', { content, align } ) + attributes.map( ( { content, textAlign: align, metadata } ) => + createBlock( 'core/paragraph', { + content, + align, + metadata: getTransformedMetadata( + metadata, + 'core/paragraph', + ( { content: contentBinding } ) => ( { + content: contentBinding, + } ) + ), + } ) ), }, ], diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 86970b588ff03d..e1e221bc9575a0 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -7,6 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import { store as blocksStore } from '@wordpress/blocks'; import { Placeholder } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { @@ -342,7 +343,7 @@ export function ImageEdit( { } const blockBindingsSource = unlock( - select( blockEditorStore ) + select( blocksStore ) ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index a0d481681ee934..ea0f82a2e1986b 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -33,7 +33,7 @@ import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; import { __, _x, sprintf, isRTL } from '@wordpress/i18n'; import { DOWN } from '@wordpress/keycodes'; import { getFilename } from '@wordpress/url'; -import { switchToBlockType } from '@wordpress/blocks'; +import { switchToBlockType, store as blocksStore } from '@wordpress/blocks'; import { crop, overlayText, upload } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; @@ -411,14 +411,16 @@ export default function Image( { lockHrefControls = false, lockAltControls = false, lockTitleControls = false, + lockCaption = false, } = useSelect( ( select ) => { if ( ! isSingleSelected ) { return {}; } - - const { getBlockBindingsSource, getBlockParentsByBlockName } = - unlock( select( blockEditorStore ) ); + const { getBlockBindingsSource } = unlock( select( blocksStore ) ); + const { getBlockParentsByBlockName } = unlock( + select( blockEditorStore ) + ); const { url: urlBinding, alt: altBinding, @@ -444,6 +446,10 @@ export default function Image( { // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. hasParentPattern, + lockCaption: + // Disable editing the caption if the image is inside a pattern instance. + // This is a temporary solution until we support overriding the caption on the frontend. + hasParentPattern, lockAltControls: !! altBinding && ( ! altBindingSource || @@ -907,6 +913,7 @@ export default function Image( { which causes duplicated image upload. */ } { ! temporaryURL && controls } { img } + ); diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 5dcada62f6feb5..3a3a654aee6126 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -554,14 +554,18 @@ private static function get_nav_element_directives( $is_interactive ) { // When adding to this array be mindful of security concerns. $nav_element_context = data_wp_context( array( - 'overlayOpenedBy' => array(), + 'overlayOpenedBy' => array( + 'click' => false, + 'hover' => false, + 'focus' => false, + ), 'type' => 'overlay', 'roleAttribute' => '', 'ariaLabel' => __( 'Menu' ), ) ); $nav_element_directives = ' - data-wp-interactive="core/navigation"' + data-wp-interactive="core/navigation" ' . $nav_element_context; return $nav_element_directives; @@ -764,7 +768,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut ) ) { // Add directives to the parent `
  • `. $tags->set_attribute( 'data-wp-interactive', 'core/navigation' ); - $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": {}, "type": "submenu" }' ); + $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu" }' ); $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 26f3439964f90e..a3b3b1ec6398a2 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -24,6 +24,7 @@ import { useBlockProps, store as blockEditorStore, __experimentalUseBorderProps as useBorderProps, + useBlockEditingMode, } from '@wordpress/block-editor'; import { useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -143,6 +144,7 @@ export default function PostFeaturedImageEdit( { style: { width, height, aspectRatio }, } ); const borderProps = useBorderProps( attributes ); + const blockEditingMode = useBlockEditingMode(); const placeholder = ( content ) => { return ( @@ -174,8 +176,13 @@ export default function PostFeaturedImageEdit( { createErrorNotice( message, { type: 'snackbar' } ); }; - const controls = ( + const controls = blockEditingMode === 'default' && ( <> + ); + let image; /** @@ -251,11 +259,6 @@ export default function PostFeaturedImageEdit( { ) : ( placeholder() ) } -
  • ); @@ -360,11 +363,6 @@ export default function PostFeaturedImageEdit( { ) : ( image ) } - ); diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index 28d9b8cdcfd8ff..1cb89bc3e5e67b 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -115,53 +115,59 @@ export default function PostTitleEdit( { return ( <> { blockEditingMode === 'default' && ( - - - setAttributes( { level: newLevel } ) - } - /> - { - setAttributes( { textAlign: nextAlign } ); - } } - /> - - ) } - - - setAttributes( { isLink: ! isLink } ) } - checked={ isLink } - /> - { isLink && ( - <> + <> + + + setAttributes( { level: newLevel } ) + } + /> + { + setAttributes( { textAlign: nextAlign } ); + } } + /> + + + - setAttributes( { - linkTarget: value ? '_blank' : '_self', - } ) + label={ __( 'Make title a link' ) } + onChange={ () => + setAttributes( { isLink: ! isLink } ) } - checked={ linkTarget === '_blank' } + checked={ isLink } /> - - setAttributes( { rel: newRel } ) - } - /> - - ) } - - + { isLink && ( + <> + + setAttributes( { + linkTarget: value + ? '_blank' + : '_self', + } ) + } + checked={ linkTarget === '_blank' } + /> + + setAttributes( { rel: newRel } ) + } + /> + + ) } + + + + ) } { titleElement } ); diff --git a/packages/block-library/src/search/style.scss b/packages/block-library/src/search/style.scss index 4e283530a0e277..1434f29de76810 100644 --- a/packages/block-library/src/search/style.scss +++ b/packages/block-library/src/search/style.scss @@ -12,6 +12,8 @@ $button-spacing-y: math.div($grid-unit-15, 2); // 6px svg { min-width: $grid-unit-30; min-height: $grid-unit-30; + width: 1.25em; + height: 1.25em; fill: currentColor; vertical-align: text-bottom; } diff --git a/packages/block-library/src/utils/caption.js b/packages/block-library/src/utils/caption.js index e9055cc29df02c..57f034c76c6ed9 100644 --- a/packages/block-library/src/utils/caption.js +++ b/packages/block-library/src/utils/caption.js @@ -10,14 +10,21 @@ import { useState, useEffect, useCallback } from '@wordpress/element'; import { usePrevious } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { - RichText, BlockControls, __experimentalGetElementClassName, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { ToolbarButton } from '@wordpress/components'; import { caption as captionIcon } from '@wordpress/icons'; import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + +const { PrivateRichText: RichText } = unlock( blockEditorPrivateApis ); + export function Caption( { key = 'caption', attributes, @@ -28,6 +35,7 @@ export function Caption( { label = __( 'Caption text' ), showToolbarButton = true, className, + disableEditing, } ) { const caption = attributes[ key ]; const prevCaption = usePrevious( caption ); @@ -101,6 +109,7 @@ export function Caption( { createBlock( getDefaultBlockName() ) ) } + disableEditing={ disableEditing } /> ) } diff --git a/packages/block-library/src/utils/get-transformed-metadata.js b/packages/block-library/src/utils/get-transformed-metadata.js new file mode 100644 index 00000000000000..53d79d3c1e42ac --- /dev/null +++ b/packages/block-library/src/utils/get-transformed-metadata.js @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { getBlockType } from '@wordpress/blocks'; + +/** + * Transform the metadata attribute with only the values and bindings specified by each transform. + * Returns `undefined` if the input metadata is falsy. + * + * @param {Object} metadata Original metadata attribute from the block that is being transformed. + * @param {Object} newBlockName Name of the final block after the transformation. + * @param {Function} bindingsCallback Optional callback to transform the `bindings` property object. + * @return {Object|undefined} New metadata object only with the relevant properties. + */ +export function getTransformedMetadata( + metadata, + newBlockName, + bindingsCallback +) { + if ( ! metadata ) { + return; + } + const { supports } = getBlockType( newBlockName ); + // Fixed until an opt-in mechanism is implemented. + const BLOCK_BINDINGS_SUPPORTED_BLOCKS = [ + 'core/paragraph', + 'core/heading', + 'core/image', + 'core/button', + ]; + // The metadata properties that should be preserved after the transform. + const transformSupportedProps = []; + // If it support bindings, and there is a transform bindings callback, add the `id` and `bindings` properties. + if ( + BLOCK_BINDINGS_SUPPORTED_BLOCKS.includes( newBlockName ) && + bindingsCallback + ) { + transformSupportedProps.push( 'id', 'bindings' ); + } + // If it support block naming (true by default), add the `name` property. + if ( supports.renaming !== false ) { + transformSupportedProps.push( 'name' ); + } + + // Return early if no supported properties. + if ( ! transformSupportedProps.length ) { + return; + } + + const newMetadata = Object.entries( metadata ).reduce( + ( obj, [ prop, value ] ) => { + // If prop is not supported, don't add it to the new metadata object. + if ( ! transformSupportedProps.includes( prop ) ) { + return obj; + } + obj[ prop ] = + prop === 'bindings' ? bindingsCallback( value ) : value; + return obj; + }, + {} + ); + + // Return undefined if object is empty. + return Object.keys( newMetadata ).length ? newMetadata : undefined; +} diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index bc06e231b17222..d609f70b91b55d 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -40,3 +40,18 @@ export function addUnprocessedBlockType( name, blockType ) { dispatch.addBlockTypes( processedBlockType ); }; } + +/** + * Register new block bindings source. + * + * @param {string} source Name of the source to register. + */ +export function registerBlockBindingsSource( source ) { + return { + type: 'REGISTER_BLOCK_BINDINGS_SOURCE', + sourceName: source.name, + sourceLabel: source.label, + useSource: source.useSource, + lockAttributesEditing: source.lockAttributesEditing, + }; +} diff --git a/packages/blocks/src/store/private-selectors.js b/packages/blocks/src/store/private-selectors.js index f2acf9c1051846..e6eac8be421466 100644 --- a/packages/blocks/src/store/private-selectors.js +++ b/packages/blocks/src/store/private-selectors.js @@ -186,3 +186,26 @@ export function getBootstrappedBlockType( state, name ) { export function getUnprocessedBlockTypes( state ) { return state.unprocessedBlockTypes; } + +/** + * Returns all the block bindings sources registered. + * + * @param {Object} state Data state. + * + * @return {Object} All the registered sources and their properties. + */ +export function getAllBlockBindingsSources( state ) { + return state.blockBindingsSources; +} + +/** + * Returns a specific block bindings source. + * + * @param {Object} state Data state. + * @param {string} sourceName Name of the source to get. + * + * @return {Object} The specific block binding source and its properties. + */ +export function getBlockBindingsSource( state, sourceName ) { + return state.blockBindingsSources[ sourceName ]; +} diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 91f061871f9e27..f92fb376b530a7 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -383,6 +383,20 @@ export function collections( state = {}, action ) { return state; } +export function blockBindingsSources( state = {}, action ) { + if ( action.type === 'REGISTER_BLOCK_BINDINGS_SOURCE' ) { + return { + ...state, + [ action.sourceName ]: { + label: action.sourceLabel, + useSource: action.useSource, + lockAttributesEditing: action.lockAttributesEditing ?? true, + }, + }; + } + return state; +} + export default combineReducers( { bootstrappedBlockTypes, unprocessedBlockTypes, @@ -395,4 +409,5 @@ export default combineReducers( { groupingBlockName, categories, collections, + blockBindingsSources, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index f59520f3c436bd..003d01e49641d9 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -661,12 +661,42 @@ export const getUserPatternCategories = export const getNavigationFallbackId = () => - async ( { dispatch } ) => { + async ( { dispatch, select } ) => { const fallback = await apiFetch( { - path: addQueryArgs( '/wp-block-editor/v1/navigation-fallback' ), + path: addQueryArgs( '/wp-block-editor/v1/navigation-fallback', { + _embed: true, + } ), } ); + const record = fallback?._embedded?.self; + dispatch.receiveNavigationFallbackId( fallback?.id ); + + if ( record ) { + // If the fallback is already in the store, don't invalidate navigation queries. + // Otherwise, invalidate the cache for the scenario where there were no Navigation + // posts in the state and the fallback created one. + const existingFallbackEntityRecord = select.getEntityRecord( + 'postType', + 'wp_navigation', + fallback.id + ); + const invalidateNavigationQueries = ! existingFallbackEntityRecord; + dispatch.receiveEntityRecords( + 'postType', + 'wp_navigation', + record, + undefined, + invalidateNavigationQueries + ); + + // Resolve to avoid further network requests. + dispatch.finishResolution( 'getEntityRecord', [ + 'postType', + 'wp_navigation', + fallback.id, + ] ); + } }; export const getDefaultTemplateId = diff --git a/packages/dataviews/src/bulk-actions.js b/packages/dataviews/src/bulk-actions.js index 9fd9f628286e09..5e4139fe0622e7 100644 --- a/packages/dataviews/src/bulk-actions.js +++ b/packages/dataviews/src/bulk-actions.js @@ -7,7 +7,7 @@ import { Modal, } from '@wordpress/components'; import { __, sprintf, _n } from '@wordpress/i18n'; -import { useMemo, useState, useCallback } from '@wordpress/element'; +import { useMemo, useState, useCallback, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -21,6 +21,24 @@ const { DropdownMenuSeparatorV2: DropdownMenuSeparator, } = unlock( componentsPrivateApis ); +export function useHasAPossibleBulkAction( actions, item ) { + return useMemo( () => { + return actions.some( ( action ) => { + return action.supportsBulk && action.isEligible( item ); + } ); + }, [ actions, item ] ); +} + +export function useSomeItemHasAPossibleBulkAction( actions, data ) { + return useMemo( () => { + return data.some( ( item ) => { + return actions.some( ( action ) => { + return action.supportsBulk && action.isEligible( item ); + } ); + } ); + }, [ actions, data ] ); +} + function ActionWithModal( { action, selectedItems, @@ -107,15 +125,47 @@ export default function BulkActions( { () => actions.filter( ( action ) => action.supportsBulk ), [ actions ] ); - const areAllSelected = selection && selection.length === data.length; const [ isMenuOpen, onMenuOpenChange ] = useState( false ); const [ actionWithModal, setActionWithModal ] = useState(); + const selectableItems = useMemo( () => { + return data.filter( ( item ) => { + return bulkActions.some( ( action ) => action.isEligible( item ) ); + } ); + }, [ data, bulkActions ] ); + + const numberSelectableItems = selectableItems.length; + const areAllSelected = + selection && selection.length === numberSelectableItems; + const selectedItems = useMemo( () => { return data.filter( ( item ) => selection.includes( getItemId( item ) ) ); }, [ selection, data, getItemId ] ); + const hasNonSelectableItemSelected = useMemo( () => { + return selectedItems.some( ( item ) => { + return ! selectableItems.includes( item ); + } ); + }, [ selectedItems, selectableItems ] ); + useEffect( () => { + if ( hasNonSelectableItemSelected ) { + onSelectionChange( + selectedItems.filter( ( selectedItem ) => { + return selectableItems.some( ( item ) => { + return getItemId( selectedItem ) === getItemId( item ); + } ); + } ) + ); + } + }, [ + hasNonSelectableItemSelected, + selectedItems, + selectableItems, + getItemId, + onSelectionChange, + ] ); + if ( bulkActions.length === 0 ) { return null; } @@ -157,9 +207,9 @@ export default function BulkActions( { disabled={ areAllSelected } hideOnClick={ false } onClick={ () => { - onSelectionChange( data ); + onSelectionChange( selectableItems ); } } - suffix={ data.length } + suffix={ numberSelectableItems } > { __( 'Select all' ) } diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index c7ae6b6e27aad8..9bcf531006406a 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -20,6 +20,16 @@ import BulkActions from './bulk-actions'; const defaultGetItemId = ( item ) => item.id; const defaultOnSelectionChange = () => {}; +function useSomeItemHasAPossibleBulkAction( actions, data ) { + return useMemo( () => { + return data.some( ( item ) => { + return actions.some( ( action ) => { + return action.supportsBulk && action.isEligible( item ); + } ); + } ); + }, [ actions, data ] ); +} + export default function DataViews( { view, onChangeView, @@ -75,30 +85,49 @@ export default function DataViews( { render: field.render || field.getValue, } ) ); }, [ fields ] ); + + const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction( + actions, + data + ); return (
    - { search && ( - + { search && ( + + ) } + - ) } - { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && ( - - ) } + + { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && + hasPossibleBulkAction && ( + + ) } - - - + { filterComponents } + + ); } ); export default Filters; diff --git a/packages/dataviews/src/pagination.js b/packages/dataviews/src/pagination.js index f69d7c7a12e193..4e44b5e9ca4b2e 100644 --- a/packages/dataviews/src/pagination.js +++ b/packages/dataviews/src/pagination.js @@ -27,7 +27,12 @@ const Pagination = memo( function Pagination( { justify="end" className="dataviews-pagination" > - + { createInterpolateElement( sprintf( // translators: %s: Total number of pages. diff --git a/packages/dataviews/src/single-selection-checkbox.js b/packages/dataviews/src/single-selection-checkbox.js index 0aa27824c4f9b2..ce21419f969cee 100644 --- a/packages/dataviews/src/single-selection-checkbox.js +++ b/packages/dataviews/src/single-selection-checkbox.js @@ -11,6 +11,7 @@ export default function SingleSelectionCheckbox( { data, getItemId, primaryField, + disabled, } ) { const id = getItemId( item ); const isSelected = selection.includes( id ); @@ -33,6 +34,7 @@ export default function SingleSelectionCheckbox( { __nextHasNoMarginBottom checked={ isSelected } label={ selectionLabel } + disabled={ disabled } onChange={ () => { if ( ! isSelected ) { onSelectionChange( diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 7b143958e2c16e..10e3b511cc58ae 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -13,8 +13,6 @@ .dataviews-filters__view-actions { padding: $grid-unit-15 $grid-unit-40 0; .components-search-control { - flex-grow: 1; - .components-base-control__field { max-width: 240px; } @@ -22,11 +20,7 @@ } .dataviews-filters__container { - padding: 0 $grid-unit-40; -} - -.dataviews-filters__view-actions.components-h-stack { - align-items: center; + padding-right: $grid-unit-40; } .dataviews-filters-button { @@ -44,6 +38,13 @@ color: $gray-700; } +.dataviews-pagination__page-selection { + font-size: 11px; + text-transform: uppercase; + font-weight: 500; + color: $gray-900; +} + .dataviews-filters-options { margin: $grid-unit-40 0 $grid-unit-20; } @@ -118,7 +119,7 @@ background-color: #f8f8f8; } - .components-checkbox-control__input { + .components-checkbox-control__input.components-checkbox-control__input { opacity: 0; &:checked, @@ -202,6 +203,12 @@ .dataviews-view-table__actions-column { width: 1%; } + + &:has(tr.is-selected) { + .components-checkbox-control__input { + opacity: 1; + } + } } .dataviews-view-list__primary-field, @@ -648,7 +655,8 @@ } &:hover, - &:focus-visible { + &:focus-visible, + &[aria-expanded="true"] { background: $gray-200; color: $gray-900; } @@ -657,8 +665,9 @@ color: var(--wp-admin-theme-color); background: rgba(var(--wp-admin-theme-color--rgb), 0.04); - &:hover { - background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + &:hover, + &[aria-expanded="true"] { + background: rgba(var(--wp-admin-theme-color--rgb), 0.12); } } diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 44ab1822a60750..3b46a7424dc1b1 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -22,6 +22,8 @@ import { useState } from '@wordpress/element'; import ItemActions from './item-actions'; import SingleSelectionCheckbox from './single-selection-checkbox'; +import { useHasAPossibleBulkAction } from './bulk-actions'; + function GridItem( { selection, data, @@ -34,6 +36,7 @@ function GridItem( { visibleFields, } ) { const [ hasNoPointerEvents, setHasNoPointerEvents ] = useState( false ); + const hasBulkAction = useHasAPossibleBulkAction( actions, item ); const id = getItemId( item ); const isSelected = selection.includes( id ); return ( @@ -41,11 +44,11 @@ function GridItem( { spacing={ 0 } key={ id } className={ classnames( 'dataviews-view-grid__card', { - 'is-selected': isSelected, + 'is-selected': hasBulkAction && isSelected, 'has-no-pointer-events': hasNoPointerEvents, } ) } onMouseDown={ ( event ) => { - if ( event.ctrlKey || event.metaKey ) { + if ( hasBulkAction && ( event.ctrlKey || event.metaKey ) ) { setHasNoPointerEvents( true ); if ( ! isSelected ) { onSelectionChange( @@ -91,6 +94,7 @@ function GridItem( { getItemId={ getItemId } data={ data } primaryField={ primaryField } + disabled={ ! hasBulkAction } /> { primaryField?.render( { item } ) } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index dc166577034257..9737aa3d462835 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -23,6 +23,7 @@ import { useState, Children, Fragment, + useMemo, } from '@wordpress/element'; /** @@ -33,6 +34,10 @@ import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; import { sanitizeOperators } from './utils'; import { ENUMERATION_TYPE, SORTING_DIRECTIONS } from './constants'; +import { + useSomeItemHasAPossibleBulkAction, + useHasAPossibleBulkAction, +} from './bulk-actions'; const { DropdownMenuV2: DropdownMenu, @@ -186,8 +191,20 @@ const HeaderMenu = forwardRef( function HeaderMenu( ); } ); -function BulkSelectionCheckbox( { selection, onSelectionChange, data } ) { - const areAllSelected = selection.length === data.length; +function BulkSelectionCheckbox( { + selection, + onSelectionChange, + data, + actions, +} ) { + const selectableItems = useMemo( () => { + return data.filter( ( item ) => { + return actions.some( + ( action ) => action.supportsBulk && action.isEligible( item ) + ); + } ); + }, [ data, actions ] ); + const areAllSelected = selection.length === selectableItems.length; return ( + { hasBulkActions && ( + +
    + +
    + + ) } + { visibleFields.map( ( field ) => ( + +
    + { field.render( { + item, + } ) } +
    + + ) ) } + { !! actions?.length && ( + + + + ) } + + ); +} + function ViewTable( { view, onChangeView, @@ -219,10 +311,10 @@ function ViewTable( { onSelectionChange, setOpenedFilter, } ) { - const hasBulkActions = actions?.some( ( action ) => action.supportsBulk ); const headerMenuRefs = useRef( new Map() ); const headerMenuToFocusRef = useRef(); const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = useState(); + const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data ); useEffect( () => { if ( headerMenuToFocusRef.current ) { @@ -285,6 +377,7 @@ function ViewTable( { selection={ selection } onSelectionChange={ onSelectionChange } data={ data } + actions={ actions } /> ) } @@ -347,78 +440,19 @@ function ViewTable( { { hasData && usedData.map( ( item, index ) => ( - - { hasBulkActions && ( - -
    - -
    - - ) } - { visibleFields.map( ( field ) => ( - -
    - { field.render( { - item, - } ) } -
    - - ) ) } - { !! actions?.length && ( - - - - ) } - + item={ item } + hasBulkActions={ hasBulkActions } + actions={ actions } + id={ getItemId( item ) || index } + visibleFields={ visibleFields } + primaryField={ primaryField } + selection={ selection } + getItemId={ getItemId } + onSelectionChange={ onSelectionChange } + data={ data } + /> ) ) } diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php index 428d47ec397957..6b20b2dba8376e 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php @@ -18,6 +18,7 @@ > + +
    @@ -59,6 +68,7 @@ > + +
    @@ -143,3 +158,41 @@
    + +
    + + +
    +
    + +
    + +
    + + +
    +
    diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js index 7238185bac3aa1..9437edced74a44 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js @@ -9,6 +9,10 @@ store( 'directive-context', { const ctx = getContext(); return JSON.stringify( ctx, undefined, 2 ); }, + get selected() { + const { list, selected } = getContext(); + return list.find( ( obj ) => obj === selected )?.text; + } }, actions: { updateContext( event ) { @@ -22,6 +26,15 @@ store( 'directive-context', { const ctx = getContext(); ctx.text = ctx.text === 'Text 1' ? 'Text 2' : 'Text 1'; }, + selectItem( event ) { + const ctx = getContext(); + const value = parseInt( event.target.value ); + ctx.selected = ctx.list.find( ( { id } ) => id === value ); + }, + replaceObj() { + const ctx = getContext(); + ctx.obj = { overwritten: true }; + } }, } ); @@ -29,12 +42,17 @@ const html = `
    +
    +
    +
    +
    - + +
    `; @@ -49,13 +67,17 @@ const { actions } = store( 'directive-context-navigate', { const ctx = getContext(); ctx.newText = 'some new text'; }, + addText2() { + const ctx = getContext(); + ctx.text2 = 'some new text'; + }, navigate() { return import( '@wordpress/interactivity-router' ).then( - ( { actions: routerActions } ) => - routerActions.navigate( - window.location, - { force: true, html }, - ) + ( { actions: routerActions } ) => { + const url = new URL( window.location.href ); + url.searchParams.set( 'next_page', 'true' ); + return routerActions.navigate( url, { force: true, html } ); + } ); }, @@ -66,3 +88,21 @@ const { actions } = store( 'directive-context-navigate', { }, }, } ); + +store( 'directive-context-watch', { + actions: { + increment: () => { + const ctx = getContext(); + ctx.counter = ctx.counter + 1; + }, + }, + callbacks: { + countChanges: () => { + const ctx = getContext(); + // Subscribe to changes in counter. + // eslint-disable-next-line no-unused-expressions + ctx.counter; + ctx.changes = ctx.changes + 1; + }, + }, +}); diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php index 0b8e6e1012d1a4..d1a7aa9211f105 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php @@ -15,6 +15,13 @@ array( 'clientNavigationDisabled' => true ) ); } + +if ( isset( $attributes['data'] ) ) { + wp_interactivity_state( + 'router', + array( 'data' => $attributes['data'] ) + ); +} ?>
    NaN + NaN NaN - $link ) { - $i = $key += 1; - echo <<link $i - link $i with hash + +
    +
    +
    +
    diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js index 1e137969936a09..b2d4ad0dc1ddeb 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js @@ -6,14 +6,23 @@ import { store } from '@wordpress/interactivity'; const { state } = store( 'router', { state: { status: 'idle', - navigations: 0, + navigations: { + pending: 0, + count: 0, + }, timeout: 10000, + data: { + get getterProp() { + return `value from getter (${ state.data.prop1 })`; + } + } }, actions: { *navigate( e ) { e.preventDefault(); - state.navigations += 1; + state.navigations.count += 1; + state.navigations.pending += 1; state.status = 'busy'; const force = e.target.dataset.forceNavigation === 'true'; @@ -24,9 +33,9 @@ const { state } = store( 'router', { ); yield actions.navigate( e.target.href, { force, timeout } ); - state.navigations -= 1; + state.navigations.pending -= 1; - if ( state.navigations === 0 ) { + if ( state.navigations.pending === 0 ) { state.status = 'idle'; } }, diff --git a/packages/e2e-tests/plugins/interactive-blocks/store/block.json b/packages/e2e-tests/plugins/interactive-blocks/store/block.json new file mode 100644 index 00000000000000..cddd765ba3db53 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/store/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/store", + "title": "E2E Interactivity tests - store definition", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "store-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/store/render.php b/packages/e2e-tests/plugins/interactive-blocks/store/render.php new file mode 100644 index 00000000000000..295872137dbdae --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/store/render.php @@ -0,0 +1,17 @@ + + +
    +
    +
    diff --git a/packages/e2e-tests/plugins/interactive-blocks/store/view.js b/packages/e2e-tests/plugins/interactive-blocks/store/view.js new file mode 100644 index 00000000000000..b4a987b5e85b5e --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/store/view.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { store, getElement } from '@wordpress/interactivity'; + + +const { state } = store( 'test/store', { + state: { + get isNotProxified() { + const { ref } = getElement(); + return state.elementRef === ref; + } + }, + callbacks: { + init() { + const { ref } = getElement(); + state.elementRef = ref; // HTMLElement + } + } +} ) diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index b573ac43fe5fa7..b1aba274a155c0 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -61,7 +61,7 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { const blockToolbarRef = useRef(); const { isTextEditor, - hasBlockSelection, + blockSelectionStart, hasActiveMetaboxes, hasFixedToolbar, isPublishSidebarOpened, @@ -73,8 +73,8 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { return { isTextEditor: getEditorMode() === 'text', - hasBlockSelection: - !! select( blockEditorStore ).getBlockSelectionStart(), + blockSelectionStart: + select( blockEditorStore ).getBlockSelectionStart(), hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), hasHistory: !! select( editorStore ).getEditorSettings() @@ -88,13 +88,14 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = useState( true ); + const hasBlockSelection = !! blockSelectionStart; useEffect( () => { // If we have a new block selection, show the block tools - if ( hasBlockSelection ) { + if ( blockSelectionStart ) { setIsBlockToolsCollapsed( false ); } - }, [ hasBlockSelection ] ); + }, [ blockSelectionStart ] ); return (
    @@ -121,7 +122,9 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { className={ classnames( 'selected-block-tools-wrapper', { - 'is-collapsed': isBlockToolsCollapsed, + 'is-collapsed': + isBlockToolsCollapsed || + ! hasBlockSelection, } ) } > diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index f5b4a27e158ea0..98f67e7e4ee5c6 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -556,7 +556,7 @@ export const isEditingTemplate = createRegistrySelector( ( select ) => () => { since: '6.5', alternative: `select( 'core/editor' ).getRenderingMode`, } ); - return select( editorStore ).getCurrentPostType() !== 'post-only'; + return select( editorStore ).getCurrentPostType() === 'wp_template'; } ); /** diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js deleted file mode 100644 index a6962952661939..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalVStack as VStack, - __experimentalSpacer as Spacer, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import CollectionFontVariant from './collection-font-variant'; -import { isFontFontFaceInOutline } from './utils/fonts-outline'; -import { sortFontFaces } from './utils/sort-font-faces'; - -function CollectionFontDetails( { - font, - handleToggleVariant, - fontToInstallOutline, -} ) { - const fontFaces = - font.fontFace && font.fontFace.length - ? sortFontFaces( font.fontFace ) - : [ - { - fontFamily: font.fontFamily, - fontStyle: 'normal', - fontWeight: '400', - }, - ]; - - return ( - <> - - - - { fontFaces.map( ( face, i ) => ( - - ) ) } - - - - ); -} - -export default CollectionFontDetails; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js index 0db8932a98e34d..04300be95a0cff 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js @@ -11,7 +11,7 @@ import { * Internal dependencies */ import { getFontFaceVariantName } from './utils'; -import FontFaceDemo from './font-demo'; +import FontDemo from './font-demo'; import { unlock } from '../../../lock-unlock'; function CollectionFontVariant( { @@ -35,7 +35,7 @@ function CollectionFontVariant( { ); return ( -
    +
    ) } - - { ! renderConfirmDialog && - ! selectedCollection?.font_families && - ! notice && } - - { ! renderConfirmDialog && - !! selectedCollection?.font_families?.length && - ! fonts.length && ( - - { __( - 'No fonts found. Try with a different search term' + { ! selectedFont && ( + + + + + { createInterpolateElement( + sprintf( + // translators: %s: Total number of pages. + _x( + 'Page of %s', + 'paging' + ), + totalPages + ), + { + CurrenPageControl: ( + { + return { + label: i + 1, + value: i + 1, + }; + } ) } + onChange={ ( newPage ) => + setPage( parseInt( newPage ) ) + } + size={ 'compact' } + __nextHasNoMarginBottom + /> + ), + } ) } - - ) } - - { ! renderConfirmDialog && selectedFont && ( - - ) } - - { ! renderConfirmDialog && ! selectedFont && ( -
    - { items.map( ( font ) => ( - { - setSelectedFont( font.font_family_settings ); - } } - /> - ) ) } -
    + + + + ) } - - ); -} - -function PaginationFooter( { page, totalPages, setPage } ) { - return ( - - - - - { createInterpolateElement( - sprintf( - // translators: %s: Total number of pages. - _x( 'Page of %s', 'paging' ), - totalPages - ), - { - CurrenPageControl: ( - { - return { - label: i + 1, - value: i + 1, - }; - } - ) } - onChange={ ( newPage ) => - setPage( parseInt( newPage ) ) - } - size={ 'compact' } - __nextHasNoMarginBottom - /> - ), - } - ) } - - - - - ); -} - -function InstallFooter( { handleInstall, isDisabled } ) { - const { isInstalling } = useContext( FontLibraryContext ); - - return ( - - - +
    ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js index ee745caf45932e..0320bdd4e4893f 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js @@ -8,7 +8,10 @@ import { useContext, useEffect, useState, useRef } from '@wordpress/element'; * Internal dependencies */ import { FontLibraryContext } from './context'; -import { getFacePreviewStyle } from './utils/preview-styles'; +import { + getFacePreviewStyle, + getFamilyPreviewStyle, +} from './utils/preview-styles'; function getPreviewUrl( fontFace ) { if ( fontFace.preview ) { @@ -19,8 +22,39 @@ function getPreviewUrl( fontFace ) { } } -function FontFaceDemo( { customPreviewUrl, fontFace, text, style = {} } ) { +function getDisplayFontFace( font ) { + // if this IS a font face return it + if ( font.fontStyle || font.fontWeight ) { + return font; + } + // if this is a font family with a collection of font faces + // return the first one that is normal and 400 OR just the first one + if ( font.fontFace && font.fontFace.length ) { + return ( + font.fontFace.find( + ( face ) => + face.fontStyle === 'normal' && face.fontWeight === '400' + ) || font.fontFace[ 0 ] + ); + } + // This must be a font family with no font faces + // return a fake font face + return { + fontStyle: 'normal', + fontWeight: '400', + fontFamily: font.fontFamily, + fake: true, + }; +} + +function FontDemo( { font, text } ) { const ref = useRef( null ); + + const fontFace = getDisplayFontFace( font ); + const style = getFamilyPreviewStyle( font ); + text = text || font.name; + const customPreviewUrl = font.preview; + const [ isIntersecting, setIsIntersecting ] = useState( false ); const [ isAssetLoaded, setIsAssetLoaded ] = useState( false ); const { loadFontFaceAsset } = useContext( FontLibraryContext ); @@ -34,8 +68,8 @@ function FontFaceDemo( { customPreviewUrl, fontFace, text, style = {} } ) { fontSize: '18px', lineHeight: 1, opacity: isAssetLoaded ? '1' : '0', - ...faceStyles, ...style, + ...faceStyles, }; useEffect( () => { @@ -79,4 +113,4 @@ function FontFaceDemo( { customPreviewUrl, fontFace, text, style = {} } ) { ); } -export default FontFaceDemo; +export default FontDemo; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 99c99e44a43afc..9425cb9c2d27ba 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -1,27 +1,35 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; -import { useContext, useEffect, useState } from '@wordpress/element'; import { - privateApis as componentsPrivateApis, + Button, + __experimentalConfirmDialog as ConfirmDialog, __experimentalHStack as HStack, + __experimentalHeading as Heading, + __experimentalNavigatorProvider as NavigatorProvider, + __experimentalNavigatorScreen as NavigatorScreen, + __experimentalNavigatorToParentButton as NavigatorToParentButton, + __experimentalUseNavigator as useNavigator, __experimentalSpacer as Spacer, __experimentalText as Text, - Button, + __experimentalVStack as VStack, + Flex, + Notice, Spinner, - FlexItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { useContext, useEffect, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { chevronLeft } from '@wordpress/icons'; /** * Internal dependencies */ -import TabPanelLayout from './tab-panel-layout'; -import { FontLibraryContext } from './context'; -import LibraryFontDetails from './library-font-details'; -import LibraryFontCard from './library-font-card'; -import ConfirmDeleteDialog from './confirm-delete-dialog'; import { unlock } from '../../../lock-unlock'; +import { FontLibraryContext } from './context'; +import FontCard from './font-card'; +import LibraryFontVariant from './library-font-variant'; +import { sortFontFaces } from './utils/sort-font-faces'; const { ProgressBar } = unlock( componentsPrivateApis ); function InstalledFonts() { @@ -33,102 +41,100 @@ function InstalledFonts() { refreshLibrary, uninstallFontFamily, isResolvingLibrary, + isInstalling, + saveFontFamilies, + getFontFacesActivated, + fontFamiliesHasChanges, notice, setNotice, } = useContext( FontLibraryContext ); const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState( false ); - const handleUnselectFont = () => { - handleSetLibraryFontSelected( null ); - }; + const shouldDisplayDeleteButton = + !! libraryFontSelected && libraryFontSelected?.source !== 'theme'; - const handleSelectFont = ( font ) => { - handleSetLibraryFontSelected( font ); + const handleUninstallClick = () => { + setIsConfirmDeleteOpen( true ); }; - const handleConfirmUninstall = async () => { - setNotice( null ); - - try { - await uninstallFontFamily( libraryFontSelected ); - setNotice( { - type: 'success', - message: __( 'Font family uninstalled successfully.' ), - } ); - - // If the font was succesfully uninstalled it is unselected. - handleUnselectFont(); - setIsConfirmDeleteOpen( false ); - } catch ( error ) { - setNotice( { - type: 'error', - message: - __( 'There was an error uninstalling the font family. ' ) + - error.message, - } ); + const getFontFacesToDisplay = ( font ) => { + if ( ! font ) { + return []; } + if ( ! font.fontFace || ! font.fontFace.length ) { + return [ + { + fontFamily: font.fontFamily, + fontStyle: 'normal', + fontWeight: '400', + }, + ]; + } + return sortFontFaces( font.fontFace ); }; - const handleUninstallClick = async () => { - setIsConfirmDeleteOpen( true ); - }; - - const handleCancelUninstall = () => { - setIsConfirmDeleteOpen( false ); + const getFontCardVariantsText = ( font ) => { + const variantsInstalled = + font?.fontFace?.length > 0 ? font.fontFace.length : 1; + const variantsActive = getFontFacesActivated( + font.slug, + font.source + ).length; + return sprintf( + /* translators: 1: Active font variants, 2: Total font variants. */ + __( '%1$s/%2$s variants active' ), + variantsActive, + variantsInstalled + ); }; - const tabDescription = !! libraryFontSelected - ? __( - 'Choose font variants. Keep in mind that too many variants could make your site slower.' - ) - : null; - - const shouldDisplayDeleteButton = - !! libraryFontSelected && libraryFontSelected?.source !== 'theme'; - useEffect( () => { - handleSelectFont( libraryFontSelected ); + handleSetLibraryFontSelected( libraryFontSelected ); refreshLibrary(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); return ( - - } - > - - - { ! libraryFontSelected && ( - <> - { isResolvingLibrary && ( - - - - - +
    + { isResolvingLibrary && ( + + + + + + ) } + + + + { notice && ( + <> + + setNotice( null ) } + > + { notice.message } + + + ) } { baseCustomFonts.length > 0 && ( <> + + { __( 'Installed Fonts' ) } + + { baseCustomFonts.map( ( font ) => ( - { - handleSelectFont( font ); + handleSetLibraryFontSelected( font ); } } /> ) ) } @@ -141,42 +147,93 @@ function InstalledFonts() { { __( 'Theme Fonts' ) } - { baseThemeFonts.map( ( font ) => ( - { - handleSelectFont( font ); + handleSetLibraryFontSelected( font ); } } /> ) ) } ) } - - ) } + - { libraryFontSelected && ( - - ) } - - ); -} + + -function Footer( { shouldDisplayDeleteButton, handleUninstallClick } ) { - const { saveFontFamilies, fontFamiliesHasChanges, isInstalling } = - useContext( FontLibraryContext ); - return ( - - { isInstalling && } -
    + + { + handleSetLibraryFontSelected( null ); + } } + aria-label={ __( 'Navigate to the previous view' ) } + /> + + { libraryFontSelected?.name } + + + { notice && ( + <> + + setNotice( null ) } + > + { notice.message } + + + + ) } + + + { __( + 'Choose font variants. Keep in mind that too many variants could make your site slower.' + ) } + + + + + { getFontFacesToDisplay( libraryFontSelected ).map( + ( face, i ) => ( + + ) + ) } + + + + + + { isInstalling && } { shouldDisplayDeleteButton && (
    - -
    + + +
    + ); +} + +function ConfirmDeleteDialog( { + font, + isOpen, + setIsOpen, + setNotice, + uninstallFontFamily, + handleSetLibraryFontSelected, +} ) { + const navigator = useNavigator(); + + const handleConfirmUninstall = async () => { + setNotice( null ); + setIsOpen( false ); + try { + await uninstallFontFamily( font ); + navigator.goBack(); + handleSetLibraryFontSelected( null ); + setNotice( { + type: 'success', + message: __( 'Font family uninstalled successfully.' ), + } ); + } catch ( error ) { + setNotice( { + type: 'error', + message: + __( 'There was an error uninstalling the font family. ' ) + + error.message, + } ); + } + }; + + const handleCancelUninstall = () => { + setIsOpen( false ); + }; + + return ( + + { font && + sprintf( + /* translators: %s: Name of the font. */ + __( + 'Are you sure you want to delete "%s" font and all its variants and assets?' + ), + font.name + ) } + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-card.js b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-card.js deleted file mode 100644 index aae43fcf63ebae..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-card.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { useContext } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import FontCard from './font-card'; -import { FontLibraryContext } from './context'; - -function LibraryFontCard( { font, ...props } ) { - const { getFontFacesActivated } = useContext( FontLibraryContext ); - - const variantsInstalled = - font?.fontFace?.length > 0 ? font.fontFace.length : 1; - const variantsActive = getFontFacesActivated( - font.slug, - font.source - ).length; - const variantsText = sprintf( - /* translators: 1: Active font variants, 2: Total font variants. */ - __( '%1$s/%2$s variants active' ), - variantsActive, - variantsInstalled - ); - - return ( - - ); -} - -export default LibraryFontCard; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js index f58eccd2b6ec2d..a81d12bbe858dc 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js @@ -13,7 +13,7 @@ import { */ import { getFontFaceVariantName } from './utils'; import { FontLibraryContext } from './context'; -import FontFaceDemo from './font-demo'; +import FontDemo from './font-demo'; import { unlock } from '../../../lock-unlock'; function LibraryFontVariant( { face, font } ) { @@ -45,7 +45,7 @@ function LibraryFontVariant( { face, font } ) { ); return ( -
    +