From efbbebd44c94e5a910d2671a97ea9a54a290c37f Mon Sep 17 00:00:00 2001 From: Christian Nyffenegger Date: Fri, 10 May 2019 07:06:09 +0200 Subject: [PATCH 01/34] Fix broken Link (#15527) --- .../block-editor/src/components/button-block-appender/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/button-block-appender/README.md b/packages/block-editor/src/components/button-block-appender/README.md index 6f29d56bea1596..87d917ab50b582 100644 --- a/packages/block-editor/src/components/button-block-appender/README.md +++ b/packages/block-editor/src/components/button-block-appender/README.md @@ -40,4 +40,4 @@ A CSS `class` to be _prepended_ to the default class of `"button-block-appender" ## Examples -The [`` component](packages/block-editor/src/components/inner-blocks/) exposes an enhanced version of `ButtonBlockAppender` to allow consumers to choose it as an alternative to the standard behaviour of auto-inserting the default Block (typically `core/paragraph`). \ No newline at end of file +The [`` component](../inner-blocks/) exposes an enhanced version of `ButtonBlockAppender` to allow consumers to choose it as an alternative to the standard behaviour of auto-inserting the default Block (typically `core/paragraph`). From 18a5443521ae96a33d5de94652919a59185e259f Mon Sep 17 00:00:00 2001 From: Garrett Hyder Date: Fri, 10 May 2019 00:47:47 -0700 Subject: [PATCH 02/34] Update links in ToC for Checkbox Control (#15508) Removing the 'http://' prefix on the ToC to fix anchor links --- packages/components/src/checkbox-control/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/checkbox-control/README.md b/packages/components/src/checkbox-control/README.md index c011fd2c56fe1f..43165e8ae1b59c 100644 --- a/packages/components/src/checkbox-control/README.md +++ b/packages/components/src/checkbox-control/README.md @@ -8,9 +8,9 @@ Selected and unselected checkboxes ## Table of contents -1. [Design guidelines](http://#design-guidelines) -2. [Development guidelines](http://#development-guidelines) -3. [Related components](http://#related-components) +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) ## Design guidelines From ae0d1e96616d80a1974f101147c37fcf041a1740 Mon Sep 17 00:00:00 2001 From: Garrett Hyder Date: Fri, 10 May 2019 00:48:18 -0700 Subject: [PATCH 03/34] Removing Related components from Table of Contents as it's not present on the page (#15505) 3. [Related components](#related-components) --- packages/components/src/menu-items-choice/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/menu-items-choice/README.md b/packages/components/src/menu-items-choice/README.md index cbd7fd9f9d663a..bd294f35663894 100644 --- a/packages/components/src/menu-items-choice/README.md +++ b/packages/components/src/menu-items-choice/README.md @@ -10,7 +10,6 @@ 1. [Design guidelines](#design-guidelines) 2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) ## Design guidelines From f723d021799604607eabd14b0bfa500f28b1d2e8 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 10 May 2019 09:17:05 +0100 Subject: [PATCH 04/34] Fix: Use block-editor instead of editor in cover block. (#15547) --- packages/block-library/src/cover/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/cover/edit.js b/packages/block-library/src/cover/edit.js index bf9ba990cb9219..f908be2423d654 100644 --- a/packages/block-library/src/cover/edit.js +++ b/packages/block-library/src/cover/edit.js @@ -28,7 +28,7 @@ import { MediaUploadCheck, PanelColorSettings, withColors, -} from '@wordpress/editor'; +} from '@wordpress/block-editor'; import { Component, createRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; From 8195c7447ff2350055793b6e01a71b7a5b07720f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= Date: Fri, 10 May 2019 11:18:16 +0200 Subject: [PATCH 05/34] Fix gallery tab order (#15540) * Structural changes - Reorder: img goes first than the inline menu items - Always render the inline menu items. We want to keep a consistent tabbable experience. We'll style them down when not in focus. * Make button visible * Mark GalleryImage as selected on button focused * Fix color button on focus * Prevent the button from showing up if the GalleryImage is not selected * Make button and caption part of the tab path We can't use the isSelected property to conditionally render them, otherwise, they won't be tabbed through when tabbing backward. * We can actually disable the button depending on isSelected status The GalleryImage gets selected by focusing on the caption (tabbing backward) and the element (tabbing forwards). At that time, the button is enabled and will get the focus. * Do not show caption when GalleryImage is not selected --- .../block-library/src/gallery/editor.scss | 24 +++++++----- .../src/gallery/gallery-image.js | 38 +++++++++---------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss index 419fc365f468af..ffd0f7847a09cd 100644 --- a/packages/block-library/src/gallery/editor.scss +++ b/packages/block-library/src/gallery/editor.scss @@ -59,6 +59,19 @@ ul.wp-block-gallery { } } + .is-selected .block-library-gallery-item__inline-menu { + background-color: theme(primary); + + .components-button { + color: $white; + } + + .components-button:focus { + color: inherit; + } + + } + .block-editor-rich-text figcaption { a { color: $white; @@ -71,25 +84,16 @@ ul.wp-block-gallery { position: absolute; top: -2px; right: -2px; - background-color: theme(primary); display: inline-flex; z-index: z-index(".block-library-gallery-item__inline-menu"); .components-button { - color: $white; - &:hover, - &:focus { - color: $white; - } + color: transparent; } } .blocks-gallery-item__remove { padding: 0; - - &.components-button:focus { - color: inherit; - } } .blocks-gallery-item .components-spinner { diff --git a/packages/block-library/src/gallery/gallery-image.js b/packages/block-library/src/gallery/gallery-image.js index e69b8385fe7e52..2c91f13df2c6ab 100644 --- a/packages/block-library/src/gallery/gallery-image.js +++ b/packages/block-library/src/gallery/gallery-image.js @@ -127,28 +127,26 @@ class GalleryImage extends Component { return (
- { isSelected && -
- -
- } { href ? { img } : img } - { ( ! RichText.isEmpty( caption ) || isSelected ) ? ( - setAttributes( { caption: newCaption } ) } - unstableOnFocus={ this.onSelectCaption } - inlineToolbar +
+ - ) : null } +
+ setAttributes( { caption: newCaption } ) } + unstableOnFocus={ this.onSelectCaption } + inlineToolbar + />
); } From 5746ac33459f3dd5f6f2d191de975ad882e0f02b Mon Sep 17 00:00:00 2001 From: Florian Truchot <36343370+truchot@users.noreply.github.com> Date: Fri, 10 May 2019 11:56:11 +0200 Subject: [PATCH 06/34] Fix focus color contrast (#15544) --- assets/stylesheets/_mixins.scss | 4 ++-- packages/components/src/button/style.scss | 6 ++++-- packages/components/src/date-time/style.scss | 11 +++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/assets/stylesheets/_mixins.scss b/assets/stylesheets/_mixins.scss index 111f65dfc411bd..8551c934d6edb9 100644 --- a/assets/stylesheets/_mixins.scss +++ b/assets/stylesheets/_mixins.scss @@ -180,8 +180,8 @@ @mixin input-style__focus() { color: $dark-gray-900; - border-color: $blue-medium-500; - box-shadow: 0 0 0 1px $blue-medium-500; + border-color: $blue-medium-focus; + box-shadow: 0 0 0 1px $blue-medium-focus; // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 12cb7e45cb2f07..5eeb1fa0f8a559 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -40,7 +40,8 @@ border-color: #999; box-shadow: inset 0 -1px 0 #999, - 0 0 0 2px $blue-medium-200; + 0 0 0 1px $white, + 0 0 0 3px $blue-medium-focus; text-decoration: none; } @@ -88,7 +89,8 @@ &:focus:enabled { box-shadow: inset 0 -1px 0 color(theme(button) shade(50%)), - 0 0 0 2px $blue-medium-200; + 0 0 0 1px $white, + 0 0 0 3px $blue-medium-focus; } &:active:enabled { diff --git a/packages/components/src/date-time/style.scss b/packages/components/src/date-time/style.scss index 9412de832e3ea6..e9c84135c0a320 100644 --- a/packages/components/src/date-time/style.scss +++ b/packages/components/src/date-time/style.scss @@ -50,6 +50,11 @@ .DayPickerNavigation_button__horizontalDefault { padding: 2px 8px; top: 20px; + + &:focus { + border-color: $blue-medium-focus; + box-shadow: 0 0 0 1px $blue-medium-focus; + } } .DayPicker_weekHeader { @@ -94,6 +99,12 @@ border-radius: 0 3px 3px 0; } + .components-datetime__time-am-button:focus, + .components-datetime__time-pm-button:focus { + position: relative; + z-index: 1; + } + .components-datetime__time-am-button.is-toggled, .components-datetime__time-pm-button.is-toggled { background: $light-gray-300; From 94d48e09dcfb547ad1b142d137a51a378d57077f Mon Sep 17 00:00:00 2001 From: Jorge Bernal Date: Fri, 10 May 2019 13:03:38 +0200 Subject: [PATCH 07/34] Improve accessibility on missing block (#15457) * Improve accessibility on missing block * Update packages/block-library/src/missing/edit.native.js Co-Authored-By: koke * Remove unnecessary dot for accessibility label Co-Authored-By: koke --- .../block-library/src/missing/edit.native.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/missing/edit.native.js b/packages/block-library/src/missing/edit.native.js index 838c64d88b56c4..970975aa711418 100644 --- a/packages/block-library/src/missing/edit.native.js +++ b/packages/block-library/src/missing/edit.native.js @@ -10,7 +10,7 @@ import { Icon } from '@wordpress/components'; import { coreBlocks } from '@wordpress/block-library'; import { normalizeIconObject } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -25,7 +25,20 @@ export default class UnsupportedBlockEdit extends Component { const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins'; return ( - + { title } From 4108e37d3771b5871cd720216432b5bbd5255551 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 10 May 2019 18:43:58 +0100 Subject: [PATCH 08/34] Add experimental indication to name and description of block area cpt (#15563) --- lib/widgets.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/widgets.php b/lib/widgets.php index e26f0b5574f597..e35ccf5b0ef385 100644 --- a/lib/widgets.php +++ b/lib/widgets.php @@ -134,9 +134,10 @@ function gutenberg_create_wp_area_post_type() { register_post_type( 'wp_area', array( + 'description' => __( 'Experimental custom post type that will store block areas referenced by themes.', 'gutenberg' ), 'labels' => array( - 'name' => _x( 'Block Area', 'post type general name', 'gutenberg' ), - 'singular_name' => _x( 'Block Area', 'post type singular name', 'gutenberg' ), + 'name' => _x( 'Block Area (Experimental)', 'post type general name', 'gutenberg' ), + 'singular_name' => _x( 'Block Area (Experimental)', 'post type singular name', 'gutenberg' ), 'menu_name' => _x( 'Block Areas', 'admin menu', 'gutenberg' ), 'name_admin_bar' => _x( 'Block Area', 'add new on admin bar', 'gutenberg' ), 'add_new' => _x( 'Add New', 'Block', 'gutenberg' ), From 9c5bc045e0a964375c1d6be537849e3291c200cd Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Sat, 11 May 2019 11:59:37 -0700 Subject: [PATCH 09/34] Check for selection range count before calling getRangeAt (#15576) --- packages/format-library/src/link/inline.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index e94ce68514fb3b..c4b64e9e3c45ce 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -79,7 +79,8 @@ const LinkViewerUrl = ( { url } ) => { const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => { const anchorRect = useMemo( () => { - const range = window.getSelection().getRangeAt( 0 ); + const selection = window.getSelection(); + const range = selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; if ( ! range ) { return; } From 295a381838c03ace2d239a6cbb426b1285c1cc69 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Sun, 12 May 2019 08:36:01 +0300 Subject: [PATCH 10/34] [Mobile]Add video block (first iteration) (#14912) Added video block (first iteration --- .../src/components/index.native.js | 1 + .../media-placeholder/index.native.js | 88 +++++-- .../components/media-upload/index.native.js | 116 +++++++++ .../block-library/src/image/edit.native.js | 214 ++++------------ .../src/image/media-upload-progress.native.js | 167 +++++++++++++ packages/block-library/src/index.native.js | 1 + .../block-library/src/video/edit.native.js | 232 ++++++++++++++++++ .../block-library/src/video/style.native.scss | 25 ++ .../src/video/video-player.android.js | 25 ++ .../src/video/video-player.ios.js | 73 ++++++ .../src/video/video-player.native.scss | 32 +++ packages/edit-post/src/index.native.js | 1 + 12 files changed, 795 insertions(+), 180 deletions(-) create mode 100644 packages/block-editor/src/components/media-upload/index.native.js create mode 100644 packages/block-library/src/image/media-upload-progress.native.js create mode 100644 packages/block-library/src/video/edit.native.js create mode 100644 packages/block-library/src/video/style.native.scss create mode 100644 packages/block-library/src/video/video-player.android.js create mode 100644 packages/block-library/src/video/video-player.ios.js create mode 100644 packages/block-library/src/video/video-player.native.scss diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index c1cdb633b190b8..47683399491f17 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -13,6 +13,7 @@ export { __unstableRichTextInputEvent, } from './rich-text'; export { default as MediaPlaceholder } from './media-placeholder'; +export { default as MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from './media-upload'; export { default as URLInput } from './url-input'; // Content Related Components diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index 8a50b464a79146..db98ff4f206753 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -6,8 +6,9 @@ import { View, Text, TouchableWithoutFeedback } from 'react-native'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Dashicon } from '@wordpress/components'; +import { MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/block-editor'; /** * Internal dependencies @@ -15,24 +16,75 @@ import { Dashicon } from '@wordpress/components'; import styles from './styles.scss'; function MediaPlaceholder( props ) { + const { mediaType, labels = {}, icon, onSelectURL } = props; + + const isImage = MEDIA_TYPE_IMAGE === mediaType; + const isVideo = MEDIA_TYPE_VIDEO === mediaType; + + let placeholderTitle = labels.title; + if ( placeholderTitle === undefined ) { + placeholderTitle = __( 'Media' ); + if ( isImage ) { + placeholderTitle = __( 'Image' ); + } else if ( isVideo ) { + placeholderTitle = __( 'Video' ); + } + } + + let placeholderIcon = icon; + if ( placeholderIcon === undefined ) { + if ( isImage ) { + placeholderIcon = 'format-image'; + } else if ( isVideo ) { + placeholderIcon = 'format-video'; + } + } + + let instructions = labels.instructions; + if ( instructions === undefined ) { + if ( isImage ) { + instructions = __( 'CHOOSE IMAGE' ); + } else if ( isVideo ) { + instructions = __( 'CHOOSE VIDEO' ); + } + } + + let accessibilityHint = __( 'Double tap to select' ); + if ( isImage ) { + accessibilityHint = __( 'Double tap to select an image' ); + } else if ( isVideo ) { + accessibilityHint = __( 'Double tap to select a video' ); + } + return ( - - - - - { __( 'Image' ) } - - - { __( 'CHOOSE IMAGE' ) } - - - + { + return ( + + + { getMediaOptions() } + + + { placeholderTitle } + + + { instructions } + + + + ); + } } /> ); } diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js new file mode 100644 index 00000000000000..d34b3061403ef3 --- /dev/null +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import React from 'react'; +import { + requestMediaPickFromMediaLibrary, + requestMediaPickFromDeviceLibrary, + requestMediaPickFromDeviceCamera, +} from 'react-native-gutenberg-bridge'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Picker } from '@wordpress/block-editor'; + +export const MEDIA_TYPE_IMAGE = 'image'; +export const MEDIA_TYPE_VIDEO = 'video'; + +const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE = 'choose_from_device'; +const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA = 'take_media'; +const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_media_library'; + +class MediaUpload extends React.Component { + getTakeMediaLabel() { + const { mediaType } = this.props; + + if ( mediaType === MEDIA_TYPE_IMAGE ) { + return __( 'Take a Photo' ); + } else if ( mediaType === MEDIA_TYPE_VIDEO ) { + return __( 'Take a Video' ); + } + } + + getMediaOptionsItems() { + return [ + { icon: this.getChooseFromDeviceIcon(), value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, label: __( 'Choose from device' ) }, + { icon: this.getTakeMediaIcon(), value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA, label: this.getTakeMediaLabel() }, + { icon: this.getWordPressLibraryIcon(), value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, label: __( 'WordPress Media Library' ) }, + ]; + } + + getChooseFromDeviceIcon() { + const { mediaType } = this.props; + + if ( mediaType === MEDIA_TYPE_IMAGE ) { + return 'format-image'; + } else if ( mediaType === MEDIA_TYPE_VIDEO ) { + return 'format-video'; + } + } + + getTakeMediaIcon() { + return 'camera'; + } + + getWordPressLibraryIcon() { + return 'wordpress-alt'; + } + + render() { + const { mediaType } = this.props; + + const onMediaLibraryButtonPressed = () => { + requestMediaPickFromMediaLibrary( [ mediaType ], ( mediaId, mediaUrl ) => { + if ( mediaId ) { + this.props.onSelectURL( mediaId, mediaUrl ); + } + } ); + }; + + const onMediaUploadButtonPressed = () => { + requestMediaPickFromDeviceLibrary( [ mediaType ], ( mediaId, mediaUrl ) => { + if ( mediaId ) { + this.props.onSelectURL( mediaId, mediaUrl ); + } + } ); + }; + + const onMediaCaptureButtonPressed = () => { + requestMediaPickFromDeviceCamera( [ mediaType ], ( mediaId, mediaUrl ) => { + if ( mediaId ) { + this.props.onSelectURL( mediaId, mediaUrl ); + } + } ); + }; + + const mediaOptions = this.getMediaOptionsItems(); + + let picker; + + const onPickerPresent = () => { + picker.presentPicker(); + }; + + const getMediaOptions = () => ( + picker = instance } + options={ mediaOptions } + onChange={ ( value ) => { + if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { + onMediaUploadButtonPressed(); + } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA ) { + onMediaCaptureButtonPressed(); + } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { + onMediaLibraryButtonPressed(); + } + } } + /> + ); + return this.props.render( { open: onPickerPresent, getMediaOptions } ); + } +} + +export default MediaUpload; diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 8f2aa785d3f341..93e21246f5e74a 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -2,12 +2,8 @@ * External dependencies */ import React from 'react'; -import { View, ImageBackground, TextInput, Text, TouchableWithoutFeedback } from 'react-native'; +import { View, TextInput, ImageBackground, Text, TouchableWithoutFeedback } from 'react-native'; import { - subscribeMediaUpload, - requestMediaPickFromMediaLibrary, - requestMediaPickFromDeviceLibrary, - requestMediaPickFromDeviceCamera, requestMediaImport, mediaUploadSync, requestImageFailedRetryDialog, @@ -21,16 +17,16 @@ import { isEmpty } from 'lodash'; import { Toolbar, ToolbarButton, - Spinner, Dashicon, } from '@wordpress/components'; import { MediaPlaceholder, + MediaUpload, + MEDIA_TYPE_IMAGE, RichText, BlockControls, InspectorControls, BottomSheet, - Picker, } from '@wordpress/block-editor'; import { __, sprintf } from '@wordpress/i18n'; import { isURL } from '@wordpress/url'; @@ -39,17 +35,8 @@ import { doAction, hasAction } from '@wordpress/hooks'; /** * Internal dependencies */ -import ImageSize from './image-size'; import styles from './styles.scss'; - -const MEDIA_UPLOAD_STATE_UPLOADING = 1; -const MEDIA_UPLOAD_STATE_SUCCEEDED = 2; -const MEDIA_UPLOAD_STATE_FAILED = 3; -const MEDIA_UPLOAD_STATE_RESET = 4; - -const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE = 'choose_from_device'; -const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO = 'take_photo'; -const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_media_library'; +import MediaUploadProgress from './media-upload-progress'; const LINK_DESTINATION_CUSTOM = 'custom'; const LINK_DESTINATION_NONE = 'none'; @@ -60,14 +47,12 @@ class ImageEdit extends React.Component { this.state = { showSettings: false, - progress: 0, - isUploadInProgress: false, - isUploadFailed: false, }; - this.mediaUpload = this.mediaUpload.bind( this ); this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind( this ); this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind( this ); + this.mediaUploadStateReset = this.mediaUploadStateReset.bind( this ); + this.onSelectMediaUploadOption = this.onSelectMediaUploadOption.bind( this ); this.updateMediaProgress = this.updateMediaProgress.bind( this ); this.updateAlt = this.updateAlt.bind( this ); this.updateImageURL = this.updateImageURL.bind( this ); @@ -77,11 +62,9 @@ class ImageEdit extends React.Component { } componentDidMount() { - this.addMediaUploadListener(); - const { attributes, setAttributes } = this.props; - if ( attributes.id && ! isURL( attributes.url ) ) { + if ( attributes.id && attributes.url && ! isURL( attributes.url ) ) { if ( attributes.url.indexOf( 'file:' ) === 0 ) { requestMediaImport( attributes.url, ( mediaId, mediaUri ) => { if ( mediaUri ) { @@ -98,7 +81,6 @@ class ImageEdit extends React.Component { if ( hasAction( 'blocks.onRemoveBlockCheckUpload' ) && this.state.isUploadInProgress ) { doAction( 'blocks.onRemoveBlockCheckUpload', this.props.attributes.id ); } - this.removeMediaUploadListener(); } onImagePressed() { @@ -111,35 +93,15 @@ class ImageEdit extends React.Component { } } - mediaUpload( payload ) { - const { attributes } = this.props; - - if ( payload.mediaId !== attributes.id ) { - return; - } - - switch ( payload.state ) { - case MEDIA_UPLOAD_STATE_UPLOADING: - this.updateMediaProgress( payload ); - break; - case MEDIA_UPLOAD_STATE_SUCCEEDED: - this.finishMediaUploadWithSuccess( payload ); - break; - case MEDIA_UPLOAD_STATE_FAILED: - this.finishMediaUploadWithFailure( payload ); - break; - case MEDIA_UPLOAD_STATE_RESET: - this.mediaUploadStateReset( payload ); - break; - } - } - updateMediaProgress( payload ) { const { setAttributes } = this.props; - this.setState( { progress: payload.progress, isUploadInProgress: true, isUploadFailed: false } ); if ( payload.mediaUrl ) { setAttributes( { url: payload.mediaUrl } ); } + + if ( ! this.state.isUploadInProgress ) { + this.setState( { isUploadInProgress: true } ); + } } finishMediaUploadWithSuccess( payload ) { @@ -153,30 +115,14 @@ class ImageEdit extends React.Component { const { setAttributes } = this.props; setAttributes( { id: payload.mediaId } ); - this.setState( { isUploadInProgress: false, isUploadFailed: true } ); + this.setState( { isUploadInProgress: false } ); } - mediaUploadStateReset( payload ) { + mediaUploadStateReset() { const { setAttributes } = this.props; - setAttributes( { id: payload.mediaId, url: null } ); - this.setState( { isUploadInProgress: false, isUploadFailed: false } ); - } - - addMediaUploadListener() { - //if we already have a subscription not worth doing it again - if ( this.subscriptionParentMediaUpload ) { - return; - } - this.subscriptionParentMediaUpload = subscribeMediaUpload( ( payload ) => { - this.mediaUpload( payload ); - } ); - } - - removeMediaUploadListener() { - if ( this.subscriptionParentMediaUpload ) { - this.subscriptionParentMediaUpload.remove(); - } + setAttributes( { id: null, url: null } ); + this.setState( { isUploadInProgress: false } ); } updateAlt( newAlt ) { @@ -202,41 +148,14 @@ class ImageEdit extends React.Component { } ); } - getMediaOptionsItems() { - return [ - { icon: 'format-image', value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, label: __( 'Choose from device' ) }, - { icon: 'camera', value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO, label: __( 'Take a Photo' ) }, - { icon: 'wordpress-alt', value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, label: __( 'WordPress Media Library' ) }, - ]; + onSelectMediaUploadOption( mediaId, mediaUrl ) { + const { setAttributes } = this.props; + setAttributes( { url: mediaUrl, id: mediaId } ); } render() { const { attributes, isSelected, setAttributes } = this.props; - const { url, caption, height, width, alt, href } = attributes; - - const onMediaLibraryButtonPressed = () => { - requestMediaPickFromMediaLibrary( ( mediaId, mediaUrl ) => { - if ( mediaUrl ) { - setAttributes( { id: mediaId, url: mediaUrl } ); - } - } ); - }; - - const onMediaUploadButtonPressed = () => { - requestMediaPickFromDeviceLibrary( ( mediaId, mediaUri ) => { - if ( mediaUri ) { - setAttributes( { url: mediaUri, id: mediaId } ); - } - } ); - }; - - const onMediaCaptureButtonPressed = () => { - requestMediaPickFromDeviceCamera( ( mediaId, mediaUri ) => { - if ( mediaUri ) { - setAttributes( { url: mediaUri, id: mediaId } ); - } - } ); - }; + const { url, caption, height, width, alt, href, id } = attributes; const onImageSettingsButtonPressed = () => { this.setState( { showSettings: true } ); @@ -246,20 +165,22 @@ class ImageEdit extends React.Component { this.setState( { showSettings: false } ); }; - let picker; - - const onMediaOptionsButtonPressed = () => { - picker.presentPicker(); - }; - const toolbarEditButton = ( - - - + { + return ( + + { getMediaOptions() } + + + ); + } } > + ); const getInspectorControls = () => ( @@ -294,40 +215,17 @@ class ImageEdit extends React.Component { ); - const mediaOptions = this.getMediaOptionsItems(); - - const getMediaOptions = () => ( - picker = instance } - options={ mediaOptions } - onChange={ ( value ) => { - if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { - onMediaUploadButtonPressed(); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO ) { - onMediaCaptureButtonPressed(); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { - onMediaLibraryButtonPressed(); - } - } } - /> - ); - if ( ! url ) { return ( - { getMediaOptions() } ); } - const showSpinner = this.state.isUploadInProgress; - const opacity = this.state.isUploadInProgress ? 0.3 : 1; - const progress = this.state.progress * 100; - return ( - { showSpinner && } + { getInspectorControls() } { toolbarEditButton } @@ -354,27 +252,19 @@ class ImageEdit extends React.Component { onClick={ onImageSettingsButtonPressed } /> - - { ( sizes ) => { - const { - imageWidthWithinContainer, - imageHeightWithinContainer, - } = sizes; - - let finalHeight = imageHeightWithinContainer; - if ( height > 0 && height < imageHeightWithinContainer ) { - finalHeight = height; - } - - let finalWidth = imageWidthWithinContainer; - if ( width > 0 && width < imageWidthWithinContainer ) { - finalWidth = width; - } - + { + const opacity = isUploadInProgress ? 0.3 : 1; return ( - { getInspectorControls() } - { getMediaOptions() } { ! imageWidthWithinContainer && } @@ -386,17 +276,17 @@ class ImageEdit extends React.Component { accessible={ true } accessibilityLabel={ alt } > - { this.state.isUploadFailed && + { isUploadFailed && - - { __( 'Failed to insert media.\nPlease tap for options.' ) } + + { retryMessage } } ); } } - + /> { ( ! RichText.isEmpty( caption ) > 0 || isSelected ) && ( { + this.mediaUpload( payload ); + } ); + } + + removeMediaUploadListener() { + if ( this.subscriptionParentMediaUpload ) { + this.subscriptionParentMediaUpload.remove(); + } + } + + render() { + const { coverUrl, width, height } = this.props; + const { isUploadInProgress, isUploadFailed } = this.state; + const showSpinner = this.state.isUploadInProgress; + const progress = this.state.progress * 100; + const retryIconName = 'image-rotate'; + const retryMessage = __( 'Failed to insert media.\nPlease tap for options.' ); + + return ( + + { showSpinner && } + { coverUrl && + + { ( sizes ) => { + const { + imageWidthWithinContainer, + imageHeightWithinContainer, + } = sizes; + + let finalHeight = imageHeightWithinContainer; + if ( height > 0 && height < imageHeightWithinContainer ) { + finalHeight = height; + } + + let finalWidth = imageWidthWithinContainer; + if ( width > 0 && width < imageWidthWithinContainer ) { + finalWidth = width; + } + return ( this.props.renderContent( { + isUploadInProgress, + isUploadFailed, + finalWidth, + finalHeight, + imageWidthWithinContainer, + retryIconName, + retryMessage, + } ) ); + } } + + } + { ! coverUrl && this.props.renderContent( { + isUploadInProgress, + isUploadFailed, + retryIconName, + retryMessage, + } ) } + + ); + } +} + +export default MediaUploadProgress; diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index c4c0449049a067..fbe7ad59bb3b2d 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -109,6 +109,7 @@ export const registerCoreBlocks = () => { missing, more, image, + video, nextpage, separator, list, diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js new file mode 100644 index 00000000000000..4f32732ed61bec --- /dev/null +++ b/packages/block-library/src/video/edit.native.js @@ -0,0 +1,232 @@ +/** + * External dependencies + */ +import React from 'react'; +import { View, TextInput, TouchableWithoutFeedback, Text } from 'react-native'; +/** + * Internal dependencies + */ +import Video from './video-player'; +import { + mediaUploadSync, + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, +} from 'react-native-gutenberg-bridge'; + +/** + * WordPress dependencies + */ +import { + Toolbar, + ToolbarButton, + Dashicon, +} from '@wordpress/components'; +import { + MediaPlaceholder, + MediaUpload, + MEDIA_TYPE_VIDEO, + RichText, + BlockControls, + InspectorControls, +} from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { isURL } from '@wordpress/url'; +import { doAction, hasAction } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import styles from '../image/styles.scss'; +import MediaUploadProgress from '../image/media-upload-progress'; +import style from './style.scss'; + +const VIDEO_ASPECT_RATIO = 1.7; + +class VideoEdit extends React.Component { + constructor( props ) { + super( props ); + + this.state = { + showSettings: false, + isMediaRequested: false, + videoContainerHeight: 0, + }; + + this.mediaUploadStateReset = this.mediaUploadStateReset.bind( this ); + this.onSelectMediaUploadOption = this.onSelectMediaUploadOption.bind( this ); + this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind( this ); + this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind( this ); + this.updateMediaProgress = this.updateMediaProgress.bind( this ); + this.onVideoPressed = this.onVideoPressed.bind( this ); + this.onVideoContanerLayout = this.onVideoContanerLayout.bind( this ); + } + + componentDidMount() { + const { attributes } = this.props; + if ( attributes.id && attributes.url && ! isURL( attributes.src ) ) { + mediaUploadSync(); + } + } + + componentWillUnmount() { + // this action will only exist if the user pressed the trash button on the block holder + if ( hasAction( 'blocks.onRemoveBlockCheckUpload' ) && this.state.isUploadInProgress ) { + doAction( 'blocks.onRemoveBlockCheckUpload', this.props.attributes.id ); + } + } + + onVideoPressed() { + const { attributes } = this.props; + + if ( this.state.isUploadInProgress ) { + requestImageUploadCancelDialog( attributes.id ); + } else if ( attributes.id && ! isURL( attributes.src ) ) { + requestImageFailedRetryDialog( attributes.id ); + } + } + + updateMediaProgress( payload ) { + const { setAttributes } = this.props; + if ( payload.mediaUrl ) { + setAttributes( { url: payload.mediaUrl } ); + } + + if ( ! this.state.isUploadInProgress ) { + this.setState( { isUploadInProgress: true } ); + } + } + + finishMediaUploadWithSuccess( payload ) { + const { setAttributes } = this.props; + setAttributes( { src: payload.mediaUrl, id: payload.mediaServerId } ); + this.setState( { isMediaRequested: false, isUploadInProgress: false } ); + } + + finishMediaUploadWithFailure( payload ) { + const { setAttributes } = this.props; + setAttributes( { id: payload.mediaId } ); + this.setState( { isMediaRequested: false, isUploadInProgress: false } ); + } + + mediaUploadStateReset() { + const { setAttributes } = this.props; + setAttributes( { id: null, src: null } ); + this.setState( { isMediaRequested: false, isUploadInProgress: false } ); + } + + onSelectMediaUploadOption( mediaId, mediaUrl ) { + const { setAttributes } = this.props; + setAttributes( { id: mediaId, src: mediaUrl } ); + this.setState( { isMediaRequested: true } ); + } + + onVideoContanerLayout( event ) { + const { width } = event.nativeEvent.layout; + const height = width / VIDEO_ASPECT_RATIO; + if ( height !== this.state.videoContainerHeight ) { + this.setState( { videoContainerHeight: height } ); + } + } + + render() { + const { attributes, isSelected, setAttributes } = this.props; + const { caption, id, src } = attributes; + const { isMediaRequested, videoContainerHeight } = this.state; + + const toolbarEditButton = ( + { + return ( + + { getMediaOptions() } + + + ); + } } > + + ); + + if ( ! isMediaRequested && ! src ) { + return ( + + + + ); + } + + return ( + + + + { toolbarEditButton } + + + ( null ) } + /> + + { + const opacity = ( isUploadInProgress || isUploadFailed ) ? 0.3 : 1; + const showVideo = src && ! isUploadInProgress && ! isUploadFailed; + const iconName = isUploadFailed ? retryIconName : 'format-video'; + + const videoStyle = { + height: videoContainerHeight, + ...style.video, + }; + + return ( + + { showVideo && + + ); + } } + /> + { ( ! RichText.isEmpty( caption ) > 0 || isSelected ) && ( + + setAttributes( { caption: newCaption } ) } + /> + + ) } + + + ); + } +} + +export default VideoEdit; diff --git a/packages/block-library/src/video/style.native.scss b/packages/block-library/src/video/style.native.scss new file mode 100644 index 00000000000000..4d0c557203f15f --- /dev/null +++ b/packages/block-library/src/video/style.native.scss @@ -0,0 +1,25 @@ +// @format + +.video { + background-color: #e9eff3; + width: 100%; +} + +.placeholder { + flex: 1; + justify-content: center; + align-items: center; +} + +.placeholderIcon { + justify-content: center; + align-items: center; + fill: $gray-dark; + background-color: #e9eff3; +} + +.uploadFailedText { + color: $gray-dark; + font-size: 14; + margin-top: 5; +} diff --git a/packages/block-library/src/video/video-player.android.js b/packages/block-library/src/video/video-player.android.js new file mode 100644 index 00000000000000..a9dcd4cbdf52ea --- /dev/null +++ b/packages/block-library/src/video/video-player.android.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; +import { default as VideoPlayer } from 'react-native-video'; + +/** + * Internal dependencies + */ +import styles from './video-player.scss'; + +const Video = ( props ) => { + return ( + + + + ); +}; + +export default Video; diff --git a/packages/block-library/src/video/video-player.ios.js b/packages/block-library/src/video/video-player.ios.js new file mode 100644 index 00000000000000..47ea7f7cfc0b7e --- /dev/null +++ b/packages/block-library/src/video/video-player.ios.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { Dashicon } from '@wordpress/components'; + +/** + * External dependencies + */ +import { View, TouchableOpacity } from 'react-native'; +import { default as VideoPlayer } from 'react-native-video'; + +/** + * Internal dependencies + */ +import styles from './video-player.scss'; + +class Video extends Component { + constructor() { + super( ...arguments ); + this.state = { + isLoaded: false, + }; + this.onPressPlay = this.onPressPlay.bind( this ); + this.onLoad = this.onLoad.bind( this ); + this.onLoadStart = this.onLoadStart.bind( this ); + } + + onLoad() { + this.setState( { isLoaded: true } ); + } + + onLoadStart() { + this.setState( { isLoaded: false } ); + } + + onPressPlay() { + if ( this.player ) { + this.player.presentFullscreenPlayer(); + } + } + + render() { + const { style } = this.props; + const { isLoaded } = this.state; + + return ( + + { + this.player = ref; + } } + // Using built-in player controls is messing up the layout on iOS. + // So we are setting controls=false and adding a play button that + // will trigger presentFullscreenPlayer() + controls={ false } + onLoad={ this.onLoad } + onLoadStart={ this.onLoadStart } + /> + { isLoaded && + + + + + + } + + ); + } +} + +export default Video; diff --git a/packages/block-library/src/video/video-player.native.scss b/packages/block-library/src/video/video-player.native.scss new file mode 100644 index 00000000000000..ec746de8288916 --- /dev/null +++ b/packages/block-library/src/video/video-player.native.scss @@ -0,0 +1,32 @@ +// @format + +$play-icon-size: 50; + +.videoContainer { + flex: 1; + justify-content: center; + align-items: center; +} + +.overlay { + justify-content: center; + align-items: center; + align-self: center; + position: absolute; + background-color: #e9eff3; + opacity: 0.3; +} + +.playIcon { + justify-content: center; + align-items: center; + align-self: center; + position: absolute; + background-color: #000; + height: $play-icon-size; + width: $play-icon-size; + border-bottom-left-radius: $play-icon-size/8; + border-bottom-right-radius: $play-icon-size/8; + border-top-right-radius: $play-icon-size/8; + border-top-left-radius: $play-icon-size/8; +} diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 6ada9eb995eadd..a1afa49808b038 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -23,6 +23,7 @@ export function initializeEditor() { if ( typeof __DEV__ === 'undefined' || ! __DEV__ ) { unregisterBlockType( 'core/code' ); unregisterBlockType( 'core/more' ); + unregisterBlockType( 'core/video' ); } } From d77c70a3769876f7e76396c2c45cc70d099711b8 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak Date: Sun, 12 May 2019 04:06:33 -0700 Subject: [PATCH 11/34] Update i18n docs to use make-json command from wp-cli (#15303) * Update i18m docs to use make-json command from wp-cli * Apply suggestions from code review Co-Authored-By: Pascal Birchler --- .../developers/internationalization.md | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/designers-developers/developers/internationalization.md b/docs/designers-developers/developers/internationalization.md index 2ae1b8f8777f38..084459697cff3b 100644 --- a/docs/designers-developers/developers/internationalization.md +++ b/docs/designers-developers/developers/internationalization.md @@ -176,31 +176,32 @@ msgid "Hello World" msgstr "Saltuon mundo" ``` -The last step to create the translation file is to convert the `myguten-eo.po` to the JSON format needed. For this, you can use the [po2json utility](https://github.com/mikeedwards/po2json) which you install using npm. It might be easiest to install globally using: `npm install -g po2json`. Once installed, use the following command to convert to JED format: +The last step to create the translation file is to convert the `myguten-eo.po` to the JSON format needed. For this, you can use WP-CLI's [`wp i18n make-json` command](https://developer.wordpress.org/cli/commands/i18n/make-json/), which requires WP-CLI v2.2.0 and later. ``` -po2json myguten-eo.po myguten-eo.json -f jed +wp i18n make-json myguten-eo.po --no-purge ``` -This will generate the JSON file `myguten-eo.json` which looks like: +This will generate the JSON file `myguten-eo-[md5].json` with the contents: ```json { + "translation-revision-date": "2019-04-26T13:30:11-07:00", + "generator": "WP-CLI/2.2.0", + "source": "block.js", "domain": "messages", "locale_data": { "messages": { "": { "domain": "messages", - "lang": "eo" + "lang": "eo", + "plural-forms": "nplurals=2; plural=(n != 1);" }, - "Scratch Plugin": [ - "Scratch kromprogrameto" - ], "Simple Block": [ - "Simpla bloko" + "Simpla Bloko" ], "Hello World": [ - "Saltuon mundo" + "Salunton mondo" ] } } @@ -222,7 +223,7 @@ The final part is to tell WordPress where it can look to find the translation fi WordPress will check for a file in that path with the format `${domain}-${locale}-${handle}.json` as the source of translations. Alternatively, instead of the registered handle you can use the md5 hash of the relative path of the file, `${domain}-${locale} in the form of ${domain}-${locale}-${md5}.json.` -This example uses the handle, rename the `myguten-eo.json` file to `myguten-eo-myguten-script.json`. +Using `make-json` automatically names the file with the md5 hash, so it is ready as-is. You could rename the file to use the handle instead, in which case the file name would be `myguten-eo-myguten-script.json`. ### Test Translations From bd1c61642c0ce0d0c69d80088876e53df30a2e98 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Sun, 12 May 2019 14:10:07 +0300 Subject: [PATCH 12/34] Accessibility: Fixed focus state of pressed AM/PM buttons. (#15582) --- packages/components/src/date-time/style.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/components/src/date-time/style.scss b/packages/components/src/date-time/style.scss index e9c84135c0a320..3b0609258f88ca 100644 --- a/packages/components/src/date-time/style.scss +++ b/packages/components/src/date-time/style.scss @@ -110,6 +110,12 @@ background: $light-gray-300; border-color: $dark-gray-100; box-shadow: inset 0 2px 5px -3px $dark-gray-500; + &:focus { + box-shadow: + inset 0 2px 5px -3px $dark-gray-500, + 0 0 0 1px $white, + 0 0 0 3px $blue-medium-focus; + } } .components-datetime__time-field { From 5494bc57fc77de6cada4295823a3fb047e168f89 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Sun, 12 May 2019 14:47:55 -0400 Subject: [PATCH 13/34] Block Library: Add width attribute for resizable Column blocks (#15499) * Block Library: Render Column block with ButtonBlockAppender * Block Library: Add width attribute for resizable Column blocks * Block Library: Update Column width input label to clarify percentage * Block Library: Refactor Columns width redistribution to reusable utilities * Plugin: Filter safe CSS to allow column flex-basis CSS attribute * Block Library: Return undefined for non-finite column width precision * Components: Fix display of RangeControl reset button * Block Library: Allow column block width resetting * Block Library: Ensure Column width RangeControl treated as controlled input * Testing: Update E2E tests for button inserter columns block * Testing: Try to appease the E2E gods * Block Library: Restore flex-basis for mid-range viewports --- lib/compat.php | 26 ++ lib/load.php | 1 + packages/block-library/src/column/block.json | 5 + packages/block-library/src/column/edit.js | 115 +++++++-- packages/block-library/src/column/index.js | 10 + packages/block-library/src/column/save.js | 10 +- packages/block-library/src/columns/edit.js | 145 +++++++---- .../block-library/src/columns/editor.scss | 11 - packages/block-library/src/columns/style.scss | 9 +- .../block-library/src/columns/test/utils.js | 229 ++++++++++++++++++ packages/block-library/src/columns/utils.js | 128 +++++++++- packages/components/CHANGELOG.md | 8 +- .../components/src/range-control/index.js | 12 +- .../components/src/range-control/style.scss | 4 + .../specs/block-hierarchy-navigation.test.js | 22 +- packages/e2e-tests/specs/writing-flow.test.js | 23 +- 16 files changed, 658 insertions(+), 100 deletions(-) create mode 100644 lib/compat.php create mode 100644 packages/block-library/src/columns/test/utils.js diff --git a/lib/compat.php b/lib/compat.php new file mode 100644 index 00000000000000..fb09a6e49ef97f --- /dev/null +++ b/lib/compat.php @@ -0,0 +1,26 @@ + { - const { verticalAlignment } = attributes; +/** + * Internal dependencies + */ +import { + toWidthPrecision, + getTotalColumnsWidth, + getColumnWidths, + getAdjacentBlocks, + getRedistributedColumnWidths, +} from '../columns/utils'; + +function ColumnEdit( { + attributes, + updateAlignment, + updateWidth, + hasChildBlocks, +} ) { + const { verticalAlignment, width } = attributes; const classes = classnames( 'block-core-columns', { [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, } ); - const onChange = ( alignment ) => updateAlignment( alignment ); - return (
- + + + + + + + ) } + />
); -}; +} export default compose( - withSelect( ( select, { clientId } ) => { - const { getBlockRootClientId } = select( 'core/editor' ); + withSelect( ( select, ownProps ) => { + const { clientId } = ownProps; + const { getBlockOrder } = select( 'core/block-editor' ); return { - parentColumnsBlockClientId: getBlockRootClientId( clientId ), + hasChildBlocks: getBlockOrder( clientId ).length > 0, }; } ), - withDispatch( ( dispatch, { clientId, parentColumnsBlockClientId } ) => { + withDispatch( ( dispatch, ownProps, registry ) => { return { - updateAlignment( alignment ) { - // Update self... - dispatch( 'core/editor' ).updateBlockAttributes( clientId, { - verticalAlignment: alignment, - } ); + updateAlignment( verticalAlignment ) { + const { clientId, setAttributes } = ownProps; + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + const { getBlockRootClientId } = registry.select( 'core/block-editor' ); + + // Update own alignment. + setAttributes( { verticalAlignment } ); // Reset Parent Columns Block - dispatch( 'core/editor' ).updateBlockAttributes( parentColumnsBlockClientId, { - verticalAlignment: null, + const rootClientId = getBlockRootClientId( clientId ); + updateBlockAttributes( rootClientId, { verticalAlignment: null } ); + }, + updateWidth( width ) { + const { clientId } = ownProps; + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + const { getBlockRootClientId, getBlocks } = registry.select( 'core/block-editor' ); + + // Constrain or expand siblings to account for gain or loss of + // total columns area. + const columns = getBlocks( getBlockRootClientId( clientId ) ); + const adjacentColumns = getAdjacentBlocks( columns, clientId ); + + // The occupied width is calculated as the sum of the new width + // and the total width of blocks _not_ in the adjacent set. + const occupiedWidth = width + getTotalColumnsWidth( + difference( columns, [ + find( columns, { clientId } ), + ...adjacentColumns, + ] ) + ); + + // Compute _all_ next column widths, in case the updated column + // is in the middle of a set of columns which don't yet have + // any explicit widths assigned (include updates to those not + // part of the adjacent blocks). + const nextColumnWidths = { + ...getColumnWidths( columns, columns.length ), + [ clientId ]: toWidthPrecision( width ), + ...getRedistributedColumnWidths( adjacentColumns, 100 - occupiedWidth, columns.length ), + }; + + forEach( nextColumnWidths, ( nextColumnWidth, columnClientId ) => { + updateBlockAttributes( columnClientId, { width: nextColumnWidth } ); } ); }, }; diff --git a/packages/block-library/src/column/index.js b/packages/block-library/src/column/index.js index 869093459a83bb..250ce0bad65a42 100644 --- a/packages/block-library/src/column/index.js +++ b/packages/block-library/src/column/index.js @@ -25,6 +25,16 @@ export const settings = { reusable: false, html: false, }, + getEditWrapperProps( attributes ) { + const { width } = attributes; + if ( Number.isFinite( width ) ) { + return { + style: { + flexBasis: width + '%', + }, + }; + } + }, edit, save, }; diff --git a/packages/block-library/src/column/save.js b/packages/block-library/src/column/save.js index 9e8ea1ac4a3b32..d0dda9de3174b3 100644 --- a/packages/block-library/src/column/save.js +++ b/packages/block-library/src/column/save.js @@ -9,13 +9,19 @@ import classnames from 'classnames'; import { InnerBlocks } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { verticalAlignment } = attributes; + const { verticalAlignment, width } = attributes; + const wrapperClasses = classnames( { [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, } ); + let style; + if ( Number.isFinite( width ) ) { + style = { flexBasis: width + '%' }; + } + return ( -
+
); diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index 15adfff384dfff..71625625233815 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -2,12 +2,12 @@ * External dependencies */ import classnames from 'classnames'; +import { dropRight } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { compose } from '@wordpress/compose'; import { PanelBody, RangeControl, @@ -18,12 +18,19 @@ import { BlockControls, BlockVerticalAlignmentToolbar, } from '@wordpress/block-editor'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { withDispatch } from '@wordpress/data'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies */ -import { getColumnsTemplate } from './utils'; +import { + getColumnsTemplate, + hasExplicitColumnWidths, + getMappedColumnWidths, + getRedistributedColumnWidths, + toWidthPrecision, +} from './utils'; /** * Allowed blocks constant is passed to InnerBlocks precisely as specified here. @@ -36,18 +43,18 @@ import { getColumnsTemplate } from './utils'; */ const ALLOWED_BLOCKS = [ 'core/column' ]; -export const ColumnsEdit = function( { attributes, setAttributes, className, updateAlignment } ) { +export function ColumnsEdit( { + attributes, + className, + updateAlignment, + updateColumns, +} ) { const { columns, verticalAlignment } = attributes; const classes = classnames( className, `has-${ columns }-columns`, { [ `are-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, } ); - const onChange = ( alignment ) => { - // Update all the (immediate) child Column Blocks - updateAlignment( alignment ); - }; - return ( <> @@ -55,11 +62,7 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd { - setAttributes( { - columns: nextColumns, - } ); - } } + onChange={ updateColumns } min={ 2 } max={ 6 } /> @@ -67,7 +70,7 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd @@ -79,45 +82,81 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd
); -}; +} + +export default withDispatch( ( dispatch, ownProps, registry ) => ( { + /** + * Update all child Column blocks with a new vertical alignment setting + * based on whatever alignment is passed in. This allows change to parent + * to overide anything set on a individual column basis. + * + * @param {string} verticalAlignment the vertical alignment setting + */ + updateAlignment( verticalAlignment ) { + const { clientId, setAttributes } = ownProps; + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + const { getBlockOrder } = registry.select( 'core/block-editor' ); + + // Update own alignment. + setAttributes( { verticalAlignment } ); -const DEFAULT_EMPTY_ARRAY = []; + // Update all child Column Blocks to match + const innerBlockClientIds = getBlockOrder( clientId ); + innerBlockClientIds.forEach( ( innerBlockClientId ) => { + updateBlockAttributes( innerBlockClientId, { + verticalAlignment, + } ); + } ); + }, -export default compose( /** - * Selects the child column Blocks for this parent Column + * Updates the column count, including necessary revisions to child Column + * blocks to grant required or redistribute available space. + * + * @param {number} columns New column count. */ - withSelect( ( select, { clientId } ) => { - const { getBlocksByClientId } = select( 'core/editor' ); - const block = getBlocksByClientId( clientId )[ 0 ]; - - return { - childColumns: block ? block.innerBlocks : DEFAULT_EMPTY_ARRAY, - }; - } ), - withDispatch( ( dispatch, { clientId, childColumns } ) => { - return { - /** - * Update all child column Blocks with a new - * vertical alignment setting based on whatever - * alignment is passed in. This allows change to parent - * to overide anything set on a individual column basis - * - * @param {string} alignment the vertical alignment setting - */ - updateAlignment( alignment ) { - // Update self... - dispatch( 'core/editor' ).updateBlockAttributes( clientId, { - verticalAlignment: alignment, - } ); - - // Update all child Column Blocks to match - childColumns.forEach( ( childColumn ) => { - dispatch( 'core/editor' ).updateBlockAttributes( childColumn.clientId, { - verticalAlignment: alignment, - } ); - } ); - }, - }; - } ), -)( ColumnsEdit ); + updateColumns( columns ) { + const { clientId, setAttributes, attributes } = ownProps; + const { replaceInnerBlocks } = dispatch( 'core/block-editor' ); + const { getBlocks } = registry.select( 'core/block-editor' ); + + // Update columns count. + setAttributes( { columns } ); + + let innerBlocks = getBlocks( clientId ); + if ( ! hasExplicitColumnWidths( innerBlocks ) ) { + return; + } + + // Redistribute available width for existing inner blocks. + const { columns: previousColumns } = attributes; + const isAddingColumn = columns > previousColumns; + + if ( isAddingColumn ) { + // If adding a new column, assign width to the new column equal to + // as if it were `1 / columns` of the total available space. + const newColumnWidth = toWidthPrecision( 100 / columns ); + + // Redistribute in consideration of pending block insertion as + // constraining the available working width. + const widths = getRedistributedColumnWidths( innerBlocks, 100 - newColumnWidth ); + + innerBlocks = [ + ...getMappedColumnWidths( innerBlocks, widths ), + createBlock( 'core/column', { + width: newColumnWidth, + } ), + ]; + } else { + // The removed column will be the last of the inner blocks. + innerBlocks = dropRight( innerBlocks ); + + // Redistribute as if block is already removed. + const widths = getRedistributedColumnWidths( innerBlocks, 100 ); + + innerBlocks = getMappedColumnWidths( innerBlocks, widths ); + } + + replaceInnerBlocks( clientId, innerBlocks, false ); + }, +} ) )( ColumnsEdit ); diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index 7830f7780880a9..c771c79f5f9d3c 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -178,14 +178,3 @@ div.block-core-columns.is-vertically-aligned-bottom { display: none; } } - -// In absence of making the individual columns resizable, we prevent them from being clickable. -// This makes them less fiddly. @todo: This should be revisited as the interface is refined. -.wp-block-columns [data-type="core/column"] { - pointer-events: none; - - // This selector re-enables clicking on any child of a column block. - .block-core-columns .block-editor-block-list__layout { - pointer-events: all; - } -} diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss index ab2ec375ca1e8e..7a2c0877889332 100644 --- a/packages/block-library/src/columns/style.scss +++ b/packages/block-library/src/columns/style.scss @@ -14,8 +14,11 @@ margin-bottom: 1em; flex-grow: 1; - // Responsiveness: Show at most one columns on mobile. - flex-basis: 100%; + @media (max-width: #{ ($break-small - 1) }) { + // Responsiveness: Show at most one columns on mobile. This must be + // important since the Column assigns its own width as an inline style. + flex-basis: 100% !important; + } // Prevent the columns from growing wider than their distributed sizes. min-width: 0; @@ -30,7 +33,7 @@ flex-basis: calc(50% - #{$grid-size-large}); flex-grow: 0; - // Add space between the 2 columns. Themes can customize this if they wish to work differently. + // Add space between the multiple columns. Themes can customize this if they wish to work differently. // Only apply this beyond the mobile breakpoint, as there's only a single column on mobile. &:nth-child(even) { margin-left: $grid-size-large * 2; diff --git a/packages/block-library/src/columns/test/utils.js b/packages/block-library/src/columns/test/utils.js new file mode 100644 index 00000000000000..cb69e1740e1f32 --- /dev/null +++ b/packages/block-library/src/columns/test/utils.js @@ -0,0 +1,229 @@ +/** + * Internal dependencies + */ +import { + getColumnsTemplate, + toWidthPrecision, + getAdjacentBlocks, + getEffectiveColumnWidth, + getTotalColumnsWidth, + getColumnWidths, + getRedistributedColumnWidths, + hasExplicitColumnWidths, + getMappedColumnWidths, +} from '../utils'; + +describe( 'getColumnsTemplate', () => { + it( 'should return a template corresponding to columns count', () => { + const template = getColumnsTemplate( 4 ); + + expect( template ).toEqual( [ + [ 'core/column' ], + [ 'core/column' ], + [ 'core/column' ], + [ 'core/column' ], + ] ); + } ); +} ); + +describe( 'toWidthPrecision', () => { + it( 'should round value to standard precision', () => { + const value = toWidthPrecision( 50.108 ); + + expect( value ).toBe( 50.11 ); + } ); + + it( 'should return undefined for invalid number', () => { + expect( toWidthPrecision( null ) ).toBe( undefined ); + expect( toWidthPrecision( undefined ) ).toBe( undefined ); + } ); +} ); + +describe( 'getAdjacentBlocks', () => { + const blockA = { clientId: 'a' }; + const blockB = { clientId: 'b' }; + const blockC = { clientId: 'c' }; + const blocks = [ blockA, blockB, blockC ]; + + it( 'should return blocks after clientId', () => { + const result = getAdjacentBlocks( blocks, 'b' ); + + expect( result ).toEqual( [ blockC ] ); + } ); + + it( 'should return blocks before clientId if clientId is last', () => { + const result = getAdjacentBlocks( blocks, 'c' ); + + expect( result ).toEqual( [ blockA, blockB ] ); + } ); +} ); + +describe( 'getEffectiveColumnWidth', () => { + it( 'should return attribute value if set, rounded to precision', () => { + const block = { attributes: { width: 50.108 } }; + + const width = getEffectiveColumnWidth( block, 3 ); + + expect( width ).toBe( 50.11 ); + } ); + + it( 'should return assumed width if attribute value not set, rounded to precision', () => { + const block = { attributes: {} }; + + const width = getEffectiveColumnWidth( block, 3 ); + + expect( width ).toBe( 33.33 ); + } ); +} ); + +describe( 'getTotalColumnsWidth', () => { + describe( 'explicit width', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30 } }, + { clientId: 'b', attributes: { width: 40 } }, + ]; + + it( 'returns the sum total of columns width', () => { + const width = getTotalColumnsWidth( blocks ); + + expect( width ).toBe( 70 ); + } ); + } ); + + describe( 'implicit width', () => { + const blocks = [ + { clientId: 'a', attributes: {} }, + { clientId: 'b', attributes: {} }, + ]; + + it( 'returns the sum total of columns width', () => { + const widths = getTotalColumnsWidth( blocks ); + + expect( widths ).toBe( 100 ); + } ); + } ); +} ); + +describe( 'getColumnWidths', () => { + describe( 'explicit width', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30.459 } }, + { clientId: 'b', attributes: { width: 29.543 } }, + ]; + + it( 'returns the column widths', () => { + const widths = getColumnWidths( blocks ); + + expect( widths ).toEqual( { + a: 30.46, + b: 29.54, + } ); + } ); + } ); + + describe( 'implicit width', () => { + const blocks = [ + { clientId: 'a', attributes: {} }, + { clientId: 'b', attributes: {} }, + ]; + + it( 'returns the column widths', () => { + const widths = getColumnWidths( blocks ); + + expect( widths ).toEqual( { + a: 50, + b: 50, + } ); + } ); + } ); +} ); + +describe( 'getRedistributedColumnWidths', () => { + describe( 'explicit width', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30 } }, + { clientId: 'b', attributes: { width: 40 } }, + ]; + + it( 'should constrain to fit available width', () => { + const widths = getRedistributedColumnWidths( blocks, 60 ); + + expect( widths ).toEqual( { + a: 25, + b: 35, + } ); + } ); + + it( 'should expand to fit available width', () => { + const widths = getRedistributedColumnWidths( blocks, 80 ); + + expect( widths ).toEqual( { + a: 35, + b: 45, + } ); + } ); + } ); + + describe( 'implicit width', () => { + const blocks = [ + { clientId: 'a', attributes: {} }, + { clientId: 'b', attributes: {} }, + ]; + + it( 'should equally distribute to available width', () => { + const widths = getRedistributedColumnWidths( blocks, 60 ); + + expect( widths ).toEqual( { + a: 30, + b: 30, + } ); + } ); + + it( 'should constrain to fit available width', () => { + const widths = getRedistributedColumnWidths( blocks, 66.66, 3 ); + + expect( widths ).toEqual( { + a: 33.33, + b: 33.33, + } ); + } ); + } ); +} ); + +describe( 'hasExplicitColumnWidths', () => { + it( 'returns false if no blocks have explicit width', () => { + const blocks = [ { attributes: {} } ]; + + const result = hasExplicitColumnWidths( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if a block has explicit width', () => { + const blocks = [ { attributes: { width: 10 } } ]; + + const result = hasExplicitColumnWidths( blocks ); + + expect( result ).toBe( true ); + } ); +} ); + +describe( 'getMappedColumnWidths', () => { + it( 'merges to block attributes using provided widths', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30 } }, + { clientId: 'b', attributes: { width: 40 } }, + ]; + const widths = { + a: 25, + b: 35, + }; + + const result = getMappedColumnWidths( blocks, widths ); + + expect( result ).toEqual( [ + { clientId: 'a', attributes: { width: 25 } }, + { clientId: 'b', attributes: { width: 35 } }, + ] ); + } ); +} ); diff --git a/packages/block-library/src/columns/utils.js b/packages/block-library/src/columns/utils.js index e7e3f90df70fd2..0c4e4c59e9ccd3 100644 --- a/packages/block-library/src/columns/utils.js +++ b/packages/block-library/src/columns/utils.js @@ -2,7 +2,7 @@ * External dependencies */ import memoize from 'memize'; -import { times } from 'lodash'; +import { times, findIndex, sumBy, merge, mapValues } from 'lodash'; /** * Returns the layouts configuration for a given number of columns. @@ -14,3 +14,129 @@ import { times } from 'lodash'; export const getColumnsTemplate = memoize( ( columns ) => { return times( columns, () => [ 'core/column' ] ); } ); + +/** + * Returns a column width attribute value rounded to standard precision. + * Returns `undefined` if the value is not a valid finite number. + * + * @param {?number} value Raw value. + * + * @return {number} Value rounded to standard precision. + */ +export const toWidthPrecision = ( value ) => + Number.isFinite( value ) ? + parseFloat( value.toFixed( 2 ) ) : + undefined; + +/** + * Returns the considered adjacent to that of the specified `clientId` for + * resizing consideration. Adjacent blocks are those occurring after, except + * when the given block is the last block in the set. For the last block, the + * behavior is reversed. + * + * @param {WPBlock[]} blocks Block objects. + * @param {string} clientId Client ID to consider for adjacent blocks. + * + * @return {WPBlock[]} Adjacent block objects. + */ +export function getAdjacentBlocks( blocks, clientId ) { + const index = findIndex( blocks, { clientId } ); + const isLastBlock = index === blocks.length - 1; + + return isLastBlock ? blocks.slice( 0, index ) : blocks.slice( index + 1 ); +} + +/** + * Returns an effective width for a given block. An effective width is equal to + * its attribute value if set, or a computed value assuming equal distribution. + * + * @param {WPBlock} block Block object. + * @param {number} totalBlockCount Total number of blocks in Columns. + * + * @return {number} Effective column width. + */ +export function getEffectiveColumnWidth( block, totalBlockCount ) { + const { width = 100 / totalBlockCount } = block.attributes; + return toWidthPrecision( width ); +} + +/** + * Returns the total width occupied by the given set of column blocks. + * + * @param {WPBlock[]} blocks Block objects. + * @param {?number} totalBlockCount Total number of blocks in Columns. + * Defaults to number of blocks passed. + * + * @return {number} Total width occupied by blocks. + */ +export function getTotalColumnsWidth( blocks, totalBlockCount = blocks.length ) { + return sumBy( blocks, ( block ) => getEffectiveColumnWidth( block, totalBlockCount ) ); +} + +/** + * Returns an object of `clientId` → `width` of effective column widths. + * + * @param {WPBlock[]} blocks Block objects. + * @param {?number} totalBlockCount Total number of blocks in Columns. + * Defaults to number of blocks passed. + * + * @return {Object} Column widths. + */ +export function getColumnWidths( blocks, totalBlockCount = blocks.length ) { + return blocks.reduce( ( result, block ) => { + const width = getEffectiveColumnWidth( block, totalBlockCount ); + return Object.assign( result, { [ block.clientId ]: width } ); + }, {} ); +} + +/** + * Returns an object of `clientId` → `width` of column widths as redistributed + * proportional to their current widths, constrained or expanded to fit within + * the given available width. + * + * @param {WPBlock[]} blocks Block objects. + * @param {number} availableWidth Maximum width to fit within. + * @param {?number} totalBlockCount Total number of blocks in Columns. + * Defaults to number of blocks passed. + * + * @return {Object} Redistributed column widths. + */ +export function getRedistributedColumnWidths( blocks, availableWidth, totalBlockCount = blocks.length ) { + const totalWidth = getTotalColumnsWidth( blocks, totalBlockCount ); + const difference = availableWidth - totalWidth; + const adjustment = difference / blocks.length; + + return mapValues( + getColumnWidths( blocks, totalBlockCount ), + ( width ) => toWidthPrecision( width + adjustment ), + ); +} + +/** + * Returns true if column blocks within the provided set are assigned with + * explicit widths, or false otherwise. + * + * @param {WPBlock[]} blocks Block objects. + * + * @return {boolean} Whether columns have explicit widths. + */ +export function hasExplicitColumnWidths( blocks ) { + return blocks.some( ( block ) => Number.isFinite( block.attributes.width ) ); +} + +/** + * Returns a copy of the given set of blocks with new widths assigned from the + * provided object of redistributed column widths. + * + * @param {WPBlock[]} blocks Block objects. + * @param {Object} widths Redistributed column widths. + * + * @return {WPBlock[]} blocks Mapped block objects. + */ +export function getMappedColumnWidths( blocks, widths ) { + return blocks.map( ( block ) => merge( {}, block, { + attributes: { + width: widths[ block.clientId ], + }, + } ) ); +} diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 97028d2638a19d..abde63ef28198d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,7 +1,13 @@ -## 7.4.0 (Unreleased) +## Master + +### New Features - Added a new `HorizontalRule` component. +### Bug Fixes + +- Fixed display of reset button when using RangeControl `allowReset` prop. + ## 7.3.0 (2019-04-16) ### New Features diff --git a/packages/components/src/range-control/index.js b/packages/components/src/range-control/index.js index 2fd28d31bcae13..3b33f2b499c34b 100644 --- a/packages/components/src/range-control/index.js +++ b/packages/components/src/range-control/index.js @@ -98,11 +98,17 @@ function RangeControl( { onBlur={ resetCurrentInput } { ...props } /> - { allowReset && - - } + ) } ); } diff --git a/packages/components/src/range-control/style.scss b/packages/components/src/range-control/style.scss index e3dfca023ca837..b67e503592650e 100644 --- a/packages/components/src/range-control/style.scss +++ b/packages/components/src/range-control/style.scss @@ -22,6 +22,10 @@ } } +.components-range-control__reset { + margin-left: $grid-size; +} + // creating mixin because we can't do multiline variables, and we can't comma-group the selectors for styling the range slider @mixin range-thumb() { height: 18px; diff --git a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js index 9153526a262a8f..77d424d7e6381c 100644 --- a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js +++ b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js @@ -22,6 +22,11 @@ describe( 'Navigating the block hierarchy', () => { await insertBlock( 'Columns' ); // Add a paragraph in the first column. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First column' ); // Navigate to the columns blocks. @@ -44,7 +49,11 @@ describe( 'Navigating the block hierarchy', () => { await lastColumnsBlockMenuItem.click(); // Insert text in the last column block. - await pressKeyTimes( 'Tab', 5 ); // Navigate to the appender. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'Third column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -54,6 +63,11 @@ describe( 'Navigating the block hierarchy', () => { await insertBlock( 'Columns' ); // Add a paragraph in the first column. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First column' ); // Navigate to the columns blocks using the keyboard. @@ -76,7 +90,11 @@ describe( 'Navigating the block hierarchy', () => { await page.keyboard.press( 'Enter' ); // Insert text in the last column block - await pressKeyTimes( 'Tab', 5 ); // Navigate to the appender. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'Third column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index c9ca7bf2021df7..fe801a3617284d 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -16,6 +16,10 @@ describe( 'adding blocks', () => { } ); it( 'Should navigate inner blocks with arrow keys', async () => { + // TODO: The `waitForSelector` calls in this function should ultimately + // not be necessary for interactions, and exist as a stop-gap solution + // where rendering delays in slower CPU can cause intermittent failure. + let activeElementText; // Add demo content @@ -24,13 +28,22 @@ describe( 'adding blocks', () => { await page.keyboard.press( 'Enter' ); await page.keyboard.type( '/columns' ); await page.keyboard.press( 'Enter' ); + await page.click( ':focus .block-editor-button-block-appender' ); + await page.waitForSelector( ':focus.block-editor-inserter__search' ); + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First col' ); - // Arrow down should navigate through layouts in columns block (to - // its default appender). Two key presses are required since the first - // will land user on the Column wrapper block. - await page.keyboard.press( 'ArrowDown' ); - await page.keyboard.press( 'ArrowDown' ); + // TODO: ArrowDown should traverse into the second column. In slower + // CPUs, it can sometimes remain in the first column paragraph. This + // is a temporary solution. + await page.focus( '.wp-block[data-type="core/column"]:nth-child(2)' ); + await page.click( ':focus .block-editor-button-block-appender' ); + await page.waitForSelector( ':focus.block-editor-inserter__search' ); + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'Second col' ); // Arrow down from last of layouts exits nested context to default From 1d82701925bdc47b4f41e2691def7a3e7152b452 Mon Sep 17 00:00:00 2001 From: Brent Swisher Date: Mon, 13 May 2019 05:21:42 -0400 Subject: [PATCH 14/34] Add text warning about usability when autoplay is selected in audio or video blocks (#15575) --- packages/block-library/src/audio/edit.js | 5 +++++ packages/block-library/src/video/edit.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index 7a704382e36c0b..1545378ddc0c74 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -98,6 +98,10 @@ class AudioEdit extends Component { this.setState( { editing: false } ); } + getAutoplayHelp( checked ) { + return checked ? __( 'Note: Autoplaying audio may cause usability issues for some visitors.' ) : null; + } + render() { const { autoplay, caption, loop, preload, src } = this.props.attributes; const { setAttributes, isSelected, className, noticeOperations, noticeUI } = this.props; @@ -153,6 +157,7 @@ class AudioEdit extends Component { label={ __( 'Autoplay' ) } onChange={ this.toggleAttribute( 'autoplay' ) } checked={ autoplay } + help={ this.getAutoplayHelp } /> Date: Mon, 13 May 2019 11:22:21 +0200 Subject: [PATCH 15/34] Fix id attribute to match aria-owns attribute (#15564) * Fix id attribute to match aria-owns attribute * Use variables for ids used for ARIA attributes --- .../block-editor/src/components/url-input/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/url-input/index.js b/packages/block-editor/src/components/url-input/index.js index 900f21fb3eebd0..88562b719802b0 100644 --- a/packages/block-editor/src/components/url-input/index.js +++ b/packages/block-editor/src/components/url-input/index.js @@ -226,6 +226,10 @@ class URLInput extends Component { render() { const { value = '', autoFocus = true, instanceId, className } = this.props; const { showSuggestions, suggestions, selectedSuggestion, loading } = this.state; + + const suggestionsListboxId = `block-editor-url-input-suggestions-${ instanceId }`; + const suggestionOptionIdPrefix = `block-editor-url-input-suggestion-${ instanceId }`; + /* eslint-disable jsx-a11y/no-autofocus */ return (
@@ -242,8 +246,8 @@ class URLInput extends Component { role="combobox" aria-expanded={ showSuggestions } aria-autocomplete="list" - aria-owns={ `block-editor-url-input-suggestions-${ instanceId }` } - aria-activedescendant={ selectedSuggestion !== null ? `block-editor-url-input-suggestion-${ instanceId }-${ selectedSuggestion }` : undefined } + aria-owns={ suggestionsListboxId } + aria-activedescendant={ selectedSuggestion !== null ? `${ suggestionOptionIdPrefix }-${ selectedSuggestion }` : undefined } ref={ this.inputRef } /> @@ -253,7 +257,7 @@ class URLInput extends Component {
@@ -262,7 +266,7 @@ class URLInput extends Component { key={ suggestion.id } role="option" tabIndex="-1" - id={ `block-editor-url-input-suggestion-${ instanceId }-${ index }` } + id={ `${ suggestionOptionIdPrefix }-${ index }` } ref={ this.bindSuggestionNode( index ) } className={ classnames( 'editor-url-input__suggestion block-editor-url-input__suggestion', { 'is-selected': index === selectedSuggestion, From c7a8d04186d96618ce50d1ecf05425f48cd18ad2 Mon Sep 17 00:00:00 2001 From: Brent Swisher Date: Mon, 13 May 2019 05:27:51 -0400 Subject: [PATCH 16/34] Remove aria-label from block inserter list item (#15382) * Remove aria-label from block inserter list item Because the text in the button is always visible, there is no need ot add the aria-label. Having duplicated aria tags increases the chance of a conflict with the visible text from future changes. Brought up in item GUT-18 of the wpcampus accessibliity audit. * Update e2e tests that relied on aria-label on block inserter button that no longer exists * Add waitForSelector to ensure the block navigator is open before attempting to space into the first paragraph --- .../src/components/inserter-list-item/index.js | 1 - .../src/get-available-block-transforms.js | 2 +- packages/e2e-test-utils/src/insert-block.js | 5 ++++- packages/e2e-test-utils/src/transform-block-to.js | 6 ++++-- .../specs/block-hierarchy-navigation.test.js | 1 + .../specs/plugins/allowed-blocks.test.js | 15 ++++++++++----- .../specs/plugins/container-blocks.test.js | 5 ++++- .../plugins/inner-blocks-allowed-blocks.test.js | 5 ++++- 8 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js index 69d9e047418f3c..5e995c4d7a7a2a 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -43,7 +43,6 @@ function InserterListItem( { onClick(); } } disabled={ isDisabled } - aria-label={ title } // Fix for IE11 and JAWS 2018. { ...props } > { ) ).map( ( button ) => { - return button.getAttribute( 'aria-label' ); + return button.textContent; } ); }, '.block-editor-block-types-list .block-editor-block-types-list__list-item button' ); diff --git a/packages/e2e-test-utils/src/insert-block.js b/packages/e2e-test-utils/src/insert-block.js index f32cb2da1aba87..452f0432d0ed97 100644 --- a/packages/e2e-test-utils/src/insert-block.js +++ b/packages/e2e-test-utils/src/insert-block.js @@ -18,5 +18,8 @@ export async function insertBlock( searchTerm, panelName = null ) { ) )[ 0 ]; await panelButton.click(); } - await page.click( `button[aria-label="${ searchTerm }"]` ); + const insertButton = ( await page.$x( + `//button//span[contains(text(), '${ searchTerm }')]` + ) )[ 0 ]; + await insertButton.click(); } diff --git a/packages/e2e-test-utils/src/transform-block-to.js b/packages/e2e-test-utils/src/transform-block-to.js index 079adbea2f379f..64132aac7f45be 100644 --- a/packages/e2e-test-utils/src/transform-block-to.js +++ b/packages/e2e-test-utils/src/transform-block-to.js @@ -7,8 +7,10 @@ export async function transformBlockTo( name ) { await page.mouse.move( 200, 300, { steps: 10 } ); await page.mouse.move( 250, 350, { steps: 10 } ); await page.click( '.block-editor-block-switcher__toggle' ); - await page.waitForSelector( `.block-editor-block-types-list__item[aria-label="${ name }"]` ); - await page.click( `.block-editor-block-types-list__item[aria-label="${ name }"]` ); + const insertButton = ( await page.$x( + `//button//span[contains(text(), '${ name }')]` + ) )[ 0 ]; + await insertButton.click(); const BLOCK_SELECTOR = '.block-editor-block-list__block'; const BLOCK_NAME_SELECTOR = `[aria-label="Block: ${ name }"]`; // Wait for the transformed block to appear. diff --git a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js index 77d424d7e6381c..56d1ce2c3694b2 100644 --- a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js +++ b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js @@ -114,6 +114,7 @@ describe( 'Navigating the block hierarchy', () => { // Return to first block. await openBlockNavigator(); + await page.waitForSelector( '.editor-block-navigation__container' ); await page.keyboard.press( 'Space' ); // Replace its content. diff --git a/packages/e2e-tests/specs/plugins/allowed-blocks.test.js b/packages/e2e-tests/specs/plugins/allowed-blocks.test.js index 9e86e4ad019908..f7bdffcf3a703a 100644 --- a/packages/e2e-tests/specs/plugins/allowed-blocks.test.js +++ b/packages/e2e-tests/specs/plugins/allowed-blocks.test.js @@ -24,13 +24,18 @@ describe( 'Allowed Blocks Filter', () => { it( 'should restrict the allowed blocks in the inserter', async () => { // The paragraph block is available. await searchForBlock( 'Paragraph' ); - const paragraphBlock = await page.$( `button[aria-label="Paragraph"]` ); - expect( paragraphBlock ).not.toBeNull(); - await paragraphBlock.click(); + const paragraphBlockButton = ( await page.$x( + `//button//span[contains(text(), 'Paragraph')]` + ) )[ 0 ]; + expect( paragraphBlockButton ).not.toBeNull(); + await paragraphBlockButton.click(); // The gallery block is not available. await searchForBlock( 'Gallery' ); - const galleryBlock = await page.$( `button[aria-label="Gallery"]` ); - expect( galleryBlock ).toBeNull(); + + const galleryBlockButton = ( await page.$x( + `//button//span[contains(text(), 'Gallery')]` + ) )[ 0 ]; + expect( galleryBlockButton ).toBeUndefined(); } ); } ); diff --git a/packages/e2e-tests/specs/plugins/container-blocks.test.js b/packages/e2e-tests/specs/plugins/container-blocks.test.js index c01eb6eeca9a4d..dc52be82d5f61f 100644 --- a/packages/e2e-tests/specs/plugins/container-blocks.test.js +++ b/packages/e2e-tests/specs/plugins/container-blocks.test.js @@ -82,7 +82,10 @@ describe( 'Container block without paragraph support', () => { await page.click( '.block-editor-inner-blocks .block-list-appender .block-list-appender__toggle' ); // Insert an image block. - await page.click( '.block-editor-inserter__results button[aria-label="Image"]' ); + const insertButton = ( await page.$x( + `//button//span[contains(text(), 'Image')]` + ) )[ 0 ]; + await insertButton.click(); // Check the inserted content. expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/packages/e2e-tests/specs/plugins/inner-blocks-allowed-blocks.test.js b/packages/e2e-tests/specs/plugins/inner-blocks-allowed-blocks.test.js index 05c3a98b73ff43..c0a2055ade7623 100644 --- a/packages/e2e-tests/specs/plugins/inner-blocks-allowed-blocks.test.js +++ b/packages/e2e-tests/specs/plugins/inner-blocks-allowed-blocks.test.js @@ -71,7 +71,10 @@ describe( 'Allowed Blocks Setting on InnerBlocks ', () => { 'Image', 'List', ] ); - await page.click( `.block-editor-block-types-list__item[aria-label="List"]` ); + const insertButton = ( await page.$x( + `//button//span[contains(text(), 'List')]` + ) )[ 0 ]; + await insertButton.click(); await insertBlock( 'Image' ); await page.click( appenderSelector ); await openAllBlockInserterCategories(); From eea04cf2023871351ce486b02f8749aa51085fd8 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Mon, 13 May 2019 12:16:15 +0200 Subject: [PATCH 17/34] Fix broken active state on formatting buttons (#15592) At some point recently a small regression was introduced to the formatting button styles where the `:active` state box shadow that is inherited from the IconButton component lingered. To reproduce in master, click and drag out on a formatting button and note a gray box shadow lingering. This PR fixes that. I'm unsure as to how the regression was introduced, but it was likely due to the rather long selectors the IconButton component uses, which would be good to refactor. --- packages/components/src/toolbar-button/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/toolbar-button/style.scss b/packages/components/src/toolbar-button/style.scss index 65f3c0e56c11a5..b35d826ebdfc7b 100644 --- a/packages/components/src/toolbar-button/style.scss +++ b/packages/components/src/toolbar-button/style.scss @@ -10,7 +10,7 @@ height: $icon-button-size; // Unset icon button styles - &:active, + &:not([aria-disabled="true"]):not(.is-default):active, &:not([aria-disabled="true"]):hover, &:not([aria-disabled="true"]):focus { outline: none; From 6171097d4688fa28e739260616c157110f24e93f Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 13 May 2019 15:32:27 +0100 Subject: [PATCH 18/34] Fix/buttons should not appear as links (#15460) * Removes colors that visually imply a hyperlink Addresses https://github.com/WordPress/gutenberg/issues/15358#issue-438944848 * Revert "Removes colors that visually imply a hyperlink" This reverts commit 1aaec62d47cd95f3787e2f24d5e25b06bb6580a6. * Removes link styling from buttons in prepublish sidebar Addresses https://github.com/WordPress/gutenberg/issues/15358#issuecomment-489615758 * Removes superfluous text decoration rule Addresses https://github.com/WordPress/gutenberg/pull/15460#discussion_r281625728 --- packages/editor/src/components/post-publish-panel/style.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/editor/src/components/post-publish-panel/style.scss b/packages/editor/src/components/post-publish-panel/style.scss index 0faa264e871ad7..31654dcc64b001 100644 --- a/packages/editor/src/components/post-publish-panel/style.scss +++ b/packages/editor/src/components/post-publish-panel/style.scss @@ -54,10 +54,8 @@ } .editor-post-publish-panel__link { - color: $blue-medium-700; font-weight: 400; padding-left: 4px; - text-decoration: underline; } .editor-post-publish-panel__prepublish { From 449c25459c917b546cdfd786dbfd56dd3cb9a43a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 13 May 2019 11:48:20 -0400 Subject: [PATCH 19/34] Rich Text: Set applied format as active in applyFormats (#15573) * Rich Text: Set applied format as active in applyFormats * Rich Text: Update toggleFormat tests per activeFormats revision * Rich Text: Replace existing active format in applied * Rich Text: Remove active format in all cases for removeFormat * Rich Text: Update toggleFormat tests per removeFormats revision * Testing: Update E2E test case to verify link display regression * Rich Text: Verify by test of activeFormats format replacement --- .../specs/__snapshots__/links.test.js.snap | 6 +++ packages/e2e-tests/specs/links.test.js | 43 +++++++++++++++++++ packages/rich-text/src/apply-format.js | 23 +++++----- packages/rich-text/src/remove-format.js | 11 +++-- packages/rich-text/src/test/apply-format.js | 11 +++++ packages/rich-text/src/test/remove-format.js | 2 + packages/rich-text/src/test/toggle-format.js | 2 + 7 files changed, 82 insertions(+), 16 deletions(-) diff --git a/packages/e2e-tests/specs/__snapshots__/links.test.js.snap b/packages/e2e-tests/specs/__snapshots__/links.test.js.snap index d63515e5d2ee34..292ac9e5781a92 100644 --- a/packages/e2e-tests/specs/__snapshots__/links.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/links.test.js.snap @@ -47,3 +47,9 @@ exports[`Links should contain a label when it should open in a new tab 1`] = `

This is WordPress

" `; + +exports[`Links should contain a label when it should open in a new tab 2`] = ` +" +

This is WordPress

+" +`; diff --git a/packages/e2e-tests/specs/links.test.js b/packages/e2e-tests/specs/links.test.js index 84563375547b2e..a1d1dd3a46934b 100644 --- a/packages/e2e-tests/specs/links.test.js +++ b/packages/e2e-tests/specs/links.test.js @@ -503,5 +503,48 @@ describe( 'Links', () => { await page.keyboard.press( 'Enter' ); expect( await getEditedPostContent() ).toMatchSnapshot(); + + // Regression Test: This verifies that the UI is updated according to + // the expected changed values, where previously the value could have + // fallen out of sync with how the UI is displayed (specifically for + // collapsed selections). + // + // See: https://github.com/WordPress/gutenberg/pull/15573 + + // Collapse selection. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowRight' ); + // Edit link. + await pressKeyWithModifier( 'primary', 'k' ); + await waitForAutoFocus(); + await pressKeyWithModifier( 'primary', 'a' ); + await page.keyboard.type( 'wordpress.org' ); + // Navigate to the settings toggle. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + // Open settings. + await page.keyboard.press( 'Space' ); + // Navigate to the "Open in New Tab" checkbox. + await page.keyboard.press( 'Tab' ); + // Uncheck the checkbox. + await page.keyboard.press( 'Space' ); + // Navigate back to the input field. + await page.keyboard.press( 'Tab' ); + // Submit the form. + await page.keyboard.press( 'Enter' ); + + // Navigate back to inputs to verify appears as changed. + await pressKeyWithModifier( 'primary', 'k' ); + await waitForAutoFocus(); + const link = await page.evaluate( () => document.activeElement.value ); + expect( link ).toBe( 'http://wordpress.org' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Space' ); + await page.keyboard.press( 'Tab' ); + const isChecked = await page.evaluate( () => document.activeElement.checked ); + expect( isChecked ).toBe( false ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); } ); diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js index c2b96af10d6002..03a9ddc9f94d5c 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.js @@ -2,7 +2,7 @@ * External dependencies */ -import { find } from 'lodash'; +import { find, reject } from 'lodash'; /** * Internal dependencies @@ -34,7 +34,7 @@ export function applyFormat( startIndex = value.start, endIndex = value.end ) { - const { formats, activeFormats = [] } = value; + const { formats, activeFormats } = value; const newFormats = formats.slice(); // The selection is collapsed. @@ -59,13 +59,6 @@ export function applyFormat( replace( newFormats[ endIndex ], index, format ); endIndex++; } - // Otherwise, insert a placeholder with the format so new input appears - // with the format applied. - } else { - return { - ...value, - activeFormats: [ ...activeFormats, format ], - }; } } else { // Determine the highest position the new format can be inserted at. @@ -92,5 +85,15 @@ export function applyFormat( } } - return normaliseFormats( { ...value, formats: newFormats } ); + return normaliseFormats( { + ...value, + formats: newFormats, + // Always revise active formats. This serves as a placeholder for new + // inputs with the format so new input appears with the format applied, + // and ensures a format of the same type uses the latest values. + activeFormats: [ + ...reject( activeFormats, { type: format.type } ), + format, + ], + } ); } diff --git a/packages/rich-text/src/remove-format.js b/packages/rich-text/src/remove-format.js index 4a4c9c820f9008..5ce2fcd7ce8333 100644 --- a/packages/rich-text/src/remove-format.js +++ b/packages/rich-text/src/remove-format.js @@ -48,11 +48,6 @@ export function removeFormat( filterFormats( newFormats, endIndex, formatType ); endIndex++; } - } else { - return { - ...value, - activeFormats: reject( activeFormats, { type: formatType } ), - }; } } else { for ( let i = startIndex; i < endIndex; i++ ) { @@ -62,7 +57,11 @@ export function removeFormat( } } - return normaliseFormats( { ...value, formats: newFormats } ); + return normaliseFormats( { + ...value, + formats: newFormats, + activeFormats: reject( activeFormats, { type: formatType } ), + } ); } function filterFormats( formats, index, formatType ) { diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js index 67d2c3ff082971..2c74bc1d9aa269 100644 --- a/packages/rich-text/src/test/apply-format.js +++ b/packages/rich-text/src/test/apply-format.js @@ -23,6 +23,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ em ], formats: [ [ em ], [ em ], [ em ], [ em ] ], }; const result = applyFormat( deepFreeze( record ), em, 0, 4 ); @@ -39,6 +40,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ em ], formats: [ [ strong, em ], [ strong, em ], [ strong, em ], [ strong, em ] ], }; const result = applyFormat( deepFreeze( record ), em, 0, 4 ); @@ -55,6 +57,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ em ], formats: [ [ strong, em ], [ strong, em ], [ strong, em ], [ strong, em ] ], }; const result = applyFormat( deepFreeze( record ), em, 0, 4 ); @@ -71,6 +74,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ strong ], formats: [ [ strong ], [ strong, em ], [ strong, em ], [ strong ] ], }; const result = applyFormat( deepFreeze( record ), strong, 0, 4 ); @@ -87,6 +91,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ strong ], formats: [ [ strong ], [ strong, em ], [ strong, em ], , ], }; const result = applyFormat( deepFreeze( record ), strong, 0, 3 ); @@ -103,6 +108,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ strong ], formats: [ , [ strong, em ], [ strong, em ], [ strong ] ], }; const result = applyFormat( deepFreeze( record ), strong, 1, 4 ); @@ -119,6 +125,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, + activeFormats: [ strong ], formats: [ , [ strong, em ], [ strong ], [ strong, em ] ], }; const result = applyFormat( deepFreeze( record ), strong, 1, 4 ); @@ -134,6 +141,7 @@ describe( 'applyFormat', () => { text: 'one two three', }; const expected = { + activeFormats: [ strong ], formats: [ , , , [ strong ], [ strong, em ], [ strong, em ], [ em ], , , , , , , ], text: 'one two three', }; @@ -152,6 +160,7 @@ describe( 'applyFormat', () => { end: 6, }; const expected = { + activeFormats: [ strong ], formats: [ , , , [ strong ], [ strong, em ], [ strong, em ], [ em ], , , , , , , ], text: 'one two three', start: 3, @@ -184,12 +193,14 @@ describe( 'applyFormat', () => { it( 'should apply format on existing format if selection is collapsed', () => { const record = { + activeFormats: [ a ], formats: [ , , , , [ a ], [ a ], [ a ], , , , , , , ], text: 'one two three', start: 4, end: 4, }; const expected = { + activeFormats: [ a2 ], formats: [ , , , , [ a2 ], [ a2 ], [ a2 ], , , , , , , ], text: 'one two three', start: 4, diff --git a/packages/rich-text/src/test/remove-format.js b/packages/rich-text/src/test/remove-format.js index 4d6b10f0d27fbe..343eb8c47e011f 100644 --- a/packages/rich-text/src/test/remove-format.js +++ b/packages/rich-text/src/test/remove-format.js @@ -21,6 +21,7 @@ describe( 'removeFormat', () => { }; const expected = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + activeFormats: [], text: 'one two three', }; const result = removeFormat( deepFreeze( record ), 'strong', 3, 6 ); @@ -37,6 +38,7 @@ describe( 'removeFormat', () => { }; const expected = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + activeFormats: [], text: 'one two three', }; const result = removeFormat( deepFreeze( record ), 'strong', 4, 4 ); diff --git a/packages/rich-text/src/test/toggle-format.js b/packages/rich-text/src/test/toggle-format.js index 28762a37b1603c..9613e970d40a4e 100644 --- a/packages/rich-text/src/test/toggle-format.js +++ b/packages/rich-text/src/test/toggle-format.js @@ -23,6 +23,7 @@ describe( 'toggleFormat', () => { }; const expected = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + activeFormats: [], text: 'one two three', start: 3, end: 6, @@ -43,6 +44,7 @@ describe( 'toggleFormat', () => { }; const expected = { formats: [ , , , [ strong ], [ strong, em ], [ strong, em ], [ em ], , , , , , , ], + activeFormats: [ strong ], text: 'one two three', start: 3, end: 6, From f6d816dd080794b8523bb0f2f8a0e49923660f92 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 13 May 2019 23:39:48 +0200 Subject: [PATCH 20/34] Fix pinned plugins button styles when toggled. (#15609) --- .../src/components/header/pinned-plugins/style.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/edit-post/src/components/header/pinned-plugins/style.scss b/packages/edit-post/src/components/header/pinned-plugins/style.scss index b01dd0480d5ee5..030d6305ae9033 100644 --- a/packages/edit-post/src/components/header/pinned-plugins/style.scss +++ b/packages/edit-post/src/components/header/pinned-plugins/style.scss @@ -7,6 +7,10 @@ .components-icon-button { margin-left: 4px; + + &.is-toggled { + margin-left: 5px; + } } // Colorize plugin icons to ensure contrast and cohesion, but allow plugin developers to override. @@ -19,7 +23,9 @@ // Forcefully colorize hover and toggled plugin icon states to ensure legibility and consistency. .components-icon-button.is-toggled svg, - .components-icon-button.is-toggled svg * { + .components-icon-button.is-toggled svg *, + .components-icon-button.is-toggled:hover svg, + .components-icon-button.is-toggled:hover svg * { stroke: $white !important; fill: $white !important; stroke-width: 0; // !important is omitted here, so stroke-only icons can override easily. From b695b53fe536b85ca655c68d48b4297015b56bba Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak Date: Mon, 13 May 2019 14:49:10 -0700 Subject: [PATCH 21/34] Docs: Clarify migrate in deprecated blocks (#15612) The wording in migrate was confusing around which attributes should be supplied and returned. Added old/new to clarify the old attributes are passed in, and new attributes are returned. Confirmed this with the code, which you can see in the test here: https://github.com/WordPress/gutenberg/blob/master/packages/blocks/src/api/test/parser.js#L558 --- .../developers/block-api/block-deprecation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/block-api/block-deprecation.md b/docs/designers-developers/developers/block-api/block-deprecation.md index 795a5951b2fd04..0e5be3c05f27a8 100644 --- a/docs/designers-developers/developers/block-api/block-deprecation.md +++ b/docs/designers-developers/developers/block-api/block-deprecation.md @@ -12,7 +12,7 @@ Deprecations are defined on a block type as its `deprecated` property, an array - `attributes` (Object): The [attributes definition](/docs/designers-developers/developers/block-api/block-attributes.md) of the deprecated form of the block. - `support` (Object): The [supports definition](/docs/designers-developers/developers/block-api/block-registration.md) of the deprecated form of the block. - `save` (Function): The [save implementation](/docs/designers-developers/developers/block-api/block-edit-save.md) of the deprecated form of the block. -- `migrate` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, is expected to return either the attributes compatible with the deprecated block, or a tuple array of `[ attributes, innerBlocks ]`. +- `migrate` (Function, Optional): A function which, given the old attributes and inner blocks is expected to return either the new attributes or a tuple array of `[ attributes, innerBlocks ]` compatible with the block. - `isEligible` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, returns true if the deprecation can handle the block migration. This is particularly useful in cases where a block is technically valid even once deprecated, and requires updates to its attributes or inner blocks. It's important to note that `attributes`, `support`, and `save` are not automatically inherited from the current version, since they can impact parsing and serialization of a block, so they must be defined on the deprecated object in order to be processed during a migration. From 60643a384054bcea6ff9b2586bf9d0e589cfc12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Tue, 14 May 2019 09:27:52 +0200 Subject: [PATCH 22/34] Block library: refactor edit components in Legacy Widget block (#15555) --- .../dom-manager.js} | 4 ++-- .../{WidgetEditHandler.js => edit/handler.js} | 8 ++++---- .../src/legacy-widget/{edit.js => edit/index.js} | 15 +++++++-------- 3 files changed, 13 insertions(+), 14 deletions(-) rename packages/block-library/src/legacy-widget/{WidgetEditDomManager.js => edit/dom-manager.js} (97%) rename packages/block-library/src/legacy-widget/{WidgetEditHandler.js => edit/handler.js} (93%) rename packages/block-library/src/legacy-widget/{edit.js => edit/index.js} (96%) diff --git a/packages/block-library/src/legacy-widget/WidgetEditDomManager.js b/packages/block-library/src/legacy-widget/edit/dom-manager.js similarity index 97% rename from packages/block-library/src/legacy-widget/WidgetEditDomManager.js rename to packages/block-library/src/legacy-widget/edit/dom-manager.js index 30ded7a7a7a6f8..d4a0b43829f29c 100644 --- a/packages/block-library/src/legacy-widget/WidgetEditDomManager.js +++ b/packages/block-library/src/legacy-widget/edit/dom-manager.js @@ -9,7 +9,7 @@ import { includes } from 'lodash'; import { Component, createRef } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; -class WidgetEditDomManager extends Component { +class LegacyWidgetEditDomManager extends Component { constructor() { super( ...arguments ); @@ -139,5 +139,5 @@ class WidgetEditDomManager extends Component { } } -export default WidgetEditDomManager; +export default LegacyWidgetEditDomManager; diff --git a/packages/block-library/src/legacy-widget/WidgetEditHandler.js b/packages/block-library/src/legacy-widget/edit/handler.js similarity index 93% rename from packages/block-library/src/legacy-widget/WidgetEditHandler.js rename to packages/block-library/src/legacy-widget/edit/handler.js index 527e864c90037a..157ab653fb89f5 100644 --- a/packages/block-library/src/legacy-widget/WidgetEditHandler.js +++ b/packages/block-library/src/legacy-widget/edit/handler.js @@ -9,9 +9,9 @@ import { withInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ -import WidgetEditDomManager from './WidgetEditDomManager'; +import LegacyWidgetEditDomManager from './dom-manager'; -class WidgetEditHandler extends Component { +class LegacyWidgetEditHandler extends Component { constructor() { super( ...arguments ); this.state = { @@ -64,7 +64,7 @@ class WidgetEditHandler extends Component { display: this.props.isVisible ? 'block' : 'none', } } > - { this.widgetEditDomManagerRef = ref; } } @@ -118,5 +118,5 @@ class WidgetEditHandler extends Component { } } -export default withInstanceId( WidgetEditHandler ); +export default withInstanceId( LegacyWidgetEditHandler ); diff --git a/packages/block-library/src/legacy-widget/edit.js b/packages/block-library/src/legacy-widget/edit/index.js similarity index 96% rename from packages/block-library/src/legacy-widget/edit.js rename to packages/block-library/src/legacy-widget/edit/index.js index 89621422bc6650..4b645318eb2277 100644 --- a/packages/block-library/src/legacy-widget/edit.js +++ b/packages/block-library/src/legacy-widget/edit/index.js @@ -17,18 +17,17 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { withSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ import { BlockControls, BlockIcon, InspectorControls, - ServerSideRender, -} from '@wordpress/editor'; +} from '@wordpress/block-editor'; +import { ServerSideRender } from '@wordpress/editor'; -import WidgetEditHandler from './WidgetEditHandler'; +/** + * Internal dependencies + */ +import LegacyWidgetEditHandler from './handler'; class LegacyWidgetEdit extends Component { constructor() { @@ -136,7 +135,7 @@ class LegacyWidgetEdit extends Component { { inspectorControls } { ! isCallbackWidget && ( - Date: Tue, 14 May 2019 04:07:41 -0400 Subject: [PATCH 23/34] Add a DevHub manifest (#15254) * Add a devhub manifest * Move reference docs to bottom * Extract tutorials to the top-level * Rename Designers & Developers Handbook to Block Editor Handbook * Rename Contributors Guide to Contributor Documentation * Rename Components to Component Reference * Rename Packages to Package Reference * Substitute old top-level README The front-page of the handbook is the one whose slug is handbook. This is generated by looking at the path (docs/readme.md), so we substitute the old README with the new one. * Add a reference to contributors as well * Delete unused file that wasnt removed on rebase * Use manifest-devhub instead of old manifest * Fix toc.json from rebase * Updame manifest-devhub.json * Use relative paths to docs --- docs/contributors/readme.md | 2 +- .../developers/packages.md | 2 +- docs/designers-developers/readme.md | 11 - docs/manifest-devhub.json | 1322 +++++++++++++++++ docs/readme.md | 23 +- docs/toc.json | 228 ++- docs/tool/index.js | 2 +- docs/tool/manifest.js | 2 +- packages/components/README.md | 2 +- 9 files changed, 1446 insertions(+), 148 deletions(-) delete mode 100644 docs/designers-developers/readme.md create mode 100644 docs/manifest-devhub.json diff --git a/docs/contributors/readme.md b/docs/contributors/readme.md index 685af4590b4737..96c3a85424e5d8 100644 --- a/docs/contributors/readme.md +++ b/docs/contributors/readme.md @@ -1,4 +1,4 @@ -# Contributors Guide +# Contributor Documentation Welcome to the Gutenberg Project Contributors Guide. diff --git a/docs/designers-developers/developers/packages.md b/docs/designers-developers/developers/packages.md index b5ef06c18782a2..fda7d986209efd 100644 --- a/docs/designers-developers/developers/packages.md +++ b/docs/designers-developers/developers/packages.md @@ -1,4 +1,4 @@ -# Packages +# Package Reference WordPress exposes a list of JavaScript packages and tools for WordPress development. diff --git a/docs/designers-developers/readme.md b/docs/designers-developers/readme.md deleted file mode 100644 index 7b8da012e063ff..00000000000000 --- a/docs/designers-developers/readme.md +++ /dev/null @@ -1,11 +0,0 @@ -# Designer & Developer Handbook - -The Gutenberg project is transforming the way content is created on WordPress. A block editor was the first product launched creating a new methodology for working with content. This handbook provides documentation for how designers and developers can extend the editor. - -![Gutenberg Demo](https://cldup.com/kZXGDcGPMU.gif) - -Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created in the unit of blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block though). - -Blocks treat Paragraphs, Headings, Media, Embeds all as components that strung together make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embedded media and shortcodes. The new editor is designed with progressive enhancement, meaning it is back-compatible with all legacy content, offers a process to try to convert and split a Classic block into block equivalents using client-side parsing and finally the blocks offer enhanced editing and format controls. - -The Editor offers rich new value to users with visual, drag-and-drop creation tools and powerful developer enhancements with modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json new file mode 100644 index 00000000000000..09544a23854c3e --- /dev/null +++ b/docs/manifest-devhub.json @@ -0,0 +1,1322 @@ +[ + { + "title": "Block Editor Handbook", + "slug": "handbook", + "markdown_source": "../docs/readme.md", + "parent": null + }, + { + "title": "Key Concepts", + "slug": "key-concepts", + "markdown_source": "../docs/designers-developers/key-concepts.md", + "parent": null + }, + { + "title": "Developer Documentation", + "slug": "developers", + "markdown_source": "../docs/designers-developers/developers/README.md", + "parent": null + }, + { + "title": "Block API Reference", + "slug": "block-api", + "markdown_source": "../docs/designers-developers/developers/block-api/README.md", + "parent": "developers" + }, + { + "title": "Block Registration", + "slug": "block-registration", + "markdown_source": "../docs/designers-developers/developers/block-api/block-registration.md", + "parent": "block-api" + }, + { + "title": "Edit and Save", + "slug": "block-edit-save", + "markdown_source": "../docs/designers-developers/developers/block-api/block-edit-save.md", + "parent": "block-api" + }, + { + "title": "Attributes", + "slug": "block-attributes", + "markdown_source": "../docs/designers-developers/developers/block-api/block-attributes.md", + "parent": "block-api" + }, + { + "title": "Deprecated Blocks", + "slug": "block-deprecation", + "markdown_source": "../docs/designers-developers/developers/block-api/block-deprecation.md", + "parent": "block-api" + }, + { + "title": "Templates", + "slug": "block-templates", + "markdown_source": "../docs/designers-developers/developers/block-api/block-templates.md", + "parent": "block-api" + }, + { + "title": "Annotations", + "slug": "block-annotations", + "markdown_source": "../docs/designers-developers/developers/block-api/block-annotations.md", + "parent": "block-api" + }, + { + "title": "Filter Reference", + "slug": "filters", + "markdown_source": "../docs/designers-developers/developers/filters/README.md", + "parent": "developers" + }, + { + "title": "Block Filters", + "slug": "block-filters", + "markdown_source": "../docs/designers-developers/developers/filters/block-filters.md", + "parent": "filters" + }, + { + "title": "Editor Filters (Experimental)", + "slug": "editor-filters", + "markdown_source": "../docs/designers-developers/developers/filters/editor-filters.md", + "parent": "filters" + }, + { + "title": "Parser Filters", + "slug": "parser-filters", + "markdown_source": "../docs/designers-developers/developers/filters/parser-filters.md", + "parent": "filters" + }, + { + "title": "Autocomplete", + "slug": "autocomplete-filters", + "markdown_source": "../docs/designers-developers/developers/filters/autocomplete-filters.md", + "parent": "filters" + }, + { + "title": "Internationalization", + "slug": "internationalization", + "markdown_source": "../docs/designers-developers/developers/internationalization.md", + "parent": "developers" + }, + { + "title": "Accessibility", + "slug": "accessibility", + "markdown_source": "../docs/designers-developers/developers/accessibility.md", + "parent": "developers" + }, + { + "title": "Feature Flags", + "slug": "feature-flags", + "markdown_source": "../docs/designers-developers/developers/feature-flags.md", + "parent": "developers" + }, + { + "title": "Theming for the Block Editor", + "slug": "themes", + "markdown_source": "../docs/designers-developers/developers/themes/README.md", + "parent": "developers" + }, + { + "title": "Theme Support", + "slug": "theme-support", + "markdown_source": "../docs/designers-developers/developers/themes/theme-support.md", + "parent": "themes" + }, + { + "title": "Backward Compatibility", + "slug": "backward-compatibility", + "markdown_source": "../docs/designers-developers/developers/backward-compatibility/README.md", + "parent": "developers" + }, + { + "title": "Deprecations", + "slug": "deprecations", + "markdown_source": "../docs/designers-developers/developers/backward-compatibility/deprecations.md", + "parent": "backward-compatibility" + }, + { + "title": "Meta Boxes", + "slug": "meta-box", + "markdown_source": "../docs/designers-developers/developers/backward-compatibility/meta-box.md", + "parent": "backward-compatibility" + }, + { + "title": "Designer Documentation", + "slug": "designers", + "markdown_source": "../docs/designers-developers/designers/README.md", + "parent": null + }, + { + "title": "Block Design", + "slug": "block-design", + "markdown_source": "../docs/designers-developers/designers/block-design.md", + "parent": "designers" + }, + { + "title": "Patterns", + "slug": "design-patterns", + "markdown_source": "../docs/designers-developers/designers/design-patterns.md", + "parent": "designers" + }, + { + "title": "Resources", + "slug": "design-resources", + "markdown_source": "../docs/designers-developers/designers/design-resources.md", + "parent": "designers" + }, + { + "title": "Animation", + "slug": "animation", + "markdown_source": "../docs/designers-developers/designers/animation.md", + "parent": "designers" + }, + { + "title": "Contributor Documentation", + "slug": "contributors", + "markdown_source": "../docs/contributors/readme.md", + "parent": null + }, + { + "title": "Principles", + "slug": "principles", + "markdown_source": "../docs/contributors/principles.md", + "parent": "contributors" + }, + { + "title": "Design Principles & Vision", + "slug": "design", + "markdown_source": "../docs/contributors/design.md", + "parent": "contributors" + }, + { + "title": "Blocks are the Interface", + "slug": "the-block", + "markdown_source": "../docs/contributors/principles/the-block.md", + "parent": "design" + }, + { + "title": "Reference", + "slug": "reference", + "markdown_source": "../docs/contributors/reference.md", + "parent": "design" + }, + { + "title": "Developer Contributions", + "slug": "develop", + "markdown_source": "../docs/contributors/develop.md", + "parent": "contributors" + }, + { + "title": "Getting Started", + "slug": "getting-started", + "markdown_source": "../docs/contributors/getting-started.md", + "parent": "develop" + }, + { + "title": "Git Workflow", + "slug": "git-workflow", + "markdown_source": "../docs/contributors/git-workflow.md", + "parent": "develop" + }, + { + "title": "Coding Guidelines", + "slug": "coding-guidelines", + "markdown_source": "../docs/contributors/coding-guidelines.md", + "parent": "develop" + }, + { + "title": "Testing Overview", + "slug": "testing-overview", + "markdown_source": "../docs/contributors/testing-overview.md", + "parent": "develop" + }, + { + "title": "Block Grammar", + "slug": "grammar", + "markdown_source": "../docs/contributors/grammar.md", + "parent": "develop" + }, + { + "title": "Scripts", + "slug": "scripts", + "markdown_source": "../docs/contributors/scripts.md", + "parent": "develop" + }, + { + "title": "Managing Packages", + "slug": "managing-packages", + "markdown_source": "../docs/contributors/managing-packages.md", + "parent": "develop" + }, + { + "title": "Gutenberg Release Process", + "slug": "release", + "markdown_source": "../docs/contributors/release.md", + "parent": "develop" + }, + { + "title": "Localizing Gutenberg Plugin", + "slug": "localizing", + "markdown_source": "../docs/contributors/localizing.md", + "parent": "develop" + }, + { + "title": "Documentation Contributions", + "slug": "document", + "markdown_source": "../docs/contributors/document.md", + "parent": "contributors" + }, + { + "title": "Copy Guidelines", + "slug": "copy-guide", + "markdown_source": "../docs/contributors/copy-guide.md", + "parent": "document" + }, + { + "title": "History", + "slug": "history", + "markdown_source": "../docs/contributors/history.md", + "parent": "contributors" + }, + { + "title": "Glossary", + "slug": "glossary", + "markdown_source": "../docs/designers-developers/glossary.md", + "parent": "contributors" + }, + { + "title": "Frequently Asked Questions", + "slug": "faq", + "markdown_source": "../docs/designers-developers/faq.md", + "parent": "contributors" + }, + { + "title": "Repository Management", + "slug": "repository-management", + "markdown_source": "../docs/contributors/repository-management.md", + "parent": "contributors" + }, + { + "title": "Outreach", + "slug": "outreach", + "markdown_source": "../docs/contributors/outreach.md", + "parent": "contributors" + }, + { + "title": "Tutorials", + "slug": "tutorials", + "markdown_source": "../docs/designers-developers/developers/tutorials/readme.md", + "parent": null + }, + { + "title": "Getting Started with JavaScript", + "slug": "javascript", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/readme.md", + "parent": "tutorials" + }, + { + "title": "Plugins Background", + "slug": "plugins-background", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/plugins-background.md", + "parent": "javascript" + }, + { + "title": "Loading JavaScript", + "slug": "loading-javascript", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/loading-javascript.md", + "parent": "javascript" + }, + { + "title": "Extending the Block Editor", + "slug": "extending-the-block-editor", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md", + "parent": "javascript" + }, + { + "title": "Troubleshooting", + "slug": "troubleshooting", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/troubleshooting.md", + "parent": "javascript" + }, + { + "title": "JavaScript Versions and Build Step", + "slug": "versions-and-building", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/versions-and-building.md", + "parent": "javascript" + }, + { + "title": "Scope Your Code", + "slug": "scope-your-code", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/scope-your-code.md", + "parent": "javascript" + }, + { + "title": "JavaScript Build Setup", + "slug": "js-build-setup", + "markdown_source": "../docs/designers-developers/developers/tutorials/javascript/js-build-setup.md", + "parent": "javascript" + }, + { + "title": "Blocks", + "slug": "block-tutorial", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/readme.md", + "parent": "tutorials" + }, + { + "title": "Writing Your First Block Type", + "slug": "writing-your-first-block-type", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md", + "parent": "block-tutorial" + }, + { + "title": "Applying Styles From a Stylesheet", + "slug": "applying-styles-with-stylesheets", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md", + "parent": "block-tutorial" + }, + { + "title": "Introducing Attributes and Editable Fields", + "slug": "introducing-attributes-and-editable-fields", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md", + "parent": "block-tutorial" + }, + { + "title": "Block Controls: Toolbars and Inspector", + "slug": "block-controls-toolbars-and-inspector", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md", + "parent": "block-tutorial" + }, + { + "title": "Creating dynamic blocks", + "slug": "creating-dynamic-blocks", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md", + "parent": "block-tutorial" + }, + { + "title": "Generate Blocks with WP-CLI", + "slug": "generate-blocks-with-wp-cli", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md", + "parent": "block-tutorial" + }, + { + "title": "Meta Boxes", + "slug": "metabox", + "markdown_source": "../docs/designers-developers/developers/tutorials/metabox/readme.md", + "parent": "tutorials" + }, + { + "title": "Store Post Meta with a Block", + "slug": "meta-block-1-intro", + "markdown_source": "../docs/designers-developers/developers/tutorials/metabox/meta-block-1-intro.md", + "parent": "metabox" + }, + { + "title": "Register Meta Field", + "slug": "meta-block-2-register-meta", + "markdown_source": "../docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md", + "parent": "metabox" + }, + { + "title": "Create Meta Block", + "slug": "meta-block-3-add", + "markdown_source": "../docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md", + "parent": "metabox" + }, + { + "title": "Use Post Meta Data", + "slug": "meta-block-4-use-data", + "markdown_source": "../docs/designers-developers/developers/tutorials/metabox/meta-block-4-use-data.md", + "parent": "metabox" + }, + { + "title": "Finishing Touches", + "slug": "meta-block-5-finishing", + "markdown_source": "../docs/designers-developers/developers/tutorials/metabox/meta-block-5-finishing.md", + "parent": "metabox" + }, + { + "title": "Displaying Notices from Your Plugin or Theme", + "slug": "notices", + "markdown_source": "../docs/designers-developers/developers/tutorials/notices/README.md", + "parent": "tutorials" + }, + { + "title": "Creating a Sidebar for Your Plugin", + "slug": "plugin-sidebar-0", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md", + "parent": "tutorials" + }, + { + "title": "Get a Sidebar up and Running", + "slug": "plugin-sidebar-1-up-and-running", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md", + "parent": "plugin-sidebar-0" + }, + { + "title": "Tweak the sidebar style and add controls", + "slug": "plugin-sidebar-2-styles-and-controls", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md", + "parent": "plugin-sidebar-0" + }, + { + "title": "Register the Meta Field", + "slug": "plugin-sidebar-3-register-meta", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md", + "parent": "plugin-sidebar-0" + }, + { + "title": "Initialize the Input Control", + "slug": "plugin-sidebar-4-initialize-input", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md", + "parent": "plugin-sidebar-0" + }, + { + "title": "Update the Meta Field When the Input's Content Changes", + "slug": "plugin-sidebar-5-update-meta", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md", + "parent": "plugin-sidebar-0" + }, + { + "title": "Finishing Touches", + "slug": "plugin-sidebar-6-finishing-touches", + "markdown_source": "../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md", + "parent": "plugin-sidebar-0" + }, + { + "title": "Introduction to the Format API", + "slug": "format-api", + "markdown_source": "../docs/designers-developers/developers/tutorials/format-api/README.md", + "parent": "tutorials" + }, + { + "title": "Register a New Format", + "slug": "1-register-format", + "markdown_source": "../docs/designers-developers/developers/tutorials/format-api/1-register-format.md", + "parent": "format-api" + }, + { + "title": "Add a Button to the Toolbar", + "slug": "2-toolbar-button", + "markdown_source": "../docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md", + "parent": "format-api" + }, + { + "title": "Apply the Format When the Button Is Clicked", + "slug": "3-apply-format", + "markdown_source": "../docs/designers-developers/developers/tutorials/format-api/3-apply-format.md", + "parent": "format-api" + }, + { + "title": "Component Reference", + "slug": "components", + "markdown_source": "../packages/components/README.md", + "parent": null + }, + { + "title": "Animate", + "slug": "animate", + "markdown_source": "../packages/components/src/animate/README.md", + "parent": "components" + }, + { + "title": "Autocomplete", + "slug": "autocomplete", + "markdown_source": "../packages/components/src/autocomplete/README.md", + "parent": "components" + }, + { + "title": "BaseControl", + "slug": "base-control", + "markdown_source": "../packages/components/src/base-control/README.md", + "parent": "components" + }, + { + "title": "ButtonGroup", + "slug": "button-group", + "markdown_source": "../packages/components/src/button-group/README.md", + "parent": "components" + }, + { + "title": "Button", + "slug": "button", + "markdown_source": "../packages/components/src/button/README.md", + "parent": "components" + }, + { + "title": "CheckboxControl", + "slug": "checkbox-control", + "markdown_source": "../packages/components/src/checkbox-control/README.md", + "parent": "components" + }, + { + "title": "ClipboardButton", + "slug": "clipboard-button", + "markdown_source": "../packages/components/src/clipboard-button/README.md", + "parent": "components" + }, + { + "title": "ColorIndicator", + "slug": "color-indicator", + "markdown_source": "../packages/components/src/color-indicator/README.md", + "parent": "components" + }, + { + "title": "ColorPalette", + "slug": "color-palette", + "markdown_source": "../packages/components/src/color-palette/README.md", + "parent": "components" + }, + { + "title": "ColorPicker", + "slug": "color-picker", + "markdown_source": "../packages/components/src/color-picker/README.md", + "parent": "components" + }, + { + "title": "Dashicon", + "slug": "dashicon", + "markdown_source": "../packages/components/src/dashicon/README.md", + "parent": "components" + }, + { + "title": "DateTime", + "slug": "date-time", + "markdown_source": "../packages/components/src/date-time/README.md", + "parent": "components" + }, + { + "title": "Disabled", + "slug": "disabled", + "markdown_source": "../packages/components/src/disabled/README.md", + "parent": "components" + }, + { + "title": "Draggable", + "slug": "draggable", + "markdown_source": "../packages/components/src/draggable/README.md", + "parent": "components" + }, + { + "title": "DropZone", + "slug": "drop-zone", + "markdown_source": "../packages/components/src/drop-zone/README.md", + "parent": "components" + }, + { + "title": "DropdownMenu", + "slug": "dropdown-menu", + "markdown_source": "../packages/components/src/dropdown-menu/README.md", + "parent": "components" + }, + { + "title": "Dropdown", + "slug": "dropdown", + "markdown_source": "../packages/components/src/dropdown/README.md", + "parent": "components" + }, + { + "title": "ExternalLink", + "slug": "external-link", + "markdown_source": "../packages/components/src/external-link/README.md", + "parent": "components" + }, + { + "title": "FocalPointPicker", + "slug": "focal-point-picker", + "markdown_source": "../packages/components/src/focal-point-picker/README.md", + "parent": "components" + }, + { + "title": "FocusableIframe", + "slug": "focusable-iframe", + "markdown_source": "../packages/components/src/focusable-iframe/README.md", + "parent": "components" + }, + { + "title": "FontSizePicker", + "slug": "font-size-picker", + "markdown_source": "../packages/components/src/font-size-picker/README.md", + "parent": "components" + }, + { + "title": "FormFileUpload", + "slug": "form-file-upload", + "markdown_source": "../packages/components/src/form-file-upload/README.md", + "parent": "components" + }, + { + "title": "FormToggle", + "slug": "form-toggle", + "markdown_source": "../packages/components/src/form-toggle/README.md", + "parent": "components" + }, + { + "title": "FormTokenField", + "slug": "form-token-field", + "markdown_source": "../packages/components/src/form-token-field/README.md", + "parent": "components" + }, + { + "title": "NavigateRegions", + "slug": "navigate-regions", + "markdown_source": "../packages/components/src/higher-order/navigate-regions/README.md", + "parent": "components" + }, + { + "title": "HigherOrder", + "slug": "higher-order", + "markdown_source": "../packages/components/src/higher-order/README.md", + "parent": "components" + }, + { + "title": "WithConstrainedTabbing", + "slug": "with-constrained-tabbing", + "markdown_source": "../packages/components/src/higher-order/with-constrained-tabbing/README.md", + "parent": "components" + }, + { + "title": "WithFallbackStyles", + "slug": "with-fallback-styles", + "markdown_source": "../packages/components/src/higher-order/with-fallback-styles/README.md", + "parent": "components" + }, + { + "title": "WithFilters", + "slug": "with-filters", + "markdown_source": "../packages/components/src/higher-order/with-filters/README.md", + "parent": "components" + }, + { + "title": "WithFocusOutside", + "slug": "with-focus-outside", + "markdown_source": "../packages/components/src/higher-order/with-focus-outside/README.md", + "parent": "components" + }, + { + "title": "WithFocusReturn", + "slug": "with-focus-return", + "markdown_source": "../packages/components/src/higher-order/with-focus-return/README.md", + "parent": "components" + }, + { + "title": "WithNotices", + "slug": "with-notices", + "markdown_source": "../packages/components/src/higher-order/with-notices/README.md", + "parent": "components" + }, + { + "title": "WithSpokenMessages", + "slug": "with-spoken-messages", + "markdown_source": "../packages/components/src/higher-order/with-spoken-messages/README.md", + "parent": "components" + }, + { + "title": "IconButton", + "slug": "icon-button", + "markdown_source": "../packages/components/src/icon-button/README.md", + "parent": "components" + }, + { + "title": "Icon", + "slug": "icon", + "markdown_source": "../packages/components/src/icon/README.md", + "parent": "components" + }, + { + "title": "IsolatedEventContainer", + "slug": "isolated-event-container", + "markdown_source": "../packages/components/src/isolated-event-container/README.md", + "parent": "components" + }, + { + "title": "KeyboardShortcuts", + "slug": "keyboard-shortcuts", + "markdown_source": "../packages/components/src/keyboard-shortcuts/README.md", + "parent": "components" + }, + { + "title": "MenuGroup", + "slug": "menu-group", + "markdown_source": "../packages/components/src/menu-group/README.md", + "parent": "components" + }, + { + "title": "MenuItem", + "slug": "menu-item", + "markdown_source": "../packages/components/src/menu-item/README.md", + "parent": "components" + }, + { + "title": "MenuItemsChoice", + "slug": "menu-items-choice", + "markdown_source": "../packages/components/src/menu-items-choice/README.md", + "parent": "components" + }, + { + "title": "Modal", + "slug": "modal", + "markdown_source": "../packages/components/src/modal/README.md", + "parent": "components" + }, + { + "title": "NavigableContainer", + "slug": "navigable-container", + "markdown_source": "../packages/components/src/navigable-container/README.md", + "parent": "components" + }, + { + "title": "Notice", + "slug": "notice", + "markdown_source": "../packages/components/src/notice/README.md", + "parent": "components" + }, + { + "title": "Panel", + "slug": "panel", + "markdown_source": "../packages/components/src/panel/README.md", + "parent": "components" + }, + { + "title": "Placeholder", + "slug": "placeholder", + "markdown_source": "../packages/components/src/placeholder/README.md", + "parent": "components" + }, + { + "title": "Popover", + "slug": "popover", + "markdown_source": "../packages/components/src/popover/README.md", + "parent": "components" + }, + { + "title": "HorizontalRule", + "slug": "horizontal-rule", + "markdown_source": "../packages/components/src/primitives/horizontal-rule/README.md", + "parent": "components" + }, + { + "title": "Svg", + "slug": "svg", + "markdown_source": "../packages/components/src/primitives/svg/README.md", + "parent": "components" + }, + { + "title": "QueryControls", + "slug": "query-controls", + "markdown_source": "../packages/components/src/query-controls/README.md", + "parent": "components" + }, + { + "title": "RadioControl", + "slug": "radio-control", + "markdown_source": "../packages/components/src/radio-control/README.md", + "parent": "components" + }, + { + "title": "RangeControl", + "slug": "range-control", + "markdown_source": "../packages/components/src/range-control/README.md", + "parent": "components" + }, + { + "title": "ResizableBox", + "slug": "resizable-box", + "markdown_source": "../packages/components/src/resizable-box/README.md", + "parent": "components" + }, + { + "title": "ResponsiveWrapper", + "slug": "responsive-wrapper", + "markdown_source": "../packages/components/src/responsive-wrapper/README.md", + "parent": "components" + }, + { + "title": "Sandbox", + "slug": "sandbox", + "markdown_source": "../packages/components/src/sandbox/README.md", + "parent": "components" + }, + { + "title": "ScrollLock", + "slug": "scroll-lock", + "markdown_source": "../packages/components/src/scroll-lock/README.md", + "parent": "components" + }, + { + "title": "SelectControl", + "slug": "select-control", + "markdown_source": "../packages/components/src/select-control/README.md", + "parent": "components" + }, + { + "title": "ServerSideRender", + "slug": "server-side-render", + "markdown_source": "../packages/components/src/server-side-render/README.md", + "parent": "components" + }, + { + "title": "SlotFill", + "slug": "slot-fill", + "markdown_source": "../packages/components/src/slot-fill/README.md", + "parent": "components" + }, + { + "title": "Spinner", + "slug": "spinner", + "markdown_source": "../packages/components/src/spinner/README.md", + "parent": "components" + }, + { + "title": "TabPanel", + "slug": "tab-panel", + "markdown_source": "../packages/components/src/tab-panel/README.md", + "parent": "components" + }, + { + "title": "TextControl", + "slug": "text-control", + "markdown_source": "../packages/components/src/text-control/README.md", + "parent": "components" + }, + { + "title": "TextareaControl", + "slug": "textarea-control", + "markdown_source": "../packages/components/src/textarea-control/README.md", + "parent": "components" + }, + { + "title": "ToggleControl", + "slug": "toggle-control", + "markdown_source": "../packages/components/src/toggle-control/README.md", + "parent": "components" + }, + { + "title": "Toolbar", + "slug": "toolbar", + "markdown_source": "../packages/components/src/toolbar/README.md", + "parent": "components" + }, + { + "title": "Tooltip", + "slug": "tooltip", + "markdown_source": "../packages/components/src/tooltip/README.md", + "parent": "components" + }, + { + "title": "TreeSelect", + "slug": "tree-select", + "markdown_source": "../packages/components/src/tree-select/README.md", + "parent": "components" + }, + { + "title": "Data Module Reference", + "slug": "data", + "markdown_source": "../docs/designers-developers/developers/data/README.md", + "parent": null + }, + { + "title": "WordPress Core Data", + "slug": "data-core", + "markdown_source": "../docs/designers-developers/developers/data/data-core.md", + "parent": "data" + }, + { + "title": "Annotations", + "slug": "data-core-annotations", + "markdown_source": "../docs/designers-developers/developers/data/data-core-annotations.md", + "parent": "data" + }, + { + "title": "Block Types Data", + "slug": "data-core-blocks", + "markdown_source": "../docs/designers-developers/developers/data/data-core-blocks.md", + "parent": "data" + }, + { + "title": "The Block Editor’s Data", + "slug": "data-core-block-editor", + "markdown_source": "../docs/designers-developers/developers/data/data-core-block-editor.md", + "parent": "data" + }, + { + "title": "The Post Editor’s Data", + "slug": "data-core-editor", + "markdown_source": "../docs/designers-developers/developers/data/data-core-editor.md", + "parent": "data" + }, + { + "title": "The Editor’s UI Data", + "slug": "data-core-edit-post", + "markdown_source": "../docs/designers-developers/developers/data/data-core-edit-post.md", + "parent": "data" + }, + { + "title": "Notices Data", + "slug": "data-core-notices", + "markdown_source": "../docs/designers-developers/developers/data/data-core-notices.md", + "parent": "data" + }, + { + "title": "The NUX (New User Experience) Data", + "slug": "data-core-nux", + "markdown_source": "../docs/designers-developers/developers/data/data-core-nux.md", + "parent": "data" + }, + { + "title": "The Viewport Data", + "slug": "data-core-viewport", + "markdown_source": "../docs/designers-developers/developers/data/data-core-viewport.md", + "parent": "data" + }, + { + "title": "Package Reference", + "slug": "packages", + "markdown_source": "../docs/designers-developers/developers/packages.md", + "parent": null + }, + { + "title": "@wordpress/a11y", + "slug": "packages-a11y", + "markdown_source": "../packages/a11y/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/annotations", + "slug": "packages-annotations", + "markdown_source": "../packages/annotations/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/api-fetch", + "slug": "packages-api-fetch", + "markdown_source": "../packages/api-fetch/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/autop", + "slug": "packages-autop", + "markdown_source": "../packages/autop/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/babel-plugin-import-jsx-pragma", + "slug": "packages-babel-plugin-import-jsx-pragma", + "markdown_source": "../packages/babel-plugin-import-jsx-pragma/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/babel-plugin-makepot", + "slug": "packages-babel-plugin-makepot", + "markdown_source": "../packages/babel-plugin-makepot/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/babel-preset-default", + "slug": "packages-babel-preset-default", + "markdown_source": "../packages/babel-preset-default/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/blob", + "slug": "packages-blob", + "markdown_source": "../packages/blob/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/block-editor", + "slug": "packages-block-editor", + "markdown_source": "../packages/block-editor/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/block-library", + "slug": "packages-block-library", + "markdown_source": "../packages/block-library/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/block-serialization-default-parser", + "slug": "packages-block-serialization-default-parser", + "markdown_source": "../packages/block-serialization-default-parser/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/block-serialization-spec-parser", + "slug": "packages-block-serialization-spec-parser", + "markdown_source": "../packages/block-serialization-spec-parser/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/blocks", + "slug": "packages-blocks", + "markdown_source": "../packages/blocks/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/browserslist-config", + "slug": "packages-browserslist-config", + "markdown_source": "../packages/browserslist-config/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/components", + "slug": "packages-components", + "markdown_source": "../packages/components/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/compose", + "slug": "packages-compose", + "markdown_source": "../packages/compose/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/core-data", + "slug": "packages-core-data", + "markdown_source": "../packages/core-data/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/custom-templated-path-webpack-plugin", + "slug": "packages-custom-templated-path-webpack-plugin", + "markdown_source": "../packages/custom-templated-path-webpack-plugin/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/data", + "slug": "packages-data", + "markdown_source": "../packages/data/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/date", + "slug": "packages-date", + "markdown_source": "../packages/date/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/dependency-extraction-webpack-plugin", + "slug": "packages-dependency-extraction-webpack-plugin", + "markdown_source": "../packages/dependency-extraction-webpack-plugin/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/deprecated", + "slug": "packages-deprecated", + "markdown_source": "../packages/deprecated/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/docgen", + "slug": "packages-docgen", + "markdown_source": "../packages/docgen/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/dom-ready", + "slug": "packages-dom-ready", + "markdown_source": "../packages/dom-ready/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/dom", + "slug": "packages-dom", + "markdown_source": "../packages/dom/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/e2e-test-utils", + "slug": "packages-e2e-test-utils", + "markdown_source": "../packages/e2e-test-utils/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/e2e-tests", + "slug": "packages-e2e-tests", + "markdown_source": "../packages/e2e-tests/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/edit-post", + "slug": "packages-edit-post", + "markdown_source": "../packages/edit-post/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/edit-widgets", + "slug": "packages-edit-widgets", + "markdown_source": "../packages/edit-widgets/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/editor", + "slug": "packages-editor", + "markdown_source": "../packages/editor/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/element", + "slug": "packages-element", + "markdown_source": "../packages/element/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/escape-html", + "slug": "packages-escape-html", + "markdown_source": "../packages/escape-html/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/eslint-plugin", + "slug": "packages-eslint-plugin", + "markdown_source": "../packages/eslint-plugin/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/format-library", + "slug": "packages-format-library", + "markdown_source": "../packages/format-library/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/hooks", + "slug": "packages-hooks", + "markdown_source": "../packages/hooks/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/html-entities", + "slug": "packages-html-entities", + "markdown_source": "../packages/html-entities/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/i18n", + "slug": "packages-i18n", + "markdown_source": "../packages/i18n/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/is-shallow-equal", + "slug": "packages-is-shallow-equal", + "markdown_source": "../packages/is-shallow-equal/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/jest-console", + "slug": "packages-jest-console", + "markdown_source": "../packages/jest-console/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/jest-preset-default", + "slug": "packages-jest-preset-default", + "markdown_source": "../packages/jest-preset-default/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/jest-puppeteer-axe", + "slug": "packages-jest-puppeteer-axe", + "markdown_source": "../packages/jest-puppeteer-axe/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/keycodes", + "slug": "packages-keycodes", + "markdown_source": "../packages/keycodes/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/library-export-default-webpack-plugin", + "slug": "packages-library-export-default-webpack-plugin", + "markdown_source": "../packages/library-export-default-webpack-plugin/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/list-reusable-blocks", + "slug": "packages-list-reusable-blocks", + "markdown_source": "../packages/list-reusable-blocks/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/notices", + "slug": "packages-notices", + "markdown_source": "../packages/notices/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/npm-package-json-lint-config", + "slug": "packages-npm-package-json-lint-config", + "markdown_source": "../packages/npm-package-json-lint-config/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/nux", + "slug": "packages-nux", + "markdown_source": "../packages/nux/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/plugins", + "slug": "packages-plugins", + "markdown_source": "../packages/plugins/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/postcss-themes", + "slug": "packages-postcss-themes", + "markdown_source": "../packages/postcss-themes/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/priority-queue", + "slug": "packages-priority-queue", + "markdown_source": "../packages/priority-queue/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/redux-routine", + "slug": "packages-redux-routine", + "markdown_source": "../packages/redux-routine/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/rich-text", + "slug": "packages-rich-text", + "markdown_source": "../packages/rich-text/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/scripts", + "slug": "packages-scripts", + "markdown_source": "../packages/scripts/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/shortcode", + "slug": "packages-shortcode", + "markdown_source": "../packages/shortcode/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/token-list", + "slug": "packages-token-list", + "markdown_source": "../packages/token-list/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/url", + "slug": "packages-url", + "markdown_source": "../packages/url/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/viewport", + "slug": "packages-viewport", + "markdown_source": "../packages/viewport/README.md", + "parent": "packages" + }, + { + "title": "@wordpress/wordcount", + "slug": "packages-wordcount", + "markdown_source": "../packages/wordcount/README.md", + "parent": "packages" + } +] \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md index 20f6d156253889..0f7817cd0b5a3d 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,22 +1,11 @@ -# Gutenberg Handbook +# Block Editor Handbook -The Gutenberg project provides three sources of documentation: +The Gutenberg project is transforming the way content is created on WordPress. A block editor was the first product launched creating a new methodology for working with content. This handbook provides documentation for how designers and developers can extend the editor, and also how you can start contributing to the project. -## Designer & Developer Handbook +![Gutenberg Demo](https://cldup.com/kZXGDcGPMU.gif) -Learn how to build blocks and extend the editor, best practices for designing block interfaces, and how to create themes that make the most of the new features Gutenberg provides. +Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created in the unit of blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block though). -Start with [the Designer & Developer Handbook](/docs/designers-developers/readme.md) to learn the background and concepts, or jump straight to [Gutenberg Tutorials](/docs/designers-developers/developers/tutorials/readme.md) for detailed examples. +Blocks treat Paragraphs, Headings, Media, Embeds all as components that strung together make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embedded media and shortcodes. The new editor is designed with progressive enhancement, meaning it is back-compatible with all legacy content, offers a process to try to convert and split a Classic block into block equivalents using client-side parsing and finally the blocks offer enhanced editing and format controls. -## User Handbook - -Discover the new features WordPress offers, learn how your site will be affected by the new editor and how to keep using the old interface, and tips for creating beautiful posts and pages. - -See [the WordPress Editor](https://wordpress.org/support/article/wordpress-editor/) support documentation. - - -## Contributor Handbook - -Help make Gutenberg better by contributing ideas, code, testing, and more. - -[Visit the Contributor Handbook](/docs/contributors/readme.md) +The Editor offers rich new value to users with visual, drag-and-drop creation tools and powerful developer enhancements with modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. diff --git a/docs/toc.json b/docs/toc.json index e8bbe9f0a6419e..5672ab70545ad5 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -1,117 +1,115 @@ [ - {"docs/readme.md": []}, - {"docs/designers-developers/readme.md": [ - {"docs/designers-developers/key-concepts.md": []}, - {"docs/designers-developers/developers/README.md": [ - {"docs/designers-developers/developers/block-api/README.md": [ - {"docs/designers-developers/developers/block-api/block-registration.md": []}, - {"docs/designers-developers/developers/block-api/block-edit-save.md": []}, - {"docs/designers-developers/developers/block-api/block-attributes.md": []}, - {"docs/designers-developers/developers/block-api/block-deprecation.md": []}, - {"docs/designers-developers/developers/block-api/block-templates.md": []}, - {"docs/designers-developers/developers/block-api/block-annotations.md": []} - ]}, - {"docs/designers-developers/developers/filters/README.md": [ - {"docs/designers-developers/developers/filters/block-filters.md": []}, - {"docs/designers-developers/developers/filters/editor-filters.md": []}, - {"docs/designers-developers/developers/filters/parser-filters.md": []}, - {"docs/designers-developers/developers/filters/autocomplete-filters.md": []} - ]}, - {"docs/designers-developers/developers/internationalization.md": []}, - {"docs/designers-developers/developers/accessibility.md": []}, - {"docs/designers-developers/developers/feature-flags.md": []}, - {"docs/designers-developers/developers/data/README.md": [ - {"docs/designers-developers/developers/data/data-core.md": []}, - {"docs/designers-developers/developers/data/data-core-annotations.md": []}, - {"docs/designers-developers/developers/data/data-core-blocks.md": []}, - {"docs/designers-developers/developers/data/data-core-block-editor.md": []}, - {"docs/designers-developers/developers/data/data-core-editor.md": []}, - {"docs/designers-developers/developers/data/data-core-edit-post.md": []}, - {"docs/designers-developers/developers/data/data-core-notices.md": []}, - {"docs/designers-developers/developers/data/data-core-nux.md": []}, - {"docs/designers-developers/developers/data/data-core-viewport.md": []} - ]}, - {"docs/designers-developers/developers/packages.md": "{{packages}}"}, - {"packages/components/README.md": "{{components}}"}, - {"docs/designers-developers/developers/themes/README.md": [ - {"docs/designers-developers/developers/themes/theme-support.md": []} - ]}, - {"docs/designers-developers/developers/backward-compatibility/README.md": [ - {"docs/designers-developers/developers/backward-compatibility/deprecations.md": []}, - {"docs/designers-developers/developers/backward-compatibility/meta-box.md": []} - ]}, - {"docs/designers-developers/developers/tutorials/readme.md": [ - {"docs/designers-developers/developers/tutorials/javascript/readme.md": [ - {"docs/designers-developers/developers/tutorials/javascript/plugins-background.md": []}, - {"docs/designers-developers/developers/tutorials/javascript/loading-javascript.md": []}, - {"docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md": []}, - {"docs/designers-developers/developers/tutorials/javascript/troubleshooting.md": []}, - {"docs/designers-developers/developers/tutorials/javascript/versions-and-building.md": []}, - {"docs/designers-developers/developers/tutorials/javascript/scope-your-code.md": []}, - {"docs/designers-developers/developers/tutorials/javascript/js-build-setup.md": []} - ]}, - {"docs/designers-developers/developers/tutorials/block-tutorial/readme.md" :[ - {"docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md" :[]}, - {"docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md" :[]}, - {"docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md" :[]}, - {"docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md" :[]}, - {"docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md" :[]}, - {"docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md" :[]} - ]}, - {"docs/designers-developers/developers/tutorials/metabox/readme.md": [ - {"docs/designers-developers/developers/tutorials/metabox/meta-block-1-intro.md": []}, - {"docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md": []}, - {"docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md": []}, - {"docs/designers-developers/developers/tutorials/metabox/meta-block-4-use-data.md": []}, - {"docs/designers-developers/developers/tutorials/metabox/meta-block-5-finishing.md": []} - ]}, - {"docs/designers-developers/developers/tutorials/notices/README.md": []}, - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md": [ - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md": []}, - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md": []}, - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md": []}, - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md": []}, - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md": []}, - {"docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md": []} - ]}, - {"docs/designers-developers/developers/tutorials/format-api/README.md": [ - {"docs/designers-developers/developers/tutorials/format-api/1-register-format.md": []}, - {"docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md": []}, - {"docs/designers-developers/developers/tutorials/format-api/3-apply-format.md": []} - ]} - ]} - ]}, - {"docs/designers-developers/designers/README.md": [ - {"docs/designers-developers/designers/block-design.md": []}, - {"docs/designers-developers/designers/design-patterns.md": []}, - {"docs/designers-developers/designers/design-resources.md": []}, - {"docs/designers-developers/designers/animation.md": []} - ]} - ]}, - {"docs/contributors/readme.md": [ - {"docs/contributors/principles.md": []}, - {"docs/contributors/design.md": [ - {"docs/contributors/principles/the-block.md": []}, - {"docs/contributors/reference.md": []} - ]}, - {"docs/contributors/develop.md": [ - {"docs/contributors/getting-started.md": []}, - {"docs/contributors/git-workflow.md": []}, - {"docs/contributors/coding-guidelines.md": []}, - {"docs/contributors/testing-overview.md": []}, - {"docs/contributors/grammar.md": []}, - {"docs/contributors/scripts.md": []}, - {"docs/contributors/managing-packages.md": []}, - {"docs/contributors/release.md": []}, - {"docs/contributors/localizing.md": []} - ]}, - {"docs/contributors/document.md": [ - {"docs/contributors/copy-guide.md": []} - ]}, - {"docs/contributors/history.md": []}, - {"docs/designers-developers/glossary.md": []}, - {"docs/designers-developers/faq.md": []}, - {"docs/contributors/repository-management.md": []}, - {"docs/contributors/outreach.md": []} - ]} + { "docs/readme.md": [] }, + { "docs/designers-developers/key-concepts.md": [] }, + { "docs/designers-developers/developers/README.md": [ + { "docs/designers-developers/developers/block-api/README.md": [ + { "docs/designers-developers/developers/block-api/block-registration.md": [] }, + { "docs/designers-developers/developers/block-api/block-edit-save.md": [] }, + { "docs/designers-developers/developers/block-api/block-attributes.md": [] }, + {"docs/designers-developers/developers/block-api/block-deprecation.md": [] }, + { "docs/designers-developers/developers/block-api/block-templates.md": [] }, + { "docs/designers-developers/developers/block-api/block-annotations.md": [] } + ] }, + { "docs/designers-developers/developers/filters/README.md": [ + { "docs/designers-developers/developers/filters/block-filters.md": [] }, + { "docs/designers-developers/developers/filters/editor-filters.md": [] }, + { "docs/designers-developers/developers/filters/parser-filters.md": [] }, + { "docs/designers-developers/developers/filters/autocomplete-filters.md": [] } + ] }, + { "docs/designers-developers/developers/internationalization.md": [] }, + { "docs/designers-developers/developers/accessibility.md": [] }, + { "docs/designers-developers/developers/feature-flags.md": [] }, + { "docs/designers-developers/developers/themes/README.md": [ + { "docs/designers-developers/developers/themes/theme-support.md": [] } + ] }, + { "docs/designers-developers/developers/backward-compatibility/README.md": [ + { "docs/designers-developers/developers/backward-compatibility/deprecations.md": [] }, + { "docs/designers-developers/developers/backward-compatibility/meta-box.md": [] } + ] } + ] }, + { "docs/designers-developers/designers/README.md": [ + { "docs/designers-developers/designers/block-design.md": [] }, + { "docs/designers-developers/designers/design-patterns.md": [] }, + { "docs/designers-developers/designers/design-resources.md": [] }, + { "docs/designers-developers/designers/animation.md": [] } + ] }, + { "docs/contributors/readme.md": [ + { "docs/contributors/principles.md": [] }, + { "docs/contributors/design.md": [ + { "docs/contributors/principles/the-block.md": [] }, + { "docs/contributors/reference.md": [] } + ] }, + { "docs/contributors/develop.md": [ + { "docs/contributors/getting-started.md": [] }, + { "docs/contributors/git-workflow.md": [] }, + { "docs/contributors/coding-guidelines.md": [] }, + { "docs/contributors/testing-overview.md": [] }, + { "docs/contributors/grammar.md": [] }, + { "docs/contributors/scripts.md": [] }, + { "docs/contributors/managing-packages.md": [] }, + { "docs/contributors/release.md": [] }, + { "docs/contributors/localizing.md": [] } + ] }, + { "docs/contributors/document.md": [ + { "docs/contributors/copy-guide.md": [] } + ] }, + { "docs/contributors/history.md": [] }, + { "docs/designers-developers/glossary.md": [] }, + { "docs/designers-developers/faq.md": [] }, + { "docs/contributors/repository-management.md": [] }, + { "docs/contributors/outreach.md": [] } + ] }, + { "docs/designers-developers/developers/tutorials/readme.md": [ + { "docs/designers-developers/developers/tutorials/javascript/readme.md": [ + { "docs/designers-developers/developers/tutorials/javascript/plugins-background.md": [] }, + { "docs/designers-developers/developers/tutorials/javascript/loading-javascript.md": [] }, + { "docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md": [] }, + { "docs/designers-developers/developers/tutorials/javascript/troubleshooting.md": [] }, + { "docs/designers-developers/developers/tutorials/javascript/versions-and-building.md": [] }, + { "docs/designers-developers/developers/tutorials/javascript/scope-your-code.md": [] }, + { "docs/designers-developers/developers/tutorials/javascript/js-build-setup.md": [] } + ] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/readme.md": [ + { "docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md": [] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md": [] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md": [] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md": [] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md": [] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md": [] } + ] }, + { "docs/designers-developers/developers/tutorials/metabox/readme.md": [ + { "docs/designers-developers/developers/tutorials/metabox/meta-block-1-intro.md": [] }, + { "docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md": [] }, + { "docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md": [] }, + { "docs/designers-developers/developers/tutorials/metabox/meta-block-4-use-data.md": [] }, + { "docs/designers-developers/developers/tutorials/metabox/meta-block-5-finishing.md": [] } + ] }, + { "docs/designers-developers/developers/tutorials/notices/README.md": [] }, + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md": [ + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md": [] }, + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md": [] }, + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md": [] }, + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md": [] }, + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md": [] }, + { "docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md": [] } + ] }, + { "docs/designers-developers/developers/tutorials/format-api/README.md": [ + { "docs/designers-developers/developers/tutorials/format-api/1-register-format.md": [] }, + { "docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md": [] }, + { "docs/designers-developers/developers/tutorials/format-api/3-apply-format.md": [] } + ] } + ] }, + { "packages/components/README.md": "{{components}}" }, + { "docs/designers-developers/developers/data/README.md": [ + { "docs/designers-developers/developers/data/data-core.md": []}, + { "docs/designers-developers/developers/data/data-core-annotations.md": [] }, + { "docs/designers-developers/developers/data/data-core-blocks.md": [] }, + { "docs/designers-developers/developers/data/data-core-block-editor.md": [] }, + { "docs/designers-developers/developers/data/data-core-editor.md": [] }, + { "docs/designers-developers/developers/data/data-core-edit-post.md": [] }, + { "docs/designers-developers/developers/data/data-core-notices.md": [] }, + { "docs/designers-developers/developers/data/data-core-nux.md": [] }, + { "docs/designers-developers/developers/data/data-core-viewport.md": [] } + ] }, + { "docs/designers-developers/developers/packages.md": "{{packages}}" } ] diff --git a/docs/tool/index.js b/docs/tool/index.js index c75767b3d85d96..bb1ffdc599b104 100644 --- a/docs/tool/index.js +++ b/docs/tool/index.js @@ -12,7 +12,7 @@ const path = require( 'path' ); const { getRootManifest } = require( './manifest' ); const tocFileInput = path.resolve( __dirname, '../toc.json' ); -const manifestOutput = path.resolve( __dirname, '../manifest.json' ); +const manifestOutput = path.resolve( __dirname, '../manifest-devhub.json' ); // Update data files from code execSync( join( __dirname, 'update-data.js' ) ); diff --git a/docs/tool/manifest.js b/docs/tool/manifest.js index cb5ae4deaf17c5..80537983bf0112 100644 --- a/docs/tool/manifest.js +++ b/docs/tool/manifest.js @@ -5,7 +5,7 @@ const { camelCase, nth, upperFirst } = require( 'lodash' ); const fs = require( 'fs' ); const glob = require( 'glob' ).sync; -const baseRepoUrl = `https://raw.githubusercontent.com/WordPress/gutenberg/master`; +const baseRepoUrl = '..'; const componentPaths = glob( 'packages/components/src/*/**/README.md' ); const packagePaths = glob( 'packages/*/package.json' ).map( ( fileName ) => fileName.split( '/' )[ 1 ] diff --git a/packages/components/README.md b/packages/components/README.md index b9dd9874a5cab1..fb82b7f526f860 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -1,4 +1,4 @@ -# Components +# Component Reference This packages includes a library of generic WordPress components to be used for creating common UI elements shared between screens and features of the WordPress dashboard. From bbb37b16ced8827b6a3dedd9136c86a43528f17f Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Tue, 14 May 2019 13:06:56 +0300 Subject: [PATCH 24/34] Accessibility: Fixed links as buttons focus state in:not(Chrome) browsers. (#15601) --- packages/components/src/button/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 5eeb1fa0f8a559..1d44f3bb9cfcb2 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -173,7 +173,7 @@ opacity: 0.3; } - &:focus:enabled { + &:focus:not(:disabled) { @include button-style__focus-active; } From 5963776099b0659b79ca62d94dd89ffd74ab7b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 14 May 2019 12:14:21 +0200 Subject: [PATCH 25/34] Input Interaction: set caret correctly on merge forward (Delete) (#15599) * Input Interaction: set caret correctly on merge forward (Delete) * Reset selected block attrs --- packages/block-editor/src/store/effects.js | 79 +++++++++++-------- .../__snapshots__/writing-flow.test.js.snap | 6 ++ packages/e2e-tests/specs/writing-flow.test.js | 12 +++ 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/block-editor/src/store/effects.js b/packages/block-editor/src/store/effects.js index 0f5d359b89f9f0..7cd87ee731114c 100644 --- a/packages/block-editor/src/store/effects.js +++ b/packages/block-editor/src/store/effects.js @@ -70,8 +70,8 @@ export default { MERGE_BLOCKS( action, store ) { const { dispatch } = store; const state = store.getState(); - const [ firstBlockClientId, secondBlockClientId ] = action.blocks; - const blockA = getBlock( state, firstBlockClientId ); + const [ clientIdA, clientIdB ] = action.blocks; + const blockA = getBlock( state, clientIdA ); const blockAType = getBlockType( blockA.name ); // Only focus the previous block if it's not mergeable @@ -80,27 +80,33 @@ export default { return; } - // We can only merge blocks with similar types - // thus, we transform the block to merge first - const blockB = getBlock( state, secondBlockClientId ); + const blockB = getBlock( state, clientIdB ); const blockBType = getBlockType( blockB.name ); + const { clientId, attributeKey, offset } = getSelectionStart( state ); + const hasSelection = clientId === clientIdA || clientId === clientIdB; + const selectedBlock = clientId === clientIdA ? blockA : blockB; + const html = selectedBlock.attributes[ attributeKey ]; // A robust way to retain selection position through various transforms // is to insert a special character at the position and then recover it. const START_OF_SELECTED_AREA = '\u0086'; - const { attributeKey, offset } = getSelectionStart( state ); - const html = blockB.attributes[ attributeKey ]; - const multilineTagB = blockBType.attributes[ attributeKey ].multiline; - const value = insert( create( { - html, - multilineTag: multilineTagB, - } ), START_OF_SELECTED_AREA, offset, offset ); - - blockB.attributes[ attributeKey ] = toHTMLString( { - value, - multilineTag: multilineTagB, - } ); + if ( hasSelection ) { + const selectedBlockType = clientId === clientIdA ? blockAType : blockBType; + const multilineTag = selectedBlockType.attributes[ attributeKey ].multiline; + const value = insert( create( { + html, + multilineTag, + } ), START_OF_SELECTED_AREA, offset, offset ); + + selectedBlock.attributes[ attributeKey ] = toHTMLString( { + value, + multilineTag, + } ); + } + + // We can only merge blocks with similar types + // thus, we transform the block to merge first const blocksWithTheSameType = blockA.name === blockB.name ? [ blockB ] : switchToBlockType( blockB, blockA.name ); @@ -116,24 +122,27 @@ export default { blocksWithTheSameType[ 0 ].attributes ); - const newAttributeKey = findKey( updatedAttributes, ( v ) => - typeof v === 'string' && v.indexOf( START_OF_SELECTED_AREA ) !== -1 - ); - const convertedHtml = updatedAttributes[ newAttributeKey ]; - const multilineTagA = blockAType.attributes[ newAttributeKey ].multiline; - const convertedValue = create( { html: convertedHtml, multilineTag: multilineTagA } ); - const newOffset = convertedValue.text.indexOf( START_OF_SELECTED_AREA ); - const newValue = remove( convertedValue, newOffset, newOffset + 1 ); - const newHtml = toHTMLString( { value: newValue, multilineTag: multilineTagA } ); - - updatedAttributes[ newAttributeKey ] = newHtml; - - dispatch( selectionChange( - blockA.clientId, - newAttributeKey, - newOffset, - newOffset - ) ); + if ( hasSelection ) { + const newAttributeKey = findKey( updatedAttributes, ( v ) => + typeof v === 'string' && v.indexOf( START_OF_SELECTED_AREA ) !== -1 + ); + const convertedHtml = updatedAttributes[ newAttributeKey ]; + const multilineTag = blockAType.attributes[ newAttributeKey ].multiline; + const convertedValue = create( { html: convertedHtml, multilineTag } ); + const newOffset = convertedValue.text.indexOf( START_OF_SELECTED_AREA ); + const newValue = remove( convertedValue, newOffset, newOffset + 1 ); + const newHtml = toHTMLString( { value: newValue, multilineTag } ); + + updatedAttributes[ newAttributeKey ] = newHtml; + selectedBlock.attributes[ attributeKey ] = html; + + dispatch( selectionChange( + blockA.clientId, + newAttributeKey, + newOffset, + newOffset + ) ); + } dispatch( replaceBlocks( [ blockA.clientId, blockB.clientId ], diff --git a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap index 0e41868f3cffa7..27806248a2a8a3 100644 --- a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap @@ -100,6 +100,12 @@ exports[`adding blocks should insert line break mid text 1`] = ` " `; +exports[`adding blocks should merge forwards 1`] = ` +" +

123

+" +`; + exports[`adding blocks should navigate around inline boundaries 1`] = ` "

FirstAfter

diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index fe801a3617284d..532e50d4e677db 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -355,4 +355,16 @@ describe( 'adding blocks', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should merge forwards', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '3' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'Delete' ); + await page.keyboard.type( '2' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From c64faf4902dfeda9eb33301b12b4214da854d101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 14 May 2019 13:13:31 +0200 Subject: [PATCH 26/34] RichText: fix RTL keyboard interactions (#15496) --- .../src/components/rich-text/index.js | 163 +++++++++++------- .../src/components/writing-flow/index.js | 13 +- packages/dom/src/dom.js | 8 +- .../__snapshots__/rich-text.test.js.snap | 6 + .../specs/__snapshots__/rtl.test.js.snap | 63 +++++++ packages/e2e-tests/specs/rich-text.test.js | 15 ++ packages/e2e-tests/specs/rtl.test.js | 122 +++++++++++++ .../rich-text/src/register-format-type.js | 17 +- 8 files changed, 329 insertions(+), 78 deletions(-) create mode 100644 packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap create mode 100644 packages/e2e-tests/specs/rtl.test.js diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 90b10613f86229..541ac81ec7a1fb 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -8,7 +8,6 @@ import { omit, pickBy, } from 'lodash'; -import memize from 'memize'; /** * WordPress dependencies @@ -88,9 +87,9 @@ const globalStyle = document.createElement( 'style' ); document.head.appendChild( globalStyle ); -function createPrepareEditableTree( props ) { +function createPrepareEditableTree( props, prefix ) { const fns = Object.keys( props ).reduce( ( accumulator, key ) => { - if ( key.startsWith( 'format_prepare_functions' ) ) { + if ( key.startsWith( prefix ) ) { accumulator.push( props[ key ] ); } @@ -145,11 +144,6 @@ export class RichText extends Component { this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this ); this.onPointerDown = this.onPointerDown.bind( this ); - this.formatToValue = memize( - this.formatToValue.bind( this ), - { maxSize: 1 } - ); - this.patterns = getPatterns( { onReplace, valueToFormat: this.valueToFormat, @@ -162,10 +156,11 @@ export class RichText extends Component { this.usedDeprecatedChildrenSource = Array.isArray( value ); this.lastHistoryValue = value; - // Internal values that are update synchronously, unlike props. + // Internal values are updated synchronously, unlike props and state. this.value = value; - this.selectionStart = selectionStart; - this.selectionEnd = selectionEnd; + this.record = this.formatToValue( value ); + this.record.start = selectionStart; + this.record.end = selectionEnd; } componentWillUnmount() { @@ -195,11 +190,7 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { value, selectionStart: start, selectionEnd: end } = this.props; - const { formats, replacements, text } = this.formatToValue( value ); - const { activeFormats } = this.state; - - return { formats, replacements, text, start, end, activeFormats }; + return this.record; } createRecord() { @@ -211,7 +202,6 @@ export class RichText extends Component { range, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, - prepareEditableTree: createPrepareEditableTree( this.props ), __unstableIsEditableTree: true, } ); } @@ -222,13 +212,13 @@ export class RichText extends Component { current: this.editableRef, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, - prepareEditableTree: createPrepareEditableTree( this.props ), + prepareEditableTree: createPrepareEditableTree( this.props, 'format_prepare_functions' ), __unstableDomOnly: domOnly, } ); } isEmpty() { - return isEmpty( this.formatToValue( this.props.value ) ); + return isEmpty( this.record ); } /** @@ -378,7 +368,20 @@ export class RichText extends Component { } this.recalculateBoundaryStyle(); - this.onSelectionChange(); + + // We know for certain that on focus, the old selection is invalid. It + // will be recalculated on `selectionchange`. + const index = undefined; + const activeFormats = undefined; + + this.record = { + ...this.record, + start: index, + end: index, + activeFormats, + }; + this.props.onSelectionChange( index, index ); + this.setState( { activeFormats } ); document.addEventListener( 'selectionchange', this.onSelectionChange ); } @@ -419,8 +422,7 @@ export class RichText extends Component { } const value = this.createRecord(); - const { activeFormats = [] } = this.state; - const start = this.selectionStart; + const { start, activeFormats = [] } = this.record; // Update the formats between the last and new caret position. const change = updateFormats( { @@ -459,12 +461,23 @@ export class RichText extends Component { * Handles the `selectionchange` event: sync the selection to local state. */ onSelectionChange() { - const value = this.createRecord(); - const { start, end } = value; + const { start, end } = this.createRecord(); + const value = this.getRecord(); - if ( start !== this.selectionStart || end !== this.selectionEnd ) { + if ( start !== value.start || end !== value.end ) { const { isCaretWithinFormattedText } = this.props; - const activeFormats = getActiveFormats( value ); + const newValue = { + ...value, + start, + end, + // Allow `getActiveFormats` to get new `activeFormats`. + activeFormats: undefined, + }; + + const activeFormats = getActiveFormats( newValue ); + + // Update the value with the new active formats. + newValue.activeFormats = activeFormats; if ( ! isCaretWithinFormattedText && activeFormats.length ) { this.props.onEnterFormattedText(); @@ -472,11 +485,12 @@ export class RichText extends Component { this.props.onExitFormattedText(); } - this.setState( { activeFormats } ); - this.applyRecord( { ...value, activeFormats }, { domOnly: true } ); + // It is important that the internal value is updated first, + // otherwise the value will be wrong on render! + this.record = newValue; + this.applyRecord( newValue, { domOnly: true } ); this.props.onSelectionChange( start, end ); - this.selectionStart = start; - this.selectionEnd = end; + this.setState( { activeFormats } ); if ( activeFormats.length > 0 ) { this.recalculateBoundaryStyle(); @@ -521,11 +535,10 @@ export class RichText extends Component { } ); this.value = this.valueToFormat( record ); + this.record = record; this.props.onChange( this.value ); - this.setState( { activeFormats } ); this.props.onSelectionChange( start, end ); - this.selectionStart = start; - this.selectionEnd = end; + this.setState( { activeFormats } ); if ( ! withoutHistory ) { this.onCreateUndoLevel(); @@ -739,11 +752,13 @@ export class RichText extends Component { * @param {SyntheticEvent} event A synthetic keyboard event. */ handleHorizontalNavigation( event ) { - const value = this.createRecord(); - const { formats, text, start, end } = value; - const { activeFormats = [] } = this.state; + const value = this.getRecord(); + const { text, formats, start, end, activeFormats = [] } = value; const collapsed = isCollapsed( value ); - const isReverse = event.keyCode === LEFT; + // To do: ideally, we should look at visual position instead. + const { direction } = getComputedStyle( this.editableRef ); + const reverseKey = direction === 'rtl' ? RIGHT : LEFT; + const isReverse = event.keyCode === reverseKey; // If the selection is collapsed and at the very start, do nothing if // navigating backward. @@ -804,24 +819,26 @@ export class RichText extends Component { if ( newActiveFormatsLength !== activeFormats.length ) { const newActiveFormats = source.slice( 0, newActiveFormatsLength ); - this.applyRecord( { ...value, activeFormats: newActiveFormats } ); + const newValue = { ...value, activeFormats: newActiveFormats }; + this.record = newValue; + this.applyRecord( newValue ); this.setState( { activeFormats: newActiveFormats } ); return; } const newPos = value.start + ( isReverse ? -1 : 1 ); - const newActiveFormats = isReverse ? formatsBefore.length : formatsAfter.length; - - this.setState( { selectedFormat: newActiveFormats } ); - this.props.onSelectionChange( newPos, newPos ); - this.selectionStart = newPos; - this.selectionEnd = newPos; - this.applyRecord( { + const newActiveFormats = isReverse ? formatsBefore : formatsAfter; + const newValue = { ...value, start: newPos, end: newPos, activeFormats: newActiveFormats, - } ); + }; + + this.record = newValue; + this.applyRecord( newValue ); + this.props.onSelectionChange( newPos, newPos ); + this.setState( { activeFormats: newActiveFormats } ); } /** @@ -898,8 +915,7 @@ export class RichText extends Component { } componentDidUpdate( prevProps ) { - const { tagName, value, isSelected } = this.props; - const record = this.getRecord(); + const { tagName, value, selectionStart, selectionEnd, isSelected } = this.props; // Check if the content changed. let shouldReapply = ( @@ -911,8 +927,8 @@ export class RichText extends Component { // Check if the selection changed. shouldReapply = shouldReapply || ( isSelected && ! prevProps.isSelected && ( - this.selectionStart !== record.start || - this.selectionEnd !== record.end + this.record.start !== selectionStart || + this.record.end !== selectionEnd ) ); @@ -925,18 +941,32 @@ export class RichText extends Component { shouldReapply = shouldReapply || ! isShallowEqual( prepareProps, prevPrepareProps ); + const { activeFormats = [] } = this.record; + if ( shouldReapply ) { - if ( ! isSelected ) { - delete record.start; - delete record.end; - } + this.value = value; + this.record = this.formatToValue( value ); + this.record.start = selectionStart; + this.record.end = selectionEnd; + + updateFormats( { + value: this.record, + start: this.record.start, + end: this.record.end, + formats: activeFormats, + } ); - this.applyRecord( record ); + this.applyRecord( this.record ); + } else if ( + this.record.start !== selectionStart || + this.record.end !== selectionEnd + ) { + this.record = { + ...this.record, + start: selectionStart, + end: selectionEnd, + }; } - - this.value = value; - this.selectionStart = record.start; - this.selectionEnd = record.end; } /** @@ -948,19 +978,20 @@ export class RichText extends Component { formatToValue( value ) { // Handle deprecated `children` and `node` sources. if ( Array.isArray( value ) ) { - return create( { - html: children.toHTML( value ), - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, - } ); + value = children.toHTML( value ); } if ( this.props.format === 'string' ) { - return create( { + const prepare = createPrepareEditableTree( this.props, 'format_value_functions' ); + + value = create( { html: value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ); + value.formats = prepare( value ); + + return value; } // Guard for blocks passing `null` in onSplit callbacks. May be removed @@ -976,7 +1007,7 @@ export class RichText extends Component { return toDom( { value, multilineTag: this.multilineTag, - prepareEditableTree: createPrepareEditableTree( this.props ), + prepareEditableTree: createPrepareEditableTree( this.props, 'format_prepare_functions' ), } ).body.innerHTML; } diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index 1acb85b1695584..656ac5b4ed49e7 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -34,7 +34,7 @@ import { * Browser constants */ -const { getSelection } = window; +const { getSelection, getComputedStyle } = window; /** * Given an element, returns true if the element is a tabbable text field, or @@ -287,6 +287,11 @@ class WritingFlow extends Component { this.verticalRect = computeCaretRect(); } + // In the case of RTL scripts, right means previous and left means next, + // which is the exact reverse of LTR. + const { direction } = getComputedStyle( target ); + const isReverseDir = direction === 'rtl' ? ( ! isReverse ) : isReverse; + if ( isShift ) { if ( ( @@ -316,9 +321,9 @@ class WritingFlow extends Component { placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect ); event.preventDefault(); } - } else if ( isHorizontal && getSelection().isCollapsed && isHorizontalEdge( target, isReverse ) ) { - const closestTabbable = this.getClosestTabbable( target, isReverse ); - placeCaretAtHorizontalEdge( closestTabbable, isReverse ); + } else if ( isHorizontal && getSelection().isCollapsed && isHorizontalEdge( target, isReverseDir ) ) { + const closestTabbable = this.getClosestTabbable( target, isReverseDir ); + placeCaretAtHorizontalEdge( closestTabbable, isReverseDir ); event.preventDefault(); } } diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js index b676448ec4424f..64fb97534f60a2 100644 --- a/packages/dom/src/dom.js +++ b/packages/dom/src/dom.js @@ -143,12 +143,16 @@ function isEdge( container, isReverse, onlyVertical ) { return true; } + // In the case of RTL scripts, the horizontal edge is at the opposite side. + const { direction } = computedStyle; + const isReverseDir = direction === 'rtl' ? ( ! isReverse ) : isReverse; + // To calculate the horizontal position, we insert a test range and see if // this test range has the same horizontal position. This method proves to // be better than a DOM-based calculation, because it ignores empty text // nodes and a trailing line break element. In other words, we need to check // visual positioning, not DOM positioning. - const x = isReverse ? containerRect.left + 1 : containerRect.right - 1; + const x = isReverseDir ? containerRect.left + 1 : containerRect.right - 1; const y = isReverse ? containerRect.top + buffer : containerRect.bottom - buffer; const testRange = hiddenCaretRangeFromPoint( document, x, y, container ); @@ -156,7 +160,7 @@ function isEdge( container, isReverse, onlyVertical ) { return false; } - const side = isReverse ? 'left' : 'right'; + const side = isReverseDir ? 'left' : 'right'; const testRect = getRectangleFromRange( testRange ); return Math.round( testRect[ side ] ) === Math.round( rangeRect[ side ] ); diff --git a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap index bf5fefd31a7e32..8aab0dcd5b734b 100644 --- a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap @@ -24,6 +24,12 @@ exports[`RichText should apply multiple formats when selection is collapsed 1`] " `; +exports[`RichText should handle Home and End keys 1`] = ` +" +

-12+

+" +`; + exports[`RichText should handle change in tag name gracefully 1`] = ` "

diff --git a/packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap b/packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap new file mode 100644 index 00000000000000..19b4e8c305acc2 --- /dev/null +++ b/packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RTL should arrow navigate 1`] = ` +" +

٠١٢

+" +`; + +exports[`RTL should arrow navigate between blocks 1`] = ` +" +

٠
١

+ + + +

٠
١
٢

+" +`; + +exports[`RTL should merge backward 1`] = ` +" +

٠١

+" +`; + +exports[`RTL should merge forward 1`] = ` +" +

٠١

+" +`; + +exports[`RTL should navigate inline boundaries 1`] = ` +" +

١٠٢

+" +`; + +exports[`RTL should navigate inline boundaries 2`] = ` +" +

١٠٢

+" +`; + +exports[`RTL should navigate inline boundaries 3`] = ` +" +

٠١٢

+" +`; + +exports[`RTL should navigate inline boundaries 4`] = ` +" +

٠١٢

+" +`; + +exports[`RTL should split 1`] = ` +" +

٠

+ + + +

١

+" +`; diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js index dcb1434abd89be..1bd6d79c1b10fc 100644 --- a/packages/e2e-tests/specs/rich-text.test.js +++ b/packages/e2e-tests/specs/rich-text.test.js @@ -208,4 +208,19 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should handle Home and End keys', async () => { + await page.keyboard.press( 'Enter' ); + + await pressKeyWithModifier( 'primary', 'b' ); + await page.keyboard.type( '12' ); + await pressKeyWithModifier( 'primary', 'b' ); + + await page.keyboard.press( 'Home' ); + await page.keyboard.type( '-' ); + await page.keyboard.press( 'End' ); + await page.keyboard.type( '+' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/e2e-tests/specs/rtl.test.js b/packages/e2e-tests/specs/rtl.test.js new file mode 100644 index 00000000000000..76e6d15ff9aa30 --- /dev/null +++ b/packages/e2e-tests/specs/rtl.test.js @@ -0,0 +1,122 @@ +/** + * WordPress dependencies + */ +import { + createNewPost, + getEditedPostContent, + pressKeyWithModifier, +} from '@wordpress/e2e-test-utils'; + +// Avoid using three, as it looks too much like two with some fonts. +const ARABIC_ZERO = '٠'; +const ARABIC_ONE = '١'; +const ARABIC_TWO = '٢'; + +describe( 'RTL', () => { + beforeEach( async () => { + await createNewPost(); + } ); + + it( 'should arrow navigate', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + // We need at least three characters as arrow navigation *from* the + // edges might be handled differently. + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.type( ARABIC_TWO ); + await page.keyboard.press( 'ArrowRight' ); + // This is the important key press: arrow nav *from* the middle. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ARABIC_ZERO ); + + // Expect: ARABIC_ZERO + ARABIC_ONE + ARABIC_TWO (

٠١٢

). + // N.b.: HTML is LTR, so direction will be reversed! + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should split', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should merge backward', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should merge forward', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Delete' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should arrow navigate between blocks', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + await pressKeyWithModifier( 'shift', 'Enter' ); + await page.keyboard.type( ARABIC_TWO ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + + // Move to the previous block with two lines in the current block. + await page.keyboard.press( 'ArrowRight' ); + await pressKeyWithModifier( 'shift', 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + + // Move to the next block with two lines in the current block. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( ARABIC_ZERO ); + await pressKeyWithModifier( 'shift', 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should navigate inline boundaries', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await pressKeyWithModifier( 'primary', 'b' ); + await page.keyboard.type( ARABIC_ONE ); + await pressKeyWithModifier( 'primary', 'b' ); + await page.keyboard.type( ARABIC_TWO ); + + // Insert a character at each boundary position. + for ( let i = 4; i > 0; i-- ) { + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ARABIC_ZERO ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + } + } ); +} ); diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js index eb6d48c9e56368..e6efc99f5fec97 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.js @@ -146,18 +146,23 @@ export function registerFormatType( name, settings ) { blockClientId: props.clientId, }; - newProps[ `format_prepare_functions_(${ name })` ] = - settings.__experimentalCreatePrepareEditableTree( - propsByPrefix, - args - ); - if ( settings.__experimentalCreateOnChangeEditableValue ) { + newProps[ `format_value_functions_(${ name })` ] = + settings.__experimentalCreatePrepareEditableTree( + propsByPrefix, + args + ); newProps[ `format_on_change_functions_(${ name })` ] = settings.__experimentalCreateOnChangeEditableValue( propsByPrefix, args ); + } else { + newProps[ `format_prepare_functions_(${ name })` ] = + settings.__experimentalCreatePrepareEditableTree( + propsByPrefix, + args + ); } return ; From 13a6b263d6dcc765152bf894533f9a0debcd0ca9 Mon Sep 17 00:00:00 2001 From: Thorsten Frommen Date: Tue, 14 May 2019 13:45:34 +0200 Subject: [PATCH 27/34] Remove (non-special) comment nodes when pasting content (#15557) --- .../src/api/raw-handling/comment-remover.js | 21 +++++++++ .../src/api/raw-handling/paste-handler.js | 4 +- .../api/raw-handling/test/comment-remover.js | 43 +++++++++++++++++++ test/integration/blocks-raw-handling.spec.js | 2 + test/integration/fixtures/google-docs-in.html | 2 +- .../fixtures/google-docs-table-in.html | 2 +- .../google-docs-table-with-comments-in.html | 4 ++ .../google-docs-table-with-comments-out.html | 3 ++ .../google-docs-with-comments-in.html | 4 ++ .../google-docs-with-comments-out.html | 35 +++++++++++++++ 10 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 packages/blocks/src/api/raw-handling/comment-remover.js create mode 100644 packages/blocks/src/api/raw-handling/test/comment-remover.js create mode 100644 test/integration/fixtures/google-docs-table-with-comments-in.html create mode 100644 test/integration/fixtures/google-docs-table-with-comments-out.html create mode 100644 test/integration/fixtures/google-docs-with-comments-in.html create mode 100644 test/integration/fixtures/google-docs-with-comments-out.html diff --git a/packages/blocks/src/api/raw-handling/comment-remover.js b/packages/blocks/src/api/raw-handling/comment-remover.js new file mode 100644 index 00000000000000..fbfa319ccb8316 --- /dev/null +++ b/packages/blocks/src/api/raw-handling/comment-remover.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { remove } from '@wordpress/dom'; + +/** + * Browser dependencies + */ +const { COMMENT_NODE } = window.Node; + +/** + * Looks for comments, and removes them. + * + * @param {Node} node The node to be processed. + * @return {void} + */ +export default function( node ) { + if ( node.nodeType === COMMENT_NODE ) { + remove( node ); + } +} diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js index 1d67540c0b9225..c99b4ea5c5d0a2 100644 --- a/packages/blocks/src/api/raw-handling/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/paste-handler.js @@ -11,6 +11,7 @@ import { getBlockContent } from '../serializer'; import { getBlockAttributes, parseWithGrammar } from '../parser'; import normaliseBlocks from './normalise-blocks'; import specialCommentConverter from './special-comment-converter'; +import commentRemover from './comment-remover'; import isInlineContent from './is-inline-content'; import phrasingContentReducer from './phrasing-content-reducer'; import headRemover from './head-remover'; @@ -44,7 +45,7 @@ const { console } = window; * @return {string} HTML only containing phrasing content. */ function filterInlineHTML( HTML ) { - HTML = deepFilterHTML( HTML, [ googleDocsUIDRemover, phrasingContentReducer ] ); + HTML = deepFilterHTML( HTML, [ googleDocsUIDRemover, phrasingContentReducer, commentRemover ] ); HTML = removeInvalidHTML( HTML, getPhrasingContentSchema(), { inline: true } ); // Allows us to ask for this information when we get a report. @@ -204,6 +205,7 @@ export function pasteHandler( { HTML = '', plainText = '', mode = 'AUTO', tagNam imageCorrector, phrasingContentReducer, specialCommentConverter, + commentRemover, figureContentReducer, blockquoteNormaliser, ]; diff --git a/packages/blocks/src/api/raw-handling/test/comment-remover.js b/packages/blocks/src/api/raw-handling/test/comment-remover.js new file mode 100644 index 00000000000000..6721e20bec23b6 --- /dev/null +++ b/packages/blocks/src/api/raw-handling/test/comment-remover.js @@ -0,0 +1,43 @@ +/** + * Internal dependencies + */ +import commentRemover from '../comment-remover'; +import { deepFilterHTML } from '../utils'; + +describe( 'commentRemover', () => { + it( 'should remove a single comment', () => { + expect( deepFilterHTML( + '', + [ commentRemover ] + ) ).toEqual( + '' + ); + } ); + it( 'should remove multiple comments', () => { + expect( deepFilterHTML( + '

First paragraph.

Second paragraph.

', + [ commentRemover ] + ) ).toEqual( + '

First paragraph.

Second paragraph.

' + ); + } ); + it( 'should remove nested comments', () => { + expect( deepFilterHTML( + '

Paragraph.

', + [ commentRemover ] + ) ).toEqual( + '

Paragraph.

' + ); + } ); + it( 'should remove multi-line comments', () => { + expect( deepFilterHTML( + `

First paragraph.

Second paragraph.

`, + [ commentRemover ] + ) ).toEqual( + '

First paragraph.

Second paragraph.

' + ); + } ); +} ); diff --git a/test/integration/blocks-raw-handling.spec.js b/test/integration/blocks-raw-handling.spec.js index 83315ba0d38a68..fda576a042cbb3 100644 --- a/test/integration/blocks-raw-handling.spec.js +++ b/test/integration/blocks-raw-handling.spec.js @@ -236,6 +236,8 @@ describe( 'Blocks raw handling', () => { 'apple', 'google-docs', 'google-docs-table', + 'google-docs-table-with-comments', + 'google-docs-with-comments', 'ms-word', 'ms-word-styled', 'ms-word-online', diff --git a/test/integration/fixtures/google-docs-in.html b/test/integration/fixtures/google-docs-in.html index c667fddceab447..e70f79825bf305 100644 --- a/test/integration/fixtures/google-docs-in.html +++ b/test/integration/fixtures/google-docs-in.html @@ -1 +1 @@ -

This is a title


This is a heading


Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.


  • A

  • Bulleted

    • Indented

  • List


  1. One

  2. Two

  3. Three


One

Two

Three

1

2

3

I

II

III




An image:



\ No newline at end of file +

This is a title


This is a heading


Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.


  • A

  • Bulleted

    • Indented

  • List


  1. One

  2. Two

  3. Three


One

Two

Three

1

2

3

I

II

III




An image:



\ No newline at end of file diff --git a/test/integration/fixtures/google-docs-table-in.html b/test/integration/fixtures/google-docs-table-in.html index 8a6b117fa6ed55..ad03a2dff05172 100644 --- a/test/integration/fixtures/google-docs-table-in.html +++ b/test/integration/fixtures/google-docs-table-in.html @@ -1 +1 @@ -

One

Two

Three

1

2

3

I

II

III

+

One

Two

Three

1

2

3

I

II

III

\ No newline at end of file diff --git a/test/integration/fixtures/google-docs-table-with-comments-in.html b/test/integration/fixtures/google-docs-table-with-comments-in.html new file mode 100644 index 00000000000000..a23b54c17a5e33 --- /dev/null +++ b/test/integration/fixtures/google-docs-table-with-comments-in.html @@ -0,0 +1,4 @@ + +

One

Two

Three

1

2

3

I

II

III

+ + \ No newline at end of file diff --git a/test/integration/fixtures/google-docs-table-with-comments-out.html b/test/integration/fixtures/google-docs-table-with-comments-out.html new file mode 100644 index 00000000000000..697c2d41ea5cd9 --- /dev/null +++ b/test/integration/fixtures/google-docs-table-with-comments-out.html @@ -0,0 +1,3 @@ + +
OneTwoThree
123
IIIIII
+ diff --git a/test/integration/fixtures/google-docs-with-comments-in.html b/test/integration/fixtures/google-docs-with-comments-in.html new file mode 100644 index 00000000000000..e838e0198fe8f4 --- /dev/null +++ b/test/integration/fixtures/google-docs-with-comments-in.html @@ -0,0 +1,4 @@ + +

This is a title


This is a heading


Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.


  • A

  • Bulleted

    • Indented

  • List


  1. One

  2. Two

  3. Three


One

Two

Three

1

2

3

I

II

III




An image:



+ + \ No newline at end of file diff --git a/test/integration/fixtures/google-docs-with-comments-out.html b/test/integration/fixtures/google-docs-with-comments-out.html new file mode 100644 index 00000000000000..7733ca660bdd02 --- /dev/null +++ b/test/integration/fixtures/google-docs-with-comments-out.html @@ -0,0 +1,35 @@ + +

This is a title

+ + + +

This is a heading

+ + + +

Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.

+ + + +
  • A
  • Bulleted
    • Indented
  • List
+ + + +
  1. One
  2. Two
  3. Three
+ + + +
OneTwoThree
123
IIIIII
+ + + +
+ + + +

An image:

+ + + +
+ From f9151c8e776f13366521e5b172777e9c455fae2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 14 May 2019 14:41:35 +0200 Subject: [PATCH 28/34] RichText: show boundary only with editable element focus (#15466) --- .../src/components/rich-text/editable.js | 15 +++++++++---- .../src/components/rich-text/index.js | 21 +++++++++++-------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/editable.js b/packages/block-editor/src/components/rich-text/editable.js index 58c7217e1944c4..c7c9a854fc0b8d 100644 --- a/packages/block-editor/src/components/rich-text/editable.js +++ b/packages/block-editor/src/components/rich-text/editable.js @@ -83,7 +83,10 @@ function applyInternetExplorerInputFix( editorNode ) { } const IS_PLACEHOLDER_VISIBLE_ATTR_NAME = 'data-is-placeholder-visible'; -const CLASS_NAME = 'editor-rich-text__editable block-editor-rich-text__editable'; + +const oldClassName = 'editor-rich-text__editable'; + +export const className = 'block-editor-rich-text__editable'; /** * Whether or not the user agent is Internet Explorer. @@ -116,7 +119,11 @@ export default class Editable extends Component { } if ( ! isEqual( this.props.className, nextProps.className ) ) { - this.editorNode.className = classnames( nextProps.className, CLASS_NAME ); + this.editorNode.className = classnames( + className, + oldClassName, + nextProps.className + ); } const { removedKeys, updatedKeys } = diffAriaProps( this.props, nextProps ); @@ -156,7 +163,7 @@ export default class Editable extends Component { style, record, valueToEditableHTML, - className, + className: additionalClassName, isPlaceholderVisible, ...remainingProps } = this.props; @@ -166,7 +173,7 @@ export default class Editable extends Component { return createElement( tagName, { role: 'textbox', 'aria-multiline': true, - className: classnames( className, CLASS_NAME ), + className: classnames( className, oldClassName, additionalClassName ), contentEditable: true, [ IS_PLACEHOLDER_VISIBLE_ATTR_NAME ]: isPlaceholderVisible, ref: this.bindEditorNode, diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 541ac81ec7a1fb..e8dad655cbaada 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -52,7 +52,7 @@ import Autocomplete from '../autocomplete'; import BlockFormatControls from '../block-format-controls'; import FormatEdit from './format-edit'; import FormatToolbar from './format-toolbar'; -import Editable from './editable'; +import Editable, { className as editableClassName } from './editable'; import { pickAriaProps } from './aria'; import { getPatterns } from './patterns'; import { withBlockEditContext } from '../block-edit/context'; @@ -502,15 +502,18 @@ export class RichText extends Component { const boundarySelector = '*[data-rich-text-format-boundary]'; const element = this.editableRef.querySelector( boundarySelector ); - if ( element ) { - const computedStyle = getComputedStyle( element ); - const newColor = computedStyle.color - .replace( ')', ', 0.2)' ) - .replace( 'rgb', 'rgba' ); - - globalStyle.innerHTML = - `*:focus ${ boundarySelector }{background-color: ${ newColor }}`; + if ( ! element ) { + return; } + + const computedStyle = getComputedStyle( element ); + const newColor = computedStyle.color + .replace( ')', ', 0.2)' ) + .replace( 'rgb', 'rgba' ); + const selector = `.${ editableClassName }:focus ${ boundarySelector }`; + const rule = `background-color: ${ newColor }`; + + globalStyle.innerHTML = `${ selector } {${ rule }}`; } /** From 9683d000ab5aa25263e64feb5a0570820e72f8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 14 May 2019 15:01:51 +0200 Subject: [PATCH 29/34] Input Interaction: always reset initial vertical position rect (#15624) --- .../src/components/writing-flow/index.js | 18 ++++++++----- .../__snapshots__/writing-flow.test.js.snap | 20 ++++++++++++++ packages/e2e-tests/specs/writing-flow.test.js | 27 +++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index 656ac5b4ed49e7..f4c4844f5bf722 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -241,6 +241,18 @@ class WritingFlow extends Component { const hasModifier = isShift || event.ctrlKey || event.altKey || event.metaKey; const isNavEdge = isVertical ? isVerticalEdge : isHorizontalEdge; + // When presing any key other than up or down, the initial vertical + // position must ALWAYS be reset. The vertical position is saved so it + // can be restored as well as possible on sebsequent vertical arrow key + // presses. It may not always be possible to restore the exact same + // position (such as at an empty line), so it wouldn't be good to + // compute the position right before any vertical arrow key press. + if ( ! isVertical ) { + this.verticalRect = null; + } else if ( ! this.verticalRect ) { + this.verticalRect = computeCaretRect(); + } + // This logic inside this condition needs to be checked before // the check for event.nativeEvent.defaultPrevented. // The logic handles meta+a keypress and this event is default prevented @@ -281,12 +293,6 @@ class WritingFlow extends Component { return; } - if ( ! isVertical ) { - this.verticalRect = null; - } else if ( ! this.verticalRect ) { - this.verticalRect = computeCaretRect(); - } - // In the case of RTL scripts, right means previous and left means next, // which is the exact reverse of LTR. const { direction } = getComputedStyle( target ); diff --git a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap index 27806248a2a8a3..7852dacfc4174f 100644 --- a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap @@ -203,3 +203,23 @@ exports[`adding blocks should not prematurely multi-select 1`] = `

>

" `; + +exports[`adding blocks should preserve horizontal position when navigating vertically between blocks 1`] = ` +" +

abc

+ + + +

123

+" +`; + +exports[`adding blocks should remember initial vertical position 1`] = ` +" +

1x

+ + + +


2

+" +`; diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index 532e50d4e677db..5bcf9869c86469 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -367,4 +367,31 @@ describe( 'adding blocks', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should preserve horizontal position when navigating vertically between blocks', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'abc' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '23' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.type( '1' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should remember initial vertical position', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await pressKeyWithModifier( 'shift', 'Enter' ); + await page.keyboard.type( '2' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.type( 'x' ); // Should be right after "1". + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 94d311cf0e51abc337eabd35cb8c74e1c6973e7e Mon Sep 17 00:00:00 2001 From: Kjell Reigstad Date: Tue, 14 May 2019 09:41:42 -0400 Subject: [PATCH 30/34] Replace nextpage icon with a Material version. (#15627) --- packages/block-library/src/nextpage/icon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/nextpage/icon.js b/packages/block-library/src/nextpage/icon.js index c4abf194ec7596..a69aac47adf07c 100644 --- a/packages/block-library/src/nextpage/icon.js +++ b/packages/block-library/src/nextpage/icon.js @@ -1,8 +1,8 @@ /** * WordPress dependencies */ -import { G, Path, SVG } from '@wordpress/components'; +import { Path, SVG } from '@wordpress/components'; export default ( - + ); From 8de2ba501b77bc36a535cf4dfbfc4bf1aa7dbc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Tue, 14 May 2019 16:58:11 +0200 Subject: [PATCH 31/34] Bump version number to 5.7.0-rc.1 (#15632) --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 80d8252a4fed3d..6d49c7dba65ac6 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 5.6.1 + * Version: 5.7.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index e7af47a4f0dbdf..25bfdd2ef9e859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "5.6.1", + "version": "5.7.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ef38c002ece9ca..67d2e21445635c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "5.6.1", + "version": "5.7.0-rc.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From 8bc45fa922bbdc10b4e60268ea65359de7e05ffc Mon Sep 17 00:00:00 2001 From: Marek Hrabe Date: Tue, 14 May 2019 17:04:47 +0200 Subject: [PATCH 32/34] Content structure: Fix Semantics (#15474) * use list markup * remove extra margin that was added with
    usage * remove
  • margin * use h2 for heading * add focusable wrapper with note role, add list role to
      --- .../src/components/table-of-contents/panel.js | 61 +++++++++++-------- .../components/table-of-contents/style.scss | 12 ++-- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/editor/src/components/table-of-contents/panel.js b/packages/editor/src/components/table-of-contents/panel.js index 7c956651af3199..cd88216810d1ee 100644 --- a/packages/editor/src/components/table-of-contents/panel.js +++ b/packages/editor/src/components/table-of-contents/panel.js @@ -12,46 +12,57 @@ import DocumentOutline from '../document-outline'; function TableOfContentsPanel( { headingCount, paragraphCount, numberOfBlocks, hasOutlineItemsDisabled, onRequestClose } ) { return ( + /* + * Disable reason: The `list` ARIA role is redundant but + * Safari+VoiceOver won't announce the list otherwise. + */ + /* eslint-disable jsx-a11y/no-redundant-roles */ <>
      -
      - { __( 'Words' ) } - -
      -
      - { __( 'Headings' ) } - - { headingCount } - -
      -
      - { __( 'Paragraphs' ) } - - { paragraphCount } - -
      -
      - { __( 'Blocks' ) } - - { numberOfBlocks } - -
      +
        +
      • + { __( 'Words' ) } + +
      • +
      • + { __( 'Headings' ) } + + { headingCount } + +
      • +
      • + { __( 'Paragraphs' ) } + + { paragraphCount } + +
      • +
      • + { __( 'Blocks' ) } + + { numberOfBlocks } + +
      • +
      { headingCount > 0 && ( <>
      - +

      { __( 'Document Outline' ) } - +

      ) } + /* eslint-enable jsx-a11y/no-redundant-roles */ ); } diff --git a/packages/editor/src/components/table-of-contents/style.scss b/packages/editor/src/components/table-of-contents/style.scss index bd748416c6b8ab..2365aa4c534387 100644 --- a/packages/editor/src/components/table-of-contents/style.scss +++ b/packages/editor/src/components/table-of-contents/style.scss @@ -17,14 +17,15 @@ } } +.table-of-contents__wrapper:focus { + @include square-style__focus(); + outline-offset: $grid-size; +} + .table-of-contents__counts { display: flex; flex-wrap: wrap; - - &:focus { - @include square-style__focus(); - outline-offset: $grid-size; - } + margin: 0; } .table-of-contents__count { @@ -33,6 +34,7 @@ flex-direction: column; font-size: $default-font-size; color: $dark-gray-300; + margin-bottom: 0; } .table-of-contents__number, From 5bbda3656a530616a7a78c0a101d6ec2d8fa6a7a Mon Sep 17 00:00:00 2001 From: Suzen Fylke Date: Tue, 14 May 2019 12:16:19 -0400 Subject: [PATCH 33/34] changed inline @link tag to block-level @see tag (#15616) --- .../src/components/rich-text/index.js | 4 +-- packages/blocks/src/api/parser.js | 4 +-- packages/blocks/src/api/validation.js | 8 +++--- packages/components/src/disabled/index.js | 2 +- .../src/test/fixtures/tags-function/code.js | 2 +- .../src/press-key-with-modifier.js | 4 +-- .../hooks/components/media-upload/index.js | 2 +- packages/escape-html/src/index.js | 12 ++++---- packages/redux-routine/src/is-generator.js | 2 +- packages/token-list/src/index.js | 28 +++++++++---------- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index e8dad655cbaada..a34e315adefac1 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -68,7 +68,7 @@ const { getSelection, getComputedStyle } = window; /** * All inserting input types that would insert HTML into the DOM. * - * @see https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes + * @see https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes * * @type {Set} */ @@ -563,7 +563,7 @@ export class RichText extends Component { * selection where caret is at directional edge: forward for a delete key, * reverse for a backspace key. * - * @link https://en.wikipedia.org/wiki/Caret_navigation + * @see https://en.wikipedia.org/wiki/Caret_navigation * * @param {KeyboardEvent} event Keydown event. */ diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index 2132f6fa45f4f3..162c5cec83c508 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -120,7 +120,7 @@ export function isOfTypes( value, types ) { * Returns true if value is valid per the given block attribute schema type * definition, or false otherwise. * - * @link https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1 + * @see https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1 * * @param {*} value Value to test. * @param {?(Array|string)} type Block attribute schema type. @@ -135,7 +135,7 @@ export function isValidByType( value, type ) { * Returns true if value is valid per the given block attribute schema enum * definition, or false otherwise. * - * @link https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.2 + * @see https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.2 * * @param {*} value Value to test. * @param {?Array} enumSet Block attribute schema enum. diff --git a/packages/blocks/src/api/validation.js b/packages/blocks/src/api/validation.js index 50692a7414eff6..7105b5bba1890f 100644 --- a/packages/blocks/src/api/validation.js +++ b/packages/blocks/src/api/validation.js @@ -170,8 +170,8 @@ const TEXT_NORMALIZATIONS = [ * references.every( ( reference ) => /^[\da-z]+$/i.test( reference ) ) * ``` * - * @link https://html.spec.whatwg.org/multipage/syntax.html#character-references - * @link https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references + * @see https://html.spec.whatwg.org/multipage/syntax.html#character-references + * @see https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references * * @type {RegExp} */ @@ -183,7 +183,7 @@ const REGEXP_NAMED_CHARACTER_REFERENCE = /^[\da-z]+$/i; * "The ampersand must be followed by a U+0023 NUMBER SIGN character (#), * followed by one or more ASCII digits, representing a base-ten integer" * - * @link https://html.spec.whatwg.org/multipage/syntax.html#character-references + * @see https://html.spec.whatwg.org/multipage/syntax.html#character-references * * @type {RegExp} */ @@ -197,7 +197,7 @@ const REGEXP_DECIMAL_CHARACTER_REFERENCE = /^#\d+$/; * U+0058 LATIN CAPITAL LETTER X character (X), which must then be followed by * one or more ASCII hex digits, representing a hexadecimal integer" * - * @link https://html.spec.whatwg.org/multipage/syntax.html#character-references + * @see https://html.spec.whatwg.org/multipage/syntax.html#character-references * * @type {RegExp} */ diff --git a/packages/components/src/disabled/index.js b/packages/components/src/disabled/index.js index d335c6553c036b..73c9ecc9349d81 100644 --- a/packages/components/src/disabled/index.js +++ b/packages/components/src/disabled/index.js @@ -17,7 +17,7 @@ const { Consumer, Provider } = createContext( false ); * * See WHATWG HTML Standard: 4.10.18.5: "Enabling and disabling form controls: the disabled attribute". * - * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute * * @type {string[]} */ diff --git a/packages/docgen/src/test/fixtures/tags-function/code.js b/packages/docgen/src/test/fixtures/tags-function/code.js index 8d4261e0f63289..cbde0e0b9b0456 100644 --- a/packages/docgen/src/test/fixtures/tags-function/code.js +++ b/packages/docgen/src/test/fixtures/tags-function/code.js @@ -5,7 +5,7 @@ * @since v2 * * @see addition - * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators * * @param {number} firstParam The first param to add. * @param {number} secondParam The second param to add. diff --git a/packages/e2e-test-utils/src/press-key-with-modifier.js b/packages/e2e-test-utils/src/press-key-with-modifier.js index 59f5c8330672ec..1989f0d7236ffd 100644 --- a/packages/e2e-test-utils/src/press-key-with-modifier.js +++ b/packages/e2e-test-utils/src/press-key-with-modifier.js @@ -16,8 +16,8 @@ import { modifiers, SHIFT, ALT, CTRL } from '@wordpress/keycodes'; * that any `Event#preventDefault` which would have normally occurred in the * application as a result of Ctrl+A is respected. * - * @link https://github.com/GoogleChrome/puppeteer/issues/1313 - * @link https://w3c.github.io/uievents/tools/key-event-viewer.html + * @see https://github.com/GoogleChrome/puppeteer/issues/1313 + * @see https://w3c.github.io/uievents/tools/key-event-viewer.html * * @return {Promise} Promise resolving once the SelectAll emulation completes. */ diff --git a/packages/edit-post/src/hooks/components/media-upload/index.js b/packages/edit-post/src/hooks/components/media-upload/index.js index eefb3450a6e1c4..f97b6b1f432985 100644 --- a/packages/edit-post/src/hooks/components/media-upload/index.js +++ b/packages/edit-post/src/hooks/components/media-upload/index.js @@ -16,7 +16,7 @@ const getGalleryDetailsMediaFrame = () => { /** * Custom gallery details frame. * - * @link https://github.com/xwp/wp-core-media-widgets/blob/905edbccfc2a623b73a93dac803c5335519d7837/wp-admin/js/widgets/media-gallery-widget.js + * @see https://github.com/xwp/wp-core-media-widgets/blob/905edbccfc2a623b73a93dac803c5335519d7837/wp-admin/js/widgets/media-gallery-widget.js * @class GalleryDetailsMediaFrame * @constructor */ diff --git a/packages/escape-html/src/index.js b/packages/escape-html/src/index.js index c29efb94bd2d04..d31f43f63f70bb 100644 --- a/packages/escape-html/src/index.js +++ b/packages/escape-html/src/index.js @@ -10,7 +10,7 @@ import __unstableEscapeGreaterThan from './escape-greater'; * U+0020 SPACE, U+0022 ("), U+0027 ('), U+003E (>), U+002F (/), U+003D (=), * and noncharacters." * - * @link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 * * @type {RegExp} */ @@ -22,9 +22,9 @@ const REGEXP_INVALID_ATTRIBUTE_NAME = /[\u007F-\u009F "'>/="\uFDD0-\uFDEF]/; * named, decimal, or hexadecimal character references are escaped. Invalid * named references (i.e. ambiguous ampersand) are are still permitted. * - * @link https://w3c.github.io/html/syntax.html#character-references - * @link https://w3c.github.io/html/syntax.html#ambiguous-ampersand - * @link https://w3c.github.io/html/syntax.html#named-character-references + * @see https://w3c.github.io/html/syntax.html#character-references + * @see https://w3c.github.io/html/syntax.html#ambiguous-ampersand + * @see https://w3c.github.io/html/syntax.html#named-character-references * * @param {string} value Original string. * @@ -59,7 +59,7 @@ export function escapeLessThan( value ) { /** * Returns an escaped attribute value. * - * @link https://w3c.github.io/html/syntax.html#elements-attributes + * @see https://w3c.github.io/html/syntax.html#elements-attributes * * "[...] the text cannot contain an ambiguous ampersand [...] must not contain * any literal U+0022 QUOTATION MARK characters (")" @@ -83,7 +83,7 @@ export function escapeAttribute( value ) { /** * Returns an escaped HTML element value. * - * @link https://w3c.github.io/html/syntax.html#writing-html-documents-elements + * @see https://w3c.github.io/html/syntax.html#writing-html-documents-elements * * "the text must not contain the character U+003C LESS-THAN SIGN (<) or an * ambiguous ampersand." diff --git a/packages/redux-routine/src/is-generator.js b/packages/redux-routine/src/is-generator.js index 70ce6002f1a102..dcddb9d59fc665 100644 --- a/packages/redux-routine/src/is-generator.js +++ b/packages/redux-routine/src/is-generator.js @@ -1,7 +1,7 @@ /** * Returns true if the given object is a generator, or false otherwise. * - * @link https://www.ecma-international.org/ecma-262/6.0/#sec-generator-objects + * @see https://www.ecma-international.org/ecma-262/6.0/#sec-generator-objects * * @param {*} object Object to test. * diff --git a/packages/token-list/src/index.js b/packages/token-list/src/index.js index b959c331e79af7..acf8b79b68dacd 100644 --- a/packages/token-list/src/index.js +++ b/packages/token-list/src/index.js @@ -6,7 +6,7 @@ import { uniq, compact, without } from 'lodash'; /** * A set of tokens. * - * @link https://dom.spec.whatwg.org/#domtokenlist + * @see https://dom.spec.whatwg.org/#domtokenlist */ export default class TokenList { /** @@ -27,7 +27,7 @@ export default class TokenList { /** * Returns the associated set as string. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-value + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-value * * @return {string} Token set as string. */ @@ -38,7 +38,7 @@ export default class TokenList { /** * Replaces the associated set with a new string value. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-value + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-value * * @param {string} value New token set as string. */ @@ -51,7 +51,7 @@ export default class TokenList { /** * Returns the number of tokens. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-length + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-length * * @return {number} Number of tokens. */ @@ -62,8 +62,8 @@ export default class TokenList { /** * Returns the stringified form of the TokenList. * - * @link https://dom.spec.whatwg.org/#DOMTokenList-stringification-behavior - * @link https://www.ecma-international.org/ecma-262/9.0/index.html#sec-tostring + * @see https://dom.spec.whatwg.org/#DOMTokenList-stringification-behavior + * @see https://www.ecma-international.org/ecma-262/9.0/index.html#sec-tostring * * @return {string} Token set as string. */ @@ -74,7 +74,7 @@ export default class TokenList { /** * Returns an iterator for the TokenList, iterating items of the set. * - * @link https://dom.spec.whatwg.org/#domtokenlist + * @see https://dom.spec.whatwg.org/#domtokenlist * * @return {Generator} TokenList iterator. */ @@ -85,7 +85,7 @@ export default class TokenList { /** * Returns the token with index `index`. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-item + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-item * * @param {number} index Index at which to return token. * @@ -98,7 +98,7 @@ export default class TokenList { /** * Returns true if `token` is present, and false otherwise. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-contains + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-contains * * @param {string} item Token to test. * @@ -111,7 +111,7 @@ export default class TokenList { /** * Adds all arguments passed, except those already present. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-add + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-add * * @param {...string} items Items to add. */ @@ -122,7 +122,7 @@ export default class TokenList { /** * Removes arguments passed, if they are present. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-remove + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-remove * * @param {...string} items Items to remove. */ @@ -136,7 +136,7 @@ export default class TokenList { * as add()). If force is false, removes token (same as remove()). Returns * true if `token` is now present, and false otherwise. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-toggle + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-toggle * * @param {string} token Token to toggle. * @param {?boolean} force Presence to force. @@ -161,7 +161,7 @@ export default class TokenList { * Replaces `token` with `newToken`. Returns true if `token` was replaced * with `newToken`, and false otherwise. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-replace + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-replace * * @param {string} token Token to replace with `newToken`. * @param {string} newToken Token to use in place of `token`. @@ -185,7 +185,7 @@ export default class TokenList { * * Always returns `true` in this implementation. * - * @link https://dom.spec.whatwg.org/#dom-domtokenlist-supports + * @see https://dom.spec.whatwg.org/#dom-domtokenlist-supports * * @return {boolean} Whether token is supported. */ From 73921f2fa6ce534e4d1d3ed270e794d863581f44 Mon Sep 17 00:00:00 2001 From: Jorge Date: Wed, 17 Apr 2019 10:45:41 +0100 Subject: [PATCH 34/34] Add end point --- lib/blocks.php | 48 ++++ lib/class-wp-rest-widget-areas-controller.php | 219 +++++++++++++++++ lib/class-wp-widgets-manager.php | 226 ++++++++++++++++++ lib/load.php | 4 + lib/rest-api.php | 11 + 5 files changed, 508 insertions(+) create mode 100644 lib/class-wp-rest-widget-areas-controller.php create mode 100644 lib/class-wp-widgets-manager.php diff --git a/lib/blocks.php b/lib/blocks.php index fc3adb8a09efa3..8b9c00d47cd633 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -45,3 +45,51 @@ function gutenberg_reregister_core_block_types() { } } add_action( 'init', 'gutenberg_reregister_core_block_types' ); + +/** + * Serializes an array of blocks. + * + * @since 5.7.0 + * + * @param array $blocks Post Array of block objects. + * @return string String representing the blocks. + */ +function serialize_blocks( $blocks ) { + return implode( array_map( 'serialize_block', $blocks ) ); +} + +/** + * Serializes a block. + * + * @since 5.7.0 + * + * @param array $block Block object. + * @return string String representing the block. + */ +function serialize_block( $block ) { + $name = $block['blockName']; + if ( 0 === strpos( $name, 'core/' ) ) { + $name = substr( $name, strlen( 'core/' ) ); + } + + if ( empty( $block['attrs'] ) ) { + $opening_tag_suffix = ''; + } else { + $opening_tag_suffix = ' ' . json_encode( $block['attrs'] ); + } + + if ( empty( $block['innerHTML'] ) ) { + return sprintf( + '', + $name, + $opening_tag_suffix + ); + } else { + return sprintf( + '%3$s', + $name, + $opening_tag_suffix, + $block['innerHTML'] + ); + } +} diff --git a/lib/class-wp-rest-widget-areas-controller.php b/lib/class-wp-rest-widget-areas-controller.php new file mode 100644 index 00000000000000..4d21299b6ce451 --- /dev/null +++ b/lib/class-wp-rest-widget-areas-controller.php @@ -0,0 +1,219 @@ +namespace = '__experimental'; + $this->rest_base = 'widget-areas'; + } + + /** + * Registers the necessary REST API routes. + * + * @access public + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + $id_argument = array( + 'description' => __( 'The sidebar’s ID.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'is_valid_sidabar_id' ), + ); + + $content_argument = array( + 'description' => __( 'Sidebar content.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P.+)', + array( + 'args' => array( + 'id' => $id_argument, + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => array( + 'id' => $id_argument, + 'content' => $content_argument, + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks whether a given request has permission to read widget areas. + * + * @since 5.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|bool True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'rest_user_cannot_view', + __( 'Sorry, you are not allowed to read sidebars.', 'gutenberg' ) + ); + } + + return true; + } + + /** + * Retrieves all widget areas. + * + * @since 5.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + global $wp_registered_sidebars; + + $data = array(); + + foreach ( array_keys( $wp_registered_sidebars ) as $sidebar_id ) { + $data[ $sidebar_id ] = $this->get_sidebar_data( $sidebar_id ); + } + + return rest_ensure_response( $data ); + } + + /** + * Retrieves a specific widget area. + * + * @since 5.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + return rest_ensure_response( $this->get_sidebar_data( $request['id'] ) ); + } + + /** + * Checks if a given REST request has access to update a widget area. + * + * @since 5.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|bool True if the request has access to update the item, error object otherwise. + */ + public function update_item_permissions_check( $request ) { + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'rest_user_cannot_edit', + __( 'Sorry, you are not allowed to edit sidebars.', 'gutenberg' ) + ); + } + + return true; + } + + /** + * Updates a single widget area. + * + * @since 5.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function update_item( $request ) { + $sidebar_id = $request->get_param( 'id' ); + $sidebar_content = $request->get_param( 'content' ); + + $id_referenced_in_sidebar = WP_Widgets_Manager::get_post_id_referenced_in_sidebar( $sidebar_id ); + + $post_id = wp_insert_post( + array( + 'ID' => $id_referenced_in_sidebar, + 'post_content' => $sidebar_content, + 'post_type' => 'wp_area', + ) + ); + + if ( 0 === $id_referenced_in_sidebar ) { + WP_Widgets_Manager::reference_post_id_in_sidebar( $sidebar_id, $post_id ); + } + + return rest_ensure_response( $this->get_sidebar_data( $request['id'] ) ); + } + + /** + * Returns the sidebar data together with a content array containing the blocks present in the sidebar. + * The bocks may be legacy widget blocks representing the widgets currently present in the sidebar, or the content of a wp_area post that the sidebar references. + * + * @since 5.7.0 + * + * @param string $sidebar_id Indentifier of the sidebar. + * @return object Sidebar data with a content array. + */ + protected function get_sidebar_data( $sidebar_id ) { + $content_string = ''; + + $post_id_referenced_in_sidebar = WP_Widgets_Manager::get_post_id_referenced_in_sidebar( $sidebar_id ); + + if ( 0 !== $post_id_referenced_in_sidebar ) { + $post = get_post( $post_id_referenced_in_sidebar ); + $content_string = $post->post_content; + } else { + $blocks = WP_Widgets_Manager::get_sidebar_as_blocks( $sidebar_id ); + $content_string = serialize_blocks( $blocks ); + } + + return array_merge( + WP_Widgets_Manager::get_wp_registered_sidebars_sidebar( $sidebar_id ), + array( + 'content' => array( + 'raw' => $content_string, + 'rendered' => apply_filters( 'the_content', $content_string ), + 'block_version' => block_version( $content_string ), + ), + ) + ); + } +} diff --git a/lib/class-wp-widgets-manager.php b/lib/class-wp-widgets-manager.php new file mode 100644 index 00000000000000..a513cc39b1179a --- /dev/null +++ b/lib/class-wp-widgets-manager.php @@ -0,0 +1,226 @@ + $post_id, + 'wp_inactive_widgets' => array_merge( + $sidebars['wp_inactive_widgets'], + $sidebar + ), + ) + ) + ); + } + + /** + * Returns a sidebar as an array of legacy widget blocks. + * + * @since 5.7.0 + * + * @param string $sidebar_id Indentifier of the sidebar. + * @return array $post_id Post id. + */ + public static function get_sidebar_as_blocks( $sidebar_id ) { + $blocks = array(); + + $sidebars_items = wp_get_sidebars_widgets(); + + foreach ( $sidebars_items[ $sidebar_id ] as $item ) { + $widget_class = self::get_widget_class( $item ); + $blocks[] = array( + 'blockName' => 'core/legacy-widget', + 'attrs' => array( + 'class' => $widget_class, + 'identifier' => $item, + 'instance' => self::get_sidebar_widget_instance( $sidebar, $item ), + ), + 'innerHTML' => '', + ); + } + return $blocks; + } + + /** + * Verifies if a sidabar id is valid or not. + * + * @since 5.7.0 + * + * @param string $sidebar_id Indentifier of the sidebar. + * @return boolean True if the $sidebar_id value is valid and false otherwise. + */ + public static function is_valid_sidabar_id( $sidebar_id ) { + $wp_registered_sidebars = self::get_wp_registered_sidebars(); + return isset( $wp_registered_sidebars[ $sidebar_id ] ); + } + + + /** + * Given a widget id returns the name of the class the represents the widget. + * + * @since 5.7.0 + * + * @param string $widget_id Indentifier of the widget. + * @return string|null Name of the class that represents the widget or null if the widget is not represented by a class. + */ + private static function get_widget_class( $widget_id ) { + $wp_registered_widgets = self::get_wp_registered_widgets(); + if ( + isset( $wp_registered_widgets[ $widget_id ]['callback'][0] ) && + $wp_registered_widgets[ $widget_id ]['callback'][0] instanceof WP_Widget + ) { + return get_class( $wp_registered_widgets[ $widget_id ]['callback'][0] ); + } + return null; + } + + /** + * Retrieves a widget instance. + * + * @since 5.7.0 + * + * @param array $sidebar sidebar data available at $wp_registered_sidebars. + * @param string $id Idenfitier of the widget instance. + * @return array Array containing the widget instance. + */ + private static function get_sidebar_widget_instance( $sidebar, $id ) { + list( $object, $number, $name ) = self::get_widget_info( $id ); + if ( ! $object ) { + return array(); + } + + $object->_set( $number ); + + $instances = $object->get_settings(); + $instance = $instances[ $number ]; + + $args = array_merge( + $sidebar, + array( + 'widget_id' => $id, + 'widget_name' => $name, + ) + ); + + /** + * Filters the settings for a particular widget instance. + * + * Returning false will effectively short-circuit display of the widget. + * + * @since 2.8.0 + * + * @param array $instance The current widget instance's settings. + * @param WP_Widget $this The current widget instance. + * @param array $args An array of default widget arguments. + */ + $instance = apply_filters( 'widget_display_callback', $instance, $object, $args ); + + if ( false === $instance ) { + return array(); + } + + return $instance; + } + + /** + * Given a widget id returns an array containing information about the widget. + * + * @since 5.7.0 + * + * @param string $widget_id Indentifier of the widget. + * @return array Array containing the the wiget object, the number, and the name. + */ + private static function get_widget_info( $widget_id ) { + $wp_registered_widgets = self::get_wp_registered_widgets(); + + if ( + ! isset( $wp_registered_widgets[ $widget_id ]['callback'][0] ) || + ! isset( $wp_registered_widgets[ $widget_id ]['params'][0]['number'] ) || + ! isset( $wp_registered_widgets[ $widget_id ]['name'] ) || + ! ( $wp_registered_widgets[ $widget_id ]['callback'][0] instanceof WP_Widget ) + ) { + return array( null, null, null ); + } + + $object = $wp_registered_widgets[ $widget_id ]['callback'][0]; + $number = $wp_registered_widgets[ $widget_id ]['params'][0]['number']; + $name = $wp_registered_widgets[ $widget_id ]['name']; + return array( $object, $number, $name ); + } +} diff --git a/lib/load.php b/lib/load.php index 9f845d95a8bf9d..386c07a6a67ad7 100644 --- a/lib/load.php +++ b/lib/load.php @@ -18,6 +18,10 @@ if ( ! class_exists( 'WP_REST_Widget_Updater_Controller' ) ) { require dirname( __FILE__ ) . '/class-wp-rest-widget-updater-controller.php'; } + if ( ! class_exists( 'WP_REST_Widget_Areas_Controller' ) ) { + require dirname( __FILE__ ) . '/class-wp-widgets-manager.php'; + require dirname( __FILE__ ) . '/class-wp-rest-widget-areas-controller.php'; + } /** * End: Include for phase 2 */ diff --git a/lib/rest-api.php b/lib/rest-api.php index eaccc09c61978d..d9671557bfcb8b 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -67,6 +67,17 @@ function gutenberg_register_rest_widget_updater_routes() { $widgets_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_rest_widget_updater_routes' ); + +/** + * Registers the widget area REST API routes. + * + * @since 5.7.0 + */ +function gutenberg_register_rest_widget_areas() { + $widget_areas_controller = new WP_REST_Widget_Areas_Controller(); + $widget_areas_controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_rest_widget_areas' ); /** * End: Include for phase 2 */