diff --git a/package.json b/package.json index 5fba4738..40b40950 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flipper-ui", - "version": "0.29.5", + "version": "0.29.6", "description": "", "main": "dist/index.js", "homepage": "https://flipper-ui.ngi.com.br/", diff --git a/src/core/inputs/text-field/index.tsx b/src/core/inputs/text-field/index.tsx index 4be0da47..24dd7445 100644 --- a/src/core/inputs/text-field/index.tsx +++ b/src/core/inputs/text-field/index.tsx @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import { InputAdornment, TextField as MuiTextField, TextFieldProps as MuiTextFieldProps, - IconButton as MuiButton + IconButton as MuiButton, + ListItem } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import React, { @@ -21,11 +23,21 @@ import { } from '@material-ui/icons' import IconButton from '../icon-button' import styled from 'styled-components' +import { when, is, pipe, split, map, zipObj, reject, propEq } from 'ramda' + +export interface IOption { + label: string + name?: string + disabled?: boolean + value: string | number + options?: IOption[] +} export interface TextFieldProps extends DefaultProps, Omit<MuiTextFieldProps, 'margin' | 'variant'> { autoComplete?: string + options?: IOption[] | string autoFocus?: boolean defaultValue?: string | number disabled?: boolean @@ -125,6 +137,23 @@ export const useStyles = makeStyles({ } }) +export const coerceComboOptions: (input: string) => IOption[] = when( + is(String), + pipe<string, string[], object, IOption[]>( + split(';'), + map(pipe(split('='), zipObj(['value', 'label']))), + reject(propEq('value', '')) + ) +) + +export const toLispCase = (name: string) => + name + .replace( + /([a-z])([A-Z])/g, + (_, first: string, second: string) => first + '-' + second + ) + .toLowerCase() + export const HelperBox = (props: IHelperProps) => ( <Helper role='helper-box'> <IconButton padding='6px 2px' onClick={props.onHelperClick}> @@ -132,6 +161,26 @@ export const HelperBox = (props: IHelperProps) => ( </IconButton> </Helper> ) +/* Jest-ignore-start ignore next */ +export const renderOptions = (options: TextFieldProps['options']) => { + console.log(options) + const comboOptions = + typeof options === 'string' ? coerceComboOptions(options) : options + + return ( + options && + // @ts-ignore + comboOptions.map((option: TextFieldProps) => ( + <ListItem + id={toLispCase(`option-${option.value}`)} + key={option.value} + disabled={option.disabled} + value={option.value}> + {option.label} + </ListItem> + )) + ) +} export const EditBox = (props: IEditProps) => { return ( @@ -164,6 +213,7 @@ const renderEndAdornment = (onClear?: () => void) => ( ) export const TextField = ({ + options, margin, padding, style = {}, @@ -178,6 +228,7 @@ export const TextField = ({ fullWidth, hasClear, onClear, + children, ...otherProps }: TextFieldProps) => { const clearStyle = makeStyles({ @@ -209,6 +260,7 @@ export const TextField = ({ return ( <Wrapper> <MuiTextField + select={!!options?.length} fullWidth={fullWidth} autoComplete={autoComplete} error={error} @@ -248,8 +300,9 @@ export const TextField = ({ ...endAdornment, ...SelectProps }} - {...otherProps} - /> + {...otherProps}> + {options ? renderOptions(options) : children} + </MuiTextField> {onHelperClick && ( <HelperBox helperIcon={helperIcon} diff --git a/src/core/inputs/text-field/text-field.spec.tsx b/src/core/inputs/text-field/text-field.spec.tsx index e8323514..fc422474 100644 --- a/src/core/inputs/text-field/text-field.spec.tsx +++ b/src/core/inputs/text-field/text-field.spec.tsx @@ -1,9 +1,17 @@ import * as React from 'react' import { fireEvent, render, screen } from '@testing-library/react' import TextField from '@/test/mocks/text-field-mock' +import TextFieldOptions from '@/test/mocks/text-field-options-mock' import userEvent from '@testing-library/user-event' import { act } from 'react-dom/test-utils' +const LIST = [ + { label: 'Elm', value: 'elm' }, + { label: 'ReasonML', value: 'reasonml' }, + { label: 'Purescript', value: 'purescript' }, + { label: 'Fable', value: 'fable' } +] + describe('TextField', () => { it('should render', () => { render(<TextField inputProps={{ placeholder: 'Description' }} />) @@ -92,4 +100,36 @@ describe('TextField', () => { expect(textField.value).toBe('Hello') }) + + it('should update on type', () => { + render( + <TextFieldOptions + inputProps={{ placeholder: 'Description' }} + options={LIST} + /> + ) + const textField = screen.getByPlaceholderText( + 'Description' + ) as HTMLInputElement + + fireEvent.change(textField, { target: { value: '' } }) + + expect(textField.value).toBe('') + }) + + it('should update on type', () => { + render( + <TextFieldOptions + inputProps={{ placeholder: 'Description' }} + options={JSON.stringify(LIST)} + /> + ) + const textField = screen.getByPlaceholderText( + 'Description' + ) as HTMLInputElement + + fireEvent.change(textField, { target: { value: '' } }) + + expect(textField.value).toBe('') + }) }) diff --git a/src/core/inputs/text-field/text-field.stories.tsx b/src/core/inputs/text-field/text-field.stories.tsx index 81ffd5b2..99c19c76 100644 --- a/src/core/inputs/text-field/text-field.stories.tsx +++ b/src/core/inputs/text-field/text-field.stories.tsx @@ -10,6 +10,12 @@ export default { } as Meta<typeof TextField> const Template: StoryFn<typeof TextField> = args => <TextField {...args} /> +const LIST = [ + { label: 'Elm', value: 'elm' }, + { label: 'ReasonML', value: 'reasonml' }, + { label: 'Purescript', value: 'purescript' }, + { label: 'Fable', value: 'fable' } +] export const Default = () => <TextField placeholder='Description' /> @@ -92,3 +98,34 @@ export const useWithSelectAndClear = () => { </div> ) } + +export const combobox = () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [value, setValue] = useState('fable') + + const onClear = () => { + setValue('') + } + + const handleChange = (event: ChangeEvent<HTMLInputElement>) => { + setValue(event.target.value) + } + + return ( + <div> + <TextField + options={LIST} + value={value} + hasClear + onClear={onClear} + onChange={handleChange} + /> + <TextField + value={value} + hasClear + onClear={onClear} + onChange={handleChange} + /> + </div> + ) +} diff --git a/src/test/mocks/text-field-options-mock.tsx b/src/test/mocks/text-field-options-mock.tsx new file mode 100644 index 00000000..552eb192 --- /dev/null +++ b/src/test/mocks/text-field-options-mock.tsx @@ -0,0 +1,32 @@ +import TextField, { IOption, TextFieldProps } from '@/core/inputs/text-field' +import * as React from 'react' + +interface IProps { + initialOption?: string + inputProps?: TextFieldProps + options?: IOption[] | string +} + +const Default = ({ inputProps, initialOption, options }: IProps) => { + const [value, setValue] = React.useState(initialOption ? initialOption : '') + + const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setValue(event.target.value) + } + + const handleClear = () => { + setValue('') + } + + return ( + <TextField + value={value} + onChange={handleChange} + onClear={handleClear} + {...inputProps} + options={options} + /> + ) +} + +export default Default