Replace deprecated wp.compose.withState with wp.element.useState (https://github.com/woocommerce/woocommerce-admin/pull/8338)

* Update search-list-control

* Update tag

* Update stories

* Add packages/components changelog
This commit is contained in:
Chi-Hsuan Huang 2022-02-23 09:35:50 +08:00 committed by GitHub
parent 1c2bc9843c
commit f052c3fed1
17 changed files with 450 additions and 393 deletions

View File

@ -1,5 +1,7 @@
# Unreleased # Unreleased
- Replace deprecated wp.compose.withState with wp.element.useState. #8338
# 9.0.0 # 9.0.0
- Update line-height of SelectControl label to avoid truncated descenders in some typefaces and zoom levels. #8186 - Update line-height of SelectControl label to avoid truncated descenders in some typefaces and zoom levels. #8186

View File

@ -2,23 +2,25 @@
* External dependencies * External dependencies
*/ */
import moment from 'moment'; import moment from 'moment';
import { withState } from '@wordpress/compose';
import { DatePicker, H, Section } from '@woocommerce/components'; import { DatePicker, H, Section } from '@woocommerce/components';
import { createElement } from '@wordpress/element'; import { useState } from '@wordpress/element';
const dateFormat = 'MM/DD/YYYY'; const dateFormat = 'MM/DD/YYYY';
const DatePickerExample = withState( { const DatePickerExample = () => {
after: null, const [ state, setState ] = useState( {
afterText: '', after: null,
before: null, afterText: '',
beforeText: '', before: null,
afterError: null, beforeText: '',
beforeError: null, afterError: null,
focusedInput: 'startDate', beforeError: null,
} )( ( { after, afterText, afterError, setState } ) => { focusedInput: 'startDate',
} );
const { after, afterText, afterError } = state;
function onDatePickerUpdate( { date, text, error } ) { function onDatePickerUpdate( { date, text, error } ) {
setState( { setState( {
...state,
after: date, after: date,
afterText: text, afterText: text,
afterError: error, afterError: error,
@ -40,7 +42,7 @@ const DatePickerExample = withState( {
</Section> </Section>
</div> </div>
); );
} ); };
export const Basic = () => <DatePickerExample />; export const Basic = () => <DatePickerExample />;

View File

@ -2,23 +2,29 @@
* External dependencies * External dependencies
*/ */
import moment from 'moment'; import moment from 'moment';
import { withState } from '@wordpress/compose';
import { DateRange, H, Section } from '@woocommerce/components'; import { DateRange, H, Section } from '@woocommerce/components';
import { createElement, Fragment } from '@wordpress/element'; import { useState } from '@wordpress/element';
const dateFormat = 'MM/DD/YYYY'; const dateFormat = 'MM/DD/YYYY';
const DateRangeExample = withState( { const DateRangeExample = () => {
after: null, const [ state, setState ] = useState( {
afterText: '', after: null,
before: null, afterText: '',
beforeText: '', before: null,
afterError: null, beforeText: '',
beforeError: null, afterError: null,
focusedInput: 'startDate', beforeError: null,
} )( ( { after, afterText, before, beforeText, focusedInput, setState } ) => { focusedInput: 'startDate',
} );
const { after, afterText, before, beforeText, focusedInput } = state;
function onRangeUpdate( update ) { function onRangeUpdate( update ) {
setState( update ); setState( {
...state,
...update,
} );
} }
return ( return (
@ -40,7 +46,7 @@ const DateRangeExample = withState( {
</Section> </Section>
</> </>
); );
} ); };
export const Basic = () => <DateRangeExample />; export const Basic = () => <DateRangeExample />;

View File

@ -1,47 +1,57 @@
/** /**
* External dependencies * External dependencies
*/ */
import { withState } from '@wordpress/compose'; import { Fragment, useState } from '@wordpress/element';
import { Fragment } from '@wordpress/element';
import { Icon } from '@wordpress/icons'; import { Icon } from '@wordpress/icons';
import CrossSmall from 'gridicons/dist/cross-small'; import CrossSmall from 'gridicons/dist/cross-small';
import { EllipsisMenu, MenuItem, MenuTitle } from '@woocommerce/components'; import { EllipsisMenu, MenuItem, MenuTitle } from '@woocommerce/components';
const ExampleEllipsisMenu = withState( { const ExampleEllipsisMenu = () => {
showCustomers: true, const [ { showCustomers, showOrders }, setState ] = useState( {
showOrders: true, showCustomers: true,
} )( ( { setState, showCustomers, showOrders } ) => ( showOrders: true,
<EllipsisMenu } );
label="Choose which analytics to display" return (
renderContent={ ( { onToggle } ) => ( <EllipsisMenu
<Fragment> label="Choose which analytics to display"
<MenuTitle>Display stats</MenuTitle> renderContent={ ( { onToggle } ) => (
<MenuItem <Fragment>
isCheckbox <MenuTitle>Display stats</MenuTitle>
isClickable <MenuItem
checked={ showCustomers } isCheckbox
onInvoke={ () => isClickable
setState( { showCustomers: ! showCustomers } ) checked={ showCustomers }
} onInvoke={ () =>
> setState( {
Show Customers showOrders,
</MenuItem> showCustomers: ! showCustomers,
<MenuItem } )
isCheckbox }
isClickable >
checked={ showOrders } Show Customers
onInvoke={ () => setState( { showOrders: ! showOrders } ) } </MenuItem>
> <MenuItem
Show Orders isCheckbox
</MenuItem> isClickable
<MenuItem isClickable onInvoke={ onToggle }> checked={ showOrders }
<Icon icon={ <CrossSmall /> } /> onInvoke={ () =>
Close Menu setState( {
</MenuItem> showCustomers,
</Fragment> showOrders: ! showOrders,
) } } )
/> }
) ); >
Show Orders
</MenuItem>
<MenuItem isClickable onInvoke={ onToggle }>
<Icon icon={ <CrossSmall /> } />
Close Menu
</MenuItem>
</Fragment>
) }
/>
);
};
export const Basic = () => <ExampleEllipsisMenu />; export const Basic = () => <ExampleEllipsisMenu />;

View File

@ -1,17 +1,19 @@
/** /**
* External dependencies * External dependencies
*/ */
import { withState } from '@wordpress/compose'; import { useState } from '@wordpress/element';
import { ImageUpload } from '@woocommerce/components'; import { ImageUpload } from '@woocommerce/components';
const ImageUploadExample = withState( { const ImageUploadExample = () => {
image: null, const [ image, setImage ] = useState( null );
} )( ( { setState, logo } ) => (
<ImageUpload return (
image={ logo } <ImageUpload
onChange={ ( image ) => setState( { logo: image } ) } image={ image }
/> onChange={ ( _image ) => setImage( _image ) }
) ); />
);
};
export const Basic = () => <ImageUploadExample />; export const Basic = () => <ImageUploadExample />;

View File

@ -8,8 +8,13 @@ import {
TextControl, TextControl,
withSpokenMessages, withSpokenMessages,
} from '@wordpress/components'; } from '@wordpress/components';
import { createElement, Component, Fragment } from '@wordpress/element'; import {
import { compose, withInstanceId, withState } from '@wordpress/compose'; createElement,
Fragment,
useState,
useEffect,
} from '@wordpress/element';
import { compose, withInstanceId } from '@wordpress/compose';
import { escapeRegExp, findIndex } from 'lodash'; import { escapeRegExp, findIndex } from 'lodash';
import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; import NoticeOutlineIcon from 'gridicons/dist/notice-outline';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -43,28 +48,33 @@ const defaultMessages = {
/** /**
* Component to display a searchable, selectable list of items. * Component to display a searchable, selectable list of items.
*
* @param {Object} props
*/ */
export class SearchListControl extends Component { export const SearchListControl = ( props ) => {
constructor() { const [ searchValue, setSearchValue ] = useState( props.search || '' );
super( ...arguments ); const {
isSingle,
isLoading,
onChange,
selected,
instanceId,
messages: propsMessages,
isCompact,
debouncedSpeak,
onSearch,
className = '',
} = props;
this.onSelect = this.onSelect.bind( this ); const messages = { ...defaultMessages, ...propsMessages };
this.onRemove = this.onRemove.bind( this );
this.onClear = this.onClear.bind( this );
this.isSelected = this.isSelected.bind( this );
this.defaultRenderItem = this.defaultRenderItem.bind( this );
this.renderList = this.renderList.bind( this );
}
componentDidUpdate( prevProps ) { useEffect( () => {
const { onSearch, search } = this.props; if ( typeof onSearch === 'function' ) {
if ( search !== prevProps.search && typeof onSearch === 'function' ) { onSearch( searchValue );
onSearch( search );
} }
} }, [ onSearch, searchValue ] );
onRemove( id ) { const onRemove = ( id ) => {
const { isSingle, onChange, selected } = this.props;
return () => { return () => {
if ( isSingle ) { if ( isSingle ) {
onChange( [] ); onChange( [] );
@ -75,13 +85,12 @@ export class SearchListControl extends Component {
...selected.slice( i + 1 ), ...selected.slice( i + 1 ),
] ); ] );
}; };
} };
onSelect( item ) { const onSelect = ( item ) => {
const { isSingle, onChange, selected } = this.props;
return () => { return () => {
if ( this.isSelected( item ) ) { if ( isSelected( item ) ) {
this.onRemove( item.id )(); onRemove( item.id )();
return; return;
} }
if ( isSingle ) { if ( isSingle ) {
@ -90,39 +99,32 @@ export class SearchListControl extends Component {
onChange( [ ...selected, item ] ); onChange( [ ...selected, item ] );
} }
}; };
} };
onClear() { const isSelected = ( item ) =>
this.props.onChange( [] ); findIndex( selected, { id: item.id } ) !== -1;
}
isSelected( item ) { const getFilteredList = ( list, search ) => {
return findIndex( this.props.selected, { id: item.id } ) !== -1; const { isHierarchical } = props;
}
getFilteredList( list, search ) {
const { isHierarchical } = this.props;
if ( ! search ) { if ( ! search ) {
return isHierarchical ? buildTermsTree( list ) : list; return isHierarchical ? buildTermsTree( list ) : list;
} }
const messages = { ...defaultMessages, ...this.props.messages };
const re = new RegExp( escapeRegExp( search ), 'i' ); const re = new RegExp( escapeRegExp( search ), 'i' );
this.props.debouncedSpeak( messages.updated ); debouncedSpeak( messages.updated );
const filteredList = list const filteredList = list
.map( ( item ) => ( re.test( item.name ) ? item : false ) ) .map( ( item ) => ( re.test( item.name ) ? item : false ) )
.filter( Boolean ); .filter( Boolean );
return isHierarchical return isHierarchical
? buildTermsTree( filteredList, list ) ? buildTermsTree( filteredList, list )
: filteredList; : filteredList;
} };
defaultRenderItem( args ) { const defaultRenderItem = ( args ) => {
return <SearchListItem { ...args } />; return <SearchListItem { ...args } />;
} };
renderList( list, depth = 0 ) { const renderList = ( list, depth = 0 ) => {
const { isSingle, search, instanceId } = this.props; const renderItem = props.renderItem || defaultRenderItem;
const renderItem = this.props.renderItem || this.defaultRenderItem;
if ( ! list ) { if ( ! list ) {
return null; return null;
} }
@ -132,23 +134,20 @@ export class SearchListControl extends Component {
<li> <li>
{ renderItem( { { renderItem( {
item, item,
isSelected: this.isSelected( item ), isSelected: isSelected( item ),
onSelect: this.onSelect, onSelect,
isSingle, isSingle,
search, search: searchValue,
depth, depth,
controlId: instanceId, controlId: instanceId,
} ) } } ) }
</li> </li>
{ this.renderList( item.children, depth + 1 ) } { renderList( item.children, depth + 1 ) }
</Fragment> </Fragment>
) ); ) );
} };
renderListSection() {
const { isLoading, search } = this.props;
const messages = { ...defaultMessages, ...this.props.messages };
const renderListSection = () => {
if ( isLoading ) { if ( isLoading ) {
return ( return (
<div className="woocommerce-search-list__list is-loading"> <div className="woocommerce-search-list__list is-loading">
@ -156,7 +155,7 @@ export class SearchListControl extends Component {
</div> </div>
); );
} }
const list = this.getFilteredList( this.props.list, search ); const list = getFilteredList( props.list, searchValue );
if ( ! list.length ) { if ( ! list.length ) {
return ( return (
@ -169,9 +168,9 @@ export class SearchListControl extends Component {
/> />
</span> </span>
<span className="woocommerce-search-list__not-found-text"> <span className="woocommerce-search-list__not-found-text">
{ search { searchValue
? // eslint-disable-next-line @wordpress/valid-sprintf ? // eslint-disable-next-line @wordpress/valid-sprintf
sprintf( messages.noResults, search ) sprintf( messages.noResults, searchValue )
: messages.noItems } : messages.noItems }
</span> </span>
</div> </div>
@ -180,15 +179,12 @@ export class SearchListControl extends Component {
return ( return (
<ul className="woocommerce-search-list__list"> <ul className="woocommerce-search-list__list">
{ this.renderList( list ) } { renderList( list ) }
</ul> </ul>
); );
} };
renderSelectedSection() {
const { isLoading, isSingle, selected } = this.props;
const messages = { ...defaultMessages, ...this.props.messages };
const renderSelectedSection = () => {
if ( isLoading || isSingle || ! selected ) { if ( isLoading || isSingle || ! selected ) {
return null; return null;
} }
@ -202,7 +198,7 @@ export class SearchListControl extends Component {
<Button <Button
isLink isLink
isDestructive isDestructive
onClick={ this.onClear } onClick={ onChange( [] ) }
aria-label={ messages.clear } aria-label={ messages.clear }
> >
{ __( 'Clear all', 'woocommerce-admin' ) } { __( 'Clear all', 'woocommerce-admin' ) }
@ -216,7 +212,7 @@ export class SearchListControl extends Component {
<Tag <Tag
label={ item.name } label={ item.name }
id={ item.id } id={ item.id }
remove={ this.onRemove } remove={ onRemove }
/> />
</li> </li>
) ) } ) ) }
@ -224,34 +220,29 @@ export class SearchListControl extends Component {
) : null } ) : null }
</div> </div>
); );
} };
render() { return (
const { className = '', isCompact, search, setState } = this.props; <div
const messages = { ...defaultMessages, ...this.props.messages }; className={ classnames( 'woocommerce-search-list', className, {
'is-compact': isCompact,
} ) }
>
{ renderSelectedSection() }
return ( <div className="woocommerce-search-list__search">
<div <TextControl
className={ classnames( 'woocommerce-search-list', className, { label={ messages.search }
'is-compact': isCompact, type="search"
} ) } value={ searchValue }
> onChange={ ( value ) => setSearchValue( value ) }
{ this.renderSelectedSection() } />
<div className="woocommerce-search-list__search">
<TextControl
label={ messages.search }
type="search"
value={ search }
onChange={ ( value ) => setState( { search: value } ) }
/>
</div>
{ this.renderListSection() }
</div> </div>
);
} { renderListSection() }
} </div>
);
};
SearchListControl.propTypes = { SearchListControl.propTypes = {
/** /**
@ -334,19 +325,12 @@ SearchListControl.propTypes = {
* The list of currently selected items. * The list of currently selected items.
*/ */
selected: PropTypes.array.isRequired, selected: PropTypes.array.isRequired,
// from withState
search: PropTypes.string,
setState: PropTypes.func,
// from withSpokenMessages // from withSpokenMessages
debouncedSpeak: PropTypes.func, debouncedSpeak: PropTypes.func,
// from withInstanceId // from withInstanceId
instanceId: PropTypes.number, instanceId: PropTypes.number,
}; };
export default compose( [ export default compose( [ withSpokenMessages, withInstanceId ] )(
withState( { SearchListControl
search: '', );
} ),
withSpokenMessages,
withInstanceId,
] )( SearchListControl );

View File

@ -3,13 +3,12 @@
*/ */
import { boolean } from '@storybook/addon-knobs'; import { boolean } from '@storybook/addon-knobs';
import { SearchListControl } from '@woocommerce/components'; import { SearchListControl } from '@woocommerce/components';
import { withState } from '@wordpress/compose'; import { useState } from '@wordpress/element';
import { createElement } from '@wordpress/element';
const SearchListControlExample = () => {
const [ selected, setSelected ] = useState( [] );
const [ loading, setLoading ] = useState( false );
const SearchListControlExample = withState( {
selected: [],
loading: false,
} )( ( { selected, loading, setState } ) => {
const showCount = boolean( 'Show count', false ); const showCount = boolean( 'Show count', false );
const isCompact = boolean( 'Compact', false ); const isCompact = boolean( 'Compact', false );
const isSingle = boolean( 'Single', false ); const isSingle = boolean( 'Single', false );
@ -30,7 +29,7 @@ const SearchListControlExample = withState( {
return ( return (
<div> <div>
<button onClick={ () => setState( { loading: ! loading } ) }> <button onClick={ () => setLoading( ! loading ) }>
Toggle loading state Toggle loading state
</button> </button>
<SearchListControl <SearchListControl
@ -38,12 +37,12 @@ const SearchListControlExample = withState( {
isCompact={ isCompact } isCompact={ isCompact }
isLoading={ loading } isLoading={ loading }
selected={ selected } selected={ selected }
onChange={ ( items ) => setState( { selected: items } ) } onChange={ ( items ) => setSelected( items ) }
isSingle={ isSingle } isSingle={ isSingle }
/> />
</div> </div>
); );
} ); };
export const Basic = () => <SearchListControlExample />; export const Basic = () => <SearchListControlExample />;

View File

@ -1,7 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { render } from '@testing-library/react'; import { render, fireEvent } from '@testing-library/react';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
@ -174,4 +174,24 @@ describe( 'SearchListControl', () => {
); );
expect( component ).toMatchSnapshot(); expect( component ).toMatchSnapshot();
} ); } );
test( 'should match options after changing search control', () => {
const { getByLabelText, getAllByText } = render(
<SearchListControl
instanceId={ 1 }
list={ list }
search=""
selected={ [] }
onChange={ noop }
debouncedSpeak={ noop }
/>
);
fireEvent.change( getByLabelText( 'Search for items' ), {
target: {
value: 'berry',
},
} );
expect( getAllByText( 'berry' ).length ).toBe( 2 );
} );
} ); } );

View File

@ -1,35 +1,37 @@
/** /**
* External dependencies * External dependencies
*/ */
import { withState } from '@wordpress/compose';
import { H, Search, Section } from '@woocommerce/components'; import { H, Search, Section } from '@woocommerce/components';
import { useState } from '@wordpress/element';
const SearchExample = withState( { const SearchExample = () => {
selected: [], const [ selected, setSelected ] = useState( [] );
inlineSelected: [], const [ inlineSelected, setInlineSelect ] = useState( [] );
} )( ( { selected, inlineSelected, setState } ) => (
<div> return (
<H>Tags Below Input</H> <div>
<Section component={ false }> <H>Tags Below Input</H>
<Search <Section component={ false }>
type="products" <Search
placeholder="Search for a product" type="products"
selected={ selected } placeholder="Search for a product"
onChange={ ( items ) => setState( { selected: items } ) } selected={ selected }
/> onChange={ ( items ) => setSelected( items ) }
</Section> />
<H>Tags Inline with Input</H> </Section>
<Section component={ false }> <H>Tags Inline with Input</H>
<Search <Section component={ false }>
type="products" <Search
placeholder="Search for a product" type="products"
selected={ inlineSelected } placeholder="Search for a product"
onChange={ ( items ) => setState( { inlineSelected: items } ) } selected={ inlineSelected }
inlineTags onChange={ ( items ) => setInlineSelect( items ) }
/> inlineTags
</Section> />
</div> </Section>
) ); </div>
);
};
export const Basic = () => <SearchExample />; export const Basic = () => <SearchExample />;

View File

@ -1,27 +1,29 @@
/** /**
* External dependencies * External dependencies
*/ */
import { withState } from '@wordpress/compose';
import { SegmentedSelection } from '@woocommerce/components'; import { SegmentedSelection } from '@woocommerce/components';
import { useState } from '@wordpress/element';
const name = 'number'; const name = 'number';
const SegmentedSelectionExample = withState( { const SegmentedSelectionExample = () => {
selected: 'two', const [ selected, setSelected ] = useState( 'two' );
} )( ( { selected, setState } ) => (
<SegmentedSelection return (
options={ [ <SegmentedSelection
{ value: 'one', label: 'One' }, options={ [
{ value: 'two', label: 'Two' }, { value: 'one', label: 'One' },
{ value: 'three', label: 'Three' }, { value: 'two', label: 'Two' },
{ value: 'four', label: 'Four' }, { value: 'three', label: 'Three' },
] } { value: 'four', label: 'Four' },
selected={ selected } ] }
legend="Select a number" selected={ selected }
onSelect={ ( data ) => setState( { selected: data[ name ] } ) } legend="Select a number"
name={ name } onSelect={ ( data ) => setSelected( data[ name ] ) }
/> name={ name }
) ); />
);
};
export const Basic = () => <SegmentedSelectionExample />; export const Basic = () => <SegmentedSelectionExample />;

