From 47bde1a7696ceae94557dde3fa57cde3cfc9d84e Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 22 Jul 2024 11:22:08 +0200 Subject: [PATCH 01/28] Update HeightControl component to label inputs (#63761) * Update HeightControl component to label inputs The current method of using a fieldset for the HeightControl inputs leaves neither of them with a functioning accessible name. This change ensures both the text field and the range slider are named, and semantically linked (with the caveat that aria-controls has little support as of now). * Changelog entry. * Tweak instance ID prefix --------- Co-authored-by: alexstine Co-authored-by: ciampo Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: andrewhayward --- .../src/components/height-control/index.js | 15 ++++++++++++--- packages/components/CHANGELOG.md | 1 + packages/components/src/range-control/index.tsx | 15 +++++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/components/height-control/index.js b/packages/block-editor/src/components/height-control/index.js index 71753a67beb021..4f5e17381728b3 100644 --- a/packages/block-editor/src/components/height-control/index.js +++ b/packages/block-editor/src/components/height-control/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { useInstanceId } from '@wordpress/compose'; import { useMemo } from '@wordpress/element'; import { BaseControl, @@ -68,6 +69,10 @@ export default function HeightControl( { value, } ) { const customRangeValue = parseFloat( value ); + const id = useInstanceId( HeightControl, 'block-editor-height-control' ); + const labelId = `${ id }__label`; + const inputId = `${ id }__input`; + const rangeId = `${ id }__range`; const [ availableUnits ] = useSettings( 'spacing.units' ); const units = useCustomUnits( { @@ -144,13 +149,14 @@ export default function HeightControl( { }; return ( -
- +
+ { label } -
+ ); } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ec1736e3adafa9..6e8250529edd81 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -18,6 +18,7 @@ - `CustomSelectControl`: Stabilize `__experimentalShowSelectedHint` and `options[]. __experimentalHint` props ([#63248](https://github.com/WordPress/gutenberg/pull/63248)). - `SelectControl`: Add `"minimal"` variant ([#63265](https://github.com/WordPress/gutenberg/pull/63265)). - `FontSizePicker`: tidy up internal logic ([#63553](https://github.com/WordPress/gutenberg/pull/63553)). +- `RangeControl`: Allow external `id` prop ([#63761](https://github.com/WordPress/gutenberg/pull/63761)). ### Internal diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx index b5fff9f56b80d7..d12c1ca913612e 100644 --- a/packages/components/src/range-control/index.tsx +++ b/packages/components/src/range-control/index.tsx @@ -41,6 +41,15 @@ import { space } from '../utils/space'; const noop = () => {}; +function useUniqueId( idProp?: string ) { + const id = useInstanceId( + UnforwardedRangeControl, + 'inspector-range-control' + ); + + return idProp || id; +} + function UnforwardedRangeControl( props: WordPressComponentProps< RangeControlProps, 'input', false >, forwardedRef: ForwardedRef< HTMLInputElement > @@ -56,6 +65,7 @@ function UnforwardedRangeControl( disabled = false, help, hideLabelFromVision = false, + id: idProp, initialPosition, isShiftStepEnabled = true, label, @@ -123,10 +133,7 @@ function UnforwardedRangeControl( !! marks && 'is-marked' ); - const id = useInstanceId( - UnforwardedRangeControl, - 'inspector-range-control' - ); + const id = useUniqueId( idProp ); const describedBy = !! help ? `${ id }__help` : undefined; const enableTooltip = hasTooltip !== false && Number.isFinite( value ); From 8df860e92ff3bce7f515204ff9f9630b59a52232 Mon Sep 17 00:00:00 2001 From: James Koster Date: Mon, 22 Jul 2024 13:28:41 +0100 Subject: [PATCH 02/28] Update 'Front page' badge to 'Homepage' (#63752) --- packages/edit-site/src/components/post-list/index.js | 2 +- packages/edit-site/src/components/post-list/style.scss | 1 + packages/editor/src/components/post-card-panel/index.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 5c8ac934a63355..a614d603316a67 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -428,7 +428,7 @@ export default function PostList( { postType } ) { if ( item.id === frontPageId ) { suffix = ( - { __( 'Front Page' ) } + { __( 'Homepage' ) } ); } else if ( item.id === postsPageId ) { diff --git a/packages/edit-site/src/components/post-list/style.scss b/packages/edit-site/src/components/post-list/style.scss index c423c2e78d6505..a86783c163c83e 100644 --- a/packages/edit-site/src/components/post-list/style.scss +++ b/packages/edit-site/src/components/post-list/style.scss @@ -72,6 +72,7 @@ font-size: 12px; font-weight: 400; flex-shrink: 0; + line-height: $grid-unit-05 * 5; } .edit-site-post-list__status-icon { diff --git a/packages/editor/src/components/post-card-panel/index.js b/packages/editor/src/components/post-card-panel/index.js index 0d06ff0dca106b..df13f1c8b65a6b 100644 --- a/packages/editor/src/components/post-card-panel/index.js +++ b/packages/editor/src/components/post-card-panel/index.js @@ -104,7 +104,7 @@ export default function PostCardPanel( { actions } ) { { title ? decodeEntities( title ) : __( 'No Title' ) } { isFrontPage && ( - { __( 'Front Page' ) } + { __( 'Homepage' ) } ) } { isPostsPage && ( From e78e4e38969933990bf5350295cc3bde6af00631 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 22 Jul 2024 15:32:49 +0200 Subject: [PATCH 03/28] Update: Grid layout: Allow users to adjust grid density (#63367) Co-authored-by: jorgefilipecosta Co-authored-by: jameskoster Co-authored-by: stokesman Co-authored-by: t-hamano Co-authored-by: colorful-tones --- .../src/hooks/use-viewport-match/index.js | 4 +- .../src/components/dataviews-context/index.ts | 2 + .../src/components/dataviews-layout/index.tsx | 2 + .../src/components/dataviews/index.tsx | 10 ++ .../src/layouts/grid/density-picker.tsx | 136 ++++++++++++++++++ packages/dataviews/src/layouts/grid/index.tsx | 5 + .../dataviews/src/layouts/grid/style.scss | 36 +++-- packages/dataviews/src/types.ts | 1 + 8 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 packages/dataviews/src/layouts/grid/density-picker.tsx diff --git a/packages/compose/src/hooks/use-viewport-match/index.js b/packages/compose/src/hooks/use-viewport-match/index.js index ebec934b480123..4186045f7d08c4 100644 --- a/packages/compose/src/hooks/use-viewport-match/index.js +++ b/packages/compose/src/hooks/use-viewport-match/index.js @@ -9,7 +9,7 @@ import { createContext, useContext } from '@wordpress/element'; import useMediaQuery from '../use-media-query'; /** - * @typedef {"huge" | "wide" | "large" | "medium" | "small" | "mobile"} WPBreakpoint + * @typedef {"xhuge" | "huge" | "wide" | "xlarge" | "large" | "medium" | "small" | "mobile"} WPBreakpoint */ /** @@ -20,8 +20,10 @@ import useMediaQuery from '../use-media-query'; * @type {Record} */ const BREAKPOINTS = { + xhuge: 1920, huge: 1440, wide: 1280, + xlarge: 1080, large: 960, medium: 782, small: 600, diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts index 39e5e09015658e..3936288b3095b0 100644 --- a/packages/dataviews/src/components/dataviews-context/index.ts +++ b/packages/dataviews/src/components/dataviews-context/index.ts @@ -26,6 +26,7 @@ type DataViewsContextType< Item > = { openedFilter: string | null; setOpenedFilter: ( openedFilter: string | null ) => void; getItemId: ( item: Item ) => string; + density: number; }; const DataViewsContext = createContext< DataViewsContextType< any > >( { @@ -42,6 +43,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( { setOpenedFilter: () => {}, openedFilter: null, getItemId: ( item ) => item.id, + density: 0, } ); export default DataViewsContext; diff --git a/packages/dataviews/src/components/dataviews-layout/index.tsx b/packages/dataviews/src/components/dataviews-layout/index.tsx index 960dcf304c0180..eac70763e143c1 100644 --- a/packages/dataviews/src/components/dataviews-layout/index.tsx +++ b/packages/dataviews/src/components/dataviews-layout/index.tsx @@ -27,6 +27,7 @@ export default function DataViewsLayout() { selection, onChangeSelection, setOpenedFilter, + density, } = useContext( DataViewsContext ); const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ) @@ -44,6 +45,7 @@ export default function DataViewsLayout() { selection={ selection } setOpenedFilter={ setOpenedFilter } view={ view } + density={ density } /> ); } diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index 618e04773c084e..5d45413f03b65a 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -18,6 +18,8 @@ import DataViewsViewConfig from '../dataviews-view-config'; import { normalizeFields } from '../../normalize-fields'; import type { Action, Field, View, SupportedLayouts } from '../../types'; import type { SelectionOrUpdater } from '../../private-types'; +import DensityPicker from '../../layouts/grid/density-picker'; +import { LAYOUT_GRID } from '../../constants'; type ItemWithId = { id: string }; @@ -59,6 +61,7 @@ export default function DataViews< Item >( { onChangeSelection, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); + const [ density, setDensity ] = useState< number >( 0 ); const isUncontrolled = selectionProperty === undefined || onChangeSelection === undefined; const selection = isUncontrolled ? selectionState : selectionProperty; @@ -95,6 +98,7 @@ export default function DataViews< Item >( { openedFilter, setOpenedFilter, getItemId, + density, } } >
@@ -111,6 +115,12 @@ export default function DataViews< Item >( { { search && } + { view.type === LAYOUT_GRID && ( + + ) } diff --git a/packages/dataviews/src/layouts/grid/density-picker.tsx b/packages/dataviews/src/layouts/grid/density-picker.tsx new file mode 100644 index 00000000000000..df347507fb6340 --- /dev/null +++ b/packages/dataviews/src/layouts/grid/density-picker.tsx @@ -0,0 +1,136 @@ +/** + * WordPress dependencies + */ +import { RangeControl, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useViewportMatch } from '@wordpress/compose'; +import { plus, lineSolid } from '@wordpress/icons'; +import { useEffect } from '@wordpress/element'; + +const viewportBreaks = { + xhuge: { min: 3, max: 6, default: 5 }, + huge: { min: 2, max: 4, default: 4 }, + xlarge: { min: 2, max: 3, default: 3 }, + large: { min: 1, max: 2, default: 2 }, + mobile: { min: 1, max: 2, default: 2 }, +}; + +function useViewPortBreakpoint() { + const isXHuge = useViewportMatch( 'xhuge', '>=' ); + const isHuge = useViewportMatch( 'huge', '>=' ); + const isXlarge = useViewportMatch( 'xlarge', '>=' ); + const isLarge = useViewportMatch( 'large', '>=' ); + const isMobile = useViewportMatch( 'mobile', '>=' ); + + if ( isXHuge ) { + return 'xhuge'; + } + if ( isHuge ) { + return 'huge'; + } + if ( isXlarge ) { + return 'xlarge'; + } + if ( isLarge ) { + return 'large'; + } + if ( isMobile ) { + return 'mobile'; + } + return null; +} + +// Value is number from 0 to 100 representing how big an item is in the grid +// 100 being the biggest and 0 being the smallest. +// The size is relative to the viewport size, if one a given viewport the +// number of allowed items in a grid is 3 to 6 a 0 ( the smallest ) will mean that the grid will +// have 6 items in a row, a 100 ( the biggest ) will mean that the grid will have 3 items in a row. +// A value of 75 will mean that the grid will have 4 items in a row. +function getRangeValue( + density: number, + breakValues: { min: number; max: number; default: number } +) { + const inverseDensity = breakValues.max - density; + const max = breakValues.max - breakValues.min; + return Math.round( ( inverseDensity * 100 ) / max ); +} + +export default function DensityPicker( { + density, + setDensity, +}: { + density: number; + setDensity: React.Dispatch< React.SetStateAction< number > >; +} ) { + const viewport = useViewPortBreakpoint(); + useEffect( () => { + setDensity( ( _density ) => { + if ( ! viewport || ! _density ) { + return 0; + } + const breakValues = viewportBreaks[ viewport ]; + if ( _density < breakValues.min ) { + return breakValues.min; + } + if ( _density > breakValues.max ) { + return breakValues.max; + } + return _density; + } ); + }, [ setDensity, viewport ] ); + if ( ! viewport ) { + return null; + } + const breakValues = viewportBreaks[ viewport ]; + const densityToUse = density || breakValues.default; + const rangeValue = getRangeValue( densityToUse, breakValues ); + + const step = 100 / ( breakValues.max - breakValues.min + 1 ); + return ( + <> +
) } + { ! isMobileViewport && areas.edit && ( +
+ { areas.edit } +
+ ) } + { ! isMobileViewport && areas.preview && (
{ canvasResizer } diff --git a/packages/edit-site/src/components/layout/router.js b/packages/edit-site/src/components/layout/router.js index b89c1a5f256937..3fd0cc560d9433 100644 --- a/packages/edit-site/src/components/layout/router.js +++ b/packages/edit-site/src/components/layout/router.js @@ -26,6 +26,7 @@ import { TEMPLATE_PART_POST_TYPE, TEMPLATE_POST_TYPE, } from '../../utils/constants'; +import { PostEdit } from '../post-edit'; const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -74,13 +75,15 @@ function useRedirectOldPaths() { export default function useLayoutAreas() { const { params } = useLocation(); - const { postType, postId, path, layout, isCustom, canvas } = params; + const { postType, postId, path, layout, isCustom, canvas, quickEdit } = + params; const hasEditCanvasMode = canvas === 'edit'; useRedirectOldPaths(); // Page list if ( postType === 'page' ) { const isListLayout = layout === 'list' || ! layout; + const showQuickEdit = quickEdit && ! isListLayout; return { key: 'pages', areas: { @@ -92,15 +95,20 @@ export default function useLayoutAreas() { /> ), content: , - preview: ( isListLayout || hasEditCanvasMode ) && , + preview: ! showQuickEdit && + ( isListLayout || hasEditCanvasMode ) && , mobile: hasEditCanvasMode ? ( ) : ( ), + edit: showQuickEdit && ( + + ), }, widths: { content: isListLayout ? 380 : undefined, + edit: showQuickEdit ? 380 : undefined, }, }; } diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js new file mode 100644 index 00000000000000..6e556c56a9152c --- /dev/null +++ b/packages/edit-site/src/components/post-edit/index.js @@ -0,0 +1,86 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { DataForm } from '@wordpress/dataviews'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; +import { Button } from '@wordpress/components'; +import { useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Page from '../page'; +import usePostFields from '../post-fields'; + +function PostEditForm( { postType, postId } ) { + const { item } = useSelect( + ( select ) => { + return { + item: select( coreDataStore ).getEntityRecord( + 'postType', + postType, + postId + ), + }; + }, + [ postType, postId ] + ); + const { saveEntityRecord } = useDispatch( coreDataStore ); + const { fields } = usePostFields(); + const form = { + visibleFields: [ 'title' ], + }; + const [ edits, setEdits ] = useState( {} ); + const itemWithEdits = useMemo( () => { + return { + ...item, + ...edits, + }; + }, [ item, edits ] ); + const onSubmit = ( event ) => { + event.preventDefault(); + saveEntityRecord( 'postType', postType, itemWithEdits ); + setEdits( {} ); + }; + + if ( ! item ) { + return null; + } + + return ( +
+ + + + ); +} + +export function PostEdit( { postType, postId } ) { + return ( + + { postId && ( + + ) } + { ! postId &&

{ __( 'Select a page to edit' ) }

} +
+ ); +} diff --git a/packages/edit-site/src/components/post-edit/style.scss b/packages/edit-site/src/components/post-edit/style.scss new file mode 100644 index 00000000000000..4eaa41b2e8ed70 --- /dev/null +++ b/packages/edit-site/src/components/post-edit/style.scss @@ -0,0 +1,9 @@ +.edit-site-post-edit { + padding: $grid-unit-30; + + &.is-empty .edit-site-page-content { + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js new file mode 100644 index 00000000000000..5a94b60f5dde0f --- /dev/null +++ b/packages/edit-site/src/components/post-fields/index.js @@ -0,0 +1,345 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; +import { + createInterpolateElement, + useMemo, + useState, +} from '@wordpress/element'; +import { dateI18n, getDate, getSettings } from '@wordpress/date'; +import { + trash, + drafts, + published, + scheduled, + pending, + notAllowed, + commentAuthorAvatar as authorIcon, +} from '@wordpress/icons'; +import { __experimentalHStack as HStack, Icon } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { + LAYOUT_GRID, + LAYOUT_TABLE, + LAYOUT_LIST, + OPERATOR_IS_ANY, +} from '../../utils/constants'; +import { default as Link, useLink } from '../routes/link'; +import Media from '../media'; + +// See https://github.com/WordPress/gutenberg/issues/55886 +// We do not support custom statutes at the moment. +const STATUSES = [ + { value: 'draft', label: __( 'Draft' ), icon: drafts }, + { value: 'future', label: __( 'Scheduled' ), icon: scheduled }, + { value: 'pending', label: __( 'Pending Review' ), icon: pending }, + { value: 'private', label: __( 'Private' ), icon: notAllowed }, + { value: 'publish', label: __( 'Published' ), icon: published }, + { value: 'trash', label: __( 'Trash' ), icon: trash }, +]; + +const getFormattedDate = ( dateToDisplay ) => + dateI18n( + getSettings().formats.datetimeAbbreviated, + getDate( dateToDisplay ) + ); + +function FeaturedImage( { item, viewType } ) { + const isDisabled = item.status === 'trash'; + const { onClick } = useLink( { + postId: item.id, + postType: item.type, + canvas: 'edit', + } ); + const hasMedia = !! item.featured_media; + const size = + viewType === LAYOUT_GRID + ? [ 'large', 'full', 'medium', 'thumbnail' ] + : [ 'thumbnail', 'medium', 'large', 'full' ]; + const media = hasMedia ? ( + + ) : null; + const renderButton = viewType !== LAYOUT_LIST && ! isDisabled; + return ( +
+ { renderButton ? ( + + ) : ( + media + ) } +
+ ); +} + +function PostStatusField( { item } ) { + const status = STATUSES.find( ( { value } ) => value === item.status ); + const label = status?.label || item.status; + const icon = status?.icon; + return ( + + { icon && ( +
+ +
+ ) } + { label } +
+ ); +} + +function PostAuthorField( { item } ) { + const { text, imageUrl } = useSelect( + ( select ) => { + const { getUser } = select( coreStore ); + const user = getUser( item.author ); + return { + imageUrl: user?.avatar_urls?.[ 48 ], + text: user?.name, + }; + }, + [ item ] + ); + const [ isImageLoaded, setIsImageLoaded ] = useState( false ); + return ( + + { !! imageUrl && ( +
+ setIsImageLoaded( true ) } + alt={ __( 'Author avatar' ) } + src={ imageUrl } + /> +
+ ) } + { ! imageUrl && ( +
+ +
+ ) } + { text } +
+ ); +} + +function usePostFields( viewType ) { + const { records: authors, isResolving: isLoadingAuthors } = + useEntityRecords( 'root', 'user', { per_page: -1 } ); + + const { frontPageId, postsPageId } = useSelect( ( select ) => { + const { getEntityRecord } = select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + return { + frontPageId: siteSettings?.page_on_front, + postsPageId: siteSettings?.page_for_posts, + }; + }, [] ); + + const fields = useMemo( + () => [ + { + id: 'featured-image', + header: __( 'Featured Image' ), + getValue: ( { item } ) => item.featured_media, + render: ( { item } ) => ( + + ), + enableSorting: false, + }, + { + header: __( 'Title' ), + id: 'title', + type: 'text', + getValue: ( { item } ) => + typeof item.title === 'string' + ? item.title + : item.title?.raw, + render: ( { item } ) => { + const addLink = + [ LAYOUT_TABLE, LAYOUT_GRID ].includes( viewType ) && + item.status !== 'trash'; + const title = addLink ? ( + + { decodeEntities( item.title?.rendered ) || + __( '(no title)' ) } + + ) : ( + + { decodeEntities( item.title?.rendered ) || + __( '(no title)' ) } + + ); + + let suffix = ''; + if ( item.id === frontPageId ) { + suffix = ( + + { __( 'Homepage' ) } + + ); + } else if ( item.id === postsPageId ) { + suffix = ( + + { __( 'Posts Page' ) } + + ); + } + + return ( + + { title } + { suffix } + + ); + }, + enableHiding: false, + }, + { + header: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item._embedded?.author[ 0 ]?.name, + elements: + authors?.map( ( { id, name } ) => ( { + value: id, + label: name, + } ) ) || [], + render: PostAuthorField, + }, + { + header: __( 'Status' ), + id: 'status', + getValue: ( { item } ) => + STATUSES.find( ( { value } ) => value === item.status ) + ?.label ?? item.status, + elements: STATUSES, + render: PostStatusField, + enableSorting: false, + filterBy: { + operators: [ OPERATOR_IS_ANY ], + }, + }, + { + header: __( 'Date' ), + id: 'date', + render: ( { item } ) => { + const isDraftOrPrivate = [ 'draft', 'private' ].includes( + item.status + ); + if ( isDraftOrPrivate ) { + return createInterpolateElement( + sprintf( + /* translators: %s: page creation date */ + __( 'Modified: ' ), + getFormattedDate( item.date ) + ), + { + span: , + time: