diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 10ceb797e28c0f..39c2fd1ac556e2 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -700,6 +700,15 @@ Add text that respects your spacing and tabs, and also allows styling. ([Source] - **Supports:** anchor, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** content +## Progress Bar + +Display a progress bar. Useful for tracking progress on a task or project. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/progress-bar)) + +- **Name:** core/progress-bar +- **Category:** design +- **Supports:** ~~html~~ +- **Attributes:** backgroundColor, height, isReadProgress, label, max, progressColor, seprator, showTotal, showValue, symbol, symbolPosition, value + ## Pullquote Give special visual emphasis to a quote from your text. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/pullquote)) diff --git a/lib/blocks.php b/lib/blocks.php index 342cd25191e689..3efeda6bd08435 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -43,6 +43,7 @@ function gutenberg_reregister_core_block_types() { 'verse', 'video', 'embed', + 'progress-bar', ), 'block_names' => array( 'archives.php' => 'core/archives', diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 262f11de6ee22d..0c25f6c0593c74 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -4,8 +4,8 @@ import { setDefaultBlockName, setFreeformContentHandlerName, - setUnregisteredTypeHandlerName, setGroupingBlockName, + setUnregisteredTypeHandlerName, } from '@wordpress/blocks'; /** @@ -21,17 +21,16 @@ import { // // See https://github.com/WordPress/gutenberg/pull/40655 for more context. import * as archives from './archives'; -import * as avatar from './avatar'; import * as audio from './audio'; +import * as avatar from './avatar'; +import * as reusableBlock from './block'; import * as button from './button'; import * as buttons from './buttons'; import * as calendar from './calendar'; import * as categories from './categories'; -import * as classic from './freeform'; import * as code from './code'; import * as column from './column'; import * as columns from './columns'; -import * as comments from './comments'; import * as commentAuthorAvatar from './comment-author-avatar'; import * as commentAuthorName from './comment-author-name'; import * as commentContent from './comment-content'; @@ -39,19 +38,22 @@ import * as commentDate from './comment-date'; import * as commentEditLink from './comment-edit-link'; import * as commentReplyLink from './comment-reply-link'; import * as commentTemplate from './comment-template'; -import * as commentsPaginationPrevious from './comments-pagination-previous'; +import * as comments from './comments'; import * as commentsPagination from './comments-pagination'; import * as commentsPaginationNext from './comments-pagination-next'; import * as commentsPaginationNumbers from './comments-pagination-numbers'; +import * as commentsPaginationPrevious from './comments-pagination-previous'; import * as commentsTitle from './comments-title'; import * as cover from './cover'; import * as details from './details'; import * as embed from './embed'; import * as file from './file'; +import * as footnotes from './footnotes'; import * as form from './form'; import * as formInput from './form-input'; -import * as formSubmitButton from './form-submit-button'; import * as formSubmissionNotification from './form-submission-notification'; +import * as formSubmitButton from './form-submit-button'; +import * as classic from './freeform'; import * as gallery from './gallery'; import * as group from './group'; import * as heading from './heading'; @@ -70,13 +72,13 @@ import * as navigation from './navigation'; import * as navigationLink from './navigation-link'; import * as navigationSubmenu from './navigation-submenu'; import * as nextpage from './nextpage'; -import * as pattern from './pattern'; import * as pageList from './page-list'; import * as pageListItem from './page-list-item'; import * as paragraph from './paragraph'; +import * as pattern from './pattern'; import * as postAuthor from './post-author'; -import * as postAuthorName from './post-author-name'; import * as postAuthorBiography from './post-author-biography'; +import * as postAuthorName from './post-author-name'; import * as postComment from './post-comment'; import * as postCommentsCount from './post-comments-count'; import * as postCommentsForm from './post-comments-form'; @@ -91,6 +93,7 @@ import * as postTerms from './post-terms'; import * as postTimeToRead from './post-time-to-read'; import * as postTitle from './post-title'; import * as preformatted from './preformatted'; +import * as progressBar from './progress-bar'; import * as pullquote from './pullquote'; import * as query from './query'; import * as queryNoResults from './query-no-results'; @@ -101,7 +104,6 @@ import * as queryPaginationPrevious from './query-pagination-previous'; import * as queryTitle from './query-title'; import * as queryTotal from './query-total'; import * as quote from './quote'; -import * as reusableBlock from './block'; import * as readMore from './read-more'; import * as rss from './rss'; import * as search from './search'; @@ -121,7 +123,6 @@ import * as termDescription from './term-description'; import * as textColumns from './text-columns'; import * as verse from './verse'; import * as video from './video'; -import * as footnotes from './footnotes'; import isBlockMetadataExperimental from './utils/is-block-metadata-experimental'; @@ -182,6 +183,7 @@ const getAllBlocks = () => { verse, video, footnotes, + progressBar, // theme blocks navigation, diff --git a/packages/block-library/src/progress-bar/block.json b/packages/block-library/src/progress-bar/block.json new file mode 100644 index 00000000000000..bb5f96a644f2c7 --- /dev/null +++ b/packages/block-library/src/progress-bar/block.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "core/progress-bar", + "category": "design", + "title": "Progress Bar", + "description": "Display a progress bar. Useful for tracking progress on a task or project.", + "textdomain": "default", + "attributes": { + "label": { + "type": "string", + "default": "" + }, + "value": { + "type": "number", + "default": 50 + }, + "max": { + "type": "number", + "default": 100 + }, + "backgroundColor": { + "type": "string", + "default": "#f0f0f0" + }, + "progressColor": { + "type": "string", + "default": "#1E1E1E" + }, + "height": { + "type": "number", + "default": 11 + }, + "showValue": { + "type": "boolean", + "default": true + }, + "isReadProgress": { + "type": "boolean", + "default": false + }, + "symbol": { + "type": "string", + "default": "%" + }, + "symbolPosition": { + "type": "string", + "default": "suffix" + }, + "showTotal": { + "type": "boolean", + "default": false + }, + "seprator": { + "type": "string", + "default": "/" + } + }, + "supports": { + "html": false + }, + "style": "wp-block-progress-bar" +} diff --git a/packages/block-library/src/progress-bar/edit.js b/packages/block-library/src/progress-bar/edit.js new file mode 100644 index 00000000000000..36dde1da405674 --- /dev/null +++ b/packages/block-library/src/progress-bar/edit.js @@ -0,0 +1,368 @@ +/** + * WordPress dependencies + */ +import { + InspectorControls, + PanelColorSettings, + RichText, + useBlockProps, +} from '@wordpress/block-editor'; +import { + __experimentalNumberControl as NumberControl, + RangeControl, + SelectControl, + TextControl, + ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + +export default function Edit( { attributes, setAttributes } ) { + const { + label, + value, + max = 100, + backgroundColor, + progressColor, + height, + showValue, + isReadProgress, + symbol, + symbolPosition, + showTotal, + seprator, + } = attributes; + + const blockProps = useBlockProps( { + className: 'wp-block-progress-bar', + } ); + + const progressBarStyle = { + backgroundColor, + height: `${ height }px`, + }; + + const progressStyle = { + backgroundColor: progressColor, + width: `${ ( value / max ) * 100 }%`, + }; + + const readProgressStyle = { + backgroundColor: progressColor, + height: `${ height }px`, + }; + + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + + const formatValue = ( val ) => { + return symbolPosition === 'prefix' + ? `${ symbol }${ val }` + : `${ val }${ symbol }`; + }; + + const valueDisplay = showTotal + ? `${ formatValue( value ) } ${ seprator } ${ formatValue( max ) }` + : formatValue( value ); + + return ( +
+ + { + setAttributes( { + label: '', + value: 50, + max: 100, + backgroundColor: '#f0f0f0', + progressColor: '#1E1E1E', + height: 11, + showValue: true, + isReadProgress: false, + symbol: '%', + symbolPosition: 'suffix', + showTotal: false, + seprator: '/', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + isReadProgress } + onDeselect={ () => + setAttributes( { isReadProgress: false } ) + } + > + + setAttributes( { + isReadProgress: ! isReadProgress, + } ) + } + /> + + { ! isReadProgress && ( + <> + value !== 50 } + onDeselect={ () => + setAttributes( { value: 50 } ) + } + > + + setAttributes( { value: currentValue } ) + } + min={ 0 } + max={ max } + /> + + max !== 100 } + onDeselect={ () => + setAttributes( { max: 100 } ) + } + > + + setAttributes( { max: maxValue } ) + } + /> + + + ) } + height !== 11 } + onDeselect={ () => setAttributes( { height: 11 } ) } + > + + setAttributes( { height: heightValue } ) + } + min={ 1 } + max={ 30 } + /> + + + { ! isReadProgress && ( + <> + ! showValue } + onDeselect={ () => + setAttributes( { showValue: true } ) + } + > + + setAttributes( { + showValue: ! showValue, + } ) + } + /> + + { showValue && ( + showTotal } + onDeselect={ () => + setAttributes( { showTotal: false } ) + } + > + + setAttributes( { + showTotal: ! showTotal, + } ) + } + /> + + ) } + { showValue && showTotal && ( + seprator !== '/' } + onDeselect={ () => + setAttributes( { seprator: '/' } ) + } + > + + setAttributes( { + seprator: newSeprator, + } ) + } + /> + + ) } + { showValue && ( + <> + symbol !== '%' } + onDeselect={ () => + setAttributes( { symbol: '%' } ) + } + > + + setAttributes( { + symbol: newSymbol, + } ) + } + /> + + + symbolPosition !== 'suffix' + } + onDeselect={ () => + setAttributes( { + symbolPosition: 'suffix', + } ) + } + > + + setAttributes( { + symbolPosition: position, + } ) + } + /> + + + ) } + + ) } + + + setAttributes( { backgroundColor: bgColor } ), + label: __( 'Background Color' ), + }, + { + value: progressColor, + onChange: ( progressBarColor ) => + setAttributes( { + progressColor: progressBarColor, + } ), + label: __( 'Progress Color' ), + }, + ] } + /> + + +
+ { ! isReadProgress && ( +
+ + setAttributes( { label: content } ) + } + placeholder={ __( 'Write heading…' ) } + /> + { showValue &&

{ valueDisplay }

} +
+ ) } +
+
+
+
+
+ ); +} diff --git a/packages/block-library/src/progress-bar/index.js b/packages/block-library/src/progress-bar/index.js new file mode 100644 index 00000000000000..b7f1257c98b142 --- /dev/null +++ b/packages/block-library/src/progress-bar/index.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { _x } from '@wordpress/i18n'; +import { progressBar as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + icon, + edit, + save, + example: { + attributes: { + label: _x( 'Progress Bar', 'block example' ), + }, + }, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/progress-bar/init.js b/packages/block-library/src/progress-bar/init.js new file mode 100644 index 00000000000000..a7f22ef02d640f --- /dev/null +++ b/packages/block-library/src/progress-bar/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from '.'; + +export default init(); diff --git a/packages/block-library/src/progress-bar/save.js b/packages/block-library/src/progress-bar/save.js new file mode 100644 index 00000000000000..375ae2921f43e1 --- /dev/null +++ b/packages/block-library/src/progress-bar/save.js @@ -0,0 +1,87 @@ +/** + * WordPress dependencies + */ +import { RichText, useBlockProps } from '@wordpress/block-editor'; + +export default function Save( { attributes } ) { + const { + label, + value, + max = 100, + backgroundColor, + progressColor, + height, + showValue, + isReadProgress, + symbol, + symbolPosition, + showTotal, + seprator, + } = attributes; + + // eslint-disable-next-line react-compiler/react-compiler + const blockProps = useBlockProps.save( { + className: 'wp-block-progress-bar', + } ); + + const progressBarStyle = { + backgroundColor, + height: `${ height }px`, + }; + + const progressStyle = { + backgroundColor: progressColor, + width: `${ ( value / max ) * 100 }%`, + }; + + const readProgressStyle = { + backgroundColor: progressColor, + height: `${ height }px`, + }; + + const formatValue = ( val ) => { + return symbolPosition === 'prefix' + ? `${ symbol }${ val }` + : `${ val }${ symbol }`; + }; + + const valueDisplay = showTotal + ? `${ formatValue( value ) } ${ seprator } ${ formatValue( max ) }` + : formatValue( value ); + + return ( +
+
+ { ! isReadProgress && ( +
+ + { showValue &&

{ valueDisplay }

} +
+ ) } +
+
+
+
+
+ ); +} diff --git a/packages/block-library/src/progress-bar/style.scss b/packages/block-library/src/progress-bar/style.scss new file mode 100644 index 00000000000000..9427e4c028677d --- /dev/null +++ b/packages/block-library/src/progress-bar/style.scss @@ -0,0 +1,77 @@ +@keyframes grow-progress { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +.wp-block-progress-bar { + &__container { + width: 100%; + } + + &__container > div:first-child { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__bar { + width: 100%; + overflow: hidden; + position: relative; + } + + &__progress { + height: 100%; + + @media not (prefers-reduced-motion) { + transition: width 0.3s ease; + } + } + + &__read-bar { + width: 100%; + position: fixed; + left: 0; + top: 0; + z-index: 1000; + + .admin-bar & { + top: 32px; + + @media screen and (max-width: 779px) { + top: 46px; + } + + @media screen and (max-width: 594px) { + top: 0; + } + } + } + + &__read-progress { + position: fixed; + left: 0; + top: 0; + width: 100%; + transform-origin: 0 50%; + animation: grow-progress auto linear; + z-index: 1000; + animation-timeline: scroll() !important; + + .admin-bar & { + top: 32px; + + @media screen and (max-width: 779px) { + top: 46px; + } + + @media screen and (max-width: 594px) { + top: 0; + } + } + } +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index c61049c23151b9..298371ff72dee2 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -70,5 +70,6 @@ @import "./verse/style.scss"; @import "./video/style.scss"; @import "./footnotes/style.scss"; +@import "./progress-bar/style.scss"; @import "common.scss"; diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index e82b09e5d5afe9..fad74665820624 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -218,6 +218,7 @@ export { default as previous } from './library/previous'; export { default as next } from './library/next'; export { default as offline } from './library/offline'; export { default as preformatted } from './library/preformatted'; +export { default as progressBar } from './library/progress-bar'; export { default as published } from './library/published'; export { default as pullLeft } from './library/pull-left'; export { default as pullRight } from './library/pull-right'; diff --git a/packages/icons/src/library/progress-bar.js b/packages/icons/src/library/progress-bar.js new file mode 100644 index 00000000000000..ae16345ba45f2c --- /dev/null +++ b/packages/icons/src/library/progress-bar.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { G, Path, SVG } from '@wordpress/primitives'; + +const progressBar = ( + +); + +export default progressBar; diff --git a/test/e2e/specs/editor/blocks/progress.spec.js b/test/e2e/specs/editor/blocks/progress.spec.js new file mode 100644 index 00000000000000..0acda6fc003722 --- /dev/null +++ b/test/e2e/specs/editor/blocks/progress.spec.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Progress Bar', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test( 'should render progress bar with correct prefix symbol and value', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/progress-bar' } ); + await page.getByLabel( 'Value Symbol' ).click(); + await page.getByLabel( 'Value Symbol' ).fill( '$' ); + await page.getByLabel( 'Symbol Position' ).selectOption( 'prefix' ); + + const editorFrame = page + .locator( 'iframe[name="editor-canvas"]' ) + .contentFrame(); + + const progressBarValue = editorFrame + .locator( '.wp-block-progress-bar__container > div > p' ) + .nth( 1 ); + + await expect( progressBarValue ).toHaveText( '$50' ); + } ); + + test( 'should apply custom background and progress colors', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/progress-bar' } ); + + await page.getByRole( 'button', { name: 'Background Color' } ).click(); + await page.getByLabel( 'Cyan bluish gray' ).click(); + await page.getByRole( 'button', { name: 'Progress Color' } ).click(); + await page.getByLabel( 'Vivid purple' ).click(); + + const editorFrame = page + .locator( 'iframe[name="editor-canvas"]' ) + .contentFrame(); + + const barContainer = editorFrame.locator( + '.wp-block-progress-bar__bar' + ); + const progressBar = editorFrame.locator( + '.wp-block-progress-bar__progress' + ); + + await expect( barContainer ).toHaveCSS( + 'background-color', + 'rgb(171, 184, 195)' + ); + await expect( progressBar ).toHaveCSS( + 'background-color', + 'rgb(155, 81, 224)' + ); + } ); +} );