View File

@ -2,11 +2,7 @@
* External dependencies * External dependencies
*/ */
import { SelectControl } from '@woocommerce/components'; import { SelectControl } from '@woocommerce/components';
import { useState } from '@wordpress/element';
/**
* External dependencies
*/
import { withState } from '@wordpress/compose';
const options = [ const options = [
{ {
@ -52,16 +48,18 @@ const options = [
}, },
]; ];
const SelectControlExample = withState( { const SelectControlExample = () => {
simpleSelected: [], const [ state, setState ] = useState( {
simpleMultipleSelected: [], simpleSelected: [],
singleSelected: [], simpleMultipleSelected: [],
singleSelectedShowAll: [], singleSelected: [],
multipleSelected: [], singleSelectedShowAll: [],
inlineSelected: [], multipleSelected: [],
allOptionsIncludingSelected: options[ options.length - 1 ].key, inlineSelected: [],
} )( allOptionsIncludingSelected: options[ options.length - 1 ].key,
( { } );
const {
simpleSelected, simpleSelected,
simpleMultipleSelected, simpleMultipleSelected,
singleSelected, singleSelected,
@ -69,13 +67,14 @@ const SelectControlExample = withState( {
multipleSelected, multipleSelected,
inlineSelected, inlineSelected,
allOptionsIncludingSelected, allOptionsIncludingSelected,
setState, } = state;
} ) => (
return (
<div> <div>
<SelectControl <SelectControl
label="Simple single value" label="Simple single value"
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { simpleSelected: selected } ) setState( { ...state, simpleSelected: selected } )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -86,7 +85,7 @@ const SelectControlExample = withState( {
label="Multiple values" label="Multiple values"
multiple multiple
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { simpleMultipleSelected: selected } ) setState( { ...state, simpleMultipleSelected: selected } )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -96,7 +95,10 @@ const SelectControlExample = withState( {
<SelectControl <SelectControl
label="Show all options with default selected" label="Show all options with default selected"
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { allOptionsIncludingSelected: selected } ) setState( {
...state,
allOptionsIncludingSelected: selected,
} )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -110,7 +112,7 @@ const SelectControlExample = withState( {
label="Single value searchable" label="Single value searchable"
isSearchable isSearchable
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { singleSelected: selected } ) setState( { ...state, singleSelected: selected } )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -121,7 +123,7 @@ const SelectControlExample = withState( {
label="Single value searchable with options on refocus" label="Single value searchable with options on refocus"
isSearchable isSearchable
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { singleSelectedShowAll: selected } ) setState( { ...state, singleSelectedShowAll: selected } )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -135,7 +137,7 @@ const SelectControlExample = withState( {
multiple multiple
inlineTags inlineTags
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { inlineSelected: selected } ) setState( { ...state, inlineSelected: selected } )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -148,7 +150,7 @@ const SelectControlExample = withState( {
label="Hidden options before search" label="Hidden options before search"
multiple multiple
onChange={ ( selected ) => onChange={ ( selected ) =>
setState( { multipleSelected: selected } ) setState( { ...state, multipleSelected: selected } )
} }
options={ options } options={ options }
placeholder="Start typing to filter options..." placeholder="Start typing to filter options..."
@ -156,8 +158,8 @@ const SelectControlExample = withState( {
showClearButton showClearButton
/> />
</div> </div>
) );
); };
export const Basic = () => <SelectControlExample />; export const Basic = () => <SelectControlExample />;

View File

@ -1,15 +1,17 @@
/** /**
* External dependencies * External dependencies
*/ */
import { withState } from '@wordpress/compose';
import { Stepper } from '@woocommerce/components'; import { Stepper } from '@woocommerce/components';
import { createElement } from '@wordpress/element'; import { useState } from '@wordpress/element';
const BasicExamples = () => {
const [ state, setState ] = useState( {
currentStep: 'first',
isComplete: false,
isPending: false,
} );
const { currentStep, isComplete, isPending } = state;
const BasicExamples = withState( {
currentStep: 'first',
isComplete: false,
isPending: false,
} )( ( { currentStep, isComplete, isPending, setState } ) => {
const goToStep = ( key ) => { const goToStep = ( key ) => {
setState( { currentStep: key } ); setState( { currentStep: key } );
}; };
@ -56,7 +58,11 @@ const BasicExamples = withState( {
{ isComplete ? ( { isComplete ? (
<button <button
onClick={ () => onClick={ () =>
setState( { currentStep: 'first', isComplete: false } ) setState( {
...state,
currentStep: 'first',
isComplete: false,
} )
} }
> >
Reset Reset
@ -66,6 +72,7 @@ const BasicExamples = withState( {
<button <button
onClick={ () => onClick={ () =>
setState( { setState( {
...state,
currentStep: steps[ currentIndex - 1 ].key, currentStep: steps[ currentIndex - 1 ].key,
} ) } )
} }
@ -76,6 +83,7 @@ const BasicExamples = withState( {
<button <button
onClick={ () => onClick={ () =>
setState( { setState( {
...state,
currentStep: steps[ currentIndex + 1 ].key, currentStep: steps[ currentIndex + 1 ].key,
} ) } )
} }
@ -84,13 +92,17 @@ const BasicExamples = withState( {
Next step Next step
</button> </button>
<button <button
onClick={ () => setState( { isComplete: true } ) } onClick={ () =>
setState( { ...state, isComplete: true } )
}
disabled={ currentIndex !== steps.length - 1 } disabled={ currentIndex !== steps.length - 1 }
> >
Complete Complete
</button> </button>
<button <button
onClick={ () => setState( { isPending: ! isPending } ) } onClick={ () =>
setState( { ...state, isPending: ! isPending } )
}
> >
Toggle Spinner Toggle Spinner
</button> </button>
@ -113,7 +125,7 @@ const BasicExamples = withState( {
/> />
</div> </div>
); );
} ); };
export const Examples = () => <BasicExamples />; export const Examples = () => <BasicExamples />;

View File

@ -2,38 +2,37 @@
* External dependencies * External dependencies
*/ */
import { TableCard } from '@woocommerce/components'; import { TableCard } from '@woocommerce/components';
import { useState } from '@wordpress/element';
/**
* External dependencies
*/
import { withState } from '@wordpress/compose';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { headers, rows, summary } from './index'; import { headers, rows, summary } from './index';
const TableCardExample = withState( { const TableCardExample = () => {
query: { const [ { query }, setState ] = useState( {
paged: 1, query: {
}, paged: 1,
} )( ( { query, setState } ) => ( },
<TableCard } );
title="Revenue last week" return (
rows={ rows } <TableCard
headers={ headers } title="Revenue last week"
onQueryChange={ ( param ) => ( value ) => rows={ rows }
setState( { headers={ headers }
query: { onQueryChange={ ( param ) => ( value ) =>
[ param ]: value, setState( {
}, query: {
} ) } [ param ]: value,
query={ query } },
rowsPerPage={ 7 } } ) }
totalRows={ 10 } query={ query }
summary={ summary } rowsPerPage={ 7 }
/> totalRows={ 10 }
) ); summary={ summary }
/>
);
};
export const Basic = () => <TableCardExample />; export const Basic = () => <TableCardExample />;

View File

@ -2,13 +2,13 @@
* External dependencies * External dependencies
*/ */
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { createElement, Fragment } from '@wordpress/element'; import { createElement, Fragment, useState } from '@wordpress/element';
import classnames from 'classnames'; import classnames from 'classnames';
import { Button, Popover } from '@wordpress/components'; import { Button, Popover } from '@wordpress/components';
import { Icon, cancelCircleFilled } from '@wordpress/icons'; import { Icon, cancelCircleFilled } from '@wordpress/icons';
import { decodeEntities } from '@wordpress/html-entities'; import { decodeEntities } from '@wordpress/html-entities';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withState, withInstanceId } from '@wordpress/compose'; import { withInstanceId } from '@wordpress/compose';
/** /**
* This component can be used to show an item styled as a "tag", optionally with an `X` + "remove" * This component can be used to show an item styled as a "tag", optionally with an `X` + "remove"
@ -17,26 +17,24 @@ import { withState, withInstanceId } from '@wordpress/compose';
* @param {Object} props * @param {Object} props
* @param {number|string} props.id * @param {number|string} props.id
* @param {string}props.instanceId * @param {string}props.instanceId
* @param {boolean} props.isVisible
* @param {string} props.label * @param {string} props.label
* @param {Object} props.popoverContents * @param {Object} props.popoverContents
* @param {Function} props.remove * @param {Function} props.remove
* @param {string} props.screenReaderLabel * @param {string} props.screenReaderLabel
* @param {Function} props.setState
* @param {string} props.className * @param {string} props.className
* @return {Object} - * @return {Object} -
*/ */
const Tag = ( { const Tag = ( {
id, id,
instanceId, instanceId,
isVisible,
label, label,
popoverContents, popoverContents,
remove, remove,
screenReaderLabel, screenReaderLabel,
setState,
className, className,
} ) => { } ) => {
const [ isVisible, setIsVisible ] = useState( false );
screenReaderLabel = screenReaderLabel || label; screenReaderLabel = screenReaderLabel || label;
if ( ! label ) { if ( ! label ) {
// A null label probably means something went wrong // A null label probably means something went wrong
@ -61,7 +59,7 @@ const Tag = ( {
<Button <Button
className="woocommerce-tag__text" className="woocommerce-tag__text"
id={ labelId } id={ labelId }
onClick={ () => setState( () => ( { isVisible: true } ) ) } onClick={ () => setIsVisible( true ) }
> >
{ labelTextNode } { labelTextNode }
</Button> </Button>
@ -71,9 +69,7 @@ const Tag = ( {
</span> </span>
) } ) }
{ popoverContents && isVisible && ( { popoverContents && isVisible && (
<Popover <Popover onClose={ () => setIsVisible( false ) }>
onClose={ () => setState( () => ( { isVisible: false } ) ) }
>
{ popoverContents } { popoverContents }
</Popover> </Popover>
) } ) }
@ -122,6 +118,4 @@ Tag.propTypes = {
screenReaderLabel: PropTypes.string, screenReaderLabel: PropTypes.string,
}; };
export default withState( { export default withInstanceId( Tag );
isVisible: false,
} )( withInstanceId( Tag ) );

View File

@ -1,7 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { render } from '@testing-library/react'; import { render, fireEvent } from '@testing-library/react';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
/** /**
@ -35,4 +35,26 @@ describe( 'Tag', () => {
); );
expect( component ).toMatchSnapshot(); expect( component ).toMatchSnapshot();
} ); } );
test( 'Do not show popoverContents by default', () => {
const { queryByText } = render(
<Tag label="foo" popoverContents={ <p>This is a popover</p> } />
);
expect( queryByText( 'This is a popover' ) ).toBeNull();
} );
test( 'Show popoverContents after clicking the button', () => {
const { queryByText, queryByRole } = render(
<Tag
label="foo"
instanceId="1"
popoverContents={ <p>This is a popover</p> }
/>
);
fireEvent.click(
queryByRole( 'button', { id: 'woocommerce-tag__label-1' } )
);
expect( queryByText( 'This is a popover' ) ).toBeDefined();
} );
} ); } );

View File

@ -2,91 +2,95 @@
* External dependencies * External dependencies
*/ */
import { TextControlWithAffixes } from '@woocommerce/components'; import { TextControlWithAffixes } from '@woocommerce/components';
import { useState } from '@wordpress/element';
/** const Examples = () => {
* External dependencies const [ state, setState ] = useState( {
*/ first: '',
import { withState } from '@wordpress/compose'; second: '',
third: '',
fourth: '',
fifth: '',
} );
const { first, second, third, fourth, fifth } = state;
const partialUpdate = ( partial ) => {
setState( { ...state, ...partial } );
};
const Examples = withState( { return (
first: '', <div>
second: '', <TextControlWithAffixes
third: '', label="Text field without affixes"
fourth: '', value={ first }
fifth: '', placeholder="Placeholder"
} )( ( { first, second, third, fourth, fifth, setState } ) => ( onChange={ ( value ) => partialUpdate( { first: value } ) }
<div> />
<TextControlWithAffixes <TextControlWithAffixes
label="Text field without affixes" label="Disabled text field without affixes"
value={ first } value={ first }
placeholder="Placeholder" placeholder="Placeholder"
onChange={ ( value ) => setState( { first: value } ) } onChange={ ( value ) => partialUpdate( { first: value } ) }
/> disabled
<TextControlWithAffixes />
label="Disabled text field without affixes" <TextControlWithAffixes
value={ first } prefix="$"
placeholder="Placeholder" label="Text field with a prefix"
onChange={ ( value ) => setState( { first: value } ) } value={ second }
disabled onChange={ ( value ) => partialUpdate( { second: value } ) }
/> />
<TextControlWithAffixes <TextControlWithAffixes
prefix="$" prefix="$"
label="Text field with a prefix" label="Disabled text field with a prefix"
value={ second } value={ second }
onChange={ ( value ) => setState( { second: value } ) } onChange={ ( value ) => partialUpdate( { second: value } ) }
/> disabled
<TextControlWithAffixes />
prefix="$" <TextControlWithAffixes
label="Disabled text field with a prefix" prefix="Prefix"
value={ second } suffix="Suffix"
onChange={ ( value ) => setState( { second: value } ) } label="Text field with both affixes"
disabled value={ third }
/> onChange={ ( value ) => partialUpdate( { third: value } ) }
<TextControlWithAffixes />
prefix="Prefix" <TextControlWithAffixes
suffix="Suffix" prefix="Prefix"
label="Text field with both affixes" suffix="Suffix"
value={ third } label="Disabled text field with both affixes"
onChange={ ( value ) => setState( { third: value } ) } value={ third }
/> onChange={ ( value ) => partialUpdate( { third: value } ) }
<TextControlWithAffixes disabled
prefix="Prefix" />
suffix="Suffix" <TextControlWithAffixes
label="Disabled text field with both affixes" suffix="%"
value={ third } label="Text field with a suffix"
onChange={ ( value ) => setState( { third: value } ) } value={ fourth }
disabled onChange={ ( value ) => partialUpdate( { fourth: value } ) }
/> />
<TextControlWithAffixes <TextControlWithAffixes
suffix="%" suffix="%"
label="Text field with a suffix" label="Disabled text field with a suffix"
value={ fourth } value={ fourth }
onChange={ ( value ) => setState( { fourth: value } ) } onChange={ ( value ) => partialUpdate( { fourth: value } ) }
/> disabled
<TextControlWithAffixes />
suffix="%" <TextControlWithAffixes
label="Disabled text field with a suffix" prefix="$"
value={ fourth } label="Text field with prefix and help text"
onChange={ ( value ) => setState( { fourth: value } ) } value={ fifth }
disabled onChange={ ( value ) => partialUpdate( { fifth: value } ) }
/> help="This is some help text."
<TextControlWithAffixes />
prefix="$" <TextControlWithAffixes
label="Text field with prefix and help text" prefix="$"
value={ fifth } label="Disabled text field with prefix and help text"
onChange={ ( value ) => setState( { fifth: value } ) } value={ fifth }
help="This is some help text." onChange={ ( value ) => partialUpdate( { fifth: value } ) }
/> help="This is some help text."
<TextControlWithAffixes disabled
prefix="$" />
label="Disabled text field with prefix and help text" </div>
value={ fifth } );
onChange={ ( value ) => setState( { fifth: value } ) } };
help="This is some help text."
disabled
/>
</div>
) );
export const Basic = () => <Examples />; export const Basic = () => <Examples />;

View File

@ -2,29 +2,24 @@
* External dependencies * External dependencies
*/ */
import { TextControl } from '@woocommerce/components'; import { TextControl } from '@woocommerce/components';
import { createElement } from '@wordpress/element'; import { useState } from '@wordpress/element';
/** const Example = () => {
* External dependencies const [ value, setValue ] = useState( '' );
*/
import { withState } from '@wordpress/compose';
const Example = withState( {
value: '',
} )( ( { setState, value } ) => {
return ( return (
<div> <div>
<TextControl <TextControl
name="text-control" name="text-control"
label="Enter text here" label="Enter text here"
onChange={ ( newValue ) => setState( { value: newValue } ) } onChange={ ( newValue ) => setValue( newValue ) }
value={ value } value={ value }
/> />
<br /> <br />
<TextControl label="Disabled field" disabled value="" /> <TextControl label="Disabled field" disabled value="" />
</div> </div>
); );
} ); };
export const Basic = () => <Example />; export const Basic = () => <Example />;