From b0df5feb78a604a06d56958203c429e864a1aea1 Mon Sep 17 00:00:00 2001 From: Andrew Musgrave Date: Mon, 16 Sep 2019 13:52:55 -0400 Subject: [PATCH 1/2] Hookify TextField, Toast, and TopBar examples --- src/components/TextField/README.md | 479 ++++++++++++++--------------- src/components/Toast/README.md | 308 ++++++++----------- src/components/TopBar/README.md | 462 +++++++++++++--------------- 3 files changed, 576 insertions(+), 673 deletions(-) diff --git a/src/components/TextField/README.md b/src/components/TextField/README.md index af27b2f227e..6bc80a0fa81 100644 --- a/src/components/TextField/README.md +++ b/src/components/TextField/README.md @@ -175,6 +175,7 @@ Use to allow merchants to provide text input when the expected input is short. F ```jsx function TextFieldExample() { const [value, setValue] = useState('Jaded Pixel'); + const handleChange = useCallback((newValue) => setValue(newValue), []); return ; @@ -200,6 +201,7 @@ Use when input text should be a number. ```jsx function NumberFieldExample() { const [value, setValue] = useState('1'); + const handleChange = useCallback((newValue) => setValue(newValue), []); return ( @@ -236,6 +238,7 @@ Use when the text input should be an email address. ```jsx function EmailFieldExample() { const [value, setValue] = useState('bernadette.lapresse@jadedpixel.com'); + const handleChange = useCallback((newValue) => setValue(newValue), []); return ( @@ -272,6 +275,7 @@ Use when the expected input could be more than one line. The field will automati ```jsx function MultilineFieldExample() { const [value, setValue] = useState('1776 Barnes Street\nOrlando, FL 32801'); + const handleChange = useCallback((newValue) => setValue(newValue), []); return ( @@ -307,7 +311,9 @@ Use to visually hide the label when the text field’s purpose is clear from con function HiddenLabelExample() { const [value, setValue] = useState('12'); const [selected, setSelected] = useState('yes'); + const handleTextChange = useCallback((newValue) => setValue(newValue), []); + const handleChoiceChange = useCallback( (selections) => setSelected(selections[0]), [], @@ -351,25 +357,22 @@ function HiddenLabelExample() { Use when an optional, secondary action is closely associated with a text field. For example, on a field for entering a customs tariff code, a label action might be to look up the appropriate code from a table. ```jsx -class LabelActionExample extends React.Component { - state = { - value: '6201.11.0000', - }; +function LabelActionExample() { + const [textFieldValue, setTextFieldValue] = useState('6201.11.0000'); - handleChange = (value) => { - this.setState({value}); - }; + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); - render() { - return ( - - ); - } + return ( + + ); } ``` @@ -380,29 +383,26 @@ class LabelActionExample extends React.Component { Use when input text should be aligned right. ```jsx -class RightAlignExample extends React.Component { - state = { - value: '1', - }; - - handleChange = (value) => { - this.setState({value}); - }; - - render() { - return ( - - Price - - - ); - } +function RightAlignExample() { + const [textFieldValue, setTextFieldValue] = useState('1'); + + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); + + return ( + + Price + + + ); } ``` @@ -411,25 +411,22 @@ class RightAlignExample extends React.Component { Use to provide a short, non-essential hint about the expected input. Placeholder text is low-contrast, so don’t rely on it for important information. ```jsx -class PlaceholderExample extends React.Component { - state = { - value: '', - }; +function PlaceholderExample() { + const [textFieldValue, setTextFieldValue] = useState(''); - handleChange = (value) => { - this.setState({value}); - }; + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); - render() { - return ( - - ); - } + return ( + + ); } ``` @@ -450,26 +447,25 @@ class PlaceholderExample extends React.Component { Use to show short instructional content below the text field. Help text works to help merchants understand how to fix errors that result from incorrect formatting (such as dates or passwords with specific character requirements). If more explanation is needed, link to the Shopify Help Center. ```jsx -class HelpTextExample extends React.Component { - state = { - value: 'bernadette.lapresse@jadedpixel.com', - }; +function HelpTextExample() { + const [textFieldValue, setTextFieldValue] = useState( + 'bernadette.lapresse@jadedpixel.com', + ); - handleChange = (value) => { - this.setState({value}); - }; + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); - render() { - return ( - - ); - } + return ( + + ); } ``` @@ -493,26 +489,23 @@ Use as a special form of help text that works best inline. - Use suffix for things like units of measure (“in”, “cm”). ```jsx -class PrefixExample extends React.Component { - state = { - value: '2.00', - }; +function PrefixExample() { + const [textFieldValue, setTextFieldValue] = useState('2.00'); - handleChange = (value) => { - this.setState({value}); - }; + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); - render() { - return ( - - ); - } + return ( + + ); } ``` @@ -539,39 +532,34 @@ If inputting weight as a number and a separate unit of measurement, use a text f ```jsx -class ConnectedFieldsExample extends React.Component { - state = { - value: '10.6', - selectValue: 'kg', - }; - - handleChange = (value) => { - this.setState({value}); - }; - - handleSelectChange = (selectValue) => { - this.setState({selectValue}); - }; - - render() { - return ( - - } - /> - ); - } +function ConnectedFieldsExample() { + const [textFieldValue, setTextFieldValue] = useState('10.6'); + const [selectValue, setSelectValue] = useState('kg'); + + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); + + const handleSelectChange = useCallback((value) => setSelectValue(value), []); + + return ( + + } + /> + ); } ``` @@ -616,25 +604,22 @@ For example, tap on a barcode icon to launch the camera and scan barcode for the Use to let merchants know if their input is valid or if there’s an error. Whenever possible, validate input as soon as merchants have finished interacting with a field (but not before). If a field already has an error, validate and remove errors as merchants type so they can immediately see when an error has been fixed. ```jsx -class ValidationErrorExample extends React.Component { - state = { - value: '', - }; +function ValidationErrorExample() { + const [textFieldValue, setTextFieldValue] = useState(''); - handleChange = (value) => { - this.setState({value}); - }; + const handleTextFieldChange = useCallback( + (value) => setTextFieldValue(value), + [], + ); - render() { - return ( - - ); - } + return ( + + ); } ``` @@ -665,82 +650,82 @@ To render an invalid text field and its validation error separately: - Use an [inline error component](https://polaris.shopify.com/components/forms/inline-error) to describe the invalid text field input, and set its `fieldID` prop to be the same unique indentifier as the text field component’s `id` ```jsx -class SeparateValidationErrorExample extends React.Component { - state = { - content: '', - selectTypeValue: 'Product type', - selectConditionValue: 'is equal to', - }; - - handleSelectCollectionTypeChange = (selectTypeValue) => { - this.setState({selectTypeValue}); - }; - - handleSelectCollectionConditionChange = (selectConditionValue) => { - this.setState({selectConditionValue}); - }; - - render() { - const {content} = this.state; - const textFieldID = 'ruleContent'; - const isInvalid = this.isInvalid(content); - const errorMessage = isInvalid - ? 'Enter 3 or more characters for product type is equal to' - : ''; - - const formGroupMarkup = ( - - - - - - -
- -
-
- - {toastMarkup} - - - - ); - } - - toggleToast = () => { - this.setState(({showToast}) => ({showToast: !showToast})); - }; +function ToastExample() { + const [active, setActive] = useState(false); + + const toggleActive = useCallback(() => setActive((active) => !active), []); + + const toastMarkup = active ? ( + + ) : null; + + return ( +
+ + + + {toastMarkup} + + +
+ ); } ``` @@ -202,45 +190,42 @@ class ToastExample extends React.Component { Use multiple toast messages to inform the merchant about distinct actions. ```jsx -class ToastExample extends React.Component { - state = { - showToast1: false, - showToast2: false, - }; - - render() { - const {showToast1, showToast2} = this.state; - const toastMarkup1 = showToast1 ? ( - - ) : null; - - const toastMarkup2 = showToast2 ? ( - - ) : null; - - return ( -
- - - - - - - {toastMarkup1} - {toastMarkup2} - - -
- ); - } - - toggleToast1 = () => { - this.setState(({showToast1}) => ({showToast1: !showToast1})); - }; - - toggleToast2 = () => { - this.setState(({showToast2}) => ({showToast2: !showToast2})); - }; +function MultipleToastExample() { + const [activeOne, setActiveOne] = useState(false); + const [activeTwo, setActiveTwo] = useState(false); + + const toggleActiveOne = useCallback( + () => setActiveOne((activeOne) => !activeOne), + [], + ); + + const toggleActiveTwo = useCallback( + () => setActiveTwo((activeTwo) => !activeTwo), + [], + ); + + const toastMarkup1 = activeOne ? ( + + ) : null; + + const toastMarkup2 = activeTwo ? ( + + ) : null; + + return ( +
+ + + + + + + {toastMarkup1} + {toastMarkup2} + + +
+ ); } ``` @@ -251,36 +236,25 @@ class ToastExample extends React.Component { Use to shorten or lengthen the default duration of 5000 miliseconds. ```jsx -class ToastExample extends React.Component { - state = { - showToast: false, - }; - - render() { - const {showToast} = this.state; - const toastMarkup = showToast ? ( - - ) : null; - - return ( -
- - - - {toastMarkup} - - -
- ); - } - - toggleToast = () => { - this.setState(({showToast}) => ({showToast: !showToast})); - }; +function ToastWithCustomDurationExample() { + const [active, setActive] = useState(false); + + const toggleActive = useCallback(() => setActive((active) => !active), []); + + const toastMarkup = active ? ( + + ) : null; + + return ( +
+ + + + {toastMarkup} + + +
+ ); } ``` @@ -291,40 +265,33 @@ class ToastExample extends React.Component { Use when a merchant has the ability to act on the message. For example, to undo a change or retry an action. ```jsx -class ToastExample extends React.Component { - state = { - showToast: false, - }; - - render() { - const {showToast} = this.state; - const toastMarkup = showToast ? ( - {}, - }} - duration={10000} - onDismiss={this.toggleToast} - /> - ) : null; - - return ( -
- - - - {toastMarkup} - - -
- ); - } - - toggleToast = () => { - this.setState(({showToast}) => ({showToast: !showToast})); - }; +function ToastWithActionExample() { + const [active, setActive] = useState(false); + + const toggleActive = useCallback(() => setActive((active) => !active), []); + + const toastMarkup = active ? ( + {}, + }} + duration={10000} + onDismiss={toggleActive} + /> + ) : null; + + return ( +
+ + + + {toastMarkup} + + +
+ ); } ``` @@ -377,32 +344,25 @@ Although error toast is still available and used in the system, we discourage it ```jsx -class ToastExample extends React.Component { - state = { - showToast: false, - }; - - render() { - const {showToast} = this.state; - const toastMarkup = showToast ? ( - - ) : null; - - return ( -
- - - - {toastMarkup} - - -
- ); - } - - toggleToast = () => { - this.setState(({showToast}) => ({showToast: !showToast})); - }; +function ErrorToastExample() { + const [active, setActive] = useState(false); + + const toggleActive = useCallback(() => setActive((active) => !active), []); + + const toastMarkup = active ? ( + + ) : null; + + return ( +
+ + + + {toastMarkup} + + +
+ ); } ``` diff --git a/src/components/TopBar/README.md b/src/components/TopBar/README.md index 7facc24a77a..9d4e72dec9d 100644 --- a/src/components/TopBar/README.md +++ b/src/components/TopBar/README.md @@ -146,136 +146,119 @@ A text field component that is tailor-made for a search use-case. Use to provide structure for the top of an application. Style the top bar component using the app provider component with a theme. Providing just the `background` key for the top bar component theme will result in intelligent defaults being set for complementary colors with contrasting text. ```jsx -class TopBarExample extends React.Component { - state = { - userMenuOpen: false, - searchActive: false, - searchText: '', +function TopBarExample() { + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const [isSearchActive, setIsSearchActive] = useState(false); + const [searchValue, setSearchValue] = useState(''); + + const toggleIsUserMenuOpen = useCallback( + () => setIsUserMenuOpen((isUserMenuOpen) => !isUserMenuOpen), + [], + ); + + const handleSearchResultsDismiss = useCallback(() => { + setIsSearchActive(false); + setSearchValue(''); + }, []); + + const handleSearchChange = useCallback((value) => { + setSearchValue(value); + setIsSearchActive(value.length > 0); + }, []); + + const handleNavigationToggle = useCallback(() => { + console.log('toggle navigation visibility'); + }, []); + + const theme = { + colors: { + topBar: { + background: '#357997', + }, + }, + logo: { + width: 124, + topBarSource: + 'https://cdn.shopify.com/s/files/1/0446/6937/files/jaded-pixel-logo-color.svg?6215648040070010999', + url: 'http://jadedpixel.com', + accessibilityLabel: 'Jaded Pixel', + }, }; - render() { - const { - state, - handleSearchChange, - handleSearchResultsDismiss, - toggleUserMenu, - } = this; - const {userMenuOpen, searchText, searchActive} = state; - - const theme = { - colors: { - topBar: { - background: '#357997', + const userMenuMarkup = ( + + ); + + const searchResultsMarkup = ( + + - ); - - const searchResultsMarkup = ( - - - - ); - - const searchFieldMarkup = ( - - ); - - const topBarMarkup = ( - { - console.log('toggle navigation visibility'); - }} /> - ); - - return ( -
- + ); + + const searchFieldMarkup = ( + + ); + + const topBarMarkup = ( + + ); + + return ( +
+ - - -
- ); - } - - toggleUserMenu = () => { - this.setState(({userMenuOpen}) => ({userMenuOpen: !userMenuOpen})); - }; - - handleSearchResultsDismiss = () => { - this.setState(() => { - return { - searchActive: false, - searchText: '', - }; - }); - }; - - handleSearchChange = (value) => { - this.setState({searchText: value}); - if (value.length > 0) { - this.setState({searchActive: true}); - } else { - this.setState({searchActive: false}); - } - }; + }, + }} + > + +
+
+ ); } ``` @@ -284,138 +267,121 @@ class TopBarExample extends React.Component { Provide specific keys and corresponding colors to the top bar theme for finer control. When giving more than just the `background`, providing all keys is necessary to prevent falling back to default colors. ```jsx -class TopBarExample extends React.Component { - state = { - userMenuOpen: false, - searchActive: false, - searchText: '', +function TopBarExample() { + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const [isSearchActive, setIsSearchActive] = useState(false); + const [searchValue, setSearchValue] = useState(''); + + const toggleIsUserMenuOpen = useCallback( + () => setIsUserMenuOpen((isUserMenuOpen) => !isUserMenuOpen), + [], + ); + + const handleSearchResultsDismiss = useCallback(() => { + setIsSearchActive(false); + setSearchValue(''); + }, []); + + const handleSearchChange = useCallback((value) => { + setSearchValue(value); + setIsSearchActive(value.length > 0); + }, []); + + const handleNavigationToggle = useCallback(() => { + console.log('toggle navigation visibility'); + }, []); + + const theme = { + colors: { + topBar: { + background: '#357997', + backgroundLighter: '#6192a9', + color: '#FFFFFF', + }, + }, + logo: { + width: 124, + topBarSource: + 'https://cdn.shopify.com/s/files/1/0446/6937/files/jaded-pixel-logo-color.svg?6215648040070010999', + url: 'http://jadedpixel.com', + accessibilityLabel: 'Jaded Pixel', + }, }; - render() { - const { - state, - handleSearchChange, - handleSearchResultsDismiss, - toggleUserMenu, - } = this; - const {userMenuOpen, searchText, searchActive} = state; - - const theme = { - colors: { - topBar: { - background: '#357997', - backgroundLighter: '#6192a9', - color: '#FFFFFF', + const userMenuMarkup = ( + + ); + + const searchResultsMarkup = ( + + - ); - - const searchResultsMarkup = ( - - - - ); - - const searchFieldMarkup = ( - - ); - - const topBarMarkup = ( - { - console.log('toggle navigation visibility'); - }} /> - ); - - return ( -
- + ); + + const searchFieldMarkup = ( + + ); + + const topBarMarkup = ( + + ); + + return ( +
+ - - -
- ); - } - - toggleUserMenu = () => { - this.setState(({userMenuOpen}) => ({userMenuOpen: !userMenuOpen})); - }; - - handleSearchResultsDismiss = () => { - this.setState(() => { - return { - searchActive: false, - searchText: '', - }; - }); - }; - - handleSearchChange = (value) => { - this.setState({searchText: value}); - if (value.length > 0) { - this.setState({searchActive: true}); - } else { - this.setState({searchActive: false}); - } - }; + }, + }} + > + +
+
+ ); } ``` From b3ccf749d15ad22ff2d503367b3e31f940d5809b Mon Sep 17 00:00:00 2001 From: Andrew Musgrave Date: Mon, 16 Sep 2019 13:53:00 -0400 Subject: [PATCH 2/2] changelog --- UNRELEASED.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UNRELEASED.md b/UNRELEASED.md index c9f71570bef..6c119ba4677 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -21,6 +21,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Documentation - Updated the `withContext` section in the [v3 to v4 migration guide](https://github.com/Shopify/polaris-react/blob/master/documentation/guides/migrating-from-v3-to-v4.md) ([#2124](https://github.com/Shopify/polaris-react/pull/2124)) +- Converted `TextField`, `Toast`, and `TopBar` examples to functional components ([#2135](https://github.com/Shopify/polaris-react/pull/2135)) ### Development workflow