From b1e9bb2249412005b0f036e915b22158e29cb586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Le=20Ralec?= Date: Thu, 16 Dec 2021 15:57:44 +0100 Subject: [PATCH 01/32] chore: refactoring field --- docs/pages/components/field-better.mdx | 269 +++++++++++++++++++++++++ docs/pages/components/field.mdx | 39 +--- packages/ConnectedField/package.json | 2 +- packages/Field/index.tsx | 144 +++---------- packages/Field/styles.ts | 5 +- packages/Field/utils.ts | 27 +-- 6 files changed, 314 insertions(+), 172 deletions(-) create mode 100644 docs/pages/components/field-better.mdx diff --git a/docs/pages/components/field-better.mdx b/docs/pages/components/field-better.mdx new file mode 100644 index 0000000000..be3541ac32 --- /dev/null +++ b/docs/pages/components/field-better.mdx @@ -0,0 +1,269 @@ +export { getStaticProps } from '../../getStaticProps' + +import { + component, + dependencies, + name, + peerDependencies, + version, +} from '@welcome-ui/field/package.json' + +# Field + + + +We recommend using [Final Form](https://github.com/final-form/react-final-form), [Redux Form](https://github.com/erikras/redux-form) or [Formik](https://github.com/jaredpalmer/formik) with these components. If so, use `ConnectedField` for your fields (as in the examples below). The examples below give you an overview of common props (e.g. `size`, `disable`, `required` etc.) + +You can find props for each field component in the other `Forms` pages. + +## Install and import + + + +## Classic + +```jsx + + + + + + + + + + + + +Invalid field

}> + +
+ + + +``` + +```jsx +function () { + const ref = React.useRef(null) + + const handleClick = () => { + ref.current.focus() + } + + return ( + <> + + + + + + ) +} +``` + +```jsx +function () { + const [state, setState] = React.useState('') + + const handleChange = (event) => { + setState(event.target.value + 'a') + } + + return ( + + + + ) +} +``` + +```jsx +function () { + const [state, setState] = React.useState(false) + + const handleChange = (event) => { + setState(s => !s) + } + + return ( + + + + ) +} +``` + +## Disabled + +```jsx + +``` + +## Variants + +```jsx +
+ <> + + + + + +``` + +## Disabled + +```jsx +
+ + +``` + +## Required + +```jsx +
+ + +``` + +## Refs + +You can create a `ref`\* and pass it to a component to access the underlying DOM element. + +Note: `ref`s can be created with either `React.createRef()` or `React.useRef(null)` for functional components or a callback ref if using a React `class`. + +```jsx +function() { + const connectedInputRef = createRef() + const normalInputRef = useRef(null) + const nakedInputRef = useRef(null) + const fileUploadRef = useRef(null) + const buttonRef = useRef(null) + const linkRef = useRef(null) + + const handleConnectedClick = () => { + connectedInputRef.current.focus() + } + + const handleNormalClick = () => { + normalInputRef.current.focus() + } + + const handleNakedClick = () => { + nakedInputRef.current.focus() + } + + const handleFileUploadClick = () => { + fileUploadRef.current.focus() + } + + const handleButtonClick = () => { + console.debug(' + + + + + Click the button below to read the button's ref in the console + + + + Click the link below to read the link's ref in the console + + + Test link + + + ) +} +``` + +## Properties + + + +## Dependencies + + + +## Peer dependencies + + diff --git a/docs/pages/components/field.mdx b/docs/pages/components/field.mdx index 4199ba87b8..94eb732ac3 100644 --- a/docs/pages/components/field.mdx +++ b/docs/pages/components/field.mdx @@ -55,34 +55,17 @@ You can find props for each field component in the other `Forms` pages. ## Variants ```jsx -
- <> - - - - - +<> + + + + + + + + + + ``` ## Disabled diff --git a/packages/ConnectedField/package.json b/packages/ConnectedField/package.json index 55386195b4..f7ef339a27 100644 --- a/packages/ConnectedField/package.json +++ b/packages/ConnectedField/package.json @@ -38,7 +38,7 @@ "@welcome-ui/input-text": "^4.0.0-alpha.12" }, "dependencies": { - "@welcome-ui/field": "^4.0.0-alpha.12" + "@welcome-ui/field": "^3.11.0" }, "peerDependencies": { "react": "^16.10.2 || ^17.0.1", diff --git a/packages/Field/index.tsx b/packages/Field/index.tsx index cc769719dd..a58a56d119 100644 --- a/packages/Field/index.tsx +++ b/packages/Field/index.tsx @@ -1,49 +1,33 @@ import React, { Fragment } from 'react' import { Label } from '@welcome-ui/label' import { Hint } from '@welcome-ui/hint' -import SimpleMDEEditor from 'react-simplemde-editor' -import { CreateWuiProps, forwardFieldRef } from '@welcome-ui/system' +import { CreateWuiProps, forwardRef } from '@welcome-ui/system' // Fields import { RowContainer } from './layout' import * as S from './styles' -import { getBaseType, getVariant, VariantReturn } from './utils' +import { getBaseType, getVariant } from './utils' export type Size = 'sm' | 'md' | 'lg' -export interface FieldOptions { - checked?: boolean - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: React.ComponentType - connected?: boolean +type FieldOptions = { + children: JSX.Element disabled?: boolean disabledIcon?: JSX.Element error?: string | JSX.Element - hint?: string - id?: string + id: string label?: string - modified?: boolean - name?: string - onChange?: (event: React.ChangeEvent) => void - onClick?: (event: React.MouseEvent) => void - size?: Size - touched?: boolean - type?: string - warning?: string + hint?: string required?: boolean - variant?: VariantReturn + warning?: string | JSX.Element } -export type FieldProps = CreateWuiProps<'input', FieldOptions> +type FieldProps = CreateWuiProps<'div', FieldOptions> -export const Field = forwardFieldRef<'input', FieldProps>( +export const Field = forwardRef<'div', FieldProps>( ( { - checked, children, - component: Component, - connected, - dataTestId, disabled, disabledIcon, error, @@ -51,121 +35,49 @@ export const Field = forwardFieldRef<'input', FieldProps>( hint, id, label, - modified, - name, - onChange, - onClick, required, - size = 'lg', - touched, - type, warning, ...rest }, ref ) => { - // Return early if no component - if (!Component) { - return null - } - - const baseType = getBaseType(type || Component.displayName) + const baseType = getBaseType(children.props.type || children.type.displayName) const isRadio = baseType === 'radio' - const isToggle = Component.displayName === 'Toggle' + const isToggle = children.type.displayName === 'Toggle' const isCheckbox = baseType === 'checkbox' const isCheckable = isRadio || isCheckbox - const variant = getVariant({ - warning, - error, - modified, - isCheckbox, - isRadio, - touched, - connected, - }) - const hintText = variant ? error || warning : hint - const isGroup = ['FieldGroup', 'RadioGroup'].includes(baseType) - - const isShowRequired = isRadio ? null : required const layout = flexDirection || (isCheckable ? 'row' : 'column') - const Container = flexDirection === 'row' ? RowContainer : Fragment - const uniqueId = isRadio ? id : id || name - const inputRef = ref || React.createRef() + const Container = layout === 'row' ? RowContainer : Fragment + const variant = getVariant({ error, warning }) - const handleClick = (e: React.MouseEvent) => { - const target = e.target as HTMLInputElement - e.stopPropagation() - onClick && onClick(e) - if (isCheckbox) { - target.checked = !target.checked - } - if (isCheckbox || isGroup) { - onChange && onChange(e as unknown as React.ChangeEvent) - } - } - - const handleLabelClick = () => { - const input = (inputRef as React.MutableRefObject).current - if (input) { - Component.displayName === 'MarkdownEditor' - ? (input as SimpleMDEEditor).simpleMde.codemirror.focus() - : (input as HTMLInputElement).focus() - } - } - - const Field = ( - - {children} - - ) + const child = React.cloneElement(React.Children.only(children), { + disabled, + variant, + required, + id, + }) return ( - + - {label && !isGroup && ( + {isCheckable && child} + {label && ( )} - {!isCheckable && Field} - {!label && isCheckable && Field} + {!isCheckable && child} - {hintText && ( + {hint && ( - {hintText} + {hint} )} diff --git a/packages/Field/styles.ts b/packages/Field/styles.ts index f07b6d52da..5c773d08c1 100644 --- a/packages/Field/styles.ts +++ b/packages/Field/styles.ts @@ -2,8 +2,7 @@ import styled, { css } from '@xstyled/styled-components' import { th } from '@xstyled/system' import { StyledLabel } from '@welcome-ui/label' import { StyledFieldGroup } from '@welcome-ui/field-group' -import { shouldForwardProp, system, wrapperSystem } from '@welcome-ui/system' -import { WuiProps } from '@welcome-ui/system' +import { shouldForwardProp, system, WuiProps } from '@welcome-ui/system' import { Size } from './index' @@ -38,7 +37,7 @@ export const Field = styled('div').withConfig({ shouldForwardProp }) connected && !touched - export const getBaseType = (type: string): string => TYPES[type] || type type VariantProps = { - connected?: boolean error?: string | JSX.Element - isCheckbox: boolean - isRadio: boolean - modified?: boolean - touched?: boolean - warning?: string + warning?: string | JSX.Element } -export type VariantReturn = 'error' | 'warning' | undefined +export type VariantReturn = 'error' | 'warning' -export const getVariant = ({ - connected, - error, - isCheckbox, - isRadio, - modified, - touched, - warning, -}: VariantProps): VariantReturn => { - if ( - ((isCheckbox || isRadio) && isPristine(connected, modified)) || - (!isCheckbox && !isRadio && isPristine(connected, touched)) - ) { - return undefined - } +export const getVariant = ({ error, warning }: VariantProps): VariantReturn => { if (error) { return 'error' } From a6d092cac53c5a8a5d0fb28ac0953c40ab0e6856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Le=20Ralec?= Date: Thu, 16 Dec 2021 17:49:16 +0100 Subject: [PATCH 02/32] chore: some clean field --- packages/Field/index.tsx | 1 - packages/Field/package.json | 1 - packages/Field/styles.ts | 37 ------------------------------------- packages/Field/utils.ts | 9 ++------- 4 files changed, 2 insertions(+), 46 deletions(-) diff --git a/packages/Field/index.tsx b/packages/Field/index.tsx index a58a56d119..1696c6a944 100644 --- a/packages/Field/index.tsx +++ b/packages/Field/index.tsx @@ -87,5 +87,4 @@ export const Field = forwardRef<'div', FieldProps>( Field.displayName = 'Field' -export const IconWrapper = S.IconWrapper export { getBaseType } diff --git a/packages/Field/package.json b/packages/Field/package.json index d1ce5e2c84..dbf1f911a9 100644 --- a/packages/Field/package.json +++ b/packages/Field/package.json @@ -37,7 +37,6 @@ "url": "https://github.com/WTTJ/welcome-ui/issues" }, "dependencies": { - "@welcome-ui/field-group": "^4.0.0-alpha.12", "@welcome-ui/hint": "^4.0.0-alpha.12", "@welcome-ui/label": "^4.0.0-alpha.12", "@welcome-ui/system": "^4.0.0-alpha.11", diff --git a/packages/Field/styles.ts b/packages/Field/styles.ts index 5c773d08c1..52d1f6f2f6 100644 --- a/packages/Field/styles.ts +++ b/packages/Field/styles.ts @@ -4,8 +4,6 @@ import { StyledLabel } from '@welcome-ui/label' import { StyledFieldGroup } from '@welcome-ui/field-group' import { shouldForwardProp, system, WuiProps } from '@welcome-ui/system' -import { Size } from './index' - const rowStyles = css` margin-right: sm; ` @@ -23,7 +21,6 @@ type StyledFieldProps = { checkableField: boolean flexDirection: WuiProps['flexDirection'] checked: boolean - size: Size } export const Field = styled('div').withConfig({ shouldForwardProp })( @@ -40,37 +37,3 @@ export const Field = styled('div').withConfig({ shouldForwardProp })( - ({ iconPlacement, size, ...rest }) => css` - position: absolute; - top: 0; - left: ${iconPlacement === 'left' ? 0 : 'auto'}; - right: ${iconPlacement === 'right' ? 0 : 'auto'}; - bottom: 0; - display: flex; - width: ${size ? th(`defaultFields.sizes.${size}.height`)(rest) : null}; - justify-content: center; - align-items: center; - pointer-events: none; - transition: medium; - transition-timing-function: primary; - ${system}; - - /* for button action */ - & > button { - pointer-events: auto; - } - ` -) - -export const Input = styled.div` - flex-shrink: 0; -` - -export const Content = styled.div`` diff --git a/packages/Field/utils.ts b/packages/Field/utils.ts index 6ddda027c5..cbdddb6316 100644 --- a/packages/Field/utils.ts +++ b/packages/Field/utils.ts @@ -17,12 +17,7 @@ type VariantProps = { export type VariantReturn = 'error' | 'warning' export const getVariant = ({ error, warning }: VariantProps): VariantReturn => { - if (error) { - return 'error' - } - if (warning) { - return 'warning' - } - + if (error) return 'error' + if (warning) return 'warning' return undefined } From 1e867a9fa420675e54050fd1307f1638fba6966e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Le=20Ralec?= Date: Thu, 16 Dec 2021 17:59:36 +0100 Subject: [PATCH 03/32] fix: rollback IconWrapper --- packages/Field/index.tsx | 1 + packages/Field/styles.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/Field/index.tsx b/packages/Field/index.tsx index 1696c6a944..a58a56d119 100644 --- a/packages/Field/index.tsx +++ b/packages/Field/index.tsx @@ -87,4 +87,5 @@ export const Field = forwardRef<'div', FieldProps>( Field.displayName = 'Field' +export const IconWrapper = S.IconWrapper export { getBaseType } diff --git a/packages/Field/styles.ts b/packages/Field/styles.ts index 52d1f6f2f6..94b0715111 100644 --- a/packages/Field/styles.ts +++ b/packages/Field/styles.ts @@ -4,6 +4,8 @@ import { StyledLabel } from '@welcome-ui/label' import { StyledFieldGroup } from '@welcome-ui/field-group' import { shouldForwardProp, system, WuiProps } from '@welcome-ui/system' +import { Size } from './index' + const rowStyles = css` margin-right: sm; ` @@ -37,3 +39,30 @@ export const Field = styled('div').withConfig({ shouldForwardProp })( + ({ iconPlacement, size, ...rest }) => css` + position: absolute; + top: 0; + left: ${iconPlacement === 'left' ? 0 : 'auto'}; + right: ${iconPlacement === 'right' ? 0 : 'auto'}; + bottom: 0; + display: flex; + width: ${size ? th(`defaultFields.sizes.${size}.height`)(rest) : null}; + justify-content: center; + align-items: center; + pointer-events: none; + transition: medium; + transition-timing-function: primary; + ${system}; + /* for button action */ + & > button { + pointer-events: auto; + } + ` +) From 714c99c8df5ca7efc7bc642171fd9868536d4a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Le=20Ralec?= Date: Fri, 17 Dec 2021 19:20:00 +0100 Subject: [PATCH 04/32] chore: refactoring select tests without ConnectedField --- packages/Select/index.test.tsx | 344 +++++++++------------------------ packages/Select/index.tsx | 4 +- packages/Select/utils.ts | 7 +- 3 files changed, 96 insertions(+), 259 deletions(-) diff --git a/packages/Select/index.test.tsx b/packages/Select/index.test.tsx index 3676a234c6..05327038d3 100644 --- a/packages/Select/index.test.tsx +++ b/packages/Select/index.test.tsx @@ -2,14 +2,12 @@ import React from 'react' import { fireEvent } from '@testing-library/react' import userEvent from '@testing-library/user-event' import capitalize from 'lodash.capitalize' -import { ConnectedField } from '@welcome-ui/connected-field' import { AvatarIcon } from '@welcome-ui/icons.avatar' import { DateIcon } from '@welcome-ui/icons.date' -import { Form, getFormValues } from '../../utils/Form' import { render } from '../../utils/tests' -import { Select } from './index' +import { Option, Select } from './index' const MONTHS = [ 'january', @@ -31,7 +29,7 @@ const MONTHS_WITH_INTEGER_VALUES = MONTHS.map((item, index) => ({ value: index, })) -export const SOCIAL_OPT_GROUP = [ +const SOCIAL_OPT_GROUP = [ { label: 'Professional networks', options: [ @@ -51,15 +49,7 @@ export const SOCIAL_OPT_GROUP = [ test(' ) const select = getByTestId('select') @@ -67,21 +57,10 @@ test(' has default attributes', () => { - const { container, getByTestId } = render( -
- - + const { getByTestId } = render( + has default attributes', () => { test(' ) const select = getByTestId('select') @@ -110,15 +81,7 @@ test(' shows options on click (arrow indicator)', () => { const { getByRole, getByTestId } = render( -
- - + shows options on click (arrow indicator)', () => { expect(options[0]).toHaveTextContent('January') }) -test(' can choose option', () => { const { getByRole, getByTestId } = render( -
- - + can choose option', () => { let options = getByRole('listbox').querySelectorAll('li') fireEvent.click(options[1]) - - const formValues = getFormValues(getByTestId('values')) expect(select).toHaveTextContent('February') - expect(formValues.select).toStrictEqual('february') // List is refilled fireEvent.click(select) @@ -162,16 +114,7 @@ test(' calls onChange with correct (object) values', () => { const handleChange = jest.fn() const { getByRole, getByTestId } = render( -
- - + calls onChange with correct (object) values', () => { test(' ) const select = getByTestId('select') - let formValues = getFormValues(getByTestId('values')) expect(select).toHaveTextContent('February') - expect(formValues.select).toStrictEqual('february') // Click cross to remove selected option const clearButton = getByTitle('Clear') fireEvent.click(clearButton) - - formValues = getFormValues(getByTestId('values')) expect(select).toHaveTextContent('') - expect(formValues.select).toBeUndefined() }) test(' ) const select = getByTestId('select') @@ -243,28 +169,19 @@ test(' can accept value, label or object as value', () => { const { getAllByRole, getByRole, getByTestId } = render( -
- - + can accept value, label or object as value', () => { tags = getAllByRole('listitem') expect(tags.length).toBe(4) - const formValues = getFormValues(getByTestId('values')) expect(select).toHaveTextContent('') expect(tags.map(tag => tag.textContent)).toStrictEqual(['January', 'February', 'March', 'April']) - expect(formValues.select).toStrictEqual(['january', 'february', 'march', 'april']) }) -test.skip(' can remove multiple items', () => { const { getAllByRole, getByTestId } = render( -
- - + can remove multiple items', () => { tags = getAllByRole('listitem') expect(tags.length).toBe(1) - const formValues = getFormValues(getByTestId('values')) expect(select).toHaveTextContent('') expect(tags.map(tag => tag.textContent)).toStrictEqual(['February']) - expect(formValues.select).toStrictEqual(['february']) }) -test(" doesn't show clear button", () => { const { getByRole, getByTestId, queryByTitle } = render( -
- - + doesn't show clear button", () => { const options = getByRole('listbox').querySelectorAll('li') fireEvent.click(options[1]) - - const formValues = getFormValues(getByTestId('values')) expect(select).toHaveTextContent('February') - expect(formValues.select).toStrictEqual('february') // Use `queryByTitle` to expect no clear button const clearButton = queryByTitle('Clear') expect(clearButton).toBeNull() }) -test.skip(' formats items', () => { const { getByTestId } = render( -
- ( -
- {option.label} -
- )} - /> - + shows icon', () => { +test('} + name="select" + options={MONTHS} + value="february" + /> ) const icon = container.querySelector('[alt="Avatar"]') @@ -390,16 +280,7 @@ test.skip(' filters results', () => { const { getByRole, getByTestId } = render( -
- - + filters results', () => { expect(options.length).toBe(3) // September, November, December fireEvent.click(options[1]) - const formValues = getFormValues(getByTestId('values')) expect((select as HTMLInputElement).value).toBe('November') - expect(formValues.select).toStrictEqual('november') }) test(" ) const select = getByTestId('select') @@ -442,17 +312,13 @@ test(' ) const select = getByTestId('select') @@ -477,10 +343,6 @@ test(' can create new items', () => { // Expect content to be new item expect((select as HTMLInputElement).value).toBe(secondItem.label) - - // Expect form values to have new item - formValues = getFormValues(getByTestId('values')) - expect(formValues.select).toStrictEqual(secondItem.label) }) test(' ) const select = getByTestId('select') @@ -553,26 +408,18 @@ test(' can't create an existing item", () => { const handleCreate = jest.fn() const { getByRole, getByTestId } = render( -
- - + can't create an existing item", () => { // Expect `onCreate` callback not to be called expect(handleCreate).toHaveBeenCalledTimes(0) - - const formValues = getFormValues(getByTestId('values')) expect((select as HTMLInputElement).value).toBe('October') - expect(formValues.select).toStrictEqual('october') }) test(' ( +
+

{label}

+ {options.length} +
+ )} + /> ) const select = getByTestId('select') diff --git a/packages/Select/index.tsx b/packages/Select/index.tsx index 6874643d72..5f74fe0254 100644 --- a/packages/Select/index.tsx +++ b/packages/Select/index.tsx @@ -30,12 +30,12 @@ export type OptionGroup = { label: string; options: Option[] } export type OptionItem = Option | OptionGroup export type Options = Array