diff --git a/bin/packages/build.js b/bin/packages/build.js index 02009c1e4a4d3d..8a13f8f10b048d 100755 --- a/bin/packages/build.js +++ b/bin/packages/build.js @@ -166,7 +166,7 @@ function buildJsFileFor( file, silent, environment ) { const destPath = getBuildPath( file, buildDir ); const babelOptions = getBabelConfig( environment ); babelOptions.sourceMaps = true; - babelOptions.sourceFileName = file; + babelOptions.sourceFileName = file.replace( PACKAGES_DIR, '@wordpress' ); mkdirp.sync( path.dirname( destPath ) ); const transformed = babel.transformFileSync( file, babelOptions ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 1d714ba9844d39..92afb3799eb903 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -19,7 +19,7 @@ import memize from 'memize'; import { Component, Fragment, RawHTML } from '@wordpress/element'; import { isHorizontalEdge } from '@wordpress/dom'; import { createBlobURL } from '@wordpress/blob'; -import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT } from '@wordpress/keycodes'; +import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, SPACE } from '@wordpress/keycodes'; import { withDispatch, withSelect } from '@wordpress/data'; import { pasteHandler, children, getBlockTransforms, findTransform } from '@wordpress/blocks'; import { withInstanceId, withSafeTimeout, compose } from '@wordpress/compose'; @@ -37,13 +37,11 @@ import { insertLineSeparator, isEmptyLine, unstableToDom, - getSelectionStart, - getSelectionEnd, remove, removeFormat, isCollapsed, LINE_SEPARATOR, - charAt, + indentListItems, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withFilters, IsolatedEventContainer } from '@wordpress/components'; @@ -599,10 +597,26 @@ export class RichText extends Component { this.handleHorizontalNavigation( event ); } + // Use the space key in list items (at the start of an item) to indent + // the list item. + if ( keyCode === SPACE && this.multilineTag === 'li' ) { + const value = this.createRecord(); + + if ( isCollapsed( value ) ) { + const { text, start } = value; + const characterBefore = text[ start - 1 ]; + + // The caret must be at the start of a line. + if ( ! characterBefore || characterBefore === LINE_SEPARATOR ) { + this.onChange( indentListItems( value, { type: this.props.tagName } ) ); + event.preventDefault(); + } + } + } + if ( keyCode === DELETE || keyCode === BACKSPACE ) { const value = this.createRecord(); - const start = getSelectionStart( value ); - const end = getSelectionEnd( value ); + const { replacements, text, start, end } = value; // Always handle full content deletion ourselves. if ( start === 0 && end !== 0 && end === value.text.length ) { @@ -615,22 +629,53 @@ export class RichText extends Component { let newValue; if ( keyCode === BACKSPACE ) { - if ( charAt( value, start - 1 ) === LINE_SEPARATOR ) { + const index = start - 1; + + if ( text[ index ] === LINE_SEPARATOR ) { + const collapsed = isCollapsed( value ); + + // If the line separator that is about te be removed + // contains wrappers, remove the wrappers first. + if ( collapsed && replacements[ index ] && replacements[ index ].length ) { + const newReplacements = replacements.slice(); + + newReplacements[ index ] = replacements[ index ].slice( 0, -1 ); + newValue = { + ...value, + replacements: newReplacements, + }; + } else { + newValue = remove( + value, + // Only remove the line if the selection is + // collapsed, otherwise remove the selection. + collapsed ? start - 1 : start, + end + ); + } + } + } else if ( text[ end ] === LINE_SEPARATOR ) { + const collapsed = isCollapsed( value ); + + // If the line separator that is about te be removed + // contains wrappers, remove the wrappers first. + if ( collapsed && replacements[ end ] && replacements[ end ].length ) { + const newReplacements = replacements.slice(); + + newReplacements[ end ] = replacements[ end ].slice( 0, -1 ); + newValue = { + ...value, + replacements: newReplacements, + }; + } else { newValue = remove( value, + start, // Only remove the line if the selection is - // collapsed. - isCollapsed( value ) ? start - 1 : start, - end + // collapsed, otherwise remove the selection. + collapsed ? end + 1 : end, ); } - } else if ( charAt( value, end ) === LINE_SEPARATOR ) { - newValue = remove( - value, - start, - // Only remove the line if the selection is collapsed. - isCollapsed( value ) ? end + 1 : end, - ); } if ( newValue ) { diff --git a/packages/block-editor/src/components/rich-text/list-edit.js b/packages/block-editor/src/components/rich-text/list-edit.js index 81de868f627192..aee85983258df4 100644 --- a/packages/block-editor/src/components/rich-text/list-edit.js +++ b/packages/block-editor/src/components/rich-text/list-edit.js @@ -3,7 +3,7 @@ */ import { Toolbar } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { Fragment } from '@wordpress/element'; import { indentListItems, @@ -149,6 +149,7 @@ export const ListEdit = ( { { icon: 'editor-outdent', title: __( 'Outdent list item' ), + shortcut: _x( 'Backspace', 'keyboard key' ), onClick: () => { onChange( outdentListItems( value ) ); }, @@ -156,6 +157,7 @@ export const ListEdit = ( { { icon: 'editor-indent', title: __( 'Indent list item' ), + shortcut: _x( 'Space', 'keyboard key' ), onClick: () => { onChange( indentListItems( value, { type: tagName } ) ); }, diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index 7f7667004b7dec..53b857f00c434d 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -1,29 +1,126 @@ # Button +Buttons let users take actions and make choices with a single click or tap. -Buttons express what action will occur when the user clicks or taps it. Buttons are used to trigger an action, and they can be used for any type of action, including navigation. +![Button components](https://make.wordpress.org/design/files/2019/03/button.png) -The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. +## Table of contents + +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) + +## Design guidelines + +### Usage + +Buttons tell users what actions they can take and give them a way to interact with the interface. You’ll find them throughout a UI, particularly in places like: + +- Modals +- Forms +- Toolbars + +### Best practices + +Buttons should: + +- **Be clearly and accurately labeled.** +- **Clearly communicate that clicking or tapping will trigger an action.** +- **Use established colors appropriately.** For example, only use red buttons for actions that are difficult or impossible to undo. +- **Prioritize the most important actions.** This helps users focus. Too many calls to action on one screen can be confusing, making users unsure what to do next. +- **Have consistent locations in the interface.** + +### Content guidelines + +Buttons should be clear and predictable—users should be able to anticipate what will happen when they click a button. Never deceive a user by mislabeling a button. + +Buttons text should lead with a strong verb that encourages action, and add a noun that clarifies what will actually change. The only exceptions are common actions like Save, Close, Cancel, or OK. Otherwise, use the {verb}+{noun} format to ensure that your button gives the user enough information. + +Button text should also be quickly scannable — avoid unnecessary words and articles like the, an, or a. + +### Types + +#### Link button + +Link buttons have low emphasis. They don’t stand out much on the page, so they’re used for less-important actions. What’s less important can vary based on context, but it’s usually a supplementary action to the main action we want someone to take. Link buttons are also useful when you don’t want to distract from the content. + +![Link button](https://make.wordpress.org/design/files/2019/03/link-button.png) + +#### Default button + +Default buttons have medium emphasis. The button appearance helps differentiate them from the page background, so they’re useful when you want more emphasis than a link button offers. + +![Default button](https://make.wordpress.org/design/files/2019/03/default-button.png) + +#### Primary button + +Primary buttons have high emphasis. Their color fill and shadow means they pop off the background. + +Since a high-emphasis button commands the most attention, a layout should contain a single primary button. This makes it clear that other buttons have less importance and helps users understand when an action requires their attention. -Note that this component may sometimes be confused with the Button block, which has semantically different use cases and functionality. +![Primary button](https://make.wordpress.org/design/files/2019/03/primary-button.png) -## Usage +#### Text label + +All button types use text labels to describe the action that happens when a user taps a button. If there’s no text label, there should be an icon to signify what the button does (e.g. an IconButton component). + +![](https://make.wordpress.org/design/files/2019/03/do-link-button.png) + +**Do** +Use color to distinguish link button labels from other text. + +![](https://make.wordpress.org/design/files/2019/03/dont-wrap-button-text.png) + +**Don’t** +Don’t wrap button text. For maximum legibility, keep text labels on a single line. + +### Hierarchy + +![A layout with a single prominent button](https://make.wordpress.org/design/files/2019/03/button.png) + +A layout should contain a single prominently-located button. If multiple buttons are required, a single high-emphasis button can be joined by medium- and low-emphasis buttons mapped to less-important actions. When using multiple buttons, make sure the available state of one button doesn’t look like the disabled state of another. + +![A diagram showing high emphasis at the top, medium emphasis in the middle, and low emphasis at the bottom](https://make.wordpress.org/design/files/2019/03/button-hierarchy.png) + +A button’s level of emphasis helps determine its appearance, typography, and placement. + +#### Placement + +Use button types to express different emphasis levels for all the actions a user can perform. + +![A link, default, and primary button](https://make.wordpress.org/design/files/2019/03/button-layout.png) + +This screen layout uses: + +1. A primary button for high emphasis. +2. A default button for medium emphasis. +3. A link button for low emphasis. + +Placement best practices: + +- **Do**: When using multiple buttons in a row, show users which action is more important by placing it next to a button with a lower emphasis (e.g. a primary button next to a default button, or a default button next to a link button). +- **Don’t**: Don’t place two primary buttons next to one another — they compete for focus. Only use one primary button per view. +- **Don’t**: Don’t place a button below another button if there is space to place them side by side. +- **Caution**: Avoid using too many buttons on a single page. When designing pages in the app or website, think about the most important actions for users to take. Too many calls to action can cause confusion and make users unsure what to do next — we always want users to feel confident and capable. + +## Development guidelines + +### Usage Renders a button with default style. -{% codetabs %} -{% ESNext %} ```jsx import { Button } from "@wordpress/components"; const MyButton = () => ( - + +Click me! + ); ``` -{% end %} -## Props +### Props + +The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. Name | Type | Default | Description --- | --- | --- | --- @@ -39,5 +136,5 @@ Name | Type | Default | Description ## Related components -* To group buttons together, use the `ButtonGroup` component. -* To display an icon inside the button, use the `IconButton` component. +- To group buttons together, use the `ButtonGroup` component. +- To display an icon inside the button, use the `IconButton` component. diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap index b2e8ec4875aaca..4d473f282a17a4 100644 --- a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap +++ b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap @@ -98,6 +98,44 @@ exports[`List should change the indented list type 1`] = ` " `; +exports[`List should create and remove indented list with keyboard only 1`] = ` +" + +" +`; + +exports[`List should create and remove indented list with keyboard only 2`] = ` +" + +" +`; + +exports[`List should create and remove indented list with keyboard only 3`] = ` +" + +" +`; + +exports[`List should create and remove indented list with keyboard only 4`] = ` +" + +" +`; + +exports[`List should create and remove indented list with keyboard only 5`] = ` +" + +" +`; + +exports[`List should create and remove indented list with keyboard only 6`] = ` +" + +" +`; + +exports[`List should create and remove indented list with keyboard only 7`] = `""`; + exports[`List should create paragraph on split at end and merge back with content 1`] = ` " diff --git a/packages/e2e-tests/specs/blocks/list.test.js b/packages/e2e-tests/specs/blocks/list.test.js index 23d02e0f10cb76..2bbc0386278857 100644 --- a/packages/e2e-tests/specs/blocks/list.test.js +++ b/packages/e2e-tests/specs/blocks/list.test.js @@ -304,4 +304,46 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should create and remove indented list with keyboard only', async () => { + await clickBlockAppender(); + + await page.keyboard.type( '* 1' ); // Should be at level 0. + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ' a' ); // Should be at level 1. + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ' i' ); // Should be at level 2. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); // Should be at level 1. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); // Should be at level 0. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); // Should be at level 1. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); // Should be at level 0. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); // Should be at level 0. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); // Should remove list. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + // That's 9 key presses to create the list, and 9 key presses to remove + // the list. ;) + } ); } );