diff --git a/.eslintrc.js b/.eslintrc.js index d5deabd83e0c5..13c7e260e9bd9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -81,6 +81,7 @@ const restrictedImports = [ 'flowRight', 'forEach', 'fromPairs', + 'groupBy', 'has', 'identity', 'includes', diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index ff2c73e90ac68..ca634dc8a9c7a 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -14,11 +14,11 @@ concurrency: jobs: test: - runs-on: macos-11 + runs-on: macos-12 if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: matrix: - xcode: ['13.2.1'] + xcode: ['13.3.1'] device: ['iPhone 13'] native-test-name: [gutenberg-editor-rendering] @@ -61,8 +61,8 @@ jobs: - name: Build Web Driver Agent (if needed) run: test -d packages/react-native-editor/ios/build/WDA || npm run native test:e2e:build-wda - - name: Force update Launch Database to prevent issues when opening the Simulator app - run: /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer/Applications/Simulator.app + - name: Launch simulator + run: open -a Simulator && xcrun simctl boot '${{ matrix.device }}' - name: Run iOS Device Tests run: TEST_RN_PLATFORM=ios npm run native device-tests:local ${{ matrix.native-test-name }} diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index e6fb8c7054b16..b8d37968781dc 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -1,7 +1,6 @@ /** * External dependencies */ -const { groupBy } = require( 'lodash' ); const Octokit = require( '@octokit/rest' ); const { sprintf } = require( 'sprintf-js' ); const semver = require( 'semver' ); @@ -711,9 +710,19 @@ async function fetchAllPullRequests( octokit, settings ) { function getChangelog( pullRequests ) { let changelog = '## Changelog\n\n'; - const groupedPullRequests = groupBy( - skipCreatedByBots( pullRequests ), - getIssueType + const groupedPullRequests = skipCreatedByBots( pullRequests ).reduce( + ( + /** @type {Record} */ acc, + pr + ) => { + const issueType = getIssueType( pr ); + if ( ! acc[ issueType ] ) { + acc[ issueType ] = []; + } + acc[ issueType ].push( pr ); + return acc; + }, + {} ); const sortedGroups = Object.keys( groupedPullRequests ).sort( sortGroup ); @@ -732,7 +741,20 @@ function getChangelog( pullRequests ) { changelog += '### ' + group + '\n\n'; // Group PRs within this section into "Features". - const featureGroups = groupBy( groupPullRequests, getIssueFeature ); + const featureGroups = groupPullRequests.reduce( + ( + /** @type {Record} */ acc, + pr + ) => { + const issueFeature = getIssueFeature( pr ); + if ( ! acc[ issueFeature ] ) { + acc[ issueFeature ] = []; + } + acc[ issueFeature ].push( pr ); + return acc; + }, + {} + ); const featuredGroupNames = sortFeatureGroups( featureGroups ); diff --git a/docs/README.md b/docs/README.md index 2a4c797336acf..41c694faf71f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,43 +1,74 @@ # Block Editor Handbook -**Gutenberg** is a codename for a whole new paradigm in WordPress site building and publishing, that aims to revolutionize the entire publishing experience as much as Gutenberg did the printed word. Right now, the project is in the second phase of a four-phase process that will touch every piece of WordPress -- Editing, **Customization** (which includes Full Site Editing, Block Patterns, Block Directory and Block based themes), Collaboration, and Multilingual -- and is focused on a new editing experience, the block editor (which is the topic of the current documentation). +Hi! 👋 Welcome to the Block Editor Handbook. -![Quick view of the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/quick-view-of-the-block-editor.png) +The [**Block editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content, and is designed to create rich and flexible layouts for websites and digital products. -**Legend:** +The editor consists of several primary elements, as shown in the following figure: -1. Block inserter -2. Block editor content area -3. Settings sidebar +![Quick view of the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/overview-block-editor-2023.png) -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 using blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block, though). +The elements highlighted in the figure are: -Blocks treat Paragraphs, Headings, Media, and Embeds all as components that, when 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 that it is backward compatible with all legacy content. It also offers a process to try to convert and split a Classic block into equivalent blocks using client-side parsing. Finally, the blocks offer enhanced editing and format controls. +1. **Inserter**: A panel for inserting blocks into the content canvas +2. **Content canvas**: The content editor, which holds content created with blocks +3. **Settings sidebar**: A sidebar panel for configuring a block’s settings (among other things) -The Editor offers rich new value to users by offering visual, drag-and-drop creation tools and powerful developer enhancements including modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. +Through the Block editor, you create content modularly using Blocks. There are a number of [core blocks](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/) ready to be used, and you can also [create your own custom block](https://developer.wordpress.org/block-editor/getting-started/create-block/). -## Quick links +A [Block](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks) is a discrete element such as a Paragraph, Heading, Media element, or Embed. Each block is treated as a separate element with individual editing and format controls. When all these components are pieced together, they make up the content that is then [stored in the WordPress database](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/#serialization-and-parsing). -### Create pages and posts with the block editor +The Block Editor is the result of the [work done on the **Gutenberg project**](https://developer.wordpress.org/block-editor/explanations/faq/#what-is-gutenberg) which is aimed to revolutionize the WordPress editing experience. -In the Block Editor Handbook, our tutorials will be development-focussed. However, it helps if you have some experience using the block editor the way an end-user would first. If you have no experience building with the block editor yet, we recommend you [learn to use the block editor](https://wordpress.org/documentation/article/wordpress-block-editor/) to create posts and pages. +Besides offering an [enhanced editing experience](https://wordpress.org/gutenberg/) through visual content creation tools, the Block Editor is also a powerful developer platform with a [rich feature set of APIs](https://developer.wordpress.org/block-editor/reference-guides/) that allow it to be manipulated and extended in a multitude of different ways. -### Create a Block Tutorial +## Navigating this handbook -[Learn how to create your first block](/docs/getting-started/create-block/README.md) for the WordPress block editor. From setting up your development environment, tools, and getting comfortable with the new development model, this tutorial covers all you need to know to get started with creating blocks. +This handbook is focused on block development and is divided into five sections, each serving a different purpose. -### Develop for the block editor -Whether you want to extend the functionality of the block editor, or create a plugin based on it, [see our how-to guides](/docs/how-to-guides/README.md) to find all the information about the basic concepts you need to get started, the block editor APIs and its architecture. +**[Getting Started](https://developer.wordpress.org/block-editor/getting-started/)** -- [Gutenberg Architecture](/docs/explanations/architecture/README.md) -- [Block Styles](/docs/reference-guides/block-api/block-styles.md) -- [Creating Block Patterns](/docs/reference-guides/block-api/block-patterns.md) -- [Theming for the Block Editor](/docs/how-to-guides/themes/README.md) -- [Block API Reference](/docs/reference-guides/block-api/README.md) -- [Block Editor Accessibility](/docs/how-to-guides/accessibility.md) -- [Internationalization](/docs/how-to-guides/internationalization.md) +For those just starting out with block development this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/create-block/). -### Contribute to the block editor -Everything you need to know to [start contributing to the block editor](/docs/contributors/README.md) . Whether you are interested in the design, code, triage, documentation, support or internationalization of the block editor, you will find guides to help you here. +**[How-to Guides](https://developer.wordpress.org/block-editor/how-to-guides/)** + +Here you can build on what you learned in the Getting Started section and learn how to solve particular problems that you might encounter. You can also get tutorials, and example code that you can reuse, for projects such as [building a full-featured block](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/) or [working with WordPress’ data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/). In addition you can learn [How to use JavaScript with the Block Editor](https://developer.wordpress.org/block-editor/how-to-guides/javascript/). + + +**[Reference Guides](https://developer.wordpress.org/block-editor/reference-guides/)** + +This section is the heart of the handbook and is where you can get down to the nitty-gritty and look up the details of the particular API that you’re working with or need information on. Among other things, the [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/) covers most of what you will want to do with a block, and each [component](https://developer.wordpress.org/block-editor/reference-guides/components/) and [package](https://developer.wordpress.org/block-editor/reference-guides/packages/) is also documented here. _Components are also documented via [Storybook](https://wordpress.github.io/gutenberg/?path=/story/docs-introduction--page)._ + + +**[Explanations](https://developer.wordpress.org/block-editor/explanations/)** + +This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor. Its [Glossary of terms](https://developer.wordpress.org/block-editor/explanations/glossary/) and [FAQs](https://developer.wordpress.org/block-editor/explanations/faq/) should answer any outstanding questions you may have. + + +**[Contributor Guide](https://developer.wordpress.org/block-editor/contributors/)** + +Gutenberg is open source software and anyone is welcome to contribute to the project. This section details how to contribute and can help you choose in which way you want to contribute, whether that be with [code](https://developer.wordpress.org/block-editor/contributors/code/), with [design](https://developer.wordpress.org/block-editor/contributors/design/), with [documentation](https://developer.wordpress.org/block-editor/contributors/documentation/), or in some other way. + + +## Further resources + +This handbook should be considered the canonical resource for all things related to block development. However there are other resources that can help you. + +- [**WordPress Developer Blog**](https://developer.wordpress.org/news/) - An ever-growing resource of technical articles covering specific topics related to block development and a wide variety of use cases. The blog is also an excellent way to [keep up with the latest developments in WordPress](https://developer.wordpress.org/news/tag/roundup/). +- [**Learn WordPress**](https://learn.wordpress.org/) - The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) +- [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [block-editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. +- [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository is another useful reference._ + + +## Are you in the right place? + +[This handbook](https://developer.wordpress.org/block-editor) is targeted at those seeking to develop for the block editor, but several other handbooks exist for WordPress developers under [developer.wordpress.org](http://developer.wordpress.org/): + +- [/themes](https://developer.wordpress.org/themes) - Theme Handbook +- [/plugins](https://developer.wordpress.org/plugins) - Plugin Handbook +- [/apis](https://developer.wordpress.org/apis) - Common APIs Handbook +- [/advanced-administration](https://developer.wordpress.org/advanced-administration) - WP Advanced Administration Handbook +- [/rest-api](https://developer.wordpress.org/rest-api/) - REST API Handbook +- [/coding-standards](https://developer.wordpress.org/coding-standards) - Best practices for WordPress developers \ No newline at end of file diff --git a/docs/assets/overview-block-editor-2023.png b/docs/assets/overview-block-editor-2023.png new file mode 100644 index 0000000000000..ad3a6e1ff7192 Binary files /dev/null and b/docs/assets/overview-block-editor-2023.png differ diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index d9d62450c94bf..1954020c1a3eb 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -114,133 +114,47 @@ Example: import VisualEditor from '../visual-editor'; ``` -### Experimental and Unstable APIs +### Legacy Experimental APIs, Plugin-only APIs, and Private APIs -Experimental and unstable APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. +#### Legacy Experimental APIs -_To External Consumers:_ - -**There is no support commitment for experimental and unstable APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. - -_To Project Contributors:_ - -An experimental or unstable API is named as such to communicate instability of a function whose interface is not yet finalized. Aside from references within the code, these APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. - -An experimental or unstable function or object should be prefixed respectively using `__experimental` or `__unstable`. - -```js -export { __experimentalDoExcitingExperimentalAction } from './api'; -export { __unstableDoTerribleAwfulAction } from './api'; -``` - -- An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. -- An **unstable API** is one which serves as a means to an end. It is not desired to ever be converted into a public API. - -In both cases, the API should be made stable or removed at the earliest opportunity. - -While an experimental API may often stabilize into a publicly-available API, there is no guarantee that it will. The conversion to a stable API will inherently be considered a breaking change by the mere fact that the function name must be changed to remove the `__experimental` prefix. +Historically, Gutenberg has used the `__experimental` and `__unstable` prefixes to indicate that a given API is not yet stable and may be subject to change. This is a legacy convention which should be avoided in favor of the plugin-only API pattern or a private API pattern described below. -#### Experimental APIs merged into WordPress Core become a liability +The problem with using the prefixes was that these APIs rarely got stabilized or removed. As of June 2022, WordPress Core contained 280 publicly exported experimental APIs merged from the Gutenberg plugin during the major WordPress releases. Many plugins and themes started relying on these experimental APIs for essential features that couldn't be accessed in any other way. -**Avoid introducing public experimental APIs.** +The legacy `__experimental` APIs can't be removed on a whim anymore. They became a part of the WordPress public API and fall under the [WordPress Backwards Compatibility policy](https://developer.wordpress.org/block-editor/contributors/code/backward-compatibility/). Removing them involves a deprecation process. It may be relatively easy for some APIs, but it may require effort and span multiple WordPress releases for others. -As of June 2022, WordPress Core contains 280 publicly exported experimental APIs. They got merged from the Gutenberg -plugin during the major WordPress releases. Many plugins and themes rely on these experimental APIs for essential -features that can't be accessed in any other way. Naturally, these APIs can't be removed without a warning anymore. -They are a part of the WordPress public API and fall under the -[WordPress Backwards Compatibility policy](https://developer.wordpress.org/block-editor/contributors/code/backward-compatibility/). -Removing them involves a deprecation process. It may be relatively easy for some APIs, but it may require effort and -span multiple WordPress releases for others. +All in all, don't use the `__experimental` prefix for new APIs. Use plugin-only APIs and private APIs instead. -**Use private experimental APIs instead.** +#### Plugin-only APIs -Make your experimental APIs private and don't expose them to WordPress extenders. +Plugin-only APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. -This way they'll remain internal implementation details that can be changed or removed -without a warning and without breaking WordPress plugins. - -The tactical guidelines below will help you write code without introducing new experimental APIs. - -#### General guidelines - -Some `__experimental` functions are exported in _package A_ and only used in a single _package B_ and nowhere else. Consider removing such functions from _package A_ and making them private and non-exported members of _package B_. +_To External Consumers:_ -If your experimental API is only meant for the Gutenberg Plugin but not for the next WordPress major release, consider limiting the export to the plugin environment. For example, `@wordpress/components` could do that to receive early feedback about a new Component, but avoid bringing that component to WordPress core: +**There is no support commitment for plugin-only APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. -```js -if ( IS_GUTENBERG_PLUGIN ) { - export { __experimentalFunction } from './private-apis'; -} -``` +_To Project Contributors:_ -#### Replace experimental selectors with hooks +An **plugin-only API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. It should be made stable or removed at the earliest opportunity. -Sometimes a non-exported React hook suffices as a substitute for introducing a new experimental selectors: +Plugin-only APIs are excluded from WordPress Core and only available in the Gutenberg Plugin: ```js -// Instead of this: -// selectors.js: -export function __unstableHasActiveBlockOverlayActive( state, parent ) { - /* ... */ -} -export function __unstableIsWithinBlockOverlay( state, clientId ) { - let parent = state.blocks.parents[ clientId ]; - while ( !! parent ) { - if ( __unstableHasActiveBlockOverlayActive( state, parent ) ) { - return true; - } - parent = state.blocks.parents[ parent ]; - } - return false; -} -// MyComponent.js: -function MyComponent( { clientId } ) { - const { __unstableIsWithinBlockOverlay } = useSelect( myStore ); - const isWithinBlockOverlay = __unstableIsWithinBlockOverlay( clientId ); - // ... -} - -// Consider this: -// MyComponent.js: -function hasActiveBlockOverlayActive( selectors, parent ) { - /* ... */ -} -function useIsWithinBlockOverlay( clientId ) { - return useSelect( ( select ) => { - const selectors = select( blockEditorStore ); - let parent = selectors.getBlockRootClientId( clientId ); - while ( !! parent ) { - if ( hasActiveBlockOverlayActive( selectors, parent ) ) { - return true; - } - parent = selectors.getBlockRootClientId( parent ); - } - return false; - } ); -} -function MyComponent( { clientId } ) { - const isWithinBlockOverlay = useIsWithinBlockOverlay( clientId ); - // ... +// Using process.env.IS_GUTENBERG_PLUGIN allows Webpack to exclude this +// export from WordPress core: +if ( process.env.IS_GUTENBERG_PLUGIN ) { + export { doSomethingExciting } from './api'; } ``` -#### Dispatch experimental actions in thunks - -Turning an existing public action into a [thunk](/docs/how-to-guides/thunks.md) -enables dispatching private actions inline: +The public interface of such APIs is not yet finalized. Aside from references within the code, they APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. -```js -export function toggleFeature( scope, featureName ) { - return function ( { dispatch } ) { - dispatch( { type: '__experimental_BEFORE_TOGGLE' } ); - // ... - }; -} -``` +While a plugin-only API may often stabilize into a publicly-available API, there is no guarantee that it will. -#### Use the `lock()` and `unlock()` API from `@wordpress/private-apis` to privately export almost anything +#### Private APIs -Each `@wordpress` package wanting to privately access or expose experimental APIs can +Each `@wordpress` package wanting to privately access or expose a private APIs can do so by opting-in to `@wordpress/private-apis`: ```js @@ -264,10 +178,10 @@ Once the package opted-in, you can use the `lock()` and `unlock()` utilities: export const publicObject = {}; // However, this string is internal and should not be publicly available: -const __experimentalString = '__experimental information'; +const privateString = 'private information'; // Solution: lock the string "inside" of the object: -lock( publicObject, __experimentalString ); +lock( publicObject, privateString ); // The string is not nested in the object and cannot be extracted from it: console.log( publicObject ); @@ -275,65 +189,65 @@ console.log( publicObject ); // The only way to access the string is by "unlocking" the object: console.log( unlock( publicObject ) ); -// "__experimental information" +// "private information" // lock() accepts all data types, not just strings: export const anotherObject = {}; -lock( anotherObject, function __experimentalFn() {} ); +lock( anotherObject, function privateFn() {} ); console.log( unlock( anotherObject ) ); -// function __experimentalFn() {} +// function privateFn() {} ``` Keep reading to learn how to use `lock()` and `unlock()` to avoid publicly exporting -different kinds of `__experimental` APIs. +different kinds of `private` APIs. -##### Experimental selectors and actions +##### Private selectors and actions You can attach private selectors and actions to a public store: ```js // In packages/package1/store.js: -import { __experimentalHasContentRoleAttribute, ...selectors } from './selectors'; -import { __experimentalToggleFeature, ...actions } from './selectors'; -// The `lock` function is exported from the internal experiments.js file where +import { privateHasContentRoleAttribute, ...selectors } from './selectors'; +import { privateToggleFeature, ...actions } from './selectors'; +// The `lock` function is exported from the internal private-apis.js file where // the opt-in function was called. import { lock, unlock } from './private-apis'; export const store = registerStore(/* ... */); // Attach a private action to the exported store: unlock( store ).registerPrivateActions({ - __experimentalToggleFeature + privateToggleFeature } ); // Attach a private action to the exported store: unlock( store ).registerPrivateSelectors({ - __experimentalHasContentRoleAttribute + privateHasContentRoleAttribute } ); // In packages/package2/MyComponent.js: import { store } from '@wordpress/package1'; import { useSelect } from '@wordpress/data'; -// The `unlock` function is exported from the internal experiments.js file where +// The `unlock` function is exported from the internal private-apis.js file where // the opt-in function was called. import { unlock } from './private-apis'; function MyComponent() { const hasRole = useSelect( ( select ) => ( // Use the private selector: - unlock( select( store ) ).__experimentalHasContentRoleAttribute() + unlock( select( store ) ).privateHasContentRoleAttribute() // Note the unlock() is required. This line wouldn't work: - // select( store ).__experimentalHasContentRoleAttribute() + // select( store ).privateHasContentRoleAttribute() ) ); // Use the private action: - unlock( useDispatch( store ) ).__experimentalToggleFeature(); + unlock( useDispatch( store ) ).privateToggleFeature(); // ... } ``` -##### Experimental functions, classes, and variables +##### Private functions, classes, and variables ```js // In packages/package1/index.js: @@ -342,12 +256,12 @@ import { lock } from './private-apis'; export const privateApis = {}; /* Attach private data to the exported object */ lock( privateApis, { - __experimentalCallback: function () {}, - __experimentalReactComponent: function ExperimentalComponent() { + privateCallback: function () {}, + privateReactComponent: function PrivateComponent() { return
; }, - __experimentalClass: class Experiment {}, - __experimentalVariable: 5, + privateClass: class PrivateClass {}, + privateVariable: 5, } ); // In packages/package2/index.js: @@ -355,11 +269,11 @@ import { privateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; const { - __experimentalCallback, - __experimentalReactComponent, - __experimentalClass, - __experimentalVariable, -} = unlock( experiments ); + privateCallback, + privateReactComponent, + privateClass, + privateVariable, +} = unlock( privateApis ); ``` Remember to always register the private actions and selectors on the **registered** store. @@ -391,10 +305,10 @@ unlock( registeredStore ).registerPrivateActions( { } ); ``` -#### Experimental function arguments +#### Private function arguments -To add an experimental argument to a stable function you'll need -to prepare a stable and an experimental version of that function. +To add a private argument to a stable function you'll need +to prepare a stable and a private version of that function. Then, export the stable function and `lock()` the unstable function inside it: @@ -402,11 +316,11 @@ inside it: // In @wordpress/package1/index.js: import { lock } from './private-apis'; -// The experimental function contains all the logic -function __experimentalValidateBlocks( formula, __experimentalIsStrict ) { +// A private function contains all the logic +function privateValidateBlocks( formula, privateIsStrict ) { let isValid = false; // ...complex logic we don't want to duplicate... - if ( __experimentalIsStrict ) { + if ( privateIsStrict ) { // ... } // ...complex logic we don't want to duplicate... @@ -415,25 +329,27 @@ function __experimentalValidateBlocks( formula, __experimentalIsStrict ) { } // The stable public function is a thin wrapper that calls the -// experimental function with the experimental features disabled +// private function with the private features disabled export function validateBlocks( blocks ) { - __experimentalValidateBlocks( blocks, false ); + privateValidateBlocks( blocks, false ); } -lock( validateBlocks, __experimentalValidateBlocks ); + +export const privateApis = {}; +lock( privateApis, { privateValidateBlocks } ); // In @wordpress/package2/index.js: -import { validateBlocks } from '@wordpress/package1'; +import { privateApis as package1PrivateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; -// The experimental function may be "unlocked" given the stable function: -const __experimentalValidateBlocks = unlock( validateBlocks ); -__experimentalValidateBlocks( blocks, true ); +// The private function may be "unlocked" given the stable function: +const { privateValidateBlocks } = unlock( package1PrivateApis ); +privateValidateBlocks( blocks, true ); ``` -#### Experimental React Component properties +#### Private React Component properties -To add an experimental argument to a stable component you'll need -to prepare a stable and an experimental version of that component. +To add an private argument to a stable component you'll need +to prepare a stable and an private version of that component. Then, export the stable function and `lock()` the unstable function inside it: @@ -441,41 +357,42 @@ inside it: // In @wordpress/package1/index.js: import { lock } from './private-apis'; -// The experimental component contains all the logic -const ExperimentalMyButton = ( { title, __experimentalShowIcon = true } ) => { +// The private component contains all the logic +const PrivateMyButton = ( { title, privateShowIcon = true } ) => { // ...complex logic we don't want to duplicate... return ( ); } // The stable public component is a thin wrapper that calls the -// experimental component with the experimental features disabled +// private component with the private features disabled export const MyButton = ( { title } ) => - + -lock(MyButton, ExperimentalMyButton); +export const privateApis = {}; +lock( privateApis, { PrivateMyButton } ); // In @wordpress/package2/index.js: -import { MyButton } from '@wordpress/package1'; +import { privateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; -// The experimental component may be "unlocked" given the stable component: -const ExperimentalMyButton = unlock(MyButton); +// The private component may be "unlocked" given the stable component: +const { PrivateMyButton } = unlock(privateApis); export function MyComponent() { return ( - + ) } ``` -#### Experimental editor settings +#### Private editor settings -WordPress extenders cannot update the experimental block settings on their own. The `updateSettings()` actions of the `@wordpress/block-editor` store will filter out all the settings that are **not** a part of the public API. The only way to actually store them is via private action. `__experimentalUpdateSettings()`. +WordPress extenders cannot update the private block settings on their own. The `updateSettings()` actions of the `@wordpress/block-editor` store will filter out all the settings that are **not** a part of the public API. The only way to actually store them is via the private action `__experimentalUpdateSettings()`. To privatize a block editor setting, add it to the `privateSettings` list in [/packages/block-editor/src/store/actions.js](/packages/block-editor/src/store/actions.js): @@ -486,13 +403,51 @@ const privateSettings = [ ]; ``` -#### Experimental block.json and theme.json APIs +#### Private block.json and theme.json APIs As of today, there is no way to restrict the `block.json` and `theme.json` APIs -to the Gutenberg codebase. In the future, however, the new `__experimental` APIs +to the Gutenberg codebase. In the future, however, the new private APIs will only apply to the core WordPress blocks and plugins and themes will not be able to access them. +#### Inline small actions in thunks + +Finally, instead of introducing a new action creator, consider using a [thunk](/docs/how-to-guides/thunks.md): + +```js +export function toggleFeature( scope, featureName ) { + return function ( { dispatch } ) { + dispatch( { type: '__private_BEFORE_TOGGLE' } ); + // ... + }; +} +``` + +### Exposing private APIs publicly + +Some private APIs could benefit from community feedback and it makes sense to expose them to WordPress extenders. At the same time, it doesn't make sense to turn them into a public API in WordPress core. What should you do? + +You can re-export that private API as a plugin-only API to expose it publicly only in the Gutenberg plugin: + +```js +// This function can't be used by extenders in any context: +function privateEverywhere() {} + +// This function can be used by extenders with the Gutenberg plugin but not in vanilla WordPress Core: +function privateInCorePublicInPlugin() {} + +// Gutenberg treats both functions as private APIs internally: +const privateApis = {}; +lock(privateApis, { privateEverywhere, privateInCorePublicInPlugin }); + +// The privateInCorePublicInPlugin function is explicitly exported, +// but this export will not be merged into WordPress core thanks to +// the process.env.IS_GUTENBERG_PLUGIN check. +if ( process.env.IS_GUTENBERG_PLUGIN ) { + export const privateInCorePublicInPlugin = unlock( privateApis ).privateInCorePublicInPlugin; +} +``` + ### Objects When possible, use [shorthand notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#New_notations_in_ECMAScript_2015) when defining object property values: diff --git a/docs/contributors/localizing.md b/docs/contributors/localizing.md index 1745569f68f32..8b434e32d1ce8 100644 --- a/docs/contributors/localizing.md +++ b/docs/contributors/localizing.md @@ -1,6 +1,6 @@ -# Localizing Gutenberg Plugin +# Localizing Gutenberg -The Gutenberg plugin is translated via the general plugin translation system (GlotPress) at https://translate.wordpress.org. Review the [GlotPress translation process documentation] (https://make.wordpress.org/polyglots/handbook/tools/glotpress-translate-wordpress-org/) for additional information. +The Gutenberg plugin is translated via the general plugin translation system (GlotPress) at https://translate.wordpress.org. Review the [GlotPress translation process documentation](https://make.wordpress.org/polyglots/handbook/tools/glotpress-translate-wordpress-org/) for additional information. To translate Gutenberg in your locale or language, [select your locale here](https://translate.wordpress.org/projects/wp-plugins/gutenberg) and translate *Development* (which contains the plugin's string) and/or *Development Readme* (please translate what you see in the Details tab of the [plugin page](https://wordpress.org/plugins/gutenberg/)). diff --git a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md index a65a9473d5e1d..83a3cd156251b 100644 --- a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md +++ b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md @@ -6,7 +6,9 @@ This guide takes you through creating a basic block to display a message in a po There are two main types of blocks: static and dynamic, this guide focuses on static blocks. A static block is used to insert HTML content into the post and save it with the post. A dynamic block builds the content on the fly when rendered on the front end, see the [dynamic blocks guide](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md). +
This guide focuses on just the block, see the [Create a Block tutorial](/docs/getting-started/create-block/README.md) for a complete setup. +
## Before you start @@ -141,6 +143,7 @@ In order to register the block, an asset php file is required in the same direct {% JSX %} Build the scripts and asset file which is used to keep track of dependencies and the build version. + ```bash npm run build ``` diff --git a/docs/manifest.json b/docs/manifest.json index 83f03599a71ac..a55705ebbf3cf 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -456,7 +456,7 @@ "parent": "block-api" }, { - "title": "Block Templates", + "title": "Templates", "slug": "block-templates", "markdown_source": "../docs/reference-guides/block-api/block-templates.md", "parent": "block-api" @@ -2262,7 +2262,7 @@ "parent": "contributors" }, { - "title": "Localizing Gutenberg Plugin", + "title": "Localizing Gutenberg", "slug": "localizing", "markdown_source": "../docs/contributors/localizing.md", "parent": "contributors" diff --git a/docs/reference-guides/block-api/README.md b/docs/reference-guides/block-api/README.md index e2de21d99f85e..128b9abf0f0d7 100644 --- a/docs/reference-guides/block-api/README.md +++ b/docs/reference-guides/block-api/README.md @@ -4,16 +4,18 @@ Blocks are the fundamental element of the editor. They are the primary way in wh The following sections will walk you through the existing block APIs: -- [API Versions](/docs/reference-guides/block-api/block-api-versions.md) - [Annotations](/docs/reference-guides/block-api/block-annotations.md) +- [API Versions](/docs/reference-guides/block-api/block-api-versions.md) - [Attributes](/docs/reference-guides/block-api/block-attributes.md) - [Context](/docs/reference-guides/block-api/block-context.md) - [Deprecation](/docs/reference-guides/block-api/block-deprecation.md) - [Edit and Save](/docs/reference-guides/block-api/block-edit-save.md) +- [Metadata in block.json](/docs/reference-guides/block-api/block-metadata.md) - [Patterns](/docs/reference-guides/block-api/block-patterns.md) - [Registration](/docs/reference-guides/block-api/block-registration.md) +- [Selectors](/docs/reference-guides/block-api/block-selectors.md) +- [Styles](/docs/reference-guides/block-api/block-styles.md) - [Supports](/docs/reference-guides/block-api/block-supports.md) - [Transformations](/docs/reference-guides/block-api/block-transforms.md) - [Templates](/docs/reference-guides/block-api/block-templates.md) -- [Metadata](/docs/reference-guides/block-api/block-metadata.md) - [Variations](/docs/reference-guides/block-api/block-variations.md) diff --git a/docs/reference-guides/block-api/block-edit-save.md b/docs/reference-guides/block-api/block-edit-save.md index 5d660992f6abb..f585d73454727 100644 --- a/docs/reference-guides/block-api/block-edit-save.md +++ b/docs/reference-guides/block-api/block-edit-save.md @@ -1,6 +1,6 @@ # Edit and Save -When registering a block, the `edit` and `save` functions provide the interface for how a block is going to be rendered within the editor, how it will operate and be manipulated, and how it will be saved. +When registering a block with JavaScript on the client, the `edit` and `save` functions provide the interface for how a block is going to be rendered within the editor, how it will operate and be manipulated, and how it will be saved. ## Edit diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index 86e2af8cc8135..9bfdce9279ff2 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -1,6 +1,6 @@ # Metadata in block.json -Starting in WordPress 5.8 release, we encourage using the `block.json` metadata file as the canonical way to register block types. Here is an example `block.json` file that would define the metadata for a plugin create a notice block. +Starting in WordPress 5.8 release, we recommend using the `block.json` metadata file as the canonical way to register block types with both PHP (server-side) and JavaScript (client-side). Here is an example `block.json` file that would define the metadata for a plugin create a notice block. **Example:** @@ -397,7 +397,6 @@ Providing custom selectors allows more fine grained control over which styles apply to what block elements, e.g. applying typography styles only to an inner heading while colors are still applied on the outer block wrapper etc. - See the [the selectors documentation](/docs/reference-guides/block-api/block-selectors.md) for more details. ```json diff --git a/docs/reference-guides/block-api/block-registration.md b/docs/reference-guides/block-api/block-registration.md index aed44f393b371..caaec09504f04 100644 --- a/docs/reference-guides/block-api/block-registration.md +++ b/docs/reference-guides/block-api/block-registration.md @@ -2,7 +2,11 @@ Block registration API reference. -**Note:** You can use the functions documented on this page to register a block on the client-side only, but a flexible method to register new block types is to use the `block.json` metadata file. See [metadata documentation for complete information](/docs/reference-guides/block-api/block-metadata.md). +
+You can use the functions documented on this page to register a block with JavaScript only on the client, but the recommended method is to register new block types also with PHP on the server using the `block.json` metadata file. See [metadata documentation for complete information](/docs/reference-guides/block-api/block-metadata.md). + +[Learn how to create your first block](/docs/getting-started/create-block/README.md) for the WordPress block editor. From setting up your development environment, tools, and getting comfortable with the new development model, this tutorial covers all you need to know to get started with creating blocks. +
## `registerBlockType` @@ -264,7 +268,6 @@ parent: [ 'core/columns' ], The `ancestor` property makes a block available inside the specified block types at any position of the ancestor block subtree. That allows, for example, to place a 'Comment Content' block inside a 'Column' block, as long as 'Column' is somewhere within a 'Comment Template' block. In comparison to the `parent` property blocks that specify their `ancestor` can be placed anywhere in the subtree whilst blocks with a specified `parent` need to be direct children. - ```js // Only allow this block when it is nested at any level in a Columns block. ancestor: [ 'core/columns' ], diff --git a/docs/reference-guides/block-api/block-selectors.md b/docs/reference-guides/block-api/block-selectors.md index c10aa7798f7ae..1771e54c33708 100644 --- a/docs/reference-guides/block-api/block-selectors.md +++ b/docs/reference-guides/block-api/block-selectors.md @@ -5,7 +5,6 @@ in WordPress 6.3. To use this prior to WordPress 6.3, you will need to install and activate Gutenberg >= 15.5.
-
Block Selectors is the API that allows blocks to customize the CSS selector used when their styles are generated. @@ -22,6 +21,7 @@ included under. If one is not provided through the Block Selectors API, a default is generated in the form of `.wp-block-`. ### Example + ```json { ... @@ -41,6 +41,7 @@ elements within a block. An example might be using colors on the block's wrapper but applying the typography styles to an inner heading only. ### Example + ```json { ... @@ -67,6 +68,7 @@ assigning `text-decoration` a custom selector, its style can target only the elements to which it should be applied. ### Example + ```json { ... @@ -98,6 +100,7 @@ common selector as the parent feature's `root` selector and only define the unique selectors for the subfeatures that differ. ### Example + ```json { ... @@ -120,4 +123,4 @@ example. As the `color` feature also doesn't define a `root` selector, selector, `.my-custom-block-selector`. For a subfeature such as `typography.font-size`, it would fallback to its parent -feature's selector given that is present, i.e. `.my-custom-block-selector > h2`. \ No newline at end of file +feature's selector given that is present, i.e. `.my-custom-block-selector > h2`. diff --git a/docs/reference-guides/block-api/block-templates.md b/docs/reference-guides/block-api/block-templates.md index a16ad6ea2548d..2d8a21d510691 100644 --- a/docs/reference-guides/block-api/block-templates.md +++ b/docs/reference-guides/block-api/block-templates.md @@ -1,6 +1,6 @@ -# Block Templates +# Templates -A block template is defined as a list of block items. Such blocks can have predefined attributes, placeholder content, and be static or dynamic. Block templates allow specifying a default initial state for an editor session. +A block template is defined as a list of block items. Such blocks can have predefined attributes, placeholder content, and be static or dynamic. Block templates allow specifying a default initial state for an editor session. The scope of templates include: @@ -134,7 +134,9 @@ attributes: { } } ``` + _Options:_ + - `remove` — Locks the ability of a block from being removed. - `move` — Locks the ability of a block from being moved. diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js index 1263b14707c99..60e1ff4beb6d5 100644 --- a/packages/block-editor/src/components/inserter/block-types-tab.js +++ b/packages/block-editor/src/components/inserter/block-types-tab.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { groupBy } from 'lodash'; - /** * WordPress dependencies */ @@ -59,7 +54,15 @@ export function BlockTypesTab( { itemList.filter( ( item ) => item.category && item.category !== 'reusable' ), - ( itemList ) => groupBy( itemList, 'category' ) + ( itemList ) => + itemList.reduce( ( acc, item ) => { + const { category } = item; + if ( ! acc[ category ] ) { + acc[ category ] = []; + } + acc[ category ].push( item ); + return acc; + }, {} ) )( items ); }, [ items ] ); diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 39baf78d5a7a8..21759d205845a 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -35,6 +35,9 @@ function ListViewBlockSelectButton( onDragStart, onDragEnd, draggable, + isExpanded, + ariaLabel, + ariaDescribedBy, }, ref ) { @@ -76,7 +79,9 @@ function ListViewBlockSelectButton( onDragEnd={ onDragEnd } draggable={ draggable } href={ `#block-${ clientId }` } - aria-hidden={ true } + aria-label={ ariaLabel } + aria-describedby={ ariaDescribedBy } + aria-expanded={ isExpanded } > diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index b9ccfdecdfcf9..fb8054bee7986 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -53,7 +53,6 @@ function ListViewBlock( { path, isExpanded, selectedClientIds, - preventAnnouncement, isSyncedBranch, } ) { const cellRef = useRef( null ); @@ -90,6 +89,7 @@ function ListViewBlock( { const { toggleBlockHighlight } = useDispatch( blockEditorStore ); const blockInformation = useBlockDisplayInformation( clientId ); + const blockTitle = blockInformation?.title || __( 'Untitled' ); const blockName = useSelect( ( select ) => select( blockEditorStore ).getBlockName( clientId ), [ clientId ] @@ -111,28 +111,19 @@ function ListViewBlock( { level ); - let blockAriaLabel = __( 'Link' ); - if ( blockInformation ) { - blockAriaLabel = isLocked - ? sprintf( - // translators: %s: The title of the block. This string indicates a link to select the locked block. - __( '%s link (locked)' ), - blockInformation.title - ) - : sprintf( - // translators: %s: The title of the block. This string indicates a link to select the block. - __( '%s link' ), - blockInformation.title - ); - } - - const settingsAriaLabel = blockInformation + const blockAriaLabel = isLocked ? sprintf( - // translators: %s: The title of the block. - __( 'Options for %s block' ), - blockInformation.title + // translators: %s: The title of the block. This string indicates a link to select the locked block. + __( '%s (locked)' ), + blockTitle ) - : __( 'Options' ); + : blockTitle; + + const settingsAriaLabel = sprintf( + // translators: %s: The title of the block. + __( 'Options for %s' ), + blockTitle + ); const { isTreeGridMounted, expand, collapse, BlockSettingsMenu } = useListViewContext(); @@ -249,18 +240,13 @@ function ListViewBlock( { id={ `list-view-block-${ clientId }` } data-block={ clientId } data-expanded={ canExpand ? isExpanded : undefined } - isExpanded={ canExpand ? isExpanded : undefined } - aria-selected={ !! isSelected || forceSelectionContentLock } ref={ rowRef } > { ( { ref, tabIndex, onFocus } ) => (
@@ -277,9 +263,10 @@ function ListViewBlock( { currentlyEditingBlockInCanvas ? 0 : tabIndex } onFocus={ onFocus } - isExpanded={ isExpanded } + isExpanded={ canExpand ? isExpanded : undefined } selectedClientIds={ selectedClientIds } - preventAnnouncement={ preventAnnouncement } + ariaLabel={ blockAriaLabel } + ariaDescribedBy={ descriptionId } />
{ children } diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index a04572da98e5f..3784f3be63eaa 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -15,6 +15,18 @@ // Use position relative for row animation. position: relative; + .components-button { + // When a row is expanded, retain the dark color. + &[aria-expanded="true"] { + color: $gray-900; + } + + // Ensure that on hover, the admin color is still used. + &:hover { + color: var(--wp-admin-theme-color); + } + } + // The background has to be applied to the td, not tr, or border-radius won't work. &.is-selected td { background: var(--wp-admin-theme-color); @@ -93,7 +105,7 @@ &.is-branch-selected.is-first-selected td:last-child { border-top-right-radius: $radius-block-ui; } - &[aria-expanded="false"] { + &[data-expanded="false"] { &.is-branch-selected.is-first-selected td:first-child { border-top-left-radius: $radius-block-ui; } @@ -380,7 +392,7 @@ $block-navigation-max-indent: 8; } // Point downwards when open. -.block-editor-list-view-leaf[aria-expanded="true"] .block-editor-list-view__expander svg { +.block-editor-list-view-leaf[data-expanded="true"] .block-editor-list-view__expander svg { visibility: visible; transition: transform 0.2s ease; transform: rotate(90deg); @@ -388,7 +400,7 @@ $block-navigation-max-indent: 8; } // Point rightwards when closed -.block-editor-list-view-leaf[aria-expanded="false"] .block-editor-list-view__expander svg { +.block-editor-list-view-leaf[data-expanded="false"] .block-editor-list-view__expander svg { visibility: visible; transform: rotate(0deg); transition: transform 0.2s ease; diff --git a/packages/block-editor/src/components/list-view/use-block-selection.js b/packages/block-editor/src/components/list-view/use-block-selection.js index e612ccaab6329..716995edbdd53 100644 --- a/packages/block-editor/src/components/list-view/use-block-selection.js +++ b/packages/block-editor/src/components/list-view/use-block-selection.js @@ -144,7 +144,7 @@ export default function useBlockSelection() { } if ( label ) { - speak( label ); + speak( label, 'assertive' ); } }, [ diff --git a/packages/block-library/src/list/test/edit.native.js b/packages/block-library/src/list/test/edit.native.js index 5b70952925cd9..2c9cbcd43ec3e 100644 --- a/packages/block-library/src/list/test/edit.native.js +++ b/packages/block-library/src/list/test/edit.native.js @@ -347,7 +347,10 @@ describe( 'List block', () => { // backward delete const listItemField = within( listItemBlock ).getByLabelText( /Text input. .*Two.*/ ); - changeAndSelectTextOfRichText( listItemField, 'Two' ); + changeAndSelectTextOfRichText( listItemField, 'Two', { + initialSelectionStart: 0, + initialSelectionEnd: 3, + } ); fireEvent( listItemField, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, @@ -395,7 +398,10 @@ describe( 'List block', () => { // backward delete const listItemField = within( listItemBlock ).getByLabelText( /Text input. .*One.*/ ); - changeAndSelectTextOfRichText( listItemField, 'One' ); + changeAndSelectTextOfRichText( listItemField, 'One', { + initialSelectionStart: 0, + initialSelectionEnd: 3, + } ); fireEvent( listItemField, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, @@ -406,11 +412,11 @@ describe( 'List block', () => { "

A quick brown fox.

- +

One

- +
  • Two
  • diff --git a/packages/block-library/src/preformatted/test/edit.native.js b/packages/block-library/src/preformatted/test/edit.native.js index d5ce7a4959132..81a7caa29255f 100644 --- a/packages/block-library/src/preformatted/test/edit.native.js +++ b/packages/block-library/src/preformatted/test/edit.native.js @@ -64,24 +64,25 @@ describe( 'Preformatted', () => { // Act await addBlock( screen, 'Preformatted' ); - const verseTextInput = await screen.findByPlaceholderText( + const preformattedTextInput = await screen.findByPlaceholderText( 'Write preformatted text…' ); const string = 'A great statement.'; - changeAndSelectTextOfRichText( verseTextInput, string, { + changeAndSelectTextOfRichText( preformattedTextInput, string, { selectionStart: string.length, selectionEnd: string.length, } ); - fireEvent( verseTextInput, 'onKeyDown', { + fireEvent( preformattedTextInput, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, keyCode: ENTER, } ); + changeAndSelectTextOfRichText( preformattedTextInput, 'Again' ); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` " -
    A great statement.
    +
    A great statement.
    Again
    " ` ); } ); diff --git a/packages/block-library/src/pullquote/test/edit.native.js b/packages/block-library/src/pullquote/test/edit.native.js index a3c11d445ad76..dd9508f2ec054 100644 --- a/packages/block-library/src/pullquote/test/edit.native.js +++ b/packages/block-library/src/pullquote/test/edit.native.js @@ -45,8 +45,7 @@ describe( 'Pullquote', () => { preventDefault() {}, keyCode: ENTER, } ); - - // TODO: Determine a way to type after pressing ENTER within the block. + changeAndSelectTextOfRichText( pullquoteTextInput, 'Again' ); const citationTextInput = within( citationBlock ).getByPlaceholderText( 'Add citation' ); @@ -63,7 +62,7 @@ describe( 'Pullquote', () => { // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` " -

    A great statement.

    A
    person
    +

    A great statement.
    Again

    A
    person
    " ` ); } ); diff --git a/packages/block-library/src/verse/test/edit.native.js b/packages/block-library/src/verse/test/edit.native.js index 6663926a7db64..f2dec9d7c0874 100644 --- a/packages/block-library/src/verse/test/edit.native.js +++ b/packages/block-library/src/verse/test/edit.native.js @@ -74,13 +74,12 @@ describe( 'Verse block', () => { preventDefault() {}, keyCode: ENTER, } ); - - // TODO: Determine a way to type after pressing ENTER within the block. + changeAndSelectTextOfRichText( verseTextInput, 'Again' ); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` " -
    A great statement.
    +
    A great statement.
    Again
    " ` ); } ); diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 6bb86b9ee546c..822eea532c929 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -4,7 +4,9 @@ For more context, refer to [_What Are Little Blocks Made Of?_](https://make.wordpress.org/design/2017/01/25/what-are-little-blocks-made-of/) from the [Make WordPress Design](https://make.wordpress.org/design/) blog. -The following documentation outlines steps you as a developer will need to follow to add your own custom blocks to WordPress's editor interfaces. +
    +[Learn how to create your first block](https://developer.wordpress.org/block-editor/getting-started/create-block/) for the WordPress block editor. From setting up your development environment, tools, and getting comfortable with the new development model, this tutorial covers all you need to know to get started with creating blocks. +
    ## Installation @@ -16,169 +18,6 @@ npm install @wordpress/blocks --save _This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ -## Getting Started - -If you're not already accustomed to working with JavaScript in your WordPress plugins, you may first want to reference the guide on [_Including CSS & JavaScript_](https://developer.wordpress.org/themes/basics/including-css-javascript/) in the Theme Handbook. - -At a minimum, you will need to enqueue scripts for your block as part of a `enqueue_block_editor_assets` action callback, with a dependency on the `wp-blocks` and `wp-element` script handles: - -```php - @@ -671,7 +510,7 @@ _Parameters_ Registers a new block style for the given block. -For more information on connecting the styles with CSS [the official documentation](/docs/reference-guides/block-api/block-styles.md#styles) +For more information on connecting the styles with CSS [the official documentation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/#styles). _Usage_ @@ -705,7 +544,7 @@ _Parameters_ Registers a new block provided a unique name and an object defining its behavior. Once registered, the block is made available as an option to any editor interface where blocks are implemented. -For more in-depth information on registering a custom block see the [Create a block tutorial](/docs/getting-started/create-block/README.md) +For more in-depth information on registering a custom block see the [Create a block tutorial](https://developer.wordpress.org/block-editor/getting-started/create-block/). _Usage_ @@ -733,7 +572,7 @@ _Returns_ Registers a new block variation for the given block type. -For more information on block variations see [the official documentation ](/docs/reference-guides/block-api/block-variations.md) +For more information on block variations see [the official documentation ](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/). _Usage_ diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 2c86850dc4252..f601a3f59314f 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -248,7 +248,8 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { * behavior. Once registered, the block is made available as an option to any * editor interface where blocks are implemented. * - * For more in-depth information on registering a custom block see the [Create a block tutorial](/docs/getting-started/create-block/README.md) + * For more in-depth information on registering a custom block see the + * [Create a block tutorial](https://developer.wordpress.org/block-editor/getting-started/create-block/). * * @param {string|Object} blockNameOrMetadata Block type name or its metadata. * @param {Object} settings Block settings. @@ -675,7 +676,8 @@ export const hasChildBlocksWithInserterSupport = ( blockName ) => { /** * Registers a new block style for the given block. * - * For more information on connecting the styles with CSS [the official documentation](/docs/reference-guides/block-api/block-styles.md#styles) + * For more information on connecting the styles with CSS + * [the official documentation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/#styles). * * @param {string} blockName Name of block (example: “core/latest-posts”). * @param {Object} styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user. @@ -754,7 +756,8 @@ export const getBlockVariations = ( blockName, scope ) => { /** * Registers a new block variation for the given block type. * - * For more information on block variations see [the official documentation ](/docs/reference-guides/block-api/block-variations.md) + * For more information on block variations see + * [the official documentation ](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/). * * @param {string} blockName Name of the block (example: “core/columns”). * @param {WPBlockVariation} variation Object describing a block variation. diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2cf9c66df824a..ca0fdfa789dfb 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Enhancements + +- `TreeGrid`: Modify keyboard navigation code to use a data-expanded attribute if aria-expanded is to be controlled outside of the TreeGrid component ([#48461](https://github.com/WordPress/gutenberg/pull/48461)). + +### Documentation + +- `Autocomplete`: Add heading and fix type for `onReplace` in README. ([#49798](https://github.com/WordPress/gutenberg/pull/49798)). + ## 23.8.0 (2023-04-12) ### Internal @@ -12,6 +20,11 @@ - `DropZone`: Smooth animation ([#49517](https://github.com/WordPress/gutenberg/pull/49517)). - `Navigator`: Add `skipFocus` property in `NavigateOptions`. ([#49350](https://github.com/WordPress/gutenberg/pull/49350)). +- `Spinner`: add explicit opacity and background styles ([#49695](https://github.com/WordPress/gutenberg/pull/49695)). + +### Bug Fix + +- `Snackbar`: Fix insufficient color contrast on hover ([#49682](https://github.com/WordPress/gutenberg/pull/49682)). ## 23.7.0 (2023-03-29) diff --git a/packages/components/src/autocomplete/README.md b/packages/components/src/autocomplete/README.md index 37c2396894008..2b9c9209dbed5 100644 --- a/packages/components/src/autocomplete/README.md +++ b/packages/components/src/autocomplete/README.md @@ -20,10 +20,12 @@ A function to be called when an option is selected to insert into the existing t - Required: Yes - Type: `( value: string ) => void` +### onReplace + A function to be called when an option is selected to replace the existing text. - Required: Yes -- Type: `( arg: [ OptionCompletion[ 'value' ] ] ) => void;` +- Type: `( values: RichTextValue[] ) => void` ### completers @@ -31,7 +33,7 @@ An array of all of the completers to apply to the current element. - Required: Yes - Type: `Array< WPCompleter >` - + ### contentRef A ref containing the editable element that will serve as the anchor for `Autocomplete`'s `Popover`. diff --git a/packages/components/src/snackbar/style.scss b/packages/components/src/snackbar/style.scss index 0e54d3b432b93..9e9acebb635bf 100644 --- a/packages/components/src/snackbar/style.scss +++ b/packages/components/src/snackbar/style.scss @@ -64,7 +64,8 @@ } &:hover { - color: $components-color-accent; + text-decoration: none; + color: $white; } } } diff --git a/packages/components/src/spinner/styles.ts b/packages/components/src/spinner/styles.ts index 7a5a2205c97dd..12c108801bc79 100644 --- a/packages/components/src/spinner/styles.ts +++ b/packages/components/src/spinner/styles.ts @@ -26,6 +26,8 @@ export const StyledSpinner = styled.svg` position: relative; color: ${ COLORS.ui.theme }; overflow: visible; + opacity: 1; + background-color: transparent; `; const commonPathProps = css` diff --git a/packages/components/src/tree-grid/index.tsx b/packages/components/src/tree-grid/index.tsx index 7087bafc86f70..6a258eb9b1e31 100644 --- a/packages/components/src/tree-grid/index.tsx +++ b/packages/components/src/tree-grid/index.tsx @@ -91,7 +91,8 @@ function UnforwardedTreeGrid( const canExpandCollapse = 0 === currentColumnIndex; const cannotFocusNextColumn = canExpandCollapse && - activeRow.getAttribute( 'aria-expanded' ) === 'false' && + ( activeRow.getAttribute( 'data-expanded' ) === 'false' || + activeRow.getAttribute( 'aria-expanded' ) === 'false' ) && keyCode === RIGHT; if ( ( [ LEFT, RIGHT ] as number[] ).includes( keyCode ) ) { @@ -112,6 +113,8 @@ function UnforwardedTreeGrid( // Left: // If a row is focused, and it is expanded, collapses the current row. if ( + activeRow.getAttribute( 'data-expanded' ) === + 'true' || activeRow.getAttribute( 'aria-expanded' ) === 'true' ) { onCollapseRow( activeRow ); @@ -151,8 +154,10 @@ function UnforwardedTreeGrid( // Right: // If a row is focused, and it is collapsed, expands the current row. if ( + activeRow.getAttribute( 'data-expanded' ) === + 'false' || activeRow.getAttribute( 'aria-expanded' ) === - 'false' + 'false' ) { onExpandRow( activeRow ); event.preventDefault(); diff --git a/packages/dom/README.md b/packages/dom/README.md index 4a2fc100174e1..f87ccbb3ac731 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -131,6 +131,7 @@ Given a DOM node, finds the closest scrollable container node or the node itself _Parameters_ - _node_ `Element | null`: Node from which to start. +- _direction_ `?string`: Direction of scrollable container to search for ('vertical', 'horizontal', 'all'). Defaults to 'vertical'. _Returns_ diff --git a/packages/dom/src/dom/get-scroll-container.js b/packages/dom/src/dom/get-scroll-container.js index 6c472e6195496..da46f23c660ba 100644 --- a/packages/dom/src/dom/get-scroll-container.js +++ b/packages/dom/src/dom/get-scroll-container.js @@ -7,22 +7,37 @@ import getComputedStyle from './get-computed-style'; * Given a DOM node, finds the closest scrollable container node or the node * itself, if scrollable. * - * @param {Element | null} node Node from which to start. - * + * @param {Element | null} node Node from which to start. + * @param {?string} direction Direction of scrollable container to search for ('vertical', 'horizontal', 'all'). + * Defaults to 'vertical'. * @return {Element | undefined} Scrollable container node, if found. */ -export default function getScrollContainer( node ) { +export default function getScrollContainer( node, direction = 'vertical' ) { if ( ! node ) { return undefined; } - // Scrollable if scrollable height exceeds displayed... - if ( node.scrollHeight > node.clientHeight ) { - // ...except when overflow is defined to be hidden or visible - const { overflowY } = getComputedStyle( node ); + if ( direction === 'vertical' || direction === 'all' ) { + // Scrollable if scrollable height exceeds displayed... + if ( node.scrollHeight > node.clientHeight ) { + // ...except when overflow is defined to be hidden or visible + const { overflowY } = getComputedStyle( node ); + + if ( /(auto|scroll)/.test( overflowY ) ) { + return node; + } + } + } + + if ( direction === 'horizontal' || direction === 'all' ) { + // Scrollable if scrollable width exceeds displayed... + if ( node.scrollWidth > node.clientWidth ) { + // ...except when overflow is defined to be hidden or visible + const { overflowX } = getComputedStyle( node ); - if ( /(auto|scroll)/.test( overflowY ) ) { - return node; + if ( /(auto|scroll)/.test( overflowX ) ) { + return node; + } } } @@ -31,5 +46,8 @@ export default function getScrollContainer( node ) { } // Continue traversing. - return getScrollContainer( /** @type {Element} */ ( node.parentNode ) ); + return getScrollContainer( + /** @type {Element} */ ( node.parentNode ), + direction + ); } diff --git a/packages/edit-site/src/components/start-template-options/style.scss b/packages/edit-site/src/components/start-template-options/style.scss index 78688fef4efbd..ebbc1d7421941 100644 --- a/packages/edit-site/src/components/start-template-options/style.scss +++ b/packages/edit-site/src/components/start-template-options/style.scss @@ -3,28 +3,36 @@ // and height are used instead of max-(width/height). @include break-small() { width: calc(100% - #{ $grid-unit-20 * 2 }); - height: calc(100% - #{ $header-height * 2 }); + height: auto; } @include break-medium() { - width: 50%; + width: 95%; } @include break-large() { - height: fit-content; + max-height: 95%; } } .edit-site-start-template-options__modal-content .block-editor-block-patterns-list { - display: grid; + display: flex; + flex-wrap: wrap; + gap: $grid-unit-30; width: 100%; margin-top: $grid-unit-05; - gap: $grid-unit-30; - grid-template-columns: repeat(auto-fit, minmax(min(100%/2, max(240px, 100%/10)), 1fr)); .block-editor-block-patterns-list__list-item { break-inside: avoid-column; margin-bottom: 0; - width: 100%; aspect-ratio: 3/4; + width: calc(50% - #{ $grid-unit-30 * 1 } / 2); + + @include break-medium() { + width: calc(33.333% - #{ $grid-unit-30 * 2 } / 3); + } + + @include break-wide() { + width: calc(25% - #{ $grid-unit-30 * 3 } / 4); + } .block-editor-block-preview__container { height: 100%; diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 364e6ae694797..fda61faeeb7bc 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { groupBy } from 'lodash'; - /** * WordPress dependencies */ @@ -83,7 +78,14 @@ export default function EntitiesSavedStates( { close } ) { useDispatch( noticesStore ); // To group entities by type. - const partitionedSavables = groupBy( dirtyEntityRecords, 'name' ); + const partitionedSavables = dirtyEntityRecords.reduce( ( acc, record ) => { + const { name } = record; + if ( ! acc[ name ] ) { + acc[ name ] = []; + } + acc[ name ].push( record ); + return acc; + }, {} ); // Sort entity groups. const { diff --git a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap index 9f3037693e1a8..50bd6c2bd9bd8 100644 --- a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap @@ -595,6 +595,8 @@ exports[`PostPublishPanel should render the spinner if the post is being saved 1 position: relative; color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); overflow: visible; + opacity: 1; + background-color: transparent; } .emotion-2 { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index ef86cb4d1180b..8177a934778c9 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.92.1", + "version": "1.93.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index df6fccbb6af04..864fce5bf7759 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.92.1", + "version": "1.93.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 0ab830dd2bc86..10d1831413d0c 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,7 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased -- [*] Support POST requests [#49371] + +## 1.93.0 - [***] [iOS] Fixed iOS scroll jumping issue by refactoring KeyboardAwareFlatList improving writing flow and caret focus handling. [#48791] ## 1.92.1 diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 24c2beffcc508..d9af5d1804efc 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.69.4) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.92.1): + - Gutenberg (1.93.0): - React-Core (= 0.69.4) - React-CoreModules (= 0.69.4) - React-RCTImage (= 0.69.4) @@ -360,7 +360,7 @@ PODS: - React-Core - RNSVG (9.13.6): - React-Core - - RNTAztecView (1.92.1): + - RNTAztecView (1.93.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.8) - SDWebImage (5.11.1): @@ -540,7 +540,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 2ff441cbe6e58c1778d8a5cf3311831a6a8c0809 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a - Gutenberg: 0d272fc0f7c1742329990d0dd79bbf67e3112370 + Gutenberg: cadfa57dc64b65273e8de3a00260e1c33170f895 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4 @@ -582,7 +582,7 @@ SPEC CHECKSUMS: RNReanimated: bea6acb5fdcbd8ca27641180579d09e3434f803c RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b RNSVG: 36a7359c428dcb7c6bce1cc546fbfebe069809b0 - RNTAztecView: 2d5a5eb98bc530982c15d1c20f584ba24ca035df + RNTAztecView: 8324fa38e9ee5db11ddb2d49838ffb640478f954 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: 7d11d598f14c82c727c08b56bd35fbeb7dafb504 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 8bff1e70ceea8..1bc0fc74f4657 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.92.1", + "version": "1.93.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 9ea12f358c3df..f044184b5db81 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- License check script supports conjunctive (AND) licenses ([46801](https://github.com/WordPress/gutenberg/pull/46801)). + ## 26.2.0 (2023-04-12) ## 26.1.0 (2023-03-29) @@ -14,7 +18,7 @@ ### Breaking Changes -- Started using Jest v29 instead of v27 as a dependency. See [breaking changes in Jest 28](https://jestjs.io/blog/2022/04/25/jest-28) and [in jest 29](https://jestjs.io/blog/2022/08/25/jest-29) ([#47388](https://github.com/WordPress/gutenberg/pull/47388)) +- Started using Jest v29 instead of v27 as a dependency. See [breaking changes in Jest 28](https://jestjs.io/blog/2022/04/25/jest-28) and [in jest 29](https://jestjs.io/blog/2022/08/25/jest-29) ([#47388](https://github.com/WordPress/gutenberg/pull/47388)) ## 25.5.1 (2023-03-06) diff --git a/packages/scripts/scripts/check-licenses.js b/packages/scripts/scripts/check-licenses.js index ad34c06e268a3..5e0dd37239612 100644 --- a/packages/scripts/scripts/check-licenses.js +++ b/packages/scripts/scripts/check-licenses.js @@ -88,6 +88,9 @@ const licenses = [ /* * Some packages don't included a license string in their package.json file, but they * do have a license listed elsewhere. These files are checked for matching license strings. + * Only the first matching license file with a matching license string is considered. + * + * See: licenseFileStrings. */ const licenseFiles = [ 'LICENCE', @@ -227,6 +230,7 @@ function traverseDepTree( deps ) { function detectTypeFromLicenseFiles( path ) { return licenseFiles.reduce( ( detectedType, licenseFile ) => { + // If another LICENSE file already had licenses in it, use those. if ( detectedType ) { return detectedType; } @@ -235,30 +239,39 @@ function detectTypeFromLicenseFiles( path ) { if ( existsSync( licensePath ) ) { const licenseText = readFileSync( licensePath ).toString(); - - // Check if the file contains any of the strings in licenseFileStrings. - return Object.keys( licenseFileStrings ).reduce( - ( stringDetectedType, licenseStringType ) => { - const licenseFileString = - licenseFileStrings[ licenseStringType ]; - - return licenseFileString.reduce( - ( currentDetectedType, fileString ) => { - if ( licenseText.includes( fileString ) ) { - return licenseStringType; - } - return currentDetectedType; - }, - stringDetectedType - ); - }, - detectedType - ); + return detectTypeFromLicenseText( licenseText ); } + return detectedType; }, false ); } +function detectTypeFromLicenseText( licenseText ) { + // Check if the file contains any of the strings in licenseFileStrings. + return Object.keys( licenseFileStrings ).reduce( + ( stringDetectedType, licenseStringType ) => { + const licenseFileString = licenseFileStrings[ licenseStringType ]; + + return licenseFileString.reduce( + ( currentDetectedType, fileString ) => { + if ( licenseText.includes( fileString ) ) { + if ( currentDetectedType ) { + return currentDetectedType.concat( + ' AND ', + licenseStringType + ); + } + return licenseStringType; + } + return currentDetectedType; + }, + stringDetectedType + ); + }, + false + ); +} + function checkDepLicense( path ) { if ( ! path ) { return; @@ -294,11 +307,17 @@ function checkDepLicense( path ) { licenseType = undefined; } - if ( licenseType ) { - const allowed = licenses.find( ( allowedLicense ) => - checkLicense( allowedLicense, licenseType ) - ); - if ( allowed ) { + if ( licenseType !== undefined ) { + let licenseTypes = [ licenseType ]; + if ( licenseType.includes( ' AND ' ) ) { + licenseTypes = licenseType + .replace( /^\(*/g, '' ) + .replace( /\)*$/, '' ) + .split( ' AND ' ) + .map( ( e ) => e.trim() ); + } + + if ( checkAllCompatible( licenseTypes, licenses ) ) { return; } } @@ -313,17 +332,54 @@ function checkDepLicense( path ) { return; } - // Now that we have a license to check, see if any of the allowed licenses match. - const allowed = licenses.find( ( allowedLicense ) => - checkLicense( allowedLicense, detectedLicenseType ) + let detectedLicenseTypes = [ detectedLicenseType ]; + if ( detectedLicenseType.includes( ' AND ' ) ) { + detectedLicenseTypes = detectedLicenseType + .replace( /^\(*/g, '' ) + .replace( /\)*$/, '' ) + .split( ' AND ' ) + .map( ( e ) => e.trim() ); + } + + if ( checkAllCompatible( detectedLicenseTypes, licenses ) ) { + return; + } + + process.exitCode = 1; + process.stdout.write( + `${ ERROR } Module ${ packageInfo.name } has an incompatible license '${ licenseType }'.\n` ); +} - if ( ! allowed ) { - process.exitCode = 1; - process.stdout.write( - `${ ERROR } Module ${ packageInfo.name } has an incompatible license '${ licenseType }'.\n` +/** + * Check that all of the licenses for a package are compatible. + * + * This function is invoked when the licenses are a conjunctive ("AND") list of licenses. + * In that case, the software is only compatible if all of the licenses in the list are + * compatible. + * + * @param {Array} packageLicenses The licenses that a package is licensed under. + * @param {Array} compatibleLicenses The list of compatible licenses. + * + * @return {boolean} true if all of the packageLicenses appear in compatibleLicenses. + */ +function checkAllCompatible( packageLicenses, compatibleLicenses ) { + return packageLicenses.reduce( ( compatible, packageLicense ) => { + return ( + compatible && + compatibleLicenses.reduce( + ( found, allowedLicense ) => + found || checkLicense( allowedLicense, packageLicense ), + false + ) ); - } + }, true ); } traverseDepTree( topLevelDeps ); + +// Required for unit testing +module.exports = { + detectTypeFromLicenseText, + checkAllCompatible, +}; diff --git a/packages/scripts/scripts/test/check-licenses.js b/packages/scripts/scripts/test/check-licenses.js new file mode 100644 index 0000000000000..13116bd9222a0 --- /dev/null +++ b/packages/scripts/scripts/test/check-licenses.js @@ -0,0 +1,89 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * Internal dependencies + */ +import { + detectTypeFromLicenseText, + checkAllCompatible, +} from '../check-licenses'; + +describe( 'detectTypeFromLicenseText', () => { + let licenseText; + + it( "should return 'Apache 2.0' when the license text is the Apache 2.0 license", () => { + licenseText = fs + .readFileSync( path.resolve( __dirname, 'licenses/apache2.txt' ) ) + .toString(); + + expect( detectTypeFromLicenseText( licenseText ) ).toBe( 'Apache-2.0' ); + } ); + + it( "should return 'BSD' when the license text is the BSD 3-clause license", () => { + licenseText = fs + .readFileSync( + path.resolve( __dirname, 'licenses/bsd3clause.txt' ) + ) + .toString(); + + expect( detectTypeFromLicenseText( licenseText ) ).toBe( 'BSD' ); + } ); + + it( "should return 'BSD-3-Clause-W3C' when the license text is the W3C variation of the BSD 3-clause license", () => { + licenseText = fs + .readFileSync( path.resolve( __dirname, 'licenses/w3cbsd.txt' ) ) + .toString(); + + expect( detectTypeFromLicenseText( licenseText ) ).toBe( + 'BSD-3-Clause-W3C' + ); + } ); + + it( "should return 'MIT' when the license text is the MIT license", () => { + licenseText = fs + .readFileSync( path.resolve( __dirname, 'licenses/mit.txt' ) ) + .toString(); + + expect( detectTypeFromLicenseText( licenseText ) ).toBe( 'MIT' ); + } ); + + it( "should return 'Apache2 AND MIT' when the license text is Apache2 followed by MIT license", () => { + licenseText = fs + .readFileSync( + path.resolve( __dirname, 'licenses/apache2-mit.txt' ) + ) + .toString(); + + expect( detectTypeFromLicenseText( licenseText ) ).toBe( + 'Apache-2.0 AND MIT' + ); + } ); +} ); + +describe( 'checkAllCompatible', () => { + it( "should return 'true' when single license is in the allowed list", () => { + expect( checkAllCompatible( [ 'B' ], [ 'A', 'B', 'C' ] ) ).toBe( true ); + } ); + + it( "should return 'false' when single license is not in the allowed list", () => { + expect( checkAllCompatible( [ 'D' ], [ 'A', 'B', 'C' ] ) ).toBe( + false + ); + } ); + + it( "should return 'true' when all licenses are in the allowed list", () => { + expect( checkAllCompatible( [ 'A', 'C' ], [ 'A', 'B', 'C' ] ) ).toBe( + true + ); + } ); + + it( "should return 'false' when any license is not in the allowed list", () => { + expect( checkAllCompatible( [ 'A', 'D' ], [ 'A', 'B', 'C' ] ) ).toBe( + false + ); + } ); +} ); diff --git a/packages/scripts/scripts/test/licenses/apache2-mit.txt b/packages/scripts/scripts/test/licenses/apache2-mit.txt new file mode 100644 index 0000000000000..ef274198b740d --- /dev/null +++ b/packages/scripts/scripts/test/licenses/apache2-mit.txt @@ -0,0 +1,227 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +----- + +Contains code from https://github.com/mozilla/language-mapping-list + +The MIT License (MIT) + +Copyright (c) 2013 fullname + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/scripts/scripts/test/licenses/apache2.txt b/packages/scripts/scripts/test/licenses/apache2.txt new file mode 100644 index 0000000000000..7a4a3ea2424c0 --- /dev/null +++ b/packages/scripts/scripts/test/licenses/apache2.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/scripts/scripts/test/licenses/bsd3clause.txt b/packages/scripts/scripts/test/licenses/bsd3clause.txt new file mode 100644 index 0000000000000..ae5698f13265e --- /dev/null +++ b/packages/scripts/scripts/test/licenses/bsd3clause.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) [year], [fullname] + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/scripts/scripts/test/licenses/mit.txt b/packages/scripts/scripts/test/licenses/mit.txt new file mode 100644 index 0000000000000..00217dc586444 --- /dev/null +++ b/packages/scripts/scripts/test/licenses/mit.txt @@ -0,0 +1,7 @@ +Copyright + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/scripts/scripts/test/licenses/w3cbsd.txt b/packages/scripts/scripts/test/licenses/w3cbsd.txt new file mode 100644 index 0000000000000..dbab9e7687173 --- /dev/null +++ b/packages/scripts/scripts/test/licenses/w3cbsd.txt @@ -0,0 +1,9 @@ +# W3C 3-clause BSD License + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of works must retain the original copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the original copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of the W3C nor the names of its contributors may be used to endorse or promote products derived from this work without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js index 58acffa5fee05..c2078c861c4a8 100644 --- a/test/e2e/specs/editor/blocks/columns.spec.js +++ b/test/e2e/specs/editor/blocks/columns.spec.js @@ -26,7 +26,7 @@ test.describe( 'Columns', () => { // block column add await page .locator( - 'role=treegrid[name="Block navigation structure"i] >> role=gridcell[name="Column link"i]' + 'role=treegrid[name="Block navigation structure"i] >> role=gridcell[name="Column"i]' ) .first() .click(); diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index e7437bc2acbf5..b7021752ea8cf 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -27,7 +27,8 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); @@ -43,10 +44,12 @@ test.describe( 'List View', () => { // Drag the paragraph above the heading. const paragraphBlockItem = listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, } ); const headingBlockItem = listView.getByRole( 'gridcell', { - name: 'Heading link', + name: 'Heading', + exact: true, } ); await paragraphBlockItem.dragTo( headingBlockItem, { x: 0, y: 0 } ); @@ -80,7 +83,8 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); @@ -88,11 +92,9 @@ test.describe( 'List View', () => { // Go to the image block in List View. await pageUtils.pressKeys( 'ArrowUp', { times: 2 } ); await expect( - listView - .getByRole( 'gridcell', { - name: 'Image link', - } ) - .getByRole( 'link', { includeHidden: true } ) + listView.getByRole( 'link', { + name: 'Image', + } ) ).toBeFocused(); // Select the image block in the canvas. @@ -110,9 +112,7 @@ test.describe( 'List View', () => { await page.keyboard.press( 'Backspace' ); // List View should have two rows. - await expect( - listView.getByRole( 'gridcell', { name: /link/i } ) - ).toHaveCount( 2 ); + await expect( listView.getByRole( 'row' ) ).toHaveCount( 2 ); } ); // Check for regression of https://github.com/WordPress/gutenberg/issues/39026. @@ -135,14 +135,15 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); // Remove the Paragraph block via its options menu in List View. await listView - .getByRole( 'button', { name: 'Options for Paragraph block' } ) + .getByRole( 'button', { name: 'Options for Paragraph' } ) .click(); await page.getByRole( 'menuitem', { name: /Delete/i } ).click(); @@ -174,7 +175,8 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); @@ -182,17 +184,15 @@ test.describe( 'List View', () => { // Select the image block in List View. await pageUtils.pressKeys( 'ArrowUp', { times: 2 } ); await expect( - listView - .getByRole( 'gridcell', { - name: 'Image link', - } ) - .getByRole( 'link', { includeHidden: true } ) + listView.getByRole( 'link', { + name: 'Image', + } ) ).toBeFocused(); await page.keyboard.press( 'Enter' ); // Remove the Image block via its options menu in List View. await listView - .getByRole( 'button', { name: 'Options for Image block' } ) + .getByRole( 'button', { name: 'Options for Image' } ) .click(); await page.getByRole( 'menuitem', { name: /Delete/i } ).click(); @@ -227,7 +227,8 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Heading link', + name: 'Heading', + exact: true, selected: true, } ) ).toBeVisible(); @@ -236,14 +237,15 @@ test.describe( 'List View', () => { await pageUtils.pressKeys( 'shift+ArrowUp' ); await expect( listView.getByRole( 'gridcell', { - name: 'Image link', + name: 'Image', + exact: true, selected: true, } ) ).toBeVisible(); // Remove both blocks. await listView - .getByRole( 'button', { name: 'Options for Image block' } ) + .getByRole( 'button', { name: 'Options for Image' } ) .click(); await page.getByRole( 'menuitem', { name: /Delete blocks/i } ).click(); @@ -276,8 +278,8 @@ test.describe( 'List View', () => { // Things start off expanded. await expect( - listView.getByRole( 'gridcell', { - name: 'Cover link', + listView.getByRole( 'link', { + name: 'Cover', expanded: true, } ) ).toBeVisible(); @@ -285,28 +287,27 @@ test.describe( 'List View', () => { // The child paragraph block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); // Collapse the Cover block. await listView - .getByRole( 'gridcell', { name: 'Cover link' } ) + .getByRole( 'gridcell', { name: 'Cover', exact: true } ) .getByTestId( 'list-view-expander', { includeHidden: true } ) // Force the click to bypass the visibility check. The expander is // intentionally aria-hidden. See the implementation for details. .click( { force: true } ); // Check that we're collapsed. - await expect( - listView.getByRole( 'gridcell', { name: /link/i } ) - ).toHaveCount( 1 ); + await expect( listView.getByRole( 'row' ) ).toHaveCount( 1 ); // Click the Cover block List View item. await listView - .getByRole( 'gridcell', { - name: 'Cover link', + .getByRole( 'link', { + name: 'Cover', expanded: false, } ) .click(); @@ -320,7 +321,8 @@ test.describe( 'List View', () => { // The child paragraph block in List View should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); @@ -347,7 +349,8 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Group link', + name: 'Group', + exact: true, selected: true, } ) ).toBeVisible(); @@ -355,35 +358,32 @@ test.describe( 'List View', () => { // Press Home to go to the first inserted block (image). await page.keyboard.press( 'Home' ); await expect( - listView - .getByRole( 'gridcell', { - name: 'Image link', - } ) - .getByRole( 'link', { includeHidden: true } ) + listView.getByRole( 'link', { + name: 'Image', + } ) ).toBeFocused(); // Press End followed by Arrow Up to go to the second to last block (columns). await page.keyboard.press( 'End' ); await page.keyboard.press( 'ArrowUp' ); await expect( - listView - .getByRole( 'gridcell', { - name: 'Columns link', - } ) - .getByRole( 'link', { includeHidden: true } ) + listView.getByRole( 'link', { + name: 'Columns', + exact: true, + } ) ).toBeFocused(); // Navigate the right column to image block options button via Home key. await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Home' ); await expect( - listView.getByRole( 'button', { name: 'Options for Image block' } ) + listView.getByRole( 'button', { name: 'Options for Image' } ) ).toBeFocused(); // Navigate the right column to group block options button. await page.keyboard.press( 'End' ); await expect( - listView.getByRole( 'button', { name: 'Options for Group block' } ) + listView.getByRole( 'button', { name: 'Options for Group' } ) ).toBeFocused(); } ); @@ -415,18 +415,17 @@ test.describe( 'List View', () => { // The paragraph item should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); // Navigate to the image block item. await page.keyboard.press( 'ArrowUp' ); - const imageItem = listView - .getByRole( 'gridcell', { - name: 'Image link', - } ) - .getByRole( 'link', { includeHidden: true } ); + const imageItem = listView.getByRole( 'link', { + name: 'Image', + } ); await expect( imageItem ).toBeFocused(); @@ -527,7 +526,8 @@ test.describe( 'List View', () => { // The last inserted block should be selected. await expect( listView.getByRole( 'gridcell', { - name: 'Paragraph link', + name: 'Paragraph', + exact: true, selected: true, } ) ).toBeVisible(); @@ -535,11 +535,9 @@ test.describe( 'List View', () => { // Go to the image block in List View. await pageUtils.pressKeys( 'ArrowUp', { times: 2 } ); await expect( - listView - .getByRole( 'gridcell', { - name: 'Image link', - } ) - .getByRole( 'link', { includeHidden: true } ) + listView.getByRole( 'link', { + name: 'Image', + } ) ).toBeFocused(); // Select the image block in the canvas. @@ -554,11 +552,9 @@ test.describe( 'List View', () => { // Triggering the List View shortcut should result in the image block gaining focus. await pageUtils.pressKeys( 'access+o' ); await expect( - listView - .getByRole( 'gridcell', { - name: 'Image link', - } ) - .getByRole( 'link', { includeHidden: true } ) + listView.getByRole( 'link', { + name: 'Image', + } ) ).toBeFocused(); } ); } ); diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js index 3e03a0c280562..d72ab2c6bf7db 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -916,8 +916,8 @@ test.describe( 'Multi-block selection', () => { const listView = page.getByRole( 'treegrid', { name: 'Block navigation structure', } ); - const navButtons = listView.getByRole( 'gridcell', { - name: 'Paragraph link', + const navButtons = listView.getByRole( 'link', { + name: 'Paragraph', } ); await navButtons.nth( 1 ).click(); @@ -954,9 +954,7 @@ test.describe( 'Multi-block selection', () => { // Move focus to the list view link to prepare for the keyboard navigation. await navButtons.nth( 3 ).click(); - await expect( - navButtons.nth( 3 ).getByRole( 'link', { includeHidden: true } ) - ).toBeFocused(); + await expect( navButtons.nth( 3 ) ).toBeFocused(); // Press Up twice to highlight the second block. await pageUtils.pressKeys( 'ArrowUp', { times: 2 } ); diff --git a/test/native/integration-test-helpers/rich-text-change-and-select-text.js b/test/native/integration-test-helpers/rich-text-change-and-select-text.js index ff01895e09d76..9decdd78c38d0 100644 --- a/test/native/integration-test-helpers/rich-text-change-and-select-text.js +++ b/test/native/integration-test-helpers/rich-text-change-and-select-text.js @@ -3,20 +3,42 @@ */ import { fireEvent } from '@testing-library/react-native'; +let eventCount = 0; + +function stripOuterHtmlTags( string ) { + return string.replace( /^<[^>]*>|<[^>]*>$/g, '' ); +} + +function insertTextAtPosition( text, newText, start, end ) { + return text.slice( 0, start ) + newText + text.slice( end ); +} + /** * Changes the text and selection of a RichText component. * - * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. - * @param {string} text Text to be set. - * @param {Object} options Configuration options for selection. - * @param {number} [options.selectionStart] Selection start position. - * @param {number} [options.selectionEnd] Selection end position. + * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. + * @param {string} text Text to be set. + * @param {Object} options Configuration options for selection. + * @param {number} [options.initialSelectionStart] + * @param {number} [options.initialSelectionEnd] + * @param {number} [options.selectionStart] Selection start position. + * @param {number} [options.selectionEnd] Selection end position. */ export const changeAndSelectTextOfRichText = ( richText, text, - { selectionStart = 0, selectionEnd = 0 } = {} + options = {} ) => { + const currentValueSansOuterHtmlTags = stripOuterHtmlTags( + richText.props.value + ); + const { + initialSelectionStart = currentValueSansOuterHtmlTags.length, + initialSelectionEnd = initialSelectionStart, + selectionStart = 0, + selectionEnd = selectionStart, + } = options; + fireEvent( richText, 'focus' ); fireEvent( richText, @@ -26,9 +48,14 @@ export const changeAndSelectTextOfRichText = ( text, { nativeEvent: { - eventCount: 1, + eventCount: ( eventCount += 101 ), target: undefined, - text, + text: insertTextAtPosition( + currentValueSansOuterHtmlTags, + text, + initialSelectionStart, + initialSelectionEnd + ), }, } ); diff --git a/test/native/integration-test-helpers/rich-text-change-text.js b/test/native/integration-test-helpers/rich-text-change-text.js index 495fe96dc5fb9..1eb758855f91c 100644 --- a/test/native/integration-test-helpers/rich-text-change-text.js +++ b/test/native/integration-test-helpers/rich-text-change-text.js @@ -3,19 +3,45 @@ */ import { fireEvent } from '@testing-library/react-native'; +let eventCount = 0; + +function stripOuterHtmlTags( string ) { + return string.replace( /^<[^>]*>|<[^>]*>$/g, '' ); +} + +function insertTextAtPosition( text, newText, start, end ) { + return text.slice( 0, start ) + newText + text.slice( end ); +} + /** * Changes the text of a RichText component. * - * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. - * @param {string} text Text to be set. + * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. + * @param {string} text Text to be set. + * @param {Object} options + * @param {number} [options.initialSelectionStart] + * @param {number} [options.initialSelectionEnd] */ -export const changeTextOfRichText = ( richText, text ) => { +export const changeTextOfRichText = ( richText, text, options = {} ) => { + const currentValueSansOuterHtmlTags = stripOuterHtmlTags( + richText.props.value + ); + const { + initialSelectionStart = currentValueSansOuterHtmlTags.length, + initialSelectionEnd = initialSelectionStart, + } = options; + fireEvent( richText, 'focus' ); fireEvent( richText, 'onChange', { nativeEvent: { - eventCount: 1, + eventCount: ( eventCount += 101 ), target: undefined, - text, + text: insertTextAtPosition( + currentValueSansOuterHtmlTags, + text, + initialSelectionStart, + initialSelectionEnd + ), }, } ); };