From d6cd0d97045520a261432152f745336844eaea81 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Aug 2024 16:44:20 +0200 Subject: [PATCH 01/39] Add language chooser package and component --- package-lock.json | 36 ++ package.json | 1 + packages/language-chooser/.npmrc | 1 + packages/language-chooser/CHANGELOG.md | 7 + packages/language-chooser/README.md | 23 ++ packages/language-chooser/package.json | 46 +++ .../src/components/active-controls.tsx | 139 +++++++ .../src/components/active-locales.tsx | 198 +++++++++ .../src/components/inactive-controls.tsx | 47 +++ .../components/inactive-locales-select.tsx | 72 ++++ .../src/components/inactive-locales.tsx | 99 +++++ .../src/components/language-chooser.tsx | 199 +++++++++ .../src/components/stories/index.story.tsx | 85 ++++ .../src/components/test/active-locales.tsx | 368 +++++++++++++++++ .../src/components/test/inactive-locales.tsx | 149 +++++++ .../src/components/test/language-chooser.tsx | 390 ++++++++++++++++++ packages/language-chooser/src/index.ts | 3 + packages/language-chooser/src/style.scss | 128 ++++++ packages/language-chooser/src/types.ts | 16 + packages/language-chooser/src/utils/index.ts | 9 + .../src/utils/test/reorder.ts | 15 + packages/language-chooser/tsconfig.json | 18 + storybook/main.js | 1 + storybook/package-styles/config.js | 7 + .../language-chooser-ltr.lazy.scss | 1 + .../language-chooser-rtl.lazy.scss | 1 + 26 files changed, 2059 insertions(+) create mode 100644 packages/language-chooser/.npmrc create mode 100644 packages/language-chooser/CHANGELOG.md create mode 100644 packages/language-chooser/README.md create mode 100644 packages/language-chooser/package.json create mode 100644 packages/language-chooser/src/components/active-controls.tsx create mode 100644 packages/language-chooser/src/components/active-locales.tsx create mode 100644 packages/language-chooser/src/components/inactive-controls.tsx create mode 100644 packages/language-chooser/src/components/inactive-locales-select.tsx create mode 100644 packages/language-chooser/src/components/inactive-locales.tsx create mode 100644 packages/language-chooser/src/components/language-chooser.tsx create mode 100644 packages/language-chooser/src/components/stories/index.story.tsx create mode 100644 packages/language-chooser/src/components/test/active-locales.tsx create mode 100644 packages/language-chooser/src/components/test/inactive-locales.tsx create mode 100644 packages/language-chooser/src/components/test/language-chooser.tsx create mode 100644 packages/language-chooser/src/index.ts create mode 100644 packages/language-chooser/src/style.scss create mode 100644 packages/language-chooser/src/types.ts create mode 100644 packages/language-chooser/src/utils/index.ts create mode 100644 packages/language-chooser/src/utils/test/reorder.ts create mode 100644 packages/language-chooser/tsconfig.json create mode 100644 storybook/package-styles/language-chooser-ltr.lazy.scss create mode 100644 storybook/package-styles/language-chooser-rtl.lazy.scss diff --git a/package-lock.json b/package-lock.json index 8db4860e895854..121ec511e74754 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/language-chooser": "file:packages/language-chooser", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", @@ -17083,6 +17084,10 @@ "resolved": "packages/keycodes", "link": true }, + "node_modules/@wordpress/language-chooser": { + "resolved": "packages/language-chooser", + "link": true + }, "node_modules/@wordpress/lazy-import": { "resolved": "packages/lazy-import", "link": true @@ -53902,6 +53907,24 @@ "npm": ">=8.19.2" } }, + "packages/language-chooser": { + "version": "1.0.0-prerelease", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", + "@wordpress/keycodes": "file:../keycodes" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "packages/lazy-import": { "name": "@wordpress/lazy-import", "version": "2.5.0", @@ -68336,6 +68359,19 @@ "@wordpress/i18n": "file:../i18n" } }, + "@wordpress/language-chooser": { + "version": "file:packages/language-chooser", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", + "@wordpress/keycodes": "file:../keycodes" + } + }, "@wordpress/lazy-import": { "version": "file:packages/lazy-import", "requires": { diff --git a/package.json b/package.json index 5b0bdf823e617a..6859d8c0069252 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/language-chooser": "file:packages/language-chooser", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", diff --git a/packages/language-chooser/.npmrc b/packages/language-chooser/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/language-chooser/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/language-chooser/CHANGELOG.md b/packages/language-chooser/CHANGELOG.md new file mode 100644 index 00000000000000..691cb6760320f4 --- /dev/null +++ b/packages/language-chooser/CHANGELOG.md @@ -0,0 +1,7 @@ + + +## Unreleased + +### New Features + +- Initial public release. diff --git a/packages/language-chooser/README.md b/packages/language-chooser/README.md new file mode 100644 index 00000000000000..1eb0f1312807a7 --- /dev/null +++ b/packages/language-chooser/README.md @@ -0,0 +1,23 @@ +# Language Chooser + +Package used for rendering a UI component for choosing preferred languages. + +> This package is meant to be used only with WordPress core. Feel free to use it in your own project but please keep in mind that it might never get fully documented. + +## Installation + +Install the module + +```bash +npm install @wordpress/language-chooser --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._ + +## Contributing to this package + +This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. + +To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). + +

Code is Poetry.

diff --git a/packages/language-chooser/package.json b/packages/language-chooser/package.json new file mode 100644 index 00000000000000..eb01c8ad82c286 --- /dev/null +++ b/packages/language-chooser/package.json @@ -0,0 +1,46 @@ +{ + "name": "@wordpress/language-chooser", + "version": "1.0.0-prerelease", + "description": "Component for choosing multiple preferred languages.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "templates", + "reusable blocks" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/language-chooser/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/language-chooser" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "sideEffects": [ + "build-style/**", + "src/**/*.scss" + ], + "types": "build-types", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", + "@wordpress/keycodes": "file:../keycodes" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/language-chooser/src/components/active-controls.tsx b/packages/language-chooser/src/components/active-controls.tsx new file mode 100644 index 00000000000000..556a3ca3b76f77 --- /dev/null +++ b/packages/language-chooser/src/components/active-controls.tsx @@ -0,0 +1,139 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import type { Language } from '../types'; + +interface ActiveControlsProps { + languages: Language[]; + selectedLanguage?: Language; + onMoveUp: () => void; + onMoveDown: () => void; + onRemove: () => void; +} +function ActiveControls( { + languages, + selectedLanguage, + onMoveUp, + onMoveDown, + onRemove, +}: ActiveControlsProps ) { + const isMoveUpDisabled = + ! selectedLanguage || + languages[ 0 ]?.locale === selectedLanguage?.locale; + const isMoveDownDisabled = + ! selectedLanguage || + languages[ languages.length - 1 ]?.locale === selectedLanguage?.locale; + const isRemoveDisabled = ! selectedLanguage; + + useShortcut( 'language-chooser/move-up', ( event: Event ) => { + event.preventDefault(); + + if ( isMoveUpDisabled ) { + return; + } + + onMoveUp(); + } ); + + useShortcut( 'language-chooser/move-down', ( event: Event ) => { + event.preventDefault(); + + if ( isMoveDownDisabled ) { + return; + } + + onMoveDown(); + } ); + + useShortcut( 'language-chooser/remove', ( event: Event ) => { + event.preventDefault(); + + if ( isRemoveDisabled ) { + return; + } + + onRemove(); + } ); + + return ( +
+ +
+ ); +} + +export default ActiveControls; diff --git a/packages/language-chooser/src/components/active-locales.tsx b/packages/language-chooser/src/components/active-locales.tsx new file mode 100644 index 00000000000000..e99ef4ffd8d26b --- /dev/null +++ b/packages/language-chooser/src/components/active-locales.tsx @@ -0,0 +1,198 @@ +/** + * WordPress dependencies + */ +import { useLayoutEffect, useRef } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import { reorder } from '../utils'; +import type { Language } from '../types'; +import ActiveControls from './active-controls'; + +interface ActiveLocalesProps { + languages: Language[]; + selectedLanguage?: Language; + showOptionSiteDefault?: boolean; + setLanguages: ( cb: ( languages: Language[] ) => Language[] ) => void; + setSelectedLanguage: ( language: Language ) => void; +} + +export function ActiveLocales( { + languages, + setLanguages, + showOptionSiteDefault = false, + selectedLanguage, + setSelectedLanguage, +}: ActiveLocalesProps ) { + const listRef = useRef< HTMLUListElement | null >( null ); + + const isEmpty = languages.length === 0; + + useLayoutEffect( () => { + const selectedEl = listRef.current?.querySelector( + '[aria-selected="true"]' + ); + + if ( ! selectedEl ) { + return; + } + + selectedEl.scrollIntoView( { + behavior: 'smooth', + block: 'nearest', + } ); + }, [ selectedLanguage, languages ] ); + + useShortcut( 'language-chooser/select-first', ( event: Event ) => { + event.preventDefault(); + + if ( isEmpty ) { + return; + } + + setSelectedLanguage( languages.at( 0 ) as Language ); + } ); + + useShortcut( 'language-chooser/select-last', ( event: Event ) => { + event.preventDefault(); + + if ( isEmpty ) { + return; + } + + setSelectedLanguage( languages.at( -1 ) as Language ); + } ); + + const onRemove = () => { + const foundIndex = languages.findIndex( + ( { locale } ) => locale === selectedLanguage?.locale + ); + + setSelectedLanguage( + languages[ foundIndex + 1 ] || languages[ foundIndex - 1 ] + ); + + setLanguages( ( prevLanguages ) => + prevLanguages.filter( + ( { locale } ) => locale !== selectedLanguage?.locale + ) + ); + + speak( __( 'Locale removed from list' ) ); + + if ( languages.length === 1 ) { + let emptyMessageA11y = sprintf( + /* translators: %s: English (United States) */ + __( 'No languages selected. Falling back to %s.' ), + 'English (United States)' + ); + + if ( showOptionSiteDefault ) { + emptyMessageA11y = __( + 'No languages selected. Falling back to Site Default.' + ); + } + + speak( emptyMessageA11y ); + } + }; + + const onMoveUp = () => { + setLanguages( ( prevLanguages ) => { + const srcIndex = prevLanguages.findIndex( + ( { locale } ) => locale === selectedLanguage?.locale + ); + return reorder( + Array.from( prevLanguages ), + srcIndex, + srcIndex - 1 + ); + } ); + + speak( __( 'Locale moved up' ) ); + }; + + const onMoveDown = () => { + setLanguages( ( prevLanguages ) => { + const srcIndex = prevLanguages.findIndex( + ( { locale } ) => locale === selectedLanguage?.locale + ); + return reorder< Language[] >( + Array.from( prevLanguages ), + srcIndex, + srcIndex + 1 + ); + } ); + + speak( __( 'Locale moved down' ) ); + }; + + const activeDescendant = isEmpty ? '' : selectedLanguage?.locale; + + const className = isEmpty + ? 'active-locales-list empty-list' + : 'active-locales-list'; + + let emptyMessage = sprintf( + /* translators: %s: English (United States) */ + __( 'Falling back to %s.' ), + 'English (United States)' + ); + + if ( showOptionSiteDefault ) { + emptyMessage = __( 'Falling back to Site Default.' ); + } + + return ( +
+ { isEmpty && ( +
+ { __( 'Nothing set.' ) } +
+ { emptyMessage } +
+ ) } + + +
+ ); +} + +export default ActiveLocales; diff --git a/packages/language-chooser/src/components/inactive-controls.tsx b/packages/language-chooser/src/components/inactive-controls.tsx new file mode 100644 index 00000000000000..204166780d0a4a --- /dev/null +++ b/packages/language-chooser/src/components/inactive-controls.tsx @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { _x, sprintf } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { shortcutAriaLabel, displayShortcut } from '@wordpress/keycodes'; + +interface InactiveControlsProps { + disabled: boolean; + onClick: () => void; +} + +function InactiveControls( { disabled, onClick }: InactiveControlsProps ) { + useShortcut( 'language-chooser/add', ( event: Event ) => { + event.preventDefault(); + + if ( disabled ) { + return; + } + + onClick(); + } ); + + return ( +
+ +
+ ); +} + +export default InactiveControls; diff --git a/packages/language-chooser/src/components/inactive-locales-select.tsx b/packages/language-chooser/src/components/inactive-locales-select.tsx new file mode 100644 index 00000000000000..bbfa576899281e --- /dev/null +++ b/packages/language-chooser/src/components/inactive-locales-select.tsx @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +import { SelectControl } from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { Language, Locale } from '../types'; + +interface InactiveLocalesSelectProps { + installedLanguages: Language[]; + availableLanguages: Language[]; + value: Locale; + onChange: ( value: string ) => void; +} + +function InactiveLocalesSelect( { + installedLanguages, + availableLanguages, + value, + onChange, +}: InactiveLocalesSelectProps ) { + const hasItems = installedLanguages.length || availableLanguages.length; + + return ( + // eslint-disable-next-line no-restricted-syntax -- Do not want __next40pxDefaultSize in wp-admin + + { installedLanguages.length > 0 && ( + + { installedLanguages.map( + ( { locale, lang, nativeName } ) => ( + + ) + ) } + + ) } + { availableLanguages.length > 0 && ( + + { availableLanguages.map( + ( { locale, lang, nativeName } ) => ( + + ) + ) } + + ) } + + ); +} + +export default InactiveLocalesSelect; diff --git a/packages/language-chooser/src/components/inactive-locales.tsx b/packages/language-chooser/src/components/inactive-locales.tsx new file mode 100644 index 00000000000000..75b872d9d0b5da --- /dev/null +++ b/packages/language-chooser/src/components/inactive-locales.tsx @@ -0,0 +1,99 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import type { Language } from '../types'; +import InactiveControls from './inactive-controls'; +import InactiveLocalesSelect from './inactive-locales-select'; + +interface InactiveLocalesProps { + languages: Language[]; + onAddLanguage: ( language: Language ) => void; +} + +function InactiveLocales( { languages, onAddLanguage }: InactiveLocalesProps ) { + const [ selectedInactiveLanguage, setSelectedInactiveLanguage ] = useState( + languages[ 0 ] + ); + + useEffect( () => { + if ( ! selectedInactiveLanguage ) { + setSelectedInactiveLanguage( languages[ 0 ] ); + } + }, [ selectedInactiveLanguage, languages ] ); + + const installedLanguages = languages.filter( ( { installed } ) => + Boolean( installed ) + ); + const availableLanguages = languages.filter( + ( { installed } ) => ! installed + ); + + const onChange = ( locale: string ) => { + setSelectedInactiveLanguage( + languages.find( + ( language ) => locale === language.locale + ) as Language + ); + }; + + const onClick = () => { + onAddLanguage( selectedInactiveLanguage ); + + const installedIndex = installedLanguages.findIndex( + ( { locale } ) => locale === selectedInactiveLanguage.locale + ); + + const availableIndex = availableLanguages.findIndex( + ( { locale } ) => locale === selectedInactiveLanguage.locale + ); + + let newSelected: Language | undefined; + + newSelected = installedLanguages[ installedIndex + 1 ]; + + if ( + ! newSelected && + installedLanguages[ 0 ] !== selectedInactiveLanguage + ) { + newSelected = installedLanguages[ 0 ]; + } + + if ( ! newSelected ) { + newSelected = availableLanguages[ availableIndex + 1 ]; + + if ( availableLanguages[ 0 ] !== selectedInactiveLanguage ) { + newSelected = availableLanguages[ 0 ]; + } + } + + setSelectedInactiveLanguage( newSelected ); + + speak( __( 'Locale added to list' ) ); + }; + + return ( +
+
+ +
+ +
+ ); +} + +export default InactiveLocales; diff --git a/packages/language-chooser/src/components/language-chooser.tsx b/packages/language-chooser/src/components/language-chooser.tsx new file mode 100644 index 00000000000000..5e0a352edd938e --- /dev/null +++ b/packages/language-chooser/src/components/language-chooser.tsx @@ -0,0 +1,199 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { __, _x } from '@wordpress/i18n'; +import { Notice } from '@wordpress/components'; +import { + ShortcutProvider, + store as keyboardShortcutsStore, +} from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import ActiveLocales from './active-locales'; +import InactiveLocales from './inactive-locales'; +import type { Language } from '../types'; + +function MissingTranslationsNotice() { + return ( + + { __( + 'Some of the languages are not installed. Re-save changes to download translations.' + ) } + + ); +} + +interface HiddenFormFieldProps { + preferredLanguages: Language[]; +} + +function HiddenFormField( { preferredLanguages }: HiddenFormFieldProps ) { + const value = preferredLanguages + .filter( ( language ) => Boolean( language ) ) + .map( ( { locale } ) => locale ) + .join( ',' ); + + return ; +} + +interface LanguageChooserProps { + allLanguages: Language[]; + preferredLanguages: Language[]; + hasMissingTranslations?: boolean; + showOptionSiteDefault?: boolean; +} + +function LanguageChooser( props: LanguageChooserProps ) { + const { + allLanguages, + hasMissingTranslations = false, + showOptionSiteDefault = false, + } = props; + + // @ts-ignore + const { registerShortcut } = useDispatch( keyboardShortcutsStore ); + useEffect( () => { + registerShortcut( { + name: 'language-chooser/move-up', + category: 'global', + description: __( 'Move language up' ), + keyCombination: { + character: 'ArrowUp', + }, + } ); + + registerShortcut( { + name: 'language-chooser/move-down', + category: 'global', + description: __( 'Move language down' ), + keyCombination: { + character: 'ArrowDown', + }, + } ); + + registerShortcut( { + name: 'language-chooser/select-first', + category: 'global', + description: __( 'Select first language' ), + keyCombination: { + character: 'Home', + }, + } ); + + registerShortcut( { + name: 'language-chooser/select-last', + category: 'global', + description: __( 'Select last language' ), + keyCombination: { + character: 'End', + }, + } ); + + registerShortcut( { + name: 'language-chooser/remove', + category: 'global', + description: __( 'Remove from list' ), + keyCombination: { + character: 'Backspace', + }, + } ); + + registerShortcut( { + name: 'language-chooser/add', + category: 'global', + description: _x( 'Add to list', 'language' ), + keyCombination: { + modifier: 'alt', + character: 'a', + }, + } ); + } ); + + const [ preferredLanguages, setPreferredLanguages ] = useState< + Language[] + >( props.preferredLanguages ); + + const [ selectedLanguage, setSelectedLanguage ] = useState< Language >( + props.preferredLanguages[ 0 ] + ); + + const inactiveLocales = allLanguages.filter( + ( language ) => + ! preferredLanguages.find( + ( { locale } ) => locale === language.locale + ) + ); + + const hasUninstalledPreferredLanguages = preferredLanguages.some( + ( { installed } ) => ! installed + ); + + useEffect( () => { + if ( ! hasUninstalledPreferredLanguages ) { + return; + } + + const addSpinner = () => { + const spinner = document.createElement( 'span' ); + spinner.className = + 'spinner language-install-spinner is-active language-chooser-spinner'; + + const submit = document.querySelector( '#submit' ); + + if ( ! submit ) { + return; + } + + submit.after( spinner ); + }; + + const form = document.querySelector( 'form' ); + + if ( ! form ) { + return; + } + + form.addEventListener( 'submit', addSpinner ); + + return () => { + form.removeEventListener( 'submit', addSpinner ); + }; + }, [ hasUninstalledPreferredLanguages ] ); + + const onAddLanguage = ( locale: Language ) => { + setPreferredLanguages( ( current ) => [ ...current, locale ] ); + setSelectedLanguage( locale ); + }; + + return ( + // @ts-ignore + +
+ +

+ { __( + 'Choose languages for displaying WordPress in, in order of preference.' + ) } +

+ + + { hasMissingTranslations && } +
+
+ ); +} + +export default LanguageChooser; diff --git a/packages/language-chooser/src/components/stories/index.story.tsx b/packages/language-chooser/src/components/stories/index.story.tsx new file mode 100644 index 00000000000000..55c424e2c90850 --- /dev/null +++ b/packages/language-chooser/src/components/stories/index.story.tsx @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import type { Meta } from '@storybook/react'; + +/** + * Internal dependencies + */ +import LanguageChooser from '../language-chooser'; +import type { Language } from '../../types'; + +const meta: Meta< typeof LanguageChooser > = { + title: 'Components/Language Chooser', + component: LanguageChooser, + parameters: { + argTypes: { + hasMissingTranslations: { + options: [ false, true ], + control: { type: 'radio' }, + }, + showOptionSiteDefault: { + options: [ false, true ], + control: { type: 'radio' }, + }, + }, + controls: { hideNoControlsWarning: true }, + }, +}; +export default meta; + +/* eslint-disable camelcase */ + +export const Default = ( props: {} ) => { + const de_DE: Language = { + locale: 'de_DE', + nativeName: 'Deutsch', + lang: 'de', + installed: true, + }; + + const en_US: Language = { + locale: 'en_US', + nativeName: 'English (US)', + lang: 'en', + installed: true, + }; + + const en_GB: Language = { + locale: 'en_GB', + nativeName: 'English (UK)', + lang: 'en', + installed: true, + }; + + const fr_FR: Language = { + locale: 'fr_FR', + nativeName: 'Français', + lang: 'fr', + installed: true, + }; + + const de_CH: Language = { + locale: 'de_CH', + nativeName: 'Deutsch (Schweiz)', + lang: 'de', + installed: false, + }; + + const it_IT: Language = { + locale: 'it_IT', + nativeName: 'Italiano', + lang: 'it', + installed: false, + }; + + return ( + + ); +}; + +/* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/components/test/active-locales.tsx b/packages/language-chooser/src/components/test/active-locales.tsx new file mode 100644 index 00000000000000..3f736b84d9fee9 --- /dev/null +++ b/packages/language-chooser/src/components/test/active-locales.tsx @@ -0,0 +1,368 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import ActiveLocales from '../active-locales'; +import type { Language } from '../../types'; + +jest.mock( 'uuid', () => ( {} ) ); + +jest.mock( '@wordpress/a11y', () => ( { + speak: jest.fn(), +} ) ); + +/* eslint-disable camelcase */ + +const de_DE: Language = { + locale: 'de_DE', + nativeName: 'Deutsch', + lang: 'de', + installed: true, +}; + +const en_GB: Language = { + locale: 'en_GB', + nativeName: 'English (UK)', + lang: 'en', + installed: true, +}; + +const fr_FR: Language = { + locale: 'fr_FR', + nativeName: 'Français', + lang: 'fr', + installed: true, +}; + +const scrollIntoView = jest.fn(); +window.HTMLElement.prototype.scrollIntoView = scrollIntoView; + +function renderComponent( ui, options = {} ) { + return render( ui, { + wrapper: ( { children } ) => ( + // @ts-ignore + { children } + ), + ...options, + } ); +} + +describe( 'ActiveLocales', () => { + afterEach( () => { + jest.resetAllMocks(); + } ); + + it( 'displays fallback message if list is empty', () => { + renderComponent( + {} } + setSelectedLanguage={ () => {} } + /> + ); + + expect( + screen.getByText( /Falling back to English/ ) + ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + } ); + + it( 'displays site default fallback message if list is empty', () => { + renderComponent( + {} } + setSelectedLanguage={ () => {} } + showOptionSiteDefault + /> + ); + + expect( + screen.getByText( /Falling back to Site Default/ ) + ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + } ); + + it( 'prevents moving a single item', () => { + renderComponent( + {} } + setSelectedLanguage={ () => {} } + selectedLanguage={ de_DE } + /> + ); + expect( screen.queryByText( /Falling back/ ) ).not.toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toBeEnabled(); + } ); + + it( 'prevents moving first item up', () => { + renderComponent( + {} } + setSelectedLanguage={ () => {} } + selectedLanguage={ de_DE } + /> + ); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toBeEnabled(); + } ); + + it( 'prevents moving last item down', () => { + renderComponent( + {} } + setSelectedLanguage={ () => {} } + selectedLanguage={ fr_FR } + /> + ); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toBeEnabled(); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + } ); + + it( 'selects next locale when removing one', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + expect( setSelectedLanguage ).toHaveBeenCalledWith( en_GB ); + } ); + + it( 'selects previous locale when removing one', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + expect( setSelectedLanguage ).toHaveBeenCalledWith( en_GB ); + } ); + + it( 'changes selection when clicking on locale', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ); + expect( setSelectedLanguage ).toHaveBeenCalledWith( de_DE ); + } ); + + it( 'clears selection when removing last locale', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + expect( setSelectedLanguage ).toHaveBeenCalledWith( undefined ); + } ); + + it( 'announces locale removal', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); + } ); + + it( 'announces fallback message if list is empty', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); + expect( speak ).toHaveBeenCalledWith( + expect.stringMatching( /Falling back to English/ ) + ); + } ); + + it( 'announces site default fallback message if list is empty', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); + expect( speak ).toHaveBeenCalledWith( + expect.stringMatching( /Falling back to Site Default/ ) + ); + } ); + + it( 'scrolls to newly selected locale', () => { + const { rerender } = renderComponent( + {} } + setSelectedLanguage={ () => {} } + selectedLanguage={ de_DE } + /> + ); + + rerender( + {} } + setSelectedLanguage={ () => {} } + selectedLanguage={ fr_FR } + /> + ); + + expect( scrollIntoView ).toHaveBeenCalled(); + } ); + + it( 'announces locale moving up', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Move up/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale moved up' ); + } ); + + it( 'announces locale moving down', async () => { + const setLanguages = jest.fn(); + const setSelectedLanguage = jest.fn(); + renderComponent( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Move down/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale moved down' ); + } ); +} ); + +/* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/components/test/inactive-locales.tsx b/packages/language-chooser/src/components/test/inactive-locales.tsx new file mode 100644 index 00000000000000..f3d30490825f17 --- /dev/null +++ b/packages/language-chooser/src/components/test/inactive-locales.tsx @@ -0,0 +1,149 @@ +/** + * External dependencies + */ +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import InactiveLocales from '../inactive-locales'; +import type { Language } from '../../types'; + +jest.mock( 'uuid', () => ( {} ) ); + +jest.mock( '@wordpress/a11y', () => ( { + speak: jest.fn(), +} ) ); + +/* eslint-disable camelcase */ + +const de_DE: Language = { + locale: 'de_DE', + nativeName: 'Deutsch', + lang: 'de', + installed: true, +}; + +const en_GB: Language = { + locale: 'en_GB', + nativeName: 'English (UK)', + lang: 'en', + installed: true, +}; + +const fr_FR: Language = { + locale: 'fr_FR', + nativeName: 'Français', + lang: 'fr', + installed: true, +}; + +const es_ES: Language = { + locale: 'es_ES', + nativeName: 'Español', + lang: 'es', + installed: false, +}; + +function renderComponent( ui, options = {} ) { + return render( ui, { + wrapper: ( { children } ) => ( + // @ts-ignore + { children } + ), + ...options, + } ); +} + +describe( 'InactiveLocales', () => { + afterEach( () => { + jest.resetAllMocks(); + } ); + + it( 'prevents selection if list is empty', () => { + renderComponent( + {} } /> + ); + expect( screen.getByRole( 'button', { name: /Add/ } ) ).toHaveAttribute( + 'aria-disabled', + 'true' + ); + expect( screen.getByRole( 'combobox' ) ).toHaveAttribute( + 'aria-disabled', + 'true' + ); + } ); + + it( 'adds selected locale and updates dropdown', async () => { + const onAddLanguage = jest.fn(); + const { rerender } = renderComponent( + + ); + + const add = screen.getByRole( 'button', { name: /Add/ } ); + const dropdown = screen.getByRole( 'combobox' ); + + expect( add ).toBeEnabled(); + expect( dropdown ).toBeEnabled(); + expect( dropdown ).toHaveValue( 'de_DE' ); + + fireEvent.change( dropdown, { target: { value: 'en_GB' } } ); + + await userEvent.click( add ); + + expect( onAddLanguage ).toHaveBeenCalledWith( en_GB ); + expect( dropdown ).toHaveValue( 'fr_FR' ); + expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); + + rerender( + + ); + + await userEvent.click( add ); + + expect( onAddLanguage ).toHaveBeenCalledWith( fr_FR ); + expect( dropdown ).toHaveValue( 'de_DE' ); + expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); + + rerender( + + ); + + await userEvent.click( add ); + + expect( onAddLanguage ).toHaveBeenCalledWith( de_DE ); + expect( dropdown ).toHaveValue( 'es_ES' ); + expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); + + rerender( + + ); + + await userEvent.click( add ); + + expect( onAddLanguage ).toHaveBeenCalledWith( es_ES ); + expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); + } ); +} ); + +/* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/components/test/language-chooser.tsx b/packages/language-chooser/src/components/test/language-chooser.tsx new file mode 100644 index 00000000000000..c57c1004bb6033 --- /dev/null +++ b/packages/language-chooser/src/components/test/language-chooser.tsx @@ -0,0 +1,390 @@ +/** + * External dependencies + */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { BACKSPACE, DOWN, END, HOME, UP } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import type { Language } from '../../types'; +import LanguageChooser from '../language-chooser'; + +jest.mock( 'uuid', () => ( {} ) ); + +/* eslint-disable camelcase */ + +const de_DE: Language = { + locale: 'de_DE', + nativeName: 'Deutsch', + lang: 'de', + installed: true, +}; + +const en_GB: Language = { + locale: 'en_GB', + nativeName: 'English (UK)', + lang: 'en', + installed: true, +}; + +const fr_FR: Language = { + locale: 'fr_FR', + nativeName: 'Français', + lang: 'fr', + installed: true, +}; + +const es_ES: Language = { + locale: 'es_ES', + nativeName: 'Español', + lang: 'es', + installed: false, +}; + +const it_IT: Language = { + locale: 'it_IT', + nativeName: 'Italiano', + lang: 'it', + installed: false, +}; + +const scrollIntoView = jest.fn(); +window.HTMLElement.prototype.scrollIntoView = scrollIntoView; + +/** + * Workaround to trigger keyboard events. + * + * @see https://github.com/WordPress/gutenberg/issues/45777 + */ + +function moveUp() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'ArrowUp', + keyCode: UP, + } ); +} + +function moveDown() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'ArrowDown', + keyCode: DOWN, + } ); +} + +function selectFirst() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'Home', + keyCode: HOME, + } ); +} + +function selectLast() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'End', + keyCode: END, + } ); +} + +function removeLocale() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'Backspace', + keyCode: BACKSPACE, + } ); +} + +function addLocale() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'A', + keyCode: 65, + altKey: true, + } ); +} + +describe( 'PreferredLanguages', () => { + it( 'shows missing translations notice', () => { + render( + + ); + + // Found multiple times due to aria-live. + expect( + screen.getAllByText( /Some of the languages are not installed/ ) + ).not.toHaveLength( 0 ); + } ); + + it( 'populates hidden form field', () => { + render( + + ); + + // eslint-disable-next-line testing-library/no-node-access + const hiddenInput = document.querySelector( + 'input[name="preferred_languages"][type="hidden"]' + ); + + expect( hiddenInput ).toHaveValue( 'de_DE,en_GB,fr_FR' ); + } ); + + it( 'adds language to list', async () => { + render( + + ); + + const add = screen.getByRole( 'button', { name: /Add/ } ); + await userEvent.click( add ); + + await waitFor( () => { + expect( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + } ); + } ); + + it( 're-populates selected locale when empty dropdown is filled again', async () => { + render( + + ); + + const add = screen.getByRole( 'button', { name: /Add/ } ); + const remove = screen.getByRole( 'button', { name: /Remove/ } ); + const dropdown = screen.getByRole( 'combobox' ); + + expect( add ).toBeEnabled(); + expect( dropdown ).toBeEnabled(); + expect( dropdown ).toHaveValue( 'es_ES' ); + + await userEvent.click( add ); + + await waitFor( () => { + expect( + screen.getByRole( 'option', { name: /Español/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + } ); + + expect( dropdown ).not.toHaveValue(); + expect( dropdown ).toBeDisabled(); + expect( screen.getByRole( 'button', { name: /Add/ } ) ).toHaveAttribute( + 'aria-disabled', + 'true' + ); + + await userEvent.click( remove ); + + await waitFor( () => { + expect( dropdown ).toHaveValue( 'es_ES' ); + } ); + expect( dropdown ).toBeEnabled(); + expect( screen.getByRole( 'button', { name: /Add/ } ) ).toBeEnabled(); + } ); + + it( 'supports keyboard shortcuts', async () => { + render( + + ); + + const listbox = screen.getByRole( 'listbox' ); + const dropdown = screen.getByRole( 'combobox' ); + + expect( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toBeEnabled(); + + listbox.focus(); + + moveDown(); + moveDown(); + moveDown(); + + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toBeEnabled(); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + selectFirst(); + + expect( + screen.getByRole( 'option', { name: /Français/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + + selectFirst(); + + expect( + screen.getByRole( 'option', { name: /Français/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + selectLast(); + + expect( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + + selectLast(); + + expect( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + moveUp(); + moveUp(); + moveUp(); + + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toBeEnabled(); + + removeLocale(); + + expect( + screen.queryByRole( 'option', { name: /Deutsch/ } ) + ).not.toBeInTheDocument(); + expect( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ).toBeInTheDocument(); + + removeLocale(); + removeLocale(); + + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + removeLocale(); + + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + selectFirst(); + + selectLast(); + + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + fireEvent.change( dropdown, { target: { value: 'en_GB' } } ); + expect( dropdown ).toHaveValue( 'en_GB' ); + + addLocale(); + + expect( + screen.getByRole( 'option', { name: /English/ } ) + ).toBeInTheDocument(); + + expect( dropdown ).toHaveValue( 'fr_FR' ); + + addLocale(); + + expect( dropdown ).toHaveValue( 'de_DE' ); + + addLocale(); + + expect( dropdown ).toHaveValue( 'es_ES' ); + + addLocale(); + + expect( dropdown ).toHaveValue( 'it_IT' ); + + addLocale(); + + expect( dropdown ).toHaveAttribute( 'aria-disabled', 'true' ); + + addLocale(); + + expect( dropdown ).toHaveAttribute( 'aria-disabled', 'true' ); + } ); + + it( 'adds a spinner when saving new translations', async () => { + const { container } = render( + , + { + wrapper: ( { children } ) => ( +
e.preventDefault() }> + { children } + { /* eslint-disable-next-line no-restricted-syntax */ } + +
+ ), + } + ); + + const submitButton = screen.getByRole( 'button', { name: 'Submit' } ); + await userEvent.click( submitButton ); + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const spinner = container.querySelector( '.language-install-spinner' ); + expect( spinner ).toBeInTheDocument(); + } ); + + it( 'does not add a spinner if there is no matching submit button', async () => { + const { container } = render( + , + { + wrapper: ( { children } ) => ( +
e.preventDefault() }> + { children } + +
+ ), + } + ); + + const submitButton = screen.getByRole( 'button', { name: 'Submit' } ); + await userEvent.click( submitButton ); + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const spinner = container.querySelector( '.language-install-spinner' ); + expect( spinner ).not.toBeInTheDocument(); + } ); +} ); + +/* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/index.ts b/packages/language-chooser/src/index.ts new file mode 100644 index 00000000000000..5edbda48600e83 --- /dev/null +++ b/packages/language-chooser/src/index.ts @@ -0,0 +1,3 @@ +export type * from './types'; + +export { default as LanguageChooser } from './components/language-chooser'; diff --git a/packages/language-chooser/src/style.scss b/packages/language-chooser/src/style.scss new file mode 100644 index 00000000000000..27e8143848e94a --- /dev/null +++ b/packages/language-chooser/src/style.scss @@ -0,0 +1,128 @@ +.active-locales { + margin: 1em 0; +} + +.active-locales-controls .button { + margin: 0; +} + +.active-locales-list, +.inactive-locales-list { + width: 25em; + clear: both; + float: left; +} + +.active-locales-list { + height: 120px; + overflow-y: scroll; + list-style: none; + margin: 0 1em 0 0; + padding: 0; + background: #fff; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + border: 1px solid #ddd; + color: #32373c; + box-sizing: border-box; +} + +.inactive-locales-list { + margin: 0 1em 0 0; +} + +.active-locales-list.empty-list { + background: rgba(255, 255, 255, 0.5); + border-color: rgba(222, 222, 222, 0.75); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.04); + display: flex; + align-items: center; +} + +.active-locales-list li { + box-sizing: border-box; + width: 100%; + height: 30px; + line-height: 30px; + margin: 0; + padding: 0 0 0 8px; + cursor: pointer; + font-size: 13px; +} + +.active-locales-controls { + clear: both; +} + +@media screen and (min-width: 510px) { + + .active-locales-controls { + clear: none; + } +} + + +.active-locales-controls ul { + list-style: none; + margin: 0; + padding: 0; +} + +.active-locales-controls li { + margin: 0; +} + +.active-locales-controls li + li { + margin-top: 0.5em; +} + +.active-locales-list li:hover { + background: rgba(0, 0, 0, 0.07); +} + +.active-locales-list li[aria-selected="true"] { + background: rgba(0, 0, 0, 0.15); +} + +.active-locales-empty-message { + display: flex; + justify-content: center; + flex-direction: column; + position: relative; + text-align: center; + width: 25em; + height: 120px; + margin-bottom: -120px; + box-sizing: border-box; + color: rgba(51, 51, 51, 0.5); +} + +.active-locales-empty-message.hidden { + display: none; +} + +.inactive-locales { + clear: both; +} + +.inactive-locales select { + width: 100%; +} + +.language-chooser .components-button { + height: 30px; +} + +.language-chooser .components-notice { + display: inline-block; +} + +.language-install-spinner { + display: inline-block; + float: none; + margin: -3px 5px 0; + vertical-align: middle; +} + +.language-install-spinner:not(.language-chooser-spinner) { + display: none; +} diff --git a/packages/language-chooser/src/types.ts b/packages/language-chooser/src/types.ts new file mode 100644 index 00000000000000..9723ced9836da3 --- /dev/null +++ b/packages/language-chooser/src/types.ts @@ -0,0 +1,16 @@ +export type Locale = string; + +export interface Language { + locale: Locale; + nativeName: string; + lang: string; + installed: boolean; +} + +export interface PreferredLanguagesConfig { + currentLocale: Locale; + preferredLanguages: Language[]; + allLanguages: Language[]; + hasMissingTranslations: boolean; + showOptionSiteDefault: boolean; +} diff --git a/packages/language-chooser/src/utils/index.ts b/packages/language-chooser/src/utils/index.ts new file mode 100644 index 00000000000000..0481ea2483f705 --- /dev/null +++ b/packages/language-chooser/src/utils/index.ts @@ -0,0 +1,9 @@ +export function reorder< T extends Array< unknown > >( + list: T, + srcIndex: number, + destIndex: number +) { + const item = list.splice( srcIndex, 1 )[ 0 ]; + list.splice( destIndex, 0, item ); + return list; +} diff --git a/packages/language-chooser/src/utils/test/reorder.ts b/packages/language-chooser/src/utils/test/reorder.ts new file mode 100644 index 00000000000000..0fac9156b93b06 --- /dev/null +++ b/packages/language-chooser/src/utils/test/reorder.ts @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import { reorder } from '../'; + +describe( 'reorder', () => { + it.each( [ [ [ 1, 2, 3, 4 ], 0, 3, [ 2, 3, 4, 1 ] ] ] )( + 'reorders array', + ( list, srcIndex, destIndex, result ) => { + expect( reorder( list, srcIndex, destIndex ) ).toStrictEqual( + result + ); + } + ); +} ); diff --git a/packages/language-chooser/tsconfig.json b/packages/language-chooser/tsconfig.json new file mode 100644 index 00000000000000..3e3287ea24590c --- /dev/null +++ b/packages/language-chooser/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "references": [ + { "path": "../a11y" }, + { "path": "../components" }, + { "path": "../data" }, + { "path": "../element" }, + { "path": "../i18n" }, + { "path": "../keyboard-shortcuts" }, + { "path": "../keycodes" } + ], + "include": [ "src/**/*" ] +} diff --git a/storybook/main.js b/storybook/main.js index 66e951b26b1de6..fa6d1a8bf9c6d0 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -34,6 +34,7 @@ const stories = [ '../packages/icons/src/**/stories/*.story.@(js|tsx|mdx)', '../packages/edit-site/src/**/stories/*.story.@(js|tsx|mdx)', '../packages/dataviews/src/**/stories/*.story.@(js|tsx|mdx)', + '../packages/language-chooser/src/**/stories/*.story.@(js|tsx|mdx)', ].filter( Boolean ); module.exports = { diff --git a/storybook/package-styles/config.js b/storybook/package-styles/config.js index 21215fcad5c21e..56db3d26abb479 100644 --- a/storybook/package-styles/config.js +++ b/storybook/package-styles/config.js @@ -13,6 +13,8 @@ import editSiteLtr from '../package-styles/edit-site-ltr.lazy.scss'; import editSiteRtl from '../package-styles/edit-site-rtl.lazy.scss'; import dataviewsLtr from '../package-styles/dataviews-ltr.lazy.scss'; import dataviewsRtl from '../package-styles/dataviews-rtl.lazy.scss'; +import languageChooserLtr from '../package-styles/language-chooser-ltr.lazy.scss'; +import languageChooserRtl from '../package-styles/language-chooser-rtl.lazy.scss'; /** * Stylesheets to lazy load when the story's context.componentId matches the @@ -58,6 +60,11 @@ const CONFIG = [ ltr: [ dataviewsLtr, componentsLtr ], rtl: [ dataviewsRtl, componentsRtl ], }, + { + componentIdMatcher: /^components-language-chooser/, + ltr: [ languageChooserLtr, componentsLtr ], + rtl: [ languageChooserRtl, componentsRtl ], + }, ]; export default CONFIG; diff --git a/storybook/package-styles/language-chooser-ltr.lazy.scss b/storybook/package-styles/language-chooser-ltr.lazy.scss new file mode 100644 index 00000000000000..60ba809ad75b78 --- /dev/null +++ b/storybook/package-styles/language-chooser-ltr.lazy.scss @@ -0,0 +1 @@ +@import "../../packages/language-chooser/build-style/style"; diff --git a/storybook/package-styles/language-chooser-rtl.lazy.scss b/storybook/package-styles/language-chooser-rtl.lazy.scss new file mode 100644 index 00000000000000..256fd0b74ef9b6 --- /dev/null +++ b/storybook/package-styles/language-chooser-rtl.lazy.scss @@ -0,0 +1 @@ +@import "../../packages/language-chooser/build-style/style-rtl"; From 179fbfe6ca0a3d4ae4c498af8663900e8345e970 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Aug 2024 17:00:06 +0200 Subject: [PATCH 02/39] Add missing reference --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index cf986ddbee72bf..5fb787ba0f10f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ { "path": "packages/interactivity-router" }, { "path": "packages/is-shallow-equal" }, { "path": "packages/keycodes" }, + { "path": "packages/language-chooser" }, { "path": "packages/lazy-import" }, { "path": "packages/notices" }, { "path": "packages/plugins" }, From fdf9e087313a2d3596ca6dbaa018d30f1eb562b4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 09:51:11 +0200 Subject: [PATCH 03/39] Fix tests --- .../src/components/test/language-chooser.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/language-chooser/src/components/test/language-chooser.tsx b/packages/language-chooser/src/components/test/language-chooser.tsx index c57c1004bb6033..f882a73fe9b0a3 100644 --- a/packages/language-chooser/src/components/test/language-chooser.tsx +++ b/packages/language-chooser/src/components/test/language-chooser.tsx @@ -1,7 +1,13 @@ /** * External dependencies */ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + fireEvent, + render, + screen, + waitFor, + queryByRole, +} from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; @@ -107,7 +113,7 @@ function addLocale() { } ); } -describe( 'PreferredLanguages', () => { +describe( 'LanguageChooser', () => { it( 'shows missing translations notice', () => { render( { /> ); + const dropdown = screen.getByRole( 'combobox' ); + expect( dropdown ).toBeEnabled(); + expect( dropdown ).toHaveValue( 'en_GB' ); + const add = screen.getByRole( 'button', { name: /Add/ } ); await userEvent.click( add ); await waitFor( () => { expect( - screen.getByRole( 'option', { name: /Deutsch/ } ) + screen.getByRole( 'option', { name: /English \(UK\)/ } ) ).toHaveAttribute( 'aria-selected', 'true' ); } ); } ); @@ -275,7 +285,9 @@ describe( 'PreferredLanguages', () => { removeLocale(); expect( - screen.queryByRole( 'option', { name: /Deutsch/ } ) + // We want to explicitly check if it's within the container. + // eslint-disable-next-line testing-library/prefer-screen-queries + queryByRole( listbox, 'option', { name: /Deutsch/ } ) ).not.toBeInTheDocument(); expect( screen.getByRole( 'option', { name: /Deutsch/ } ) @@ -327,11 +339,11 @@ describe( 'PreferredLanguages', () => { addLocale(); - expect( dropdown ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( dropdown ).toBeDisabled(); addLocale(); - expect( dropdown ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( dropdown ).toBeDisabled(); } ); it( 'adds a spinner when saving new translations', async () => { From ec732c2b20ac16bd79a7449df9ac97bc353dcafa Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 09:54:20 +0200 Subject: [PATCH 04/39] Fix tsconfig --- packages/language-chooser/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/language-chooser/tsconfig.json b/packages/language-chooser/tsconfig.json index 3e3287ea24590c..110c54f86133d5 100644 --- a/packages/language-chooser/tsconfig.json +++ b/packages/language-chooser/tsconfig.json @@ -11,7 +11,6 @@ { "path": "../data" }, { "path": "../element" }, { "path": "../i18n" }, - { "path": "../keyboard-shortcuts" }, { "path": "../keycodes" } ], "include": [ "src/**/*" ] From 7148422448d096142cb3f57d889baecff9c33e53 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 10:34:38 +0200 Subject: [PATCH 05/39] Make it work in core --- lib/client-assets.php | 9 ++++++ .../src/components/active-controls.tsx | 1 + .../src/components/active-locales.tsx | 1 + .../src/components/inactive-controls.tsx | 1 + .../src/components/language-chooser.tsx | 1 + .../src/components/test/active-locales.tsx | 1 + .../src/components/test/inactive-locales.tsx | 1 + packages/language-chooser/src/index.ts | 3 -- packages/language-chooser/src/index.tsx | 31 +++++++++++++++++++ packages/language-chooser/src/types.ts | 2 +- 10 files changed, 47 insertions(+), 4 deletions(-) delete mode 100644 packages/language-chooser/src/index.ts create mode 100644 packages/language-chooser/src/index.tsx diff --git a/lib/client-assets.php b/lib/client-assets.php index 62e874d6b06c82..0f2d1d610905f2 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -477,6 +477,15 @@ function gutenberg_register_packages_styles( $styles ) { $version ); $styles->add_data( 'wp-preferences', 'rtl', 'replace' ); + + gutenberg_override_style( + $styles, + 'wp-language-chooser', + gutenberg_url( 'build/language-chooser/style.css' ), + array( 'wp-components' ), + $version + ); + $styles->add_data( 'wp-language-chooser', 'rtl', 'replace' ); } add_action( 'wp_default_styles', 'gutenberg_register_packages_styles' ); diff --git a/packages/language-chooser/src/components/active-controls.tsx b/packages/language-chooser/src/components/active-controls.tsx index 556a3ca3b76f77..b36694c5c3327d 100644 --- a/packages/language-chooser/src/components/active-controls.tsx +++ b/packages/language-chooser/src/components/active-controls.tsx @@ -3,6 +3,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; +// @ts-ignore import { useShortcut } from '@wordpress/keyboard-shortcuts'; /** diff --git a/packages/language-chooser/src/components/active-locales.tsx b/packages/language-chooser/src/components/active-locales.tsx index e99ef4ffd8d26b..5f443b7802d0b0 100644 --- a/packages/language-chooser/src/components/active-locales.tsx +++ b/packages/language-chooser/src/components/active-locales.tsx @@ -4,6 +4,7 @@ import { useLayoutEffect, useRef } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; +// @ts-ignore import { useShortcut } from '@wordpress/keyboard-shortcuts'; /** diff --git a/packages/language-chooser/src/components/inactive-controls.tsx b/packages/language-chooser/src/components/inactive-controls.tsx index 204166780d0a4a..02253772cf7a03 100644 --- a/packages/language-chooser/src/components/inactive-controls.tsx +++ b/packages/language-chooser/src/components/inactive-controls.tsx @@ -3,6 +3,7 @@ */ import { _x, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; +// @ts-ignore import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { shortcutAriaLabel, displayShortcut } from '@wordpress/keycodes'; diff --git a/packages/language-chooser/src/components/language-chooser.tsx b/packages/language-chooser/src/components/language-chooser.tsx index 5e0a352edd938e..9b6753195b1d7d 100644 --- a/packages/language-chooser/src/components/language-chooser.tsx +++ b/packages/language-chooser/src/components/language-chooser.tsx @@ -8,6 +8,7 @@ import { Notice } from '@wordpress/components'; import { ShortcutProvider, store as keyboardShortcutsStore, + // @ts-ignore } from '@wordpress/keyboard-shortcuts'; /** diff --git a/packages/language-chooser/src/components/test/active-locales.tsx b/packages/language-chooser/src/components/test/active-locales.tsx index 3f736b84d9fee9..b022884525593c 100644 --- a/packages/language-chooser/src/components/test/active-locales.tsx +++ b/packages/language-chooser/src/components/test/active-locales.tsx @@ -9,6 +9,7 @@ import '@testing-library/jest-dom'; * WordPress dependencies */ import { speak } from '@wordpress/a11y'; +// @ts-ignore import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; /** diff --git a/packages/language-chooser/src/components/test/inactive-locales.tsx b/packages/language-chooser/src/components/test/inactive-locales.tsx index f3d30490825f17..4d4e6995022abe 100644 --- a/packages/language-chooser/src/components/test/inactive-locales.tsx +++ b/packages/language-chooser/src/components/test/inactive-locales.tsx @@ -9,6 +9,7 @@ import '@testing-library/jest-dom'; * WordPress dependencies */ import { speak } from '@wordpress/a11y'; +// @ts-ignore import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; /** diff --git a/packages/language-chooser/src/index.ts b/packages/language-chooser/src/index.ts deleted file mode 100644 index 5edbda48600e83..00000000000000 --- a/packages/language-chooser/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type * from './types'; - -export { default as LanguageChooser } from './components/language-chooser'; diff --git a/packages/language-chooser/src/index.tsx b/packages/language-chooser/src/index.tsx new file mode 100644 index 00000000000000..97fedacd969bcf --- /dev/null +++ b/packages/language-chooser/src/index.tsx @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { createRoot, StrictMode } from '@wordpress/element'; +/** + * Internal dependencies + */ +import type { LanguageChooserConfig } from './types'; +import LanguageChooser from './components/language-chooser'; + +export type * from './types'; + +/** + * Initializes the site editor screen. + * + * @param {string} id ID of the root element to render the screen in. + * @param {Object} settings Editor settings. + */ +export function initializeLanguageChooser( + id: string, + settings: LanguageChooserConfig +) { + const target = document.getElementById( id ) as HTMLElement; + const root = createRoot( target ); + + root.render( + + + + ); +} diff --git a/packages/language-chooser/src/types.ts b/packages/language-chooser/src/types.ts index 9723ced9836da3..3998f1399d47b0 100644 --- a/packages/language-chooser/src/types.ts +++ b/packages/language-chooser/src/types.ts @@ -7,7 +7,7 @@ export interface Language { installed: boolean; } -export interface PreferredLanguagesConfig { +export interface LanguageChooserConfig { currentLocale: Locale; preferredLanguages: Language[]; allLanguages: Language[]; From 86aa3a5a6400e21d8822c1ec4b12afd606adc98a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 10:44:45 +0200 Subject: [PATCH 06/39] Minor fixes --- .../src/components/test/inactive-locales.tsx | 5 +---- packages/language-chooser/src/index.tsx | 3 ++- packages/language-chooser/src/style.scss | 6 +----- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/language-chooser/src/components/test/inactive-locales.tsx b/packages/language-chooser/src/components/test/inactive-locales.tsx index 4d4e6995022abe..45c55e789d9d54 100644 --- a/packages/language-chooser/src/components/test/inactive-locales.tsx +++ b/packages/language-chooser/src/components/test/inactive-locales.tsx @@ -77,10 +77,7 @@ describe( 'InactiveLocales', () => { 'aria-disabled', 'true' ); - expect( screen.getByRole( 'combobox' ) ).toHaveAttribute( - 'aria-disabled', - 'true' - ); + expect( screen.getByRole( 'combobox' ) ).toBeDisabled(); } ); it( 'adds selected locale and updates dropdown', async () => { diff --git a/packages/language-chooser/src/index.tsx b/packages/language-chooser/src/index.tsx index 97fedacd969bcf..39eeffb6bd1e98 100644 --- a/packages/language-chooser/src/index.tsx +++ b/packages/language-chooser/src/index.tsx @@ -2,6 +2,7 @@ * WordPress dependencies */ import { createRoot, StrictMode } from '@wordpress/element'; + /** * Internal dependencies */ @@ -14,7 +15,7 @@ export type * from './types'; * Initializes the site editor screen. * * @param {string} id ID of the root element to render the screen in. - * @param {Object} settings Editor settings. + * @param {Object} settings Language chooser settings. */ export function initializeLanguageChooser( id: string, diff --git a/packages/language-chooser/src/style.scss b/packages/language-chooser/src/style.scss index 27e8143848e94a..d5834afa3cf658 100644 --- a/packages/language-chooser/src/style.scss +++ b/packages/language-chooser/src/style.scss @@ -116,13 +116,9 @@ display: inline-block; } -.language-install-spinner { +.language-chooser-spinner { display: inline-block; float: none; margin: -3px 5px 0; vertical-align: middle; } - -.language-install-spinner:not(.language-chooser-spinner) { - display: none; -} From ed5b7c425b20594c298d79ef48f205cd7abcfb80 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 10:45:54 +0200 Subject: [PATCH 07/39] Remove mocks --- .../language-chooser/src/components/test/active-locales.tsx | 2 -- .../language-chooser/src/components/test/inactive-locales.tsx | 2 -- .../language-chooser/src/components/test/language-chooser.tsx | 2 -- 3 files changed, 6 deletions(-) diff --git a/packages/language-chooser/src/components/test/active-locales.tsx b/packages/language-chooser/src/components/test/active-locales.tsx index b022884525593c..5d90648b52e4af 100644 --- a/packages/language-chooser/src/components/test/active-locales.tsx +++ b/packages/language-chooser/src/components/test/active-locales.tsx @@ -18,8 +18,6 @@ import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; import ActiveLocales from '../active-locales'; import type { Language } from '../../types'; -jest.mock( 'uuid', () => ( {} ) ); - jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn(), } ) ); diff --git a/packages/language-chooser/src/components/test/inactive-locales.tsx b/packages/language-chooser/src/components/test/inactive-locales.tsx index 45c55e789d9d54..91cdea5dda5224 100644 --- a/packages/language-chooser/src/components/test/inactive-locales.tsx +++ b/packages/language-chooser/src/components/test/inactive-locales.tsx @@ -18,8 +18,6 @@ import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; import InactiveLocales from '../inactive-locales'; import type { Language } from '../../types'; -jest.mock( 'uuid', () => ( {} ) ); - jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn(), } ) ); diff --git a/packages/language-chooser/src/components/test/language-chooser.tsx b/packages/language-chooser/src/components/test/language-chooser.tsx index f882a73fe9b0a3..7d180923500702 100644 --- a/packages/language-chooser/src/components/test/language-chooser.tsx +++ b/packages/language-chooser/src/components/test/language-chooser.tsx @@ -22,8 +22,6 @@ import { BACKSPACE, DOWN, END, HOME, UP } from '@wordpress/keycodes'; import type { Language } from '../../types'; import LanguageChooser from '../language-chooser'; -jest.mock( 'uuid', () => ( {} ) ); - /* eslint-disable camelcase */ const de_DE: Language = { From 2027647471515d412dd0be4f1367755c6bb4f31c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 15:37:34 +0200 Subject: [PATCH 08/39] Move to subdirectory --- .../src/components/{ => language-chooser}/active-controls.tsx | 2 +- .../src/components/{ => language-chooser}/active-locales.tsx | 2 +- .../components/{ => language-chooser}/inactive-controls.tsx | 0 .../{ => language-chooser}/inactive-locales-select.tsx | 2 +- .../components/{ => language-chooser}/inactive-locales.tsx | 2 +- .../{language-chooser.tsx => language-chooser/index.tsx} | 2 +- .../components/{ => language-chooser}/stories/index.story.tsx | 4 ++-- .../components/{ => language-chooser}/test/active-locales.tsx | 2 +- .../{ => language-chooser}/test/inactive-locales.tsx | 2 +- .../{ => language-chooser}/test/language-chooser.tsx | 2 +- .../{utils => components/language-chooser}/test/reorder.ts | 0 .../src/{ => components/language-chooser}/types.ts | 0 .../{utils/index.ts => components/language-chooser/utils.ts} | 0 packages/language-chooser/src/index.tsx | 4 +--- 14 files changed, 11 insertions(+), 13 deletions(-) rename packages/language-chooser/src/components/{ => language-chooser}/active-controls.tsx (98%) rename packages/language-chooser/src/components/{ => language-chooser}/active-locales.tsx (99%) rename packages/language-chooser/src/components/{ => language-chooser}/inactive-controls.tsx (100%) rename packages/language-chooser/src/components/{ => language-chooser}/inactive-locales-select.tsx (97%) rename packages/language-chooser/src/components/{ => language-chooser}/inactive-locales.tsx (98%) rename packages/language-chooser/src/components/{language-chooser.tsx => language-chooser/index.tsx} (99%) rename packages/language-chooser/src/components/{ => language-chooser}/stories/index.story.tsx (93%) rename packages/language-chooser/src/components/{ => language-chooser}/test/active-locales.tsx (99%) rename packages/language-chooser/src/components/{ => language-chooser}/test/inactive-locales.tsx (98%) rename packages/language-chooser/src/components/{ => language-chooser}/test/language-chooser.tsx (99%) rename packages/language-chooser/src/{utils => components/language-chooser}/test/reorder.ts (100%) rename packages/language-chooser/src/{ => components/language-chooser}/types.ts (100%) rename packages/language-chooser/src/{utils/index.ts => components/language-chooser/utils.ts} (100%) diff --git a/packages/language-chooser/src/components/active-controls.tsx b/packages/language-chooser/src/components/language-chooser/active-controls.tsx similarity index 98% rename from packages/language-chooser/src/components/active-controls.tsx rename to packages/language-chooser/src/components/language-chooser/active-controls.tsx index b36694c5c3327d..c9916024749db8 100644 --- a/packages/language-chooser/src/components/active-controls.tsx +++ b/packages/language-chooser/src/components/language-chooser/active-controls.tsx @@ -9,7 +9,7 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies */ -import type { Language } from '../types'; +import type { Language } from './types'; interface ActiveControlsProps { languages: Language[]; diff --git a/packages/language-chooser/src/components/active-locales.tsx b/packages/language-chooser/src/components/language-chooser/active-locales.tsx similarity index 99% rename from packages/language-chooser/src/components/active-locales.tsx rename to packages/language-chooser/src/components/language-chooser/active-locales.tsx index 5f443b7802d0b0..80003f4a3ed5d0 100644 --- a/packages/language-chooser/src/components/active-locales.tsx +++ b/packages/language-chooser/src/components/language-chooser/active-locales.tsx @@ -11,7 +11,7 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts'; * Internal dependencies */ import { reorder } from '../utils'; -import type { Language } from '../types'; +import type { Language } from './types'; import ActiveControls from './active-controls'; interface ActiveLocalesProps { diff --git a/packages/language-chooser/src/components/inactive-controls.tsx b/packages/language-chooser/src/components/language-chooser/inactive-controls.tsx similarity index 100% rename from packages/language-chooser/src/components/inactive-controls.tsx rename to packages/language-chooser/src/components/language-chooser/inactive-controls.tsx diff --git a/packages/language-chooser/src/components/inactive-locales-select.tsx b/packages/language-chooser/src/components/language-chooser/inactive-locales-select.tsx similarity index 97% rename from packages/language-chooser/src/components/inactive-locales-select.tsx rename to packages/language-chooser/src/components/language-chooser/inactive-locales-select.tsx index bbfa576899281e..76897aab3475f8 100644 --- a/packages/language-chooser/src/components/inactive-locales-select.tsx +++ b/packages/language-chooser/src/components/language-chooser/inactive-locales-select.tsx @@ -7,7 +7,7 @@ import { __, _x } from '@wordpress/i18n'; /** * Internal dependencies */ -import type { Language, Locale } from '../types'; +import type { Language, Locale } from './types'; interface InactiveLocalesSelectProps { installedLanguages: Language[]; diff --git a/packages/language-chooser/src/components/inactive-locales.tsx b/packages/language-chooser/src/components/language-chooser/inactive-locales.tsx similarity index 98% rename from packages/language-chooser/src/components/inactive-locales.tsx rename to packages/language-chooser/src/components/language-chooser/inactive-locales.tsx index 75b872d9d0b5da..ada3fdc11b4247 100644 --- a/packages/language-chooser/src/components/inactive-locales.tsx +++ b/packages/language-chooser/src/components/language-chooser/inactive-locales.tsx @@ -8,7 +8,7 @@ import { speak } from '@wordpress/a11y'; /** * Internal dependencies */ -import type { Language } from '../types'; +import type { Language } from './types'; import InactiveControls from './inactive-controls'; import InactiveLocalesSelect from './inactive-locales-select'; diff --git a/packages/language-chooser/src/components/language-chooser.tsx b/packages/language-chooser/src/components/language-chooser/index.tsx similarity index 99% rename from packages/language-chooser/src/components/language-chooser.tsx rename to packages/language-chooser/src/components/language-chooser/index.tsx index 9b6753195b1d7d..63d43ec0c8ab31 100644 --- a/packages/language-chooser/src/components/language-chooser.tsx +++ b/packages/language-chooser/src/components/language-chooser/index.tsx @@ -16,7 +16,7 @@ import { */ import ActiveLocales from './active-locales'; import InactiveLocales from './inactive-locales'; -import type { Language } from '../types'; +import type { Language } from './types'; function MissingTranslationsNotice() { return ( diff --git a/packages/language-chooser/src/components/stories/index.story.tsx b/packages/language-chooser/src/components/language-chooser/stories/index.story.tsx similarity index 93% rename from packages/language-chooser/src/components/stories/index.story.tsx rename to packages/language-chooser/src/components/language-chooser/stories/index.story.tsx index 55c424e2c90850..4791c5298e4d5c 100644 --- a/packages/language-chooser/src/components/stories/index.story.tsx +++ b/packages/language-chooser/src/components/language-chooser/stories/index.story.tsx @@ -6,8 +6,8 @@ import type { Meta } from '@storybook/react'; /** * Internal dependencies */ -import LanguageChooser from '../language-chooser'; -import type { Language } from '../../types'; +import LanguageChooser from '../'; +import type { Language } from '../types'; const meta: Meta< typeof LanguageChooser > = { title: 'Components/Language Chooser', diff --git a/packages/language-chooser/src/components/test/active-locales.tsx b/packages/language-chooser/src/components/language-chooser/test/active-locales.tsx similarity index 99% rename from packages/language-chooser/src/components/test/active-locales.tsx rename to packages/language-chooser/src/components/language-chooser/test/active-locales.tsx index 5d90648b52e4af..713b6ce4705f8e 100644 --- a/packages/language-chooser/src/components/test/active-locales.tsx +++ b/packages/language-chooser/src/components/language-chooser/test/active-locales.tsx @@ -16,7 +16,7 @@ import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; * Internal dependencies */ import ActiveLocales from '../active-locales'; -import type { Language } from '../../types'; +import type { Language } from '.././types'; jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn(), diff --git a/packages/language-chooser/src/components/test/inactive-locales.tsx b/packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx similarity index 98% rename from packages/language-chooser/src/components/test/inactive-locales.tsx rename to packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx index 91cdea5dda5224..623e67152dc448 100644 --- a/packages/language-chooser/src/components/test/inactive-locales.tsx +++ b/packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx @@ -16,7 +16,7 @@ import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; * Internal dependencies */ import InactiveLocales from '../inactive-locales'; -import type { Language } from '../../types'; +import type { Language } from '.././types'; jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn(), diff --git a/packages/language-chooser/src/components/test/language-chooser.tsx b/packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx similarity index 99% rename from packages/language-chooser/src/components/test/language-chooser.tsx rename to packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx index 7d180923500702..5d5cffee2cd1a7 100644 --- a/packages/language-chooser/src/components/test/language-chooser.tsx +++ b/packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx @@ -19,7 +19,7 @@ import { BACKSPACE, DOWN, END, HOME, UP } from '@wordpress/keycodes'; /** * Internal dependencies */ -import type { Language } from '../../types'; +import type { Language } from '.././types'; import LanguageChooser from '../language-chooser'; /* eslint-disable camelcase */ diff --git a/packages/language-chooser/src/utils/test/reorder.ts b/packages/language-chooser/src/components/language-chooser/test/reorder.ts similarity index 100% rename from packages/language-chooser/src/utils/test/reorder.ts rename to packages/language-chooser/src/components/language-chooser/test/reorder.ts diff --git a/packages/language-chooser/src/types.ts b/packages/language-chooser/src/components/language-chooser/types.ts similarity index 100% rename from packages/language-chooser/src/types.ts rename to packages/language-chooser/src/components/language-chooser/types.ts diff --git a/packages/language-chooser/src/utils/index.ts b/packages/language-chooser/src/components/language-chooser/utils.ts similarity index 100% rename from packages/language-chooser/src/utils/index.ts rename to packages/language-chooser/src/components/language-chooser/utils.ts diff --git a/packages/language-chooser/src/index.tsx b/packages/language-chooser/src/index.tsx index 39eeffb6bd1e98..5dde9e7825f931 100644 --- a/packages/language-chooser/src/index.tsx +++ b/packages/language-chooser/src/index.tsx @@ -6,11 +6,9 @@ import { createRoot, StrictMode } from '@wordpress/element'; /** * Internal dependencies */ -import type { LanguageChooserConfig } from './types'; +import type { LanguageChooserConfig } from './components/language-chooser/types'; import LanguageChooser from './components/language-chooser'; -export type * from './types'; - /** * Initializes the site editor screen. * From c26ef4050930facb07e782b4505f8f67ef51e649 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 Aug 2024 17:24:06 +0200 Subject: [PATCH 09/39] Manually handle keyboard shortcuts Moves all callbacks to the higher level. Moves all the tests too. --- package-lock.json | 5 +- packages/language-chooser/package.json | 2 - .../language-chooser/active-controls.tsx | 55 +-- .../language-chooser/active-locales.tsx | 110 +----- .../language-chooser/inactive-controls.tsx | 18 +- .../language-chooser/inactive-locales.tsx | 76 +--- .../src/components/language-chooser/index.tsx | 316 ++++++++++----- .../language-chooser/test/active-locales.tsx | 367 ------------------ .../test/inactive-locales.tsx | 145 ------- .../test/language-chooser.tsx | 226 ++++++++++- .../language-chooser/test/reorder.ts | 2 +- packages/language-chooser/tsconfig.json | 1 - 12 files changed, 483 insertions(+), 840 deletions(-) delete mode 100644 packages/language-chooser/src/components/language-chooser/test/active-locales.tsx delete mode 100644 packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx diff --git a/package-lock.json b/package-lock.json index d0dfeb21949313..785265c40cfe63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53908,16 +53908,15 @@ } }, "packages/language-chooser": { + "name": "@wordpress/language-chooser", "version": "1.0.0-prerelease", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", - "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", - "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes" }, "engines": { @@ -68365,10 +68364,8 @@ "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", - "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", - "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes" } }, diff --git a/packages/language-chooser/package.json b/packages/language-chooser/package.json index eb01c8ad82c286..68be61f4551d85 100644 --- a/packages/language-chooser/package.json +++ b/packages/language-chooser/package.json @@ -34,10 +34,8 @@ "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", - "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", - "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes" }, "publishConfig": { diff --git a/packages/language-chooser/src/components/language-chooser/active-controls.tsx b/packages/language-chooser/src/components/language-chooser/active-controls.tsx index c9916024749db8..46fadfdaf61c4b 100644 --- a/packages/language-chooser/src/components/language-chooser/active-controls.tsx +++ b/packages/language-chooser/src/components/language-chooser/active-controls.tsx @@ -3,66 +3,23 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -// @ts-ignore -import { useShortcut } from '@wordpress/keyboard-shortcuts'; - -/** - * Internal dependencies - */ -import type { Language } from './types'; interface ActiveControlsProps { - languages: Language[]; - selectedLanguage?: Language; onMoveUp: () => void; onMoveDown: () => void; onRemove: () => void; + isMoveUpDisabled: boolean; + isMoveDownDisabled: boolean; + isRemoveDisabled: boolean; } function ActiveControls( { - languages, - selectedLanguage, onMoveUp, onMoveDown, onRemove, + isMoveUpDisabled, + isMoveDownDisabled, + isRemoveDisabled, }: ActiveControlsProps ) { - const isMoveUpDisabled = - ! selectedLanguage || - languages[ 0 ]?.locale === selectedLanguage?.locale; - const isMoveDownDisabled = - ! selectedLanguage || - languages[ languages.length - 1 ]?.locale === selectedLanguage?.locale; - const isRemoveDisabled = ! selectedLanguage; - - useShortcut( 'language-chooser/move-up', ( event: Event ) => { - event.preventDefault(); - - if ( isMoveUpDisabled ) { - return; - } - - onMoveUp(); - } ); - - useShortcut( 'language-chooser/move-down', ( event: Event ) => { - event.preventDefault(); - - if ( isMoveDownDisabled ) { - return; - } - - onMoveDown(); - } ); - - useShortcut( 'language-chooser/remove', ( event: Event ) => { - event.preventDefault(); - - if ( isRemoveDisabled ) { - return; - } - - onRemove(); - } ); - return (
    diff --git a/packages/language-chooser/src/components/language-chooser/active-locales.tsx b/packages/language-chooser/src/components/language-chooser/active-locales.tsx index 80003f4a3ed5d0..fa5296a46976fe 100644 --- a/packages/language-chooser/src/components/language-chooser/active-locales.tsx +++ b/packages/language-chooser/src/components/language-chooser/active-locales.tsx @@ -3,14 +3,10 @@ */ import { useLayoutEffect, useRef } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { speak } from '@wordpress/a11y'; -// @ts-ignore -import { useShortcut } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies */ -import { reorder } from '../utils'; import type { Language } from './types'; import ActiveControls from './active-controls'; @@ -20,19 +16,30 @@ interface ActiveLocalesProps { showOptionSiteDefault?: boolean; setLanguages: ( cb: ( languages: Language[] ) => Language[] ) => void; setSelectedLanguage: ( language: Language ) => void; + onMoveUp: () => void; + onMoveDown: () => void; + onRemove: () => void; + isEmpty: boolean; + isMoveUpDisabled: boolean; + isMoveDownDisabled: boolean; + isRemoveDisabled: boolean; } export function ActiveLocales( { languages, - setLanguages, showOptionSiteDefault = false, selectedLanguage, setSelectedLanguage, + onMoveUp, + onMoveDown, + onRemove, + isEmpty, + isMoveUpDisabled, + isMoveDownDisabled, + isRemoveDisabled, }: ActiveLocalesProps ) { const listRef = useRef< HTMLUListElement | null >( null ); - const isEmpty = languages.length === 0; - useLayoutEffect( () => { const selectedEl = listRef.current?.querySelector( '[aria-selected="true"]' @@ -48,90 +55,6 @@ export function ActiveLocales( { } ); }, [ selectedLanguage, languages ] ); - useShortcut( 'language-chooser/select-first', ( event: Event ) => { - event.preventDefault(); - - if ( isEmpty ) { - return; - } - - setSelectedLanguage( languages.at( 0 ) as Language ); - } ); - - useShortcut( 'language-chooser/select-last', ( event: Event ) => { - event.preventDefault(); - - if ( isEmpty ) { - return; - } - - setSelectedLanguage( languages.at( -1 ) as Language ); - } ); - - const onRemove = () => { - const foundIndex = languages.findIndex( - ( { locale } ) => locale === selectedLanguage?.locale - ); - - setSelectedLanguage( - languages[ foundIndex + 1 ] || languages[ foundIndex - 1 ] - ); - - setLanguages( ( prevLanguages ) => - prevLanguages.filter( - ( { locale } ) => locale !== selectedLanguage?.locale - ) - ); - - speak( __( 'Locale removed from list' ) ); - - if ( languages.length === 1 ) { - let emptyMessageA11y = sprintf( - /* translators: %s: English (United States) */ - __( 'No languages selected. Falling back to %s.' ), - 'English (United States)' - ); - - if ( showOptionSiteDefault ) { - emptyMessageA11y = __( - 'No languages selected. Falling back to Site Default.' - ); - } - - speak( emptyMessageA11y ); - } - }; - - const onMoveUp = () => { - setLanguages( ( prevLanguages ) => { - const srcIndex = prevLanguages.findIndex( - ( { locale } ) => locale === selectedLanguage?.locale - ); - return reorder( - Array.from( prevLanguages ), - srcIndex, - srcIndex - 1 - ); - } ); - - speak( __( 'Locale moved up' ) ); - }; - - const onMoveDown = () => { - setLanguages( ( prevLanguages ) => { - const srcIndex = prevLanguages.findIndex( - ( { locale } ) => locale === selectedLanguage?.locale - ); - return reorder< Language[] >( - Array.from( prevLanguages ), - srcIndex, - srcIndex + 1 - ); - } ); - - speak( __( 'Locale moved down' ) ); - }; - const activeDescendant = isEmpty ? '' : selectedLanguage?.locale; const className = isEmpty @@ -186,11 +109,12 @@ export function ActiveLocales( { } ) }
); diff --git a/packages/language-chooser/src/components/language-chooser/inactive-controls.tsx b/packages/language-chooser/src/components/language-chooser/inactive-controls.tsx index 02253772cf7a03..4be1c3f7eae77e 100644 --- a/packages/language-chooser/src/components/language-chooser/inactive-controls.tsx +++ b/packages/language-chooser/src/components/language-chooser/inactive-controls.tsx @@ -3,26 +3,14 @@ */ import { _x, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -// @ts-ignore -import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { shortcutAriaLabel, displayShortcut } from '@wordpress/keycodes'; interface InactiveControlsProps { disabled: boolean; - onClick: () => void; + onAdd: () => void; } -function InactiveControls( { disabled, onClick }: InactiveControlsProps ) { - useShortcut( 'language-chooser/add', ( event: Event ) => { - event.preventDefault(); - - if ( disabled ) { - return; - } - - onClick(); - } ); - +function InactiveControls( { disabled, onAdd }: InactiveControlsProps ) { return (
diff --git a/packages/language-chooser/src/components/language-chooser/inactive-locales.tsx b/packages/language-chooser/src/components/language-chooser/inactive-locales.tsx index ada3fdc11b4247..9126ade17e138d 100644 --- a/packages/language-chooser/src/components/language-chooser/inactive-locales.tsx +++ b/packages/language-chooser/src/components/language-chooser/inactive-locales.tsx @@ -1,10 +1,3 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useEffect, useState } from '@wordpress/element'; -import { speak } from '@wordpress/a11y'; - /** * Internal dependencies */ @@ -14,27 +7,21 @@ import InactiveLocalesSelect from './inactive-locales-select'; interface InactiveLocalesProps { languages: Language[]; - onAddLanguage: ( language: Language ) => void; + onAdd: () => void; + selectedInactiveLanguage: Language; + setSelectedInactiveLanguage: ( locale: Language ) => void; + installedLanguages: Language[]; + availableLanguages: Language[]; } -function InactiveLocales( { languages, onAddLanguage }: InactiveLocalesProps ) { - const [ selectedInactiveLanguage, setSelectedInactiveLanguage ] = useState( - languages[ 0 ] - ); - - useEffect( () => { - if ( ! selectedInactiveLanguage ) { - setSelectedInactiveLanguage( languages[ 0 ] ); - } - }, [ selectedInactiveLanguage, languages ] ); - - const installedLanguages = languages.filter( ( { installed } ) => - Boolean( installed ) - ); - const availableLanguages = languages.filter( - ( { installed } ) => ! installed - ); - +function InactiveLocales( { + languages, + onAdd, + selectedInactiveLanguage, + setSelectedInactiveLanguage, + installedLanguages, + availableLanguages, +}: InactiveLocalesProps ) { const onChange = ( locale: string ) => { setSelectedInactiveLanguage( languages.find( @@ -43,41 +30,6 @@ function InactiveLocales( { languages, onAddLanguage }: InactiveLocalesProps ) { ); }; - const onClick = () => { - onAddLanguage( selectedInactiveLanguage ); - - const installedIndex = installedLanguages.findIndex( - ( { locale } ) => locale === selectedInactiveLanguage.locale - ); - - const availableIndex = availableLanguages.findIndex( - ( { locale } ) => locale === selectedInactiveLanguage.locale - ); - - let newSelected: Language | undefined; - - newSelected = installedLanguages[ installedIndex + 1 ]; - - if ( - ! newSelected && - installedLanguages[ 0 ] !== selectedInactiveLanguage - ) { - newSelected = installedLanguages[ 0 ]; - } - - if ( ! newSelected ) { - newSelected = availableLanguages[ availableIndex + 1 ]; - - if ( availableLanguages[ 0 ] !== selectedInactiveLanguage ) { - newSelected = availableLanguages[ 0 ]; - } - } - - setSelectedInactiveLanguage( newSelected ); - - speak( __( 'Locale added to list' ) ); - }; - return (
@@ -89,7 +41,7 @@ function InactiveLocales( { languages, onAddLanguage }: InactiveLocalesProps ) { />
diff --git a/packages/language-chooser/src/components/language-chooser/index.tsx b/packages/language-chooser/src/components/language-chooser/index.tsx index 63d43ec0c8ab31..4f1a6f1f528b55 100644 --- a/packages/language-chooser/src/components/language-chooser/index.tsx +++ b/packages/language-chooser/src/components/language-chooser/index.tsx @@ -1,15 +1,15 @@ +/** + * External dependencies + */ +import type { KeyboardEvent } from 'react'; + /** * WordPress dependencies */ import { useEffect, useState } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; -import { __, _x } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Notice } from '@wordpress/components'; -import { - ShortcutProvider, - store as keyboardShortcutsStore, - // @ts-ignore -} from '@wordpress/keyboard-shortcuts'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -17,6 +17,7 @@ import { import ActiveLocales from './active-locales'; import InactiveLocales from './inactive-locales'; import type { Language } from './types'; +import { reorder } from './utils'; function MissingTranslationsNotice() { return ( @@ -55,68 +56,9 @@ function LanguageChooser( props: LanguageChooserProps ) { showOptionSiteDefault = false, } = props; - // @ts-ignore - const { registerShortcut } = useDispatch( keyboardShortcutsStore ); - useEffect( () => { - registerShortcut( { - name: 'language-chooser/move-up', - category: 'global', - description: __( 'Move language up' ), - keyCombination: { - character: 'ArrowUp', - }, - } ); - - registerShortcut( { - name: 'language-chooser/move-down', - category: 'global', - description: __( 'Move language down' ), - keyCombination: { - character: 'ArrowDown', - }, - } ); - - registerShortcut( { - name: 'language-chooser/select-first', - category: 'global', - description: __( 'Select first language' ), - keyCombination: { - character: 'Home', - }, - } ); - - registerShortcut( { - name: 'language-chooser/select-last', - category: 'global', - description: __( 'Select last language' ), - keyCombination: { - character: 'End', - }, - } ); - - registerShortcut( { - name: 'language-chooser/remove', - category: 'global', - description: __( 'Remove from list' ), - keyCombination: { - character: 'Backspace', - }, - } ); - - registerShortcut( { - name: 'language-chooser/add', - category: 'global', - description: _x( 'Add to list', 'language' ), - keyCombination: { - modifier: 'alt', - character: 'a', - }, - } ); - } ); - - const [ preferredLanguages, setPreferredLanguages ] = useState< - Language[] - >( props.preferredLanguages ); + const [ languages, setLanguages ] = useState< Language[] >( + props.preferredLanguages + ); const [ selectedLanguage, setSelectedLanguage ] = useState< Language >( props.preferredLanguages[ 0 ] @@ -124,12 +66,28 @@ function LanguageChooser( props: LanguageChooserProps ) { const inactiveLocales = allLanguages.filter( ( language ) => - ! preferredLanguages.find( - ( { locale } ) => locale === language.locale - ) + ! languages.find( ( { locale } ) => locale === language.locale ) + ); + + const [ selectedInactiveLanguage, setSelectedInactiveLanguage ] = useState( + inactiveLocales[ 0 ] + ); + + useEffect( () => { + if ( ! selectedInactiveLanguage ) { + setSelectedInactiveLanguage( inactiveLocales[ 0 ] ); + } + }, [ selectedInactiveLanguage, inactiveLocales ] ); + + const installedLanguages = inactiveLocales.filter( ( { installed } ) => + Boolean( installed ) ); - const hasUninstalledPreferredLanguages = preferredLanguages.some( + const availableLanguages = inactiveLocales.filter( + ( { installed } ) => ! installed + ); + + const hasUninstalledPreferredLanguages = languages.some( ( { installed } ) => ! installed ); @@ -166,34 +124,198 @@ function LanguageChooser( props: LanguageChooserProps ) { }, [ hasUninstalledPreferredLanguages ] ); const onAddLanguage = ( locale: Language ) => { - setPreferredLanguages( ( current ) => [ ...current, locale ] ); + setLanguages( ( current ) => [ ...current, locale ] ); setSelectedLanguage( locale ); }; + const isEmpty = languages.length === 0; + const isMoveUpDisabled = + ! selectedLanguage || + languages[ 0 ]?.locale === selectedLanguage?.locale; + const isMoveDownDisabled = + ! selectedLanguage || + languages[ languages.length - 1 ]?.locale === selectedLanguage?.locale; + const isRemoveDisabled = ! selectedLanguage; + + const onAdd = () => { + onAddLanguage( selectedInactiveLanguage ); + + const installedIndex = installedLanguages.findIndex( + ( { locale } ) => locale === selectedInactiveLanguage.locale + ); + + const availableIndex = availableLanguages.findIndex( + ( { locale } ) => locale === selectedInactiveLanguage.locale + ); + + let newSelected: Language | undefined; + + newSelected = installedLanguages[ installedIndex + 1 ]; + + if ( + ! newSelected && + installedLanguages[ 0 ] !== selectedInactiveLanguage + ) { + newSelected = installedLanguages[ 0 ]; + } + + if ( ! newSelected ) { + newSelected = availableLanguages[ availableIndex + 1 ]; + + if ( availableLanguages[ 0 ] !== selectedInactiveLanguage ) { + newSelected = availableLanguages[ 0 ]; + } + } + + setSelectedInactiveLanguage( newSelected ); + + speak( __( 'Locale added to list' ) ); + }; + + const onRemove = () => { + const foundIndex = languages.findIndex( + ( { locale } ) => locale === selectedLanguage?.locale + ); + + setSelectedLanguage( + languages[ foundIndex + 1 ] || languages[ foundIndex - 1 ] + ); + + setLanguages( ( prevLanguages ) => + prevLanguages.filter( + ( { locale } ) => locale !== selectedLanguage?.locale + ) + ); + + speak( __( 'Locale removed from list' ) ); + + if ( languages.length === 1 ) { + let emptyMessageA11y = sprintf( + /* translators: %s: English (United States) */ + __( 'No languages selected. Falling back to %s.' ), + 'English (United States)' + ); + + if ( showOptionSiteDefault ) { + emptyMessageA11y = __( + 'No languages selected. Falling back to Site Default.' + ); + } + + speak( emptyMessageA11y ); + } + }; + + const onMoveUp = () => { + setLanguages( ( prevLanguages ) => { + const srcIndex = prevLanguages.findIndex( + ( { locale } ) => locale === selectedLanguage?.locale + ); + return reorder( + Array.from( prevLanguages ), + srcIndex, + srcIndex - 1 + ); + } ); + + speak( __( 'Locale moved up' ) ); + }; + + const onMoveDown = () => { + setLanguages( ( prevLanguages ) => { + const srcIndex = prevLanguages.findIndex( + ( { locale } ) => locale === selectedLanguage?.locale + ); + return reorder< Language[] >( + Array.from( prevLanguages ), + srcIndex, + srcIndex + 1 + ); + } ); + + speak( __( 'Locale moved down' ) ); + }; + + const onKeyDown = ( event: KeyboardEvent< HTMLElement > ) => { + switch ( event.code ) { + // Move item up. + case 'ArrowUp': + if ( ! isMoveUpDisabled ) { + onMoveUp(); + event.preventDefault(); + } + break; + // Move item down. + case 'ArrowDown': + if ( ! isMoveDownDisabled ) { + onMoveDown(); + event.preventDefault(); + } + break; + // Select first item. + case 'Home': + if ( ! isEmpty ) { + setSelectedLanguage( languages.at( 0 ) as Language ); + event.preventDefault(); + } + break; + // Select last item. + case 'End': + if ( ! isEmpty ) { + setSelectedLanguage( languages.at( -1 ) as Language ); + event.preventDefault(); + } + break; + // Remove item. + case 'Backspace': + if ( ! isRemoveDisabled ) { + onRemove(); + event.preventDefault(); + } + break; + // Add item. + case 'KeyA': + if ( event.altKey ) { + onAdd(); + event.preventDefault(); + } + break; + } + }; + return ( - // @ts-ignore - -
- -

- { __( - 'Choose languages for displaying WordPress in, in order of preference.' - ) } -

- - - { hasMissingTranslations && } -
-
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ +

+ { __( + 'Choose languages for displaying WordPress in, in order of preference.' + ) } +

+ + + { hasMissingTranslations && } +
); } diff --git a/packages/language-chooser/src/components/language-chooser/test/active-locales.tsx b/packages/language-chooser/src/components/language-chooser/test/active-locales.tsx deleted file mode 100644 index 713b6ce4705f8e..00000000000000 --- a/packages/language-chooser/src/components/language-chooser/test/active-locales.tsx +++ /dev/null @@ -1,367 +0,0 @@ -/** - * External dependencies - */ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom'; - -/** - * WordPress dependencies - */ -import { speak } from '@wordpress/a11y'; -// @ts-ignore -import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; - -/** - * Internal dependencies - */ -import ActiveLocales from '../active-locales'; -import type { Language } from '.././types'; - -jest.mock( '@wordpress/a11y', () => ( { - speak: jest.fn(), -} ) ); - -/* eslint-disable camelcase */ - -const de_DE: Language = { - locale: 'de_DE', - nativeName: 'Deutsch', - lang: 'de', - installed: true, -}; - -const en_GB: Language = { - locale: 'en_GB', - nativeName: 'English (UK)', - lang: 'en', - installed: true, -}; - -const fr_FR: Language = { - locale: 'fr_FR', - nativeName: 'Français', - lang: 'fr', - installed: true, -}; - -const scrollIntoView = jest.fn(); -window.HTMLElement.prototype.scrollIntoView = scrollIntoView; - -function renderComponent( ui, options = {} ) { - return render( ui, { - wrapper: ( { children } ) => ( - // @ts-ignore - { children } - ), - ...options, - } ); -} - -describe( 'ActiveLocales', () => { - afterEach( () => { - jest.resetAllMocks(); - } ); - - it( 'displays fallback message if list is empty', () => { - renderComponent( - {} } - setSelectedLanguage={ () => {} } - /> - ); - - expect( - screen.getByText( /Falling back to English/ ) - ).toBeInTheDocument(); - expect( - screen.getByRole( 'button', { name: /Move up/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Move down/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Remove/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - } ); - - it( 'displays site default fallback message if list is empty', () => { - renderComponent( - {} } - setSelectedLanguage={ () => {} } - showOptionSiteDefault - /> - ); - - expect( - screen.getByText( /Falling back to Site Default/ ) - ).toBeInTheDocument(); - expect( - screen.getByRole( 'button', { name: /Move up/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Move down/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Remove/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - } ); - - it( 'prevents moving a single item', () => { - renderComponent( - {} } - setSelectedLanguage={ () => {} } - selectedLanguage={ de_DE } - /> - ); - expect( screen.queryByText( /Falling back/ ) ).not.toBeInTheDocument(); - expect( - screen.getByRole( 'button', { name: /Move up/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Move down/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Remove/ } ) - ).toBeEnabled(); - } ); - - it( 'prevents moving first item up', () => { - renderComponent( - {} } - setSelectedLanguage={ () => {} } - selectedLanguage={ de_DE } - /> - ); - expect( - screen.getByRole( 'button', { name: /Move up/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - expect( - screen.getByRole( 'button', { name: /Move down/ } ) - ).toBeEnabled(); - } ); - - it( 'prevents moving last item down', () => { - renderComponent( - {} } - setSelectedLanguage={ () => {} } - selectedLanguage={ fr_FR } - /> - ); - expect( - screen.getByRole( 'button', { name: /Move up/ } ) - ).toBeEnabled(); - expect( - screen.getByRole( 'button', { name: /Move down/ } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - } ); - - it( 'selects next locale when removing one', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Remove/ } ) - ); - expect( setSelectedLanguage ).toHaveBeenCalledWith( en_GB ); - } ); - - it( 'selects previous locale when removing one', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Remove/ } ) - ); - expect( setSelectedLanguage ).toHaveBeenCalledWith( en_GB ); - } ); - - it( 'changes selection when clicking on locale', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'option', { name: /Deutsch/ } ) - ); - expect( setSelectedLanguage ).toHaveBeenCalledWith( de_DE ); - } ); - - it( 'clears selection when removing last locale', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Remove/ } ) - ); - expect( setSelectedLanguage ).toHaveBeenCalledWith( undefined ); - } ); - - it( 'announces locale removal', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Remove/ } ) - ); - - expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); - } ); - - it( 'announces fallback message if list is empty', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Remove/ } ) - ); - - expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); - expect( speak ).toHaveBeenCalledWith( - expect.stringMatching( /Falling back to English/ ) - ); - } ); - - it( 'announces site default fallback message if list is empty', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Remove/ } ) - ); - - expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); - expect( speak ).toHaveBeenCalledWith( - expect.stringMatching( /Falling back to Site Default/ ) - ); - } ); - - it( 'scrolls to newly selected locale', () => { - const { rerender } = renderComponent( - {} } - setSelectedLanguage={ () => {} } - selectedLanguage={ de_DE } - /> - ); - - rerender( - {} } - setSelectedLanguage={ () => {} } - selectedLanguage={ fr_FR } - /> - ); - - expect( scrollIntoView ).toHaveBeenCalled(); - } ); - - it( 'announces locale moving up', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Move up/ } ) - ); - - expect( speak ).toHaveBeenCalledWith( 'Locale moved up' ); - } ); - - it( 'announces locale moving down', async () => { - const setLanguages = jest.fn(); - const setSelectedLanguage = jest.fn(); - renderComponent( - - ); - - await userEvent.click( - screen.getByRole( 'button', { name: /Move down/ } ) - ); - - expect( speak ).toHaveBeenCalledWith( 'Locale moved down' ); - } ); -} ); - -/* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx b/packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx deleted file mode 100644 index 623e67152dc448..00000000000000 --- a/packages/language-chooser/src/components/language-chooser/test/inactive-locales.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/** - * External dependencies - */ -import { render, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom'; - -/** - * WordPress dependencies - */ -import { speak } from '@wordpress/a11y'; -// @ts-ignore -import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; - -/** - * Internal dependencies - */ -import InactiveLocales from '../inactive-locales'; -import type { Language } from '.././types'; - -jest.mock( '@wordpress/a11y', () => ( { - speak: jest.fn(), -} ) ); - -/* eslint-disable camelcase */ - -const de_DE: Language = { - locale: 'de_DE', - nativeName: 'Deutsch', - lang: 'de', - installed: true, -}; - -const en_GB: Language = { - locale: 'en_GB', - nativeName: 'English (UK)', - lang: 'en', - installed: true, -}; - -const fr_FR: Language = { - locale: 'fr_FR', - nativeName: 'Français', - lang: 'fr', - installed: true, -}; - -const es_ES: Language = { - locale: 'es_ES', - nativeName: 'Español', - lang: 'es', - installed: false, -}; - -function renderComponent( ui, options = {} ) { - return render( ui, { - wrapper: ( { children } ) => ( - // @ts-ignore - { children } - ), - ...options, - } ); -} - -describe( 'InactiveLocales', () => { - afterEach( () => { - jest.resetAllMocks(); - } ); - - it( 'prevents selection if list is empty', () => { - renderComponent( - {} } /> - ); - expect( screen.getByRole( 'button', { name: /Add/ } ) ).toHaveAttribute( - 'aria-disabled', - 'true' - ); - expect( screen.getByRole( 'combobox' ) ).toBeDisabled(); - } ); - - it( 'adds selected locale and updates dropdown', async () => { - const onAddLanguage = jest.fn(); - const { rerender } = renderComponent( - - ); - - const add = screen.getByRole( 'button', { name: /Add/ } ); - const dropdown = screen.getByRole( 'combobox' ); - - expect( add ).toBeEnabled(); - expect( dropdown ).toBeEnabled(); - expect( dropdown ).toHaveValue( 'de_DE' ); - - fireEvent.change( dropdown, { target: { value: 'en_GB' } } ); - - await userEvent.click( add ); - - expect( onAddLanguage ).toHaveBeenCalledWith( en_GB ); - expect( dropdown ).toHaveValue( 'fr_FR' ); - expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); - - rerender( - - ); - - await userEvent.click( add ); - - expect( onAddLanguage ).toHaveBeenCalledWith( fr_FR ); - expect( dropdown ).toHaveValue( 'de_DE' ); - expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); - - rerender( - - ); - - await userEvent.click( add ); - - expect( onAddLanguage ).toHaveBeenCalledWith( de_DE ); - expect( dropdown ).toHaveValue( 'es_ES' ); - expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); - - rerender( - - ); - - await userEvent.click( add ); - - expect( onAddLanguage ).toHaveBeenCalledWith( es_ES ); - expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); - } ); -} ); - -/* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx b/packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx index 5d5cffee2cd1a7..66d0daacfa1f1f 100644 --- a/packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx +++ b/packages/language-chooser/src/components/language-chooser/test/language-chooser.tsx @@ -8,19 +8,23 @@ import { waitFor, queryByRole, } from '@testing-library/react'; -import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; /** * WordPress dependencies */ import { BACKSPACE, DOWN, END, HOME, UP } from '@wordpress/keycodes'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies */ -import type { Language } from '.././types'; -import LanguageChooser from '../language-chooser'; +import type { Language } from '../types'; +import LanguageChooser from '../'; + +jest.mock( '@wordpress/a11y', () => ( { + speak: jest.fn(), +} ) ); /* eslint-disable camelcase */ @@ -112,6 +116,10 @@ function addLocale() { } describe( 'LanguageChooser', () => { + afterEach( () => { + jest.resetAllMocks(); + } ); + it( 'shows missing translations notice', () => { render( { screen.getByRole( 'option', { name: /English \(UK\)/ } ) ).toHaveAttribute( 'aria-selected', 'true' ); } ); + + expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); + + await userEvent.click( add ); + + await waitFor( () => { + expect( + screen.getByRole( 'option', { name: /Español/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + } ); + + expect( speak ).toHaveBeenCalledWith( 'Locale added to list' ); } ); it( 're-populates selected locale when empty dropdown is filled again', async () => { @@ -229,7 +249,6 @@ describe( 'LanguageChooser', () => { listbox.focus(); - moveDown(); moveDown(); moveDown(); @@ -395,6 +414,205 @@ describe( 'LanguageChooser', () => { const spinner = container.querySelector( '.language-install-spinner' ); expect( spinner ).not.toBeInTheDocument(); } ); + + it( 'announces site default fallback message if list is empty', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + + expect( speak ).toHaveBeenNthCalledWith( + 1, + 'Locale removed from list' + ); + expect( speak ).toHaveBeenNthCalledWith( + 2, + expect.stringMatching( /Falling back to Site Default/ ) + ); + } ); + + it( 'announces locale moving up and down', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Move down/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale moved down' ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Move up/ } ) + ); + + expect( speak ).toHaveBeenCalledWith( 'Locale moved up' ); + } ); + + it( 'prevents selection if list is empty', () => { + render( + + ); + + expect( screen.getByRole( 'button', { name: /Add/ } ) ).toHaveAttribute( + 'aria-disabled', + 'true' + ); + expect( screen.getByRole( 'combobox' ) ).toBeDisabled(); + } ); + + it( 'displays fallback message if list is empty', () => { + render( + + ); + + expect( + screen.getByText( /Falling back to English/ ) + ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + } ); + + it( 'displays site default fallback message if list is empty', () => { + render( + + ); + + expect( + screen.getByText( /Falling back to Site Default/ ) + ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + } ); + + it( 'prevents moving a single item', () => { + render( + + ); + expect( screen.queryByText( /Falling back/ ) ).not.toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: /Move up/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Move down/ } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + expect( + screen.getByRole( 'button', { name: /Remove/ } ) + ).toBeEnabled(); + } ); + + it( 'selects next locale when removing one', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + + await waitFor( () => { + expect( + screen.getByRole( 'option', { name: /English \(UK\)/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + } ); + + expect( speak ).toHaveBeenCalledWith( 'Locale removed from list' ); + } ); + + it( 'selects previous locale when removing one', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Move down/ } ) + ); + + await userEvent.click( + screen.getByRole( 'button', { name: /Remove/ } ) + ); + + await waitFor( () => { + expect( + screen.getByRole( 'option', { name: /English \(UK\)/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + } ); + } ); + + it( 'changes selection when clicking on locale', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole( 'option', { name: /Français/ } ) + ); + + expect( + screen.getByRole( 'option', { name: /Français/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + } ); + + it( 'scrolls to newly selected locale', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole( 'option', { name: /Français/ } ) + ); + + expect( scrollIntoView ).toHaveBeenCalled(); + } ); } ); /* eslint-enable camelcase */ diff --git a/packages/language-chooser/src/components/language-chooser/test/reorder.ts b/packages/language-chooser/src/components/language-chooser/test/reorder.ts index 0fac9156b93b06..c8bfb91bc64894 100644 --- a/packages/language-chooser/src/components/language-chooser/test/reorder.ts +++ b/packages/language-chooser/src/components/language-chooser/test/reorder.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { reorder } from '../'; +import { reorder } from '../utils'; describe( 'reorder', () => { it.each( [ [ [ 1, 2, 3, 4 ], 0, 3, [ 2, 3, 4, 1 ] ] ] )( diff --git a/packages/language-chooser/tsconfig.json b/packages/language-chooser/tsconfig.json index 110c54f86133d5..8f4989673b90c5 100644 --- a/packages/language-chooser/tsconfig.json +++ b/packages/language-chooser/tsconfig.json @@ -8,7 +8,6 @@ "references": [ { "path": "../a11y" }, { "path": "../components" }, - { "path": "../data" }, { "path": "../element" }, { "path": "../i18n" }, { "path": "../keycodes" } From 6fc4417c04a71a5edba784b621c33d4e9a776523 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 23 Aug 2024 11:27:40 +0200 Subject: [PATCH 10/39] Move to `components` package --- lib/client-assets.php | 9 ---- package-lock.json | 33 -------------- package.json | 1 - packages/components/src/index.ts | 1 + .../src}/language-chooser/active-controls.tsx | 2 +- .../src}/language-chooser/active-locales.tsx | 6 +-- .../language-chooser/inactive-controls.tsx | 2 +- .../inactive-locales-select.tsx | 0 .../language-chooser/inactive-locales.tsx | 4 +- .../src}/language-chooser/index.tsx | 4 +- .../language-chooser/stories/index.story.tsx | 0 .../src/language-chooser}/style.scss | 42 +++++++++--------- .../test/language-chooser.tsx | 24 ++++++---- .../src}/language-chooser/test/reorder.ts | 0 .../src}/language-chooser/types.ts | 0 .../src}/language-chooser/utils.ts | 0 packages/language-chooser/.npmrc | 1 - packages/language-chooser/CHANGELOG.md | 7 --- packages/language-chooser/README.md | 23 ---------- packages/language-chooser/package.json | 44 ------------------- packages/language-chooser/src/index.tsx | 30 ------------- packages/language-chooser/tsconfig.json | 16 ------- storybook/main.js | 1 - storybook/package-styles/config.js | 7 --- .../language-chooser-ltr.lazy.scss | 1 - .../language-chooser-rtl.lazy.scss | 1 - tsconfig.json | 1 - 27 files changed, 46 insertions(+), 214 deletions(-) rename packages/{language-chooser/src/components => components/src}/language-chooser/active-controls.tsx (96%) rename packages/{language-chooser/src/components => components/src}/language-chooser/active-locales.tsx (93%) rename packages/{language-chooser/src/components => components/src}/language-chooser/inactive-controls.tsx (91%) rename packages/{language-chooser/src/components => components/src}/language-chooser/inactive-locales-select.tsx (100%) rename packages/{language-chooser/src/components => components/src}/language-chooser/inactive-locales.tsx (88%) rename packages/{language-chooser/src/components => components/src}/language-chooser/index.tsx (98%) rename packages/{language-chooser/src/components => components/src}/language-chooser/stories/index.story.tsx (100%) rename packages/{language-chooser/src => components/src/language-chooser}/style.scss (51%) rename packages/{language-chooser/src/components => components/src}/language-chooser/test/language-chooser.tsx (97%) rename packages/{language-chooser/src/components => components/src}/language-chooser/test/reorder.ts (100%) rename packages/{language-chooser/src/components => components/src}/language-chooser/types.ts (100%) rename packages/{language-chooser/src/components => components/src}/language-chooser/utils.ts (100%) delete mode 100644 packages/language-chooser/.npmrc delete mode 100644 packages/language-chooser/CHANGELOG.md delete mode 100644 packages/language-chooser/README.md delete mode 100644 packages/language-chooser/package.json delete mode 100644 packages/language-chooser/src/index.tsx delete mode 100644 packages/language-chooser/tsconfig.json delete mode 100644 storybook/package-styles/language-chooser-ltr.lazy.scss delete mode 100644 storybook/package-styles/language-chooser-rtl.lazy.scss diff --git a/lib/client-assets.php b/lib/client-assets.php index 0f2d1d610905f2..62e874d6b06c82 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -477,15 +477,6 @@ function gutenberg_register_packages_styles( $styles ) { $version ); $styles->add_data( 'wp-preferences', 'rtl', 'replace' ); - - gutenberg_override_style( - $styles, - 'wp-language-chooser', - gutenberg_url( 'build/language-chooser/style.css' ), - array( 'wp-components' ), - $version - ); - $styles->add_data( 'wp-language-chooser', 'rtl', 'replace' ); } add_action( 'wp_default_styles', 'gutenberg_register_packages_styles' ); diff --git a/package-lock.json b/package-lock.json index 785265c40cfe63..ccf59ff66a853d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", - "@wordpress/language-chooser": "file:packages/language-chooser", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", @@ -17084,10 +17083,6 @@ "resolved": "packages/keycodes", "link": true }, - "node_modules/@wordpress/language-chooser": { - "resolved": "packages/language-chooser", - "link": true - }, "node_modules/@wordpress/lazy-import": { "resolved": "packages/lazy-import", "link": true @@ -53907,23 +53902,6 @@ "npm": ">=8.19.2" } }, - "packages/language-chooser": { - "name": "@wordpress/language-chooser", - "version": "1.0.0-prerelease", - "license": "GPL-2.0-or-later", - "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/a11y": "file:../a11y", - "@wordpress/components": "file:../components", - "@wordpress/element": "file:../element", - "@wordpress/i18n": "file:../i18n", - "@wordpress/keycodes": "file:../keycodes" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" - } - }, "packages/lazy-import": { "name": "@wordpress/lazy-import", "version": "2.6.0", @@ -68358,17 +68336,6 @@ "@wordpress/i18n": "file:../i18n" } }, - "@wordpress/language-chooser": { - "version": "file:packages/language-chooser", - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/a11y": "file:../a11y", - "@wordpress/components": "file:../components", - "@wordpress/element": "file:../element", - "@wordpress/i18n": "file:../i18n", - "@wordpress/keycodes": "file:../keycodes" - } - }, "@wordpress/lazy-import": { "version": "file:packages/lazy-import", "requires": { diff --git a/package.json b/package.json index e2ec48373af6e6..7349e60eb4c210 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", - "@wordpress/language-chooser": "file:packages/language-chooser", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 6483e34dc222a8..1b33f5392d4988 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -104,6 +104,7 @@ export { default as __experimentalInputControl } from './input-control'; export { default as __experimentalInputControlPrefixWrapper } from './input-control/input-prefix-wrapper'; export { default as __experimentalInputControlSuffixWrapper } from './input-control/input-suffix-wrapper'; export { default as KeyboardShortcuts } from './keyboard-shortcuts'; +export { default as LanguageChooser } from './language-chooser'; export { default as MenuGroup } from './menu-group'; export { default as MenuItem } from './menu-item'; export { default as MenuItemsChoice } from './menu-items-choice'; diff --git a/packages/language-chooser/src/components/language-chooser/active-controls.tsx b/packages/components/src/language-chooser/active-controls.tsx similarity index 96% rename from packages/language-chooser/src/components/language-chooser/active-controls.tsx rename to packages/components/src/language-chooser/active-controls.tsx index 46fadfdaf61c4b..c715626a2c64f6 100644 --- a/packages/language-chooser/src/components/language-chooser/active-controls.tsx +++ b/packages/components/src/language-chooser/active-controls.tsx @@ -21,7 +21,7 @@ function ActiveControls( { isRemoveDisabled, }: ActiveControlsProps ) { return ( -
+
  • @@ -67,6 +68,7 @@ function ActiveControls( { disabled={ isMoveDownDisabled } accessibleWhenDisabled onClick={ onMoveDown } + __next40pxDefaultSize > { __( 'Move Down' ) } @@ -89,6 +91,7 @@ function ActiveControls( { disabled={ isRemoveDisabled } accessibleWhenDisabled onClick={ onRemove } + __next40pxDefaultSize > { __( 'Remove' ) } diff --git a/packages/components/src/language-chooser/active-locales.tsx b/packages/components/src/language-chooser/active-locales.tsx index e7621764b97f6f..64de9c4938eb44 100644 --- a/packages/components/src/language-chooser/active-locales.tsx +++ b/packages/components/src/language-chooser/active-locales.tsx @@ -23,6 +23,7 @@ interface ActiveLocalesProps { isMoveUpDisabled: boolean; isMoveDownDisabled: boolean; isRemoveDisabled: boolean; + labelId: string; } export function ActiveLocales( { @@ -37,6 +38,7 @@ export function ActiveLocales( { isMoveUpDisabled, isMoveDownDisabled, isRemoveDisabled, + labelId, }: ActiveLocalesProps ) { const listRef = useRef< HTMLUListElement | null >( null ); @@ -58,7 +60,7 @@ export function ActiveLocales( { const activeDescendant = isEmpty ? '' : selectedLanguage?.locale; const className = isEmpty - ? 'components-language-chooser__active-locales-list empty-list' + ? 'components-language-chooser__active-locales-list components-language-chooser__active-locales-list--empty' : 'components-language-chooser__active-locales-list'; let emptyMessage = sprintf( @@ -74,7 +76,7 @@ export function ActiveLocales( { return (
    { isEmpty && ( -
    +
    { __( 'Nothing set.' ) }
    { emptyMessage } @@ -82,7 +84,7 @@ export function ActiveLocales( { ) }
      setSelectedLanguage( language ) } > { nativeName } diff --git a/packages/components/src/language-chooser/inactive-controls.tsx b/packages/components/src/language-chooser/inactive-controls.tsx index 90d1f992fcbbb9..cbfa6fc22eb697 100644 --- a/packages/components/src/language-chooser/inactive-controls.tsx +++ b/packages/components/src/language-chooser/inactive-controls.tsx @@ -30,6 +30,7 @@ function InactiveControls( { disabled, onAdd }: InactiveControlsProps ) { disabled={ disabled } accessibleWhenDisabled onClick={ onAdd } + __next40pxDefaultSize > { _x( 'Add', 'language' ) } diff --git a/packages/components/src/language-chooser/inactive-locales-select.tsx b/packages/components/src/language-chooser/inactive-locales-select.tsx index c3a2af736b5ace..073d730db61a83 100644 --- a/packages/components/src/language-chooser/inactive-locales-select.tsx +++ b/packages/components/src/language-chooser/inactive-locales-select.tsx @@ -34,6 +34,7 @@ function InactiveLocalesSelect( { onChange={ onChange } disabled={ ! hasItems } __nextHasNoMarginBottom + __next40pxDefaultSize > { installedLanguages.length > 0 && ( diff --git a/packages/components/src/language-chooser/index.tsx b/packages/components/src/language-chooser/index.tsx index 7330149b8f8773..e21c7bbb549ba1 100644 --- a/packages/components/src/language-chooser/index.tsx +++ b/packages/components/src/language-chooser/index.tsx @@ -18,6 +18,7 @@ import ActiveLocales from './active-locales'; import InactiveLocales from './inactive-locales'; import type { Language } from './types'; import { reorder } from './utils'; +import { useInstanceId } from '@wordpress/compose'; function MissingTranslationsNotice() { return ( @@ -283,11 +284,16 @@ function LanguageChooser( props: LanguageChooserProps ) { } }; + const instanceId = useInstanceId( + LanguageChooser, + 'components-language-chooser' + ); + return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
      -

      +

      { __( 'Choose languages for displaying WordPress in, in order of preference.' ) } @@ -305,6 +311,7 @@ function LanguageChooser( props: LanguageChooserProps ) { isMoveUpDisabled={ isMoveUpDisabled } isMoveDownDisabled={ isMoveDownDisabled } isRemoveDisabled={ isRemoveDisabled } + labelId={ instanceId } /> Date: Fri, 23 Aug 2024 12:25:22 +0200 Subject: [PATCH 13/39] Add changelog entry --- packages/components/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2fe344090bcdfc..57fbb8f123a5ac 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Added a new `LanguageChooser` component ([#64686](https://github.com/WordPress/gutenberg/pull/64686)). + ### Enhancements - `UnitControl`: Update unit select styles ([#64712](https://github.com/WordPress/gutenberg/pull/64712)). From f91e7be0571ac5d7f64dd0367922ac96b87b187e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 26 Aug 2024 18:37:02 +0200 Subject: [PATCH 14/39] Move back to its own package --- lib/client-assets.php | 9 ++++ package-lock.json | 33 ++++++++++++++ package.json | 1 + packages/components/src/index.ts | 1 - packages/language-chooser/.npmrc | 1 + packages/language-chooser/CHANGELOG.md | 7 +++ packages/language-chooser/README.md | 23 ++++++++++ packages/language-chooser/package.json | 45 +++++++++++++++++++ packages/language-chooser/src/index.ts | 1 + .../src/language-chooser/active-controls.tsx | 6 +-- .../src/language-chooser/active-locales.tsx | 0 .../language-chooser/inactive-controls.tsx | 6 +-- .../inactive-locales-select.tsx | 2 +- .../src/language-chooser/inactive-locales.tsx | 0 .../src/language-chooser/index.tsx | 4 +- .../language-chooser/stories/index.story.tsx | 0 .../src/language-chooser/style.scss | 0 .../test/language-chooser.tsx | 0 .../src/language-chooser/test/reorder.ts | 0 .../src/language-chooser/types.ts | 0 .../src/language-chooser/utils.ts | 0 packages/language-chooser/tsconfig.json | 17 +++++++ storybook/main.js | 1 + storybook/package-styles/config.js | 7 +++ .../language-chooser-ltr.lazy.scss | 1 + .../language-chooser-rtl.lazy.scss | 1 + tsconfig.json | 1 + 27 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 packages/language-chooser/.npmrc create mode 100644 packages/language-chooser/CHANGELOG.md create mode 100644 packages/language-chooser/README.md create mode 100644 packages/language-chooser/package.json create mode 100644 packages/language-chooser/src/index.ts rename packages/{components => language-chooser}/src/language-chooser/active-controls.tsx (97%) rename packages/{components => language-chooser}/src/language-chooser/active-locales.tsx (100%) rename packages/{components => language-chooser}/src/language-chooser/inactive-controls.tsx (92%) rename packages/{components => language-chooser}/src/language-chooser/inactive-locales-select.tsx (96%) rename packages/{components => language-chooser}/src/language-chooser/inactive-locales.tsx (100%) rename packages/{components => language-chooser}/src/language-chooser/index.tsx (99%) rename packages/{components => language-chooser}/src/language-chooser/stories/index.story.tsx (100%) rename packages/{components => language-chooser}/src/language-chooser/style.scss (100%) rename packages/{components => language-chooser}/src/language-chooser/test/language-chooser.tsx (100%) rename packages/{components => language-chooser}/src/language-chooser/test/reorder.ts (100%) rename packages/{components => language-chooser}/src/language-chooser/types.ts (100%) rename packages/{components => language-chooser}/src/language-chooser/utils.ts (100%) create mode 100644 packages/language-chooser/tsconfig.json create mode 100644 storybook/package-styles/language-chooser-ltr.lazy.scss create mode 100644 storybook/package-styles/language-chooser-rtl.lazy.scss diff --git a/lib/client-assets.php b/lib/client-assets.php index 62e874d6b06c82..0f2d1d610905f2 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -477,6 +477,15 @@ function gutenberg_register_packages_styles( $styles ) { $version ); $styles->add_data( 'wp-preferences', 'rtl', 'replace' ); + + gutenberg_override_style( + $styles, + 'wp-language-chooser', + gutenberg_url( 'build/language-chooser/style.css' ), + array( 'wp-components' ), + $version + ); + $styles->add_data( 'wp-language-chooser', 'rtl', 'replace' ); } add_action( 'wp_default_styles', 'gutenberg_register_packages_styles' ); diff --git a/package-lock.json b/package-lock.json index fe6c1dfa150dba..06e49afc270068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/language-chooser": "file:packages/language-chooser", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", @@ -17083,6 +17084,10 @@ "resolved": "packages/keycodes", "link": true }, + "node_modules/@wordpress/language-chooser": { + "resolved": "packages/language-chooser", + "link": true + }, "node_modules/@wordpress/lazy-import": { "resolved": "packages/lazy-import", "link": true @@ -53902,6 +53907,23 @@ "npm": ">=8.19.2" } }, + "packages/language-chooser": { + "name": "@wordpress/language-chooser", + "version": "1.0.0-prerelease", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/keycodes": "file:../keycodes" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "packages/lazy-import": { "name": "@wordpress/lazy-import", "version": "2.6.0", @@ -68336,6 +68358,17 @@ "@wordpress/i18n": "file:../i18n" } }, + "@wordpress/language-chooser": { + "version": "file:packages/language-chooser", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/keycodes": "file:../keycodes" + } + }, "@wordpress/lazy-import": { "version": "file:packages/lazy-import", "requires": { diff --git a/package.json b/package.json index 7349e60eb4c210..e2ec48373af6e6 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/language-chooser": "file:packages/language-chooser", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 1b33f5392d4988..6483e34dc222a8 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -104,7 +104,6 @@ export { default as __experimentalInputControl } from './input-control'; export { default as __experimentalInputControlPrefixWrapper } from './input-control/input-prefix-wrapper'; export { default as __experimentalInputControlSuffixWrapper } from './input-control/input-suffix-wrapper'; export { default as KeyboardShortcuts } from './keyboard-shortcuts'; -export { default as LanguageChooser } from './language-chooser'; export { default as MenuGroup } from './menu-group'; export { default as MenuItem } from './menu-item'; export { default as MenuItemsChoice } from './menu-items-choice'; diff --git a/packages/language-chooser/.npmrc b/packages/language-chooser/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/language-chooser/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/language-chooser/CHANGELOG.md b/packages/language-chooser/CHANGELOG.md new file mode 100644 index 00000000000000..691cb6760320f4 --- /dev/null +++ b/packages/language-chooser/CHANGELOG.md @@ -0,0 +1,7 @@ + + +## Unreleased + +### New Features + +- Initial public release. diff --git a/packages/language-chooser/README.md b/packages/language-chooser/README.md new file mode 100644 index 00000000000000..1eb0f1312807a7 --- /dev/null +++ b/packages/language-chooser/README.md @@ -0,0 +1,23 @@ +# Language Chooser + +Package used for rendering a UI component for choosing preferred languages. + +> This package is meant to be used only with WordPress core. Feel free to use it in your own project but please keep in mind that it might never get fully documented. + +## Installation + +Install the module + +```bash +npm install @wordpress/language-chooser --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._ + +## Contributing to this package + +This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. + +To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). + +

      Code is Poetry.

      diff --git a/packages/language-chooser/package.json b/packages/language-chooser/package.json new file mode 100644 index 00000000000000..943ec031648386 --- /dev/null +++ b/packages/language-chooser/package.json @@ -0,0 +1,45 @@ +{ + "name": "@wordpress/language-chooser", + "version": "1.0.0-prerelease", + "description": "Component for choosing multiple preferred languages.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "templates", + "reusable blocks" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/language-chooser/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/language-chooser" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "sideEffects": [ + "build-style/**", + "src/**/*.scss" + ], + "types": "build-types", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/keycodes": "file:../keycodes" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/language-chooser/src/index.ts b/packages/language-chooser/src/index.ts new file mode 100644 index 00000000000000..4ea4e9059f5e40 --- /dev/null +++ b/packages/language-chooser/src/index.ts @@ -0,0 +1 @@ +export { default as LanguageChooser } from './language-chooser'; diff --git a/packages/components/src/language-chooser/active-controls.tsx b/packages/language-chooser/src/language-chooser/active-controls.tsx similarity index 97% rename from packages/components/src/language-chooser/active-controls.tsx rename to packages/language-chooser/src/language-chooser/active-controls.tsx index b24e7979852625..0f603fee5c399d 100644 --- a/packages/components/src/language-chooser/active-controls.tsx +++ b/packages/language-chooser/src/language-chooser/active-controls.tsx @@ -2,11 +2,7 @@ * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { Button } from '../button'; +import { Button } from '@wordpress/components'; interface ActiveControlsProps { onMoveUp: () => void; diff --git a/packages/components/src/language-chooser/active-locales.tsx b/packages/language-chooser/src/language-chooser/active-locales.tsx similarity index 100% rename from packages/components/src/language-chooser/active-locales.tsx rename to packages/language-chooser/src/language-chooser/active-locales.tsx diff --git a/packages/components/src/language-chooser/inactive-controls.tsx b/packages/language-chooser/src/language-chooser/inactive-controls.tsx similarity index 92% rename from packages/components/src/language-chooser/inactive-controls.tsx rename to packages/language-chooser/src/language-chooser/inactive-controls.tsx index cbfa6fc22eb697..1dfab239727450 100644 --- a/packages/components/src/language-chooser/inactive-controls.tsx +++ b/packages/language-chooser/src/language-chooser/inactive-controls.tsx @@ -3,11 +3,7 @@ */ import { _x, sprintf } from '@wordpress/i18n'; import { shortcutAriaLabel, displayShortcut } from '@wordpress/keycodes'; - -/** - * Internal dependencies - */ -import { Button } from '../button'; +import { Button } from '@wordpress/components'; interface InactiveControlsProps { disabled: boolean; diff --git a/packages/components/src/language-chooser/inactive-locales-select.tsx b/packages/language-chooser/src/language-chooser/inactive-locales-select.tsx similarity index 96% rename from packages/components/src/language-chooser/inactive-locales-select.tsx rename to packages/language-chooser/src/language-chooser/inactive-locales-select.tsx index 073d730db61a83..72c959a69a0452 100644 --- a/packages/components/src/language-chooser/inactive-locales-select.tsx +++ b/packages/language-chooser/src/language-chooser/inactive-locales-select.tsx @@ -2,11 +2,11 @@ * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; +import { SelectControl } from '@wordpress/components'; /** * Internal dependencies */ -import { SelectControl } from '../select-control'; import type { Language, Locale } from './types'; interface InactiveLocalesSelectProps { diff --git a/packages/components/src/language-chooser/inactive-locales.tsx b/packages/language-chooser/src/language-chooser/inactive-locales.tsx similarity index 100% rename from packages/components/src/language-chooser/inactive-locales.tsx rename to packages/language-chooser/src/language-chooser/inactive-locales.tsx diff --git a/packages/components/src/language-chooser/index.tsx b/packages/language-chooser/src/language-chooser/index.tsx similarity index 99% rename from packages/components/src/language-chooser/index.tsx rename to packages/language-chooser/src/language-chooser/index.tsx index e21c7bbb549ba1..4db45d1cef6a16 100644 --- a/packages/components/src/language-chooser/index.tsx +++ b/packages/language-chooser/src/language-chooser/index.tsx @@ -9,16 +9,16 @@ import type { KeyboardEvent } from 'react'; import { useEffect, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; +import { Notice } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ -import Notice from '../notice'; import ActiveLocales from './active-locales'; import InactiveLocales from './inactive-locales'; import type { Language } from './types'; import { reorder } from './utils'; -import { useInstanceId } from '@wordpress/compose'; function MissingTranslationsNotice() { return ( diff --git a/packages/components/src/language-chooser/stories/index.story.tsx b/packages/language-chooser/src/language-chooser/stories/index.story.tsx similarity index 100% rename from packages/components/src/language-chooser/stories/index.story.tsx rename to packages/language-chooser/src/language-chooser/stories/index.story.tsx diff --git a/packages/components/src/language-chooser/style.scss b/packages/language-chooser/src/language-chooser/style.scss similarity index 100% rename from packages/components/src/language-chooser/style.scss rename to packages/language-chooser/src/language-chooser/style.scss diff --git a/packages/components/src/language-chooser/test/language-chooser.tsx b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx similarity index 100% rename from packages/components/src/language-chooser/test/language-chooser.tsx rename to packages/language-chooser/src/language-chooser/test/language-chooser.tsx diff --git a/packages/components/src/language-chooser/test/reorder.ts b/packages/language-chooser/src/language-chooser/test/reorder.ts similarity index 100% rename from packages/components/src/language-chooser/test/reorder.ts rename to packages/language-chooser/src/language-chooser/test/reorder.ts diff --git a/packages/components/src/language-chooser/types.ts b/packages/language-chooser/src/language-chooser/types.ts similarity index 100% rename from packages/components/src/language-chooser/types.ts rename to packages/language-chooser/src/language-chooser/types.ts diff --git a/packages/components/src/language-chooser/utils.ts b/packages/language-chooser/src/language-chooser/utils.ts similarity index 100% rename from packages/components/src/language-chooser/utils.ts rename to packages/language-chooser/src/language-chooser/utils.ts diff --git a/packages/language-chooser/tsconfig.json b/packages/language-chooser/tsconfig.json new file mode 100644 index 00000000000000..051913a3043d32 --- /dev/null +++ b/packages/language-chooser/tsconfig.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "references": [ + { "path": "../a11y" }, + { "path": "../components" }, + { "path": "../compose" }, + { "path": "../element" }, + { "path": "../i18n" }, + { "path": "../keycodes" } + ], + "include": [ "src/**/*" ] +} diff --git a/storybook/main.js b/storybook/main.js index 66e951b26b1de6..fa6d1a8bf9c6d0 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -34,6 +34,7 @@ const stories = [ '../packages/icons/src/**/stories/*.story.@(js|tsx|mdx)', '../packages/edit-site/src/**/stories/*.story.@(js|tsx|mdx)', '../packages/dataviews/src/**/stories/*.story.@(js|tsx|mdx)', + '../packages/language-chooser/src/**/stories/*.story.@(js|tsx|mdx)', ].filter( Boolean ); module.exports = { diff --git a/storybook/package-styles/config.js b/storybook/package-styles/config.js index 21215fcad5c21e..56db3d26abb479 100644 --- a/storybook/package-styles/config.js +++ b/storybook/package-styles/config.js @@ -13,6 +13,8 @@ import editSiteLtr from '../package-styles/edit-site-ltr.lazy.scss'; import editSiteRtl from '../package-styles/edit-site-rtl.lazy.scss'; import dataviewsLtr from '../package-styles/dataviews-ltr.lazy.scss'; import dataviewsRtl from '../package-styles/dataviews-rtl.lazy.scss'; +import languageChooserLtr from '../package-styles/language-chooser-ltr.lazy.scss'; +import languageChooserRtl from '../package-styles/language-chooser-rtl.lazy.scss'; /** * Stylesheets to lazy load when the story's context.componentId matches the @@ -58,6 +60,11 @@ const CONFIG = [ ltr: [ dataviewsLtr, componentsLtr ], rtl: [ dataviewsRtl, componentsRtl ], }, + { + componentIdMatcher: /^components-language-chooser/, + ltr: [ languageChooserLtr, componentsLtr ], + rtl: [ languageChooserRtl, componentsRtl ], + }, ]; export default CONFIG; diff --git a/storybook/package-styles/language-chooser-ltr.lazy.scss b/storybook/package-styles/language-chooser-ltr.lazy.scss new file mode 100644 index 00000000000000..60ba809ad75b78 --- /dev/null +++ b/storybook/package-styles/language-chooser-ltr.lazy.scss @@ -0,0 +1 @@ +@import "../../packages/language-chooser/build-style/style"; diff --git a/storybook/package-styles/language-chooser-rtl.lazy.scss b/storybook/package-styles/language-chooser-rtl.lazy.scss new file mode 100644 index 00000000000000..256fd0b74ef9b6 --- /dev/null +++ b/storybook/package-styles/language-chooser-rtl.lazy.scss @@ -0,0 +1 @@ +@import "../../packages/language-chooser/build-style/style-rtl"; diff --git a/tsconfig.json b/tsconfig.json index cf986ddbee72bf..5fb787ba0f10f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ { "path": "packages/interactivity-router" }, { "path": "packages/is-shallow-equal" }, { "path": "packages/keycodes" }, + { "path": "packages/language-chooser" }, { "path": "packages/lazy-import" }, { "path": "packages/notices" }, { "path": "packages/plugins" }, From 2b86282bf4a6a7acbff7fcb18c145995e3ca73ab Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 26 Aug 2024 18:38:22 +0200 Subject: [PATCH 15/39] Undo changelog entry --- packages/components/CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8c31ccde886a91..44536df98b5815 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,10 +2,6 @@ ## Unreleased -### New Features - -- Added a new `LanguageChooser` component ([#64686](https://github.com/WordPress/gutenberg/pull/64686)). - ### Deprecations - Deprecate `replace` from the options accepted by `useNavigator().goTo()` ([#64675](https://github.com/WordPress/gutenberg/pull/64675)). From d4c35a92e6bad3567aeecdd3c53b80f84abf7680 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 26 Aug 2024 18:38:28 +0200 Subject: [PATCH 16/39] Update lock file --- package-lock.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 06e49afc270068..f0c8bf469d7bb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53915,6 +53915,7 @@ "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/keycodes": "file:../keycodes" @@ -68364,6 +68365,7 @@ "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/keycodes": "file:../keycodes" From 525a3d235d9aa187e8d2157802c010aa8c92c664 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 26 Aug 2024 18:39:54 +0200 Subject: [PATCH 17/39] Und style change --- packages/components/src/style.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 3c22a382c4f530..70317f4a2d0e0b 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -31,7 +31,6 @@ @import "./form-token-field/style.scss"; @import "./guide/style.scss"; @import "./higher-order/navigate-regions/style.scss"; -@import "./language-chooser/style.scss"; @import "./menu-group/style.scss"; @import "./menu-item/style.scss"; @import "./menu-items-choice/style.scss"; From 2d91af3362a747d55d769e5b5c4217df0c891629 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 26 Aug 2024 19:21:30 +0200 Subject: [PATCH 18/39] Update docs manifest --- docs/manifest.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index e4eba19d99fa29..09af61c56fe5b0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1781,6 +1781,12 @@ "markdown_source": "../packages/keycodes/README.md", "parent": "packages" }, + { + "title": "@wordpress/language-chooser", + "slug": "packages-language-chooser", + "markdown_source": "../packages/language-chooser/README.md", + "parent": "packages" + }, { "title": "@wordpress/lazy-import", "slug": "packages-lazy-import", From 35eceebee71b0cb29eb6a075a9c3f6845ad4aa9d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 27 Aug 2024 14:07:19 +0200 Subject: [PATCH 19/39] Pass input name as prop --- .../src/language-chooser/index.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/language-chooser/src/language-chooser/index.tsx b/packages/language-chooser/src/language-chooser/index.tsx index 4db45d1cef6a16..ab56165710cfe7 100644 --- a/packages/language-chooser/src/language-chooser/index.tsx +++ b/packages/language-chooser/src/language-chooser/index.tsx @@ -32,15 +32,19 @@ function MissingTranslationsNotice() { interface HiddenFormFieldProps { preferredLanguages: Language[]; + inputName: string; } -function HiddenFormField( { preferredLanguages }: HiddenFormFieldProps ) { +function HiddenFormField( { + preferredLanguages, + inputName, +}: HiddenFormFieldProps ) { const value = preferredLanguages .filter( ( language ) => Boolean( language ) ) .map( ( { locale } ) => locale ) .join( ',' ); - return ; + return ; } interface LanguageChooserProps { @@ -48,6 +52,7 @@ interface LanguageChooserProps { preferredLanguages: Language[]; hasMissingTranslations?: boolean; showOptionSiteDefault?: boolean; + inputName?: string; } function LanguageChooser( props: LanguageChooserProps ) { @@ -55,6 +60,7 @@ function LanguageChooser( props: LanguageChooserProps ) { allLanguages, hasMissingTranslations = false, showOptionSiteDefault = false, + inputName, } = props; const [ languages, setLanguages ] = useState< Language[] >( @@ -292,7 +298,10 @@ function LanguageChooser( props: LanguageChooserProps ) { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
      - +

      { __( 'Choose languages for displaying WordPress in, in order of preference.' From fd761aa6b537599a5c0c244ec903a5e574d531de Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 27 Aug 2024 14:40:35 +0200 Subject: [PATCH 20/39] Update test --- .../src/language-chooser/test/language-chooser.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/language-chooser/src/language-chooser/test/language-chooser.tsx b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx index cfd61ff4b17295..f1d312b25b51bd 100644 --- a/packages/language-chooser/src/language-chooser/test/language-chooser.tsx +++ b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx @@ -139,6 +139,7 @@ describe( 'LanguageChooser', () => { ); From 773381beb1c095e1ed831cf3c65323540e5e3028 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 28 Aug 2024 20:44:05 +0200 Subject: [PATCH 21/39] Fix storybook title --- .../src/language-chooser/stories/index.story.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-chooser/src/language-chooser/stories/index.story.tsx b/packages/language-chooser/src/language-chooser/stories/index.story.tsx index 4791c5298e4d5c..0fdc4158cd7861 100644 --- a/packages/language-chooser/src/language-chooser/stories/index.story.tsx +++ b/packages/language-chooser/src/language-chooser/stories/index.story.tsx @@ -10,7 +10,7 @@ import LanguageChooser from '../'; import type { Language } from '../types'; const meta: Meta< typeof LanguageChooser > = { - title: 'Components/Language Chooser', + title: 'Language Chooser/Language Chooser', component: LanguageChooser, parameters: { argTypes: { From 469fa311d62c1c5e03c66807b1489701c452ceae Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 28 Aug 2024 21:02:10 +0200 Subject: [PATCH 22/39] Fix css class names and file location --- .../src/language-chooser/active-controls.tsx | 2 +- .../src/language-chooser/active-locales.tsx | 10 +-- .../language-chooser/inactive-controls.tsx | 2 +- .../src/language-chooser/inactive-locales.tsx | 4 +- .../src/language-chooser/index.tsx | 7 +- .../language-chooser/stories/index.story.tsx | 87 +++++++++---------- .../src/{language-chooser => }/style.scss | 40 ++++----- 7 files changed, 73 insertions(+), 79 deletions(-) rename packages/language-chooser/src/{language-chooser => }/style.scss (53%) diff --git a/packages/language-chooser/src/language-chooser/active-controls.tsx b/packages/language-chooser/src/language-chooser/active-controls.tsx index 0f603fee5c399d..f3f4a0f5c0373a 100644 --- a/packages/language-chooser/src/language-chooser/active-controls.tsx +++ b/packages/language-chooser/src/language-chooser/active-controls.tsx @@ -21,7 +21,7 @@ function ActiveControls( { isRemoveDisabled, }: ActiveControlsProps ) { return ( -

      +
      • -
      • -
      • - -
      • -
      • - -
      • -
      -
      + + + + + ); } diff --git a/packages/language-chooser/src/style.scss b/packages/language-chooser/src/style.scss index 4b27de248c8aae..740c14d20e6595 100644 --- a/packages/language-chooser/src/style.scss +++ b/packages/language-chooser/src/style.scss @@ -24,10 +24,6 @@ $white: #fff; } } -.active-locales-controls .button { - margin: 0; -} - .language-chooser__active-locales-list, .language-chooser__inactive-locales-list { width: 25em; @@ -39,7 +35,7 @@ $white: #fff; height: 130px; overflow-y: scroll; list-style: none; - margin: 0 1em 0 0; + margin: 0 1em 1em 0; padding: 0; background: #fff; box-shadow: inset 0 1px 2px rgba($black, 0.07); @@ -47,6 +43,12 @@ $white: #fff; box-sizing: border-box; } +@media screen and (min-width: 510px) { + .language-chooser__active-locales-list { + margin-bottom: 0; + } +} + .language-chooser__inactive-locales-list { margin: 0 1em 0 0; } @@ -70,30 +72,30 @@ $white: #fff; font-size: 13px; } -.language-chooser__active-locales-controls { +.language-chooser .components-button-group { clear: both; } +/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @media screen and (min-width: 510px) { - - .language-chooser__active-locales-controls { + .language-chooser .components-button-group { clear: none; } } - -.language-chooser__active-locales-controls ul { - list-style: none; - margin: 0; - padding: 0; +.language-chooser .components-button-group .components-button + .components-button { + margin: 0 0 0 5px; } -.language-chooser__active-locales-controls li { - margin: 0; -} +/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ +@media screen and (min-width: 510px) { + .language-chooser .components-button-group .components-button { + display: block; + } -.language-chooser__active-locales-controls li + li { - margin-top: 5px; + .language-chooser .components-button-group .components-button + .components-button { + margin: 5px 0 0; + } } .language-chooser__active-locales-list li:hover { From e20ff2fb8265ad58c8a6d0799b94107bf998b322 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 30 Aug 2024 14:43:52 +0200 Subject: [PATCH 33/39] Remove old comment --- .../src/language-chooser/inactive-locales-select.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/language-chooser/src/language-chooser/inactive-locales-select.tsx b/packages/language-chooser/src/language-chooser/inactive-locales-select.tsx index 72c959a69a0452..ea3de1e9062f01 100644 --- a/packages/language-chooser/src/language-chooser/inactive-locales-select.tsx +++ b/packages/language-chooser/src/language-chooser/inactive-locales-select.tsx @@ -25,7 +25,6 @@ function InactiveLocalesSelect( { const hasItems = installedLanguages.length || availableLanguages.length; return ( - // eslint-disable-next-line no-restricted-syntax -- Do not want __next40pxDefaultSize in wp-admin Date: Fri, 30 Aug 2024 14:46:32 +0200 Subject: [PATCH 34/39] Use `` component --- .../src/language-chooser/active-locales.tsx | 6 +++--- packages/language-chooser/src/language-chooser/index.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/language-chooser/src/language-chooser/active-locales.tsx b/packages/language-chooser/src/language-chooser/active-locales.tsx index d8cbaca8c734a2..3bc4f95302dbe8 100644 --- a/packages/language-chooser/src/language-chooser/active-locales.tsx +++ b/packages/language-chooser/src/language-chooser/active-locales.tsx @@ -3,6 +3,7 @@ */ import { useLayoutEffect, useRef } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { __experimentalText as Text } from '@wordpress/components'; /** * Internal dependencies @@ -76,9 +77,8 @@ export function ActiveLocales( {
      { isEmpty && (
      - { __( 'Nothing set.' ) } -
      - { emptyMessage } + { __( 'Nothing set.' ) } + { emptyMessage }
      ) }
        -

        + { __( 'Choose languages for displaying WordPress in, in order of preference.' ) } -

        + Date: Fri, 30 Aug 2024 14:57:52 +0200 Subject: [PATCH 35/39] Fix before/after selection --- .../src/language-chooser/index.tsx | 27 +++++++++++++----- .../test/language-chooser.tsx | 28 +++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/language-chooser/src/language-chooser/index.tsx b/packages/language-chooser/src/language-chooser/index.tsx index 5de9d797380a70..8c821c5dd64fbd 100644 --- a/packages/language-chooser/src/language-chooser/index.tsx +++ b/packages/language-chooser/src/language-chooser/index.tsx @@ -97,6 +97,10 @@ function LanguageChooser( props: LanguageChooserProps ) { languages[ languages.length - 1 ]?.locale === activeLanguage?.locale; const isRemoveDisabled = ! activeLanguage; + const activeLanguageIndex = languages.findIndex( + ( { locale } ) => locale === activeLanguage?.locale + ); + const onAdd = () => { onAddLanguage( inactiveLanguage ); @@ -130,12 +134,9 @@ function LanguageChooser( props: LanguageChooserProps ) { }; const onRemove = () => { - const foundIndex = languages.findIndex( - ( { locale } ) => locale === activeLanguage?.locale - ); - setActiveLanguage( - languages[ foundIndex + 1 ] || languages[ foundIndex - 1 ] + languages[ activeLanguageIndex + 1 ] || + languages[ activeLanguageIndex - 1 ] ); setLanguages( ( prevLanguages ) => @@ -203,14 +204,26 @@ function LanguageChooser( props: LanguageChooserProps ) { // Move item up. case 'ArrowUp': if ( ! isMoveUpDisabled ) { - onMoveUp(); + if ( event.altKey ) { + onMoveUp(); + } else { + setActiveLanguage( + languages[ activeLanguageIndex - 1 ] + ); + } event.preventDefault(); } break; // Move item down. case 'ArrowDown': if ( ! isMoveDownDisabled ) { - onMoveDown(); + if ( event.altKey ) { + onMoveDown(); + } else { + setActiveLanguage( + languages[ activeLanguageIndex + 1 ] + ); + } event.preventDefault(); } break; diff --git a/packages/language-chooser/src/language-chooser/test/language-chooser.tsx b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx index 9acf39923df1a7..ee7924a31e7a3e 100644 --- a/packages/language-chooser/src/language-chooser/test/language-chooser.tsx +++ b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx @@ -71,10 +71,25 @@ window.HTMLElement.prototype.scrollIntoView = scrollIntoView; * @see https://github.com/WordPress/gutenberg/issues/45777 */ +function selectPrevious() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'ArrowUp', + code: 'ArrowUp', + } ); +} + +function selectNext() { + fireEvent.keyDown( screen.getByRole( 'listbox' ), { + key: 'ArrowDown', + code: 'ArrowDown', + } ); +} + function moveUp() { fireEvent.keyDown( screen.getByRole( 'listbox' ), { key: 'ArrowUp', code: 'ArrowUp', + altKey: true, } ); } @@ -82,6 +97,7 @@ function moveDown() { fireEvent.keyDown( screen.getByRole( 'listbox' ), { key: 'ArrowDown', code: 'ArrowDown', + altKey: true, } ); } @@ -281,6 +297,18 @@ describe( 'LanguageChooser', () => { moveUp(); moveUp(); + selectNext(); + + expect( + screen.getByRole( 'option', { name: /Français/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + + selectPrevious(); + + expect( + screen.getByRole( 'option', { name: /Deutsch/ } ) + ).toHaveAttribute( 'aria-selected', 'true' ); + expect( screen.getByRole( 'button', { name: /Move down/ } ) ).toBeEnabled(); From bfa317056b9b45f0814b150726f42e09a8f8b0bd Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 30 Aug 2024 15:19:58 +0200 Subject: [PATCH 36/39] Expand translator comment --- .../language-chooser/src/language-chooser/active-locales.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/language-chooser/src/language-chooser/active-locales.tsx b/packages/language-chooser/src/language-chooser/active-locales.tsx index 3bc4f95302dbe8..4ea75809f6f4cf 100644 --- a/packages/language-chooser/src/language-chooser/active-locales.tsx +++ b/packages/language-chooser/src/language-chooser/active-locales.tsx @@ -64,12 +64,13 @@ export function ActiveLocales( { : 'language-chooser__active-locales-list'; let emptyMessage = sprintf( - /* translators: %s: English (United States) */ + /* translators: Used in language chooser, indicating fall back to the site's default language. %s: English (United States) */ __( 'Falling back to %s.' ), 'English (United States)' ); if ( showOptionSiteDefault ) { + /* translators: Used in language chooser, indicating fall back to the site's default language. */ emptyMessage = __( 'Falling back to Site Default.' ); } From 177313d40a70a6f35682cc9474387cd99324bbb5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 30 Aug 2024 15:32:57 +0200 Subject: [PATCH 37/39] Add comment --- packages/language-chooser/src/language-chooser/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/language-chooser/src/language-chooser/index.tsx b/packages/language-chooser/src/language-chooser/index.tsx index 8c821c5dd64fbd..5ca66051d87c27 100644 --- a/packages/language-chooser/src/language-chooser/index.tsx +++ b/packages/language-chooser/src/language-chooser/index.tsx @@ -261,6 +261,7 @@ function LanguageChooser( props: LanguageChooserProps ) { const instanceId = useInstanceId( LanguageChooser, 'language-chooser' ); return ( + // Legit use case as it's capturing events bubbling up from children who have shortcuts defined. // eslint-disable-next-line jsx-a11y/no-static-element-interactions
        From 71f8b6e794f9762e3652a5f49b63dfb16630de88 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 30 Aug 2024 15:39:43 +0200 Subject: [PATCH 38/39] `defaultSelectedLanguages` vs `selectedLanguages` --- .../src/language-chooser/index.tsx | 26 +++++++++++------ .../language-chooser/stories/index.story.tsx | 2 +- .../test/language-chooser.tsx | 28 +++++++++---------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/language-chooser/src/language-chooser/index.tsx b/packages/language-chooser/src/language-chooser/index.tsx index 5ca66051d87c27..c7c9fb78132bfe 100644 --- a/packages/language-chooser/src/language-chooser/index.tsx +++ b/packages/language-chooser/src/language-chooser/index.tsx @@ -32,7 +32,8 @@ function MissingTranslationsNotice() { interface LanguageChooserProps { allLanguages: Language[]; - selectedLanguages: Language[]; + defaultSelectedLanguages?: Language[]; + selectedLanguages?: Language[]; hasMissingTranslations?: boolean; showOptionSiteDefault?: boolean; onChange?: ( languages: Language[] ) => void; @@ -45,20 +46,27 @@ function LanguageChooser( props: LanguageChooserProps ) { showOptionSiteDefault = false, } = props; - const [ languages, _setLanguages ] = useState< Language[] >( - props.selectedLanguages - ); + const selectedLanguages = + props.selectedLanguages || props.defaultSelectedLanguages || []; + + const [ languages, _setLanguages ] = + useState< Language[] >( selectedLanguages ); function setLanguages( update: ( prev: Language[] ) => Language[] ) { - _setLanguages( ( prev ) => { - const newValues = update( prev ); + if ( props.selectedLanguages !== undefined ) { + const newValues = update( props.selectedLanguages ); props.onChange?.( newValues ); - return newValues; - } ); + } else { + _setLanguages( ( prev ) => { + const newValues = update( prev ); + props.onChange?.( newValues ); + return newValues; + } ); + } } const [ activeLanguage, setActiveLanguage ] = useState< Language >( - props.selectedLanguages[ 0 ] + selectedLanguages[ 0 ] ); const inactiveLocales = allLanguages.filter( diff --git a/packages/language-chooser/src/language-chooser/stories/index.story.tsx b/packages/language-chooser/src/language-chooser/stories/index.story.tsx index eaec133d9ccb2d..1651095ab004b7 100644 --- a/packages/language-chooser/src/language-chooser/stories/index.story.tsx +++ b/packages/language-chooser/src/language-chooser/stories/index.story.tsx @@ -74,7 +74,7 @@ const it_IT: Language = { export const Default: StoryObj< typeof LanguageChooser > = { args: { - selectedLanguages: [ de_DE, fr_FR ], + defaultSelectedLanguages: [ de_DE, fr_FR ], allLanguages: [ de_DE, de_CH, it_IT, en_GB, fr_FR, en_US ], }, }; diff --git a/packages/language-chooser/src/language-chooser/test/language-chooser.tsx b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx index ee7924a31e7a3e..ab776a6bc8428d 100644 --- a/packages/language-chooser/src/language-chooser/test/language-chooser.tsx +++ b/packages/language-chooser/src/language-chooser/test/language-chooser.tsx @@ -139,7 +139,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -154,7 +154,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -188,7 +188,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -228,7 +228,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -385,7 +385,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -408,7 +408,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -429,7 +429,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -444,7 +444,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -466,7 +466,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -489,7 +489,7 @@ describe( 'LanguageChooser', () => { render( ); expect( screen.queryByText( /Falling back/ ) ).not.toBeInTheDocument(); @@ -508,7 +508,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -529,7 +529,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -552,7 +552,7 @@ describe( 'LanguageChooser', () => { render( ); @@ -569,7 +569,7 @@ describe( 'LanguageChooser', () => { render( ); From 38f633139d3d67331193b62a5e3232894a23d63d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 13 Sep 2024 15:11:49 +0200 Subject: [PATCH 39/39] Disable storybook keyboard shortcuts --- .../language-chooser/stories/index.story.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/language-chooser/src/language-chooser/stories/index.story.tsx b/packages/language-chooser/src/language-chooser/stories/index.story.tsx index 1651095ab004b7..77abd6bf782661 100644 --- a/packages/language-chooser/src/language-chooser/stories/index.story.tsx +++ b/packages/language-chooser/src/language-chooser/stories/index.story.tsx @@ -2,6 +2,7 @@ * External dependencies */ import type { Meta, StoryObj } from '@storybook/react'; +import type { ComponentProps, KeyboardEvent } from 'react'; /** * Internal dependencies @@ -9,9 +10,26 @@ import type { Meta, StoryObj } from '@storybook/react'; import LanguageChooser from '../'; import type { Language } from '../types'; +function Component( props: ComponentProps< typeof LanguageChooser > ) { + const onKeyDown = ( evt: KeyboardEvent< HTMLElement > ) => { + evt.stopPropagation(); + }; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
        + +

        + Note: Typical storybook keyboard shortcuts are disabled for this + story because they clash with the ones used by the component. +

        +
        + ); +} + const meta: Meta< typeof LanguageChooser > = { title: 'Language Chooser/Language Chooser', - component: LanguageChooser, + component: Component, parameters: { argTypes: { hasMissingTranslations: {