diff --git a/assets/src/stories-editor/components/font-family-picker/autocomplete.js b/assets/src/stories-editor/components/font-family-picker/autocomplete.js new file mode 100644 index 00000000000..b8398ee8bee --- /dev/null +++ b/assets/src/stories-editor/components/font-family-picker/autocomplete.js @@ -0,0 +1,169 @@ +/** + * External dependencies + */ +import OriginalAutocomplete from 'accessible-autocomplete/react'; +/** + * WordPress dependencies + */ +import { + IconButton, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Status from './status'; + +class Autocomplete extends OriginalAutocomplete { + /** + * Overrides default method to prevent an issue with + * scrollbars appearing inadvertently. + */ + handleInputBlur() {} + + handleClearClick() { + this.state.query = ''; + this.forceUpdate(); + this.props.onConfirm( null ); + } + + /** + * Override render method, to add clear font button. + * + */ + render() { + const { + cssNamespace, + displayMenu, + id, + minLength, + name, + placeholder, + required, + tNoResults, + tStatusQueryTooShort, + tStatusSelectedOption, + tStatusResults, + } = this.props; + const { focused, hovered, menuOpen, options, query, selected } = this.state; + const autoselect = this.hasAutoselect(); + + const inputFocused = focused === -1; + const noOptionsAvailable = options.length === 0; + const queryNotEmpty = query.length !== 0; + const queryLongEnough = query.length >= minLength; + const showNoOptionsFound = this.props.showNoOptionsFound && + inputFocused && noOptionsAvailable && queryNotEmpty && queryLongEnough; + + const wrapperClassName = `${ cssNamespace }__wrapper`; + + const inputClassName = `${ cssNamespace }__input`; + const componentIsFocused = focused !== null; + const inputModifierFocused = componentIsFocused ? ` ${ inputClassName }--focused` : ''; + const inputModifierType = this.props.showAllValues ? ` ${ inputClassName }--show-all-values` : ` ${ inputClassName }--default`; + const optionFocused = focused !== -1 && focused !== null; + + const menuClassName = `${ cssNamespace }__menu`; + const menuModifierDisplayMenu = `${ menuClassName }--${ displayMenu }`; + const menuIsVisible = menuOpen || showNoOptionsFound; + const menuModifierVisibility = `${ menuClassName }--${ ( menuIsVisible ) ? 'visible' : 'hidden' }`; + + const optionClassName = `${ cssNamespace }__option`; + + const hintClassName = `${ cssNamespace }__hint`; + const selectedOptionText = this.templateInputValue( options[ selected ] ); + const optionBeginsWithQuery = selectedOptionText && + selectedOptionText.toLowerCase().indexOf( query.toLowerCase() ) === 0; + const hintValue = ( optionBeginsWithQuery && autoselect ) ? + query + selectedOptionText.substr( query.length ) : + ''; + const showHint = hintValue; + + return ( +
+ + { showHint && ( + + ) } + + this.handleInputClick( event ) } + onBlur={ this.handleInputBlur } + onChange={ this.handleInputChange } + onFocus={ this.handleInputFocus } + name={ name } + placeholder={ placeholder } + ref={ ( inputElement ) => { + this.elementReferences[ -1 ] = inputElement; + } } + type="text" + required={ required } + value={ query } + /> + { query && ! menuOpen && queryLongEnough && ( + + this.handleClearClick( event ) } + className="autocomplete__icon" + /> + ) } + +
    this.handleListMouseLeave( event ) } + id={ `${ id }__listbox` } + role="listbox" + > + { options.map( ( option, index ) => { + const showFocused = focused === -1 ? selected === index : focused === index; + const optionModifierFocused = showFocused && hovered === null ? ` ${ optionClassName }--focused` : ''; + const optionModifierOdd = ( index % 2 ) ? ` ${ optionClassName }--odd` : ''; + + return ( +
  • this.handleOptionBlur( event, index ) } + onClick={ ( event ) => this.handleOptionClick( event, index ) } + onKeyDown={ ( event ) => this.handleOptionClick( event, index ) } + onMouseEnter={ ( event ) => this.handleOptionMouseEnter( event, index ) } + ref={ ( optionEl ) => { + this.elementReferences[ index ] = optionEl; + } } + role="option" + tabIndex="-1" + /> + ); + } ) } + + { showNoOptionsFound && ( +
  • { tNoResults() }
  • + ) } +
+
+ ); + } +} + +export default Autocomplete; diff --git a/assets/src/stories-editor/components/font-family-picker/edit.css b/assets/src/stories-editor/components/font-family-picker/edit.css new file mode 100644 index 00000000000..8e047d3776b --- /dev/null +++ b/assets/src/stories-editor/components/font-family-picker/edit.css @@ -0,0 +1,37 @@ +.autocomplete__wrapper .autocomplete__menu { + border-color: #007cba; + width: 101%; + border-bottom-left-radius: 4px !important; + border-bottom-right-radius: 4px !important; + margin-top: -3px; + padding-top: 3px; + max-height: 200px; +} + +.autocomplete__icon { + position: absolute; + top: 2px; + right: 0; + padding: 4px; + box-shadow: none !important; + border: 0 none; +} + +.autocomplete__option { + border-bottom: 0 none; + padding: 6px 8px; + font-size: 13px; +} + +.autocomplete__option--focused, +.autocomplete__option:hover { + background-color: #0071a1; + border-color: #0071a1; +} + +.autocomplete__wrapper .autocomplete__input.autocomplete__input--focused { + color: #191e23; + border-color: #007cba; + box-shadow: 0 0 0 1px #007cba; + outline: 2px solid transparent; +} diff --git a/assets/src/stories-editor/components/font-family-picker/index.js b/assets/src/stories-editor/components/font-family-picker/index.js index 4fc9cc876fa..82a836f92f5 100644 --- a/assets/src/stories-editor/components/font-family-picker/index.js +++ b/assets/src/stories-editor/components/font-family-picker/index.js @@ -6,13 +6,17 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { BaseControl } from '@wordpress/components'; +import { withInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ -import { AMP_STORY_FONT_IMAGES } from '../../constants'; -import { PreviewPicker } from '../'; +import { maybeEnqueueFontStyle } from '../../helpers'; +import Autocomplete from './autocomplete'; +import 'accessible-autocomplete/src/autocomplete.css'; +import './edit.css'; /** * Font Family Picker component. @@ -23,45 +27,76 @@ function FontFamilyPicker( { fonts = [], onChange = () => {}, value = '', + instanceId, } ) { - const defaultOption = { - value: '', - label: __( 'None', 'amp' ), + const results = fonts; + const suggest = ( query, populateResults ) => { + const searchResults = query ? + results.filter( ( result ) => result.name.toLowerCase().indexOf( query.toLowerCase() ) !== -1 ) : + []; + populateResults( searchResults ); }; - const options = fonts.map( ( font ) => ( { - value: font.name, - label: font.name, - } ) ); + const suggestionTemplate = ( font ) => { + maybeEnqueueFontStyle( font.name ); + const fallbacks = ( font.fallbacks ) ? ', ' + font.fallbacks.join( ', ' ) : ''; + return font && `${ font.name }`; + }; + + const inputValueTemplate = ( result ) => { + return result && result.name; + }; - const fontLabel = ( familyName ) => AMP_STORY_FONT_IMAGES[ familyName ] ? - AMP_STORY_FONT_IMAGES[ familyName ]( { height: 13 } ) : - familyName; + const id = `amp-stories-font-family-picker-${ instanceId }`; return ( - onChange( '' === selectedValue ? undefined : selectedValue ) } + { - return sprintf( - /* translators: %s: font name */ - __( 'Font Family: %s', 'amp' ), - currentOption.label - ); - } } - renderToggle={ ( { label } ) => fontLabel( label ) } - renderOption={ ( option ) => { - return ( - - { fontLabel( option.label ) } - - ); - } } - /> + id={ id } + help={ __( 'Type to search for fonts', 'amp' ) } + > + '' } + preserveNullOptions={ true } + placeholder={ __( 'None', 'amp' ) } + displayMenu="overlay" + tNoResults={ () => + __( 'No font found', 'amp' ) + } + tStatusQueryTooShort={ ( minQueryLength ) => + // translators: %d: the number characters required to initiate a font search. + sprintf( __( 'Type in %s or more characters for results', 'amp' ), minQueryLength ) + } + tStatusSelectedOption={ ( selectedOption, length ) => + // translators: 1: the index of the selected result. 2: The total number of results. + sprintf( __( '%s (1 of %s) is selected', 'amp' ), selectedOption, length ) + } + tStatusResults={ ( length, contentSelectedOption ) => { + return ( + sprintf( + // translators: %d: The total number of results. + _n( '%d font is available. %s', '%d fonts are available. %s', length, 'amp' ), + length, + contentSelectedOption + ) + ); + } } + /> + ); } @@ -72,6 +107,7 @@ FontFamilyPicker.propTypes = { label: PropTypes.string, } ) ), onChange: PropTypes.func, + instanceId: PropTypes.number.isRequired, }; -export default FontFamilyPicker; +export default withInstanceId( FontFamilyPicker ); diff --git a/assets/src/stories-editor/components/font-family-picker/status.js b/assets/src/stories-editor/components/font-family-picker/status.js new file mode 100644 index 00000000000..fbaad0d55d0 --- /dev/null +++ b/assets/src/stories-editor/components/font-family-picker/status.js @@ -0,0 +1,68 @@ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +const Status = ( { + length, + queryLength, + minQueryLength, + selectedOption, + selectedOptionIndex, + tQueryTooShort, + tNoResults, + tSelectedOption, + tResults, +} ) => { + const queryTooShort = queryLength < minQueryLength; + const noResults = length === 0; + + const contentSelectedOption = selectedOption ? + tSelectedOption( selectedOption, length, selectedOptionIndex ) : + ''; + + let content = null; + if ( queryTooShort ) { + content = tQueryTooShort( minQueryLength ); + } else if ( noResults ) { + content = tNoResults(); + } else { + content = tResults( length, contentSelectedOption ); + } + + return
+ { content } + { queryTooShort && ','.repeat( queryLength ) } +
; +}; + +Status.propTypes = { + length: PropTypes.number.isRequired, + queryLength: PropTypes.number.isRequired, + minQueryLength: PropTypes.number.isRequired, + selectedOption: PropTypes.func, + selectedOptionIndex: PropTypes.number, + tQueryTooShort: PropTypes.func.isRequired, + tNoResults: PropTypes.func.isRequired, + tSelectedOption: PropTypes.func.isRequired, + tResults: PropTypes.func.isRequired, +}; + +export default Status; diff --git a/assets/src/stories-editor/components/font-family-picker/test/__snapshots__/index.js.snap b/assets/src/stories-editor/components/font-family-picker/test/__snapshots__/index.js.snap deleted file mode 100644 index 92357fa2dc3..00000000000 --- a/assets/src/stories-editor/components/font-family-picker/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FontFamilyPicker should render a default button if no font is selected 1`] = ` -
-
- -
-
- -
-
-
-
-`; - -exports[`FontFamilyPicker should render the selected font name 1`] = ` -
-
- -
-
- -
-
-
-
-`; - -exports[`FontFamilyPicker should render the selected font svg preview 1`] = ` -
-
- -
-
- -
-
-
-
-`; diff --git a/assets/src/stories-editor/components/font-family-picker/test/index.js b/assets/src/stories-editor/components/font-family-picker/test/index.js deleted file mode 100644 index 08907347b67..00000000000 --- a/assets/src/stories-editor/components/font-family-picker/test/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { render } from 'enzyme'; - -/** - * Internal dependencies - */ -import FontFamilyPicker from '../'; - -const { ampStoriesFonts } = window; - -describe( 'FontFamilyPicker', () => { - it( 'should render a default button if no font is selected', () => { - const fontFamilyPicker = render( ); - expect( fontFamilyPicker ).toMatchSnapshot(); - } ); - - it( 'should render the selected font name', () => { - const fontFamilyPicker = render( ); - expect( fontFamilyPicker ).toMatchSnapshot(); - } ); - - it( 'should render the selected font svg preview', () => { - const fontFamilyPicker = render( ); - expect( fontFamilyPicker ).toMatchSnapshot(); - } ); -} ); diff --git a/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js b/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js index f5d51f2a960..f1fa7dbed6d 100644 --- a/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js +++ b/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js @@ -432,9 +432,13 @@ export default createHigherOrderComponent( { - maybeEnqueueFontStyle( value ); - setAttributes( { ampFontFamily: value } ); + onChange={ ( font ) => { + if ( ! font ) { + setAttributes( { ampFontFamily: null } ); + return; + } + maybeEnqueueFontStyle( font.name ); + setAttributes( { ampFontFamily: font.name } ); } } /> { + beforeAll( async () => { + await activateExperience( 'stories' ); + } ); + + afterAll( async () => { + await deactivateExperience( 'stories' ); + } ); + + beforeEach( async () => { + await createNewPost( { postType: 'amp_story' } ); + await page.waitForSelector( `.${ textBlockClass }.is-not-editing` ); + await selectBlockByClassName( textBlockClass ); + const textToWrite = 'Hello'; + + await page.click( `.${ textBlockClass }` ); + await page.keyboard.type( textToWrite ); + await page.$eval( '.block-editor-block-list__layout .block-editor-block-list__block .wp-block-amp-amp-story-text', ( node ) => node.textContent ); + + await page.click( `.${ fontPickerID }` ); + } ); + + it( 'should be able to search for ubuntu font', async () => { + await page.keyboard.type( 'Arimo' ); + + const nodes = await page.$x( + '//ul[contains(@class,"autocomplete__menu")]//li' + ); + expect( nodes ).toHaveLength( 1 ); + await expect( page ).toMatchElement( '#arimo-font' ); + } ); + + it( 'should be able to search for Arial font and get multi results', async () => { + await page.keyboard.type( 'pt sans' ); + + const nodes = await page.$x( + '//ul[contains(@class,"autocomplete__menu")]//li' + ); + expect( nodes ).toHaveLength( 3 ); + await expect( page ).toMatchElement( '#pt-sans-font' ); + await expect( page ).toMatchElement( '#pt-sans-narrow-font' ); + await expect( page ).toMatchElement( '#pt-sans-caption-font' ); + } ); + + it( 'should be able to search for none existing font', async () => { + await page.keyboard.type( 'Wibble' ); + expect( await page.evaluate( () => { + return document.querySelector( '.autocomplete__option--no-results' ).innerHTML; + } ) ).toContain( 'No font found' ); + } ); + + it( 'should be able to search for ubuntu font and select font', async () => { + await page.keyboard.type( 'Ubuntu' ); + + await page.waitForSelector( '.autocomplete__option' ); + await page.click( '.autocomplete__option' ); + const textBlockBefore = ( await getBlocksOnPage() )[ 0 ]; + expect( textBlockBefore.attributes.ampFontFamily ).toStrictEqual( 'Ubuntu' ); + } ); + + it( 'should be able to search for ubuntu font and select font with keyboard', async () => { + await page.keyboard.type( 'Ubuntu' ); + + await page.waitForSelector( '.autocomplete__option' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Enter' ); + const textBlockBefore = ( await getBlocksOnPage() )[ 0 ]; + expect( textBlockBefore.attributes.ampFontFamily ).toStrictEqual( 'Ubuntu' ); + } ); + + it( 'should be able to search for ubuntu font and remove font', async () => { + await page.keyboard.type( 'Ubuntu' ); + + await page.waitForSelector( '.autocomplete__option' ); + await page.click( '.autocomplete__option' ); + + await page.waitForSelector( '.autocomplete__icon' ); + await page.click( '.autocomplete__icon' ); + const textBlockBefore = ( await getBlocksOnPage() )[ 0 ]; + expect( textBlockBefore.attributes.ampFontFamily ).toBeNull(); + } ); + + it( 'should be able to search for ubuntu font and save post', async () => { + await page.keyboard.type( 'Ubuntu' ); + + await page.waitForSelector( '.autocomplete__option' ); + await page.click( '.autocomplete__option' ); + + await saveDraft(); + await page.reload(); + + const textBlockBefore = ( await getBlocksOnPage() )[ 0 ]; + expect( textBlockBefore.attributes.ampFontFamily ).toStrictEqual( 'Ubuntu' ); + } ); +} );