Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ClearButton to TextField #279

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
ClearButton,
ClearButtonProvider,
TextFieldWithClear,
} from '@kitsuyui/react-textfield'
import React from 'react'

import type { Meta, StoryObj } from '@storybook/react'

const Something = () => {
return (
<ClearButtonProvider>
<TextFieldWithClear />
<ClearButton>{'reset'}</ClearButton>
</ClearButtonProvider>
)
}

const meta: Meta<typeof Something> = {
title: 'Base/TextField/Something/Example',
component: Something,
argTypes: {},
}

export default meta
type Story = StoryObj<typeof Something>

export const Default: Story = {
args: {
value: '',
placeholder: '🔍 something',
},
decorators: [
(Story) => {
return <Story />
},
],
}
96 changes: 96 additions & 0 deletions packages/textfield/src/ClearButtonProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
useCallback,
useState,
useContext,
forwardRef,
createContext,
ComponentPropsWithoutRef,
useEffect,
} from 'react'
import React from 'react'

import { TextArea, WrapperProps as TextAreaWrapperProps } from './TextArea'
import { TextField, WrapperProps as TextFieldWrapperProps } from './TextField'

const TextContext = createContext('')
const SetTextContext = createContext((_text: string) => {})
const ClearContext = createContext(() => {})

type WrappedProps = ComponentPropsWithoutRef<'button'>
type WrapperProps = WrappedProps

export const ClearButtonProvider = (props: { children: React.ReactNode }) => {
const [text, setText] = useState('')
const [handleInputChunk] = useState<(text: string) => void>()
const clear = useCallback(() => {
setText('')
handleInputChunk?.('')
}, [handleInputChunk, setText])
return (
<TextContext.Provider value={text}>
<SetTextContext.Provider value={setText}>
<ClearContext.Provider value={clear}>
{props.children}
</ClearContext.Provider>
</SetTextContext.Provider>
</TextContext.Provider>
)
}

export const TextFieldWithClear = (props: TextFieldWrapperProps) => {
const text = useContext(TextContext)
const setText = useContext(SetTextContext)
const onInputChunk = props.onInputChunk
const handleInputChunk = useCallback(
(text: string) => {
setText(text)
onInputChunk?.(text)
},
[setText, onInputChunk]
)
useEffect(() => {
setText(props.value ?? '')
}, [props.value, setText])

return <TextField {...props} value={text} onInputChunk={handleInputChunk} />
}

export const TextAreaWithClear = (props: TextAreaWrapperProps) => {
const text = useContext(TextContext)
const setText = useContext(SetTextContext)
const onInputChunk = props.onInputChunk
const handleInputChunk = useCallback(
(text: string) => {
setText(text)
onInputChunk?.(text)
},
[setText, onInputChunk]
)
useEffect(() => {
setText(props.value ?? '')
}, [props.value, setText])

return <TextArea {...props} value={text} onInputChunk={handleInputChunk} />
}

export const ClearButton = forwardRef<HTMLButtonElement, WrapperProps>(
(props, ref) => {
const propsExcludedWrapperProps = Object.assign({}, props)
const clear = useContext(ClearContext)
const { onClick } = props
const handleClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
clear()
onClick?.(e)
},
[clear, onClick]
)
return (
<button {...propsExcludedWrapperProps} ref={ref} onClick={handleClick}>
{props.children}
</button>
)
}
)

ClearButton.displayName = 'ClearButton'
7 changes: 6 additions & 1 deletion packages/textfield/src/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ComponentPropsWithoutRef,
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
Expand All @@ -24,7 +25,7 @@ type excludeProps =
| 'onCompositionEnd'
| 'value'

type WrapperProps = Omit<WrappedProps, excludeProps> & alternateProps
export type WrapperProps = Omit<WrappedProps, excludeProps> & alternateProps

export const TextArea = forwardRef<HTMLTextAreaElement, WrapperProps>(
(props, ref) => {
Expand All @@ -39,6 +40,10 @@ export const TextArea = forwardRef<HTMLTextAreaElement, WrapperProps>(
delete propsExcludedWrapperProps.onInputChunk
delete propsExcludedWrapperProps.onChangeInputting

useEffect(() => {
setInternalValue(props.value ?? '')
}, [props.value])

const handleChange = useCallback(() => {
const text = innerRef.current.value
setInternalValue(text)
Expand Down
9 changes: 7 additions & 2 deletions packages/textfield/src/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useState,
forwardRef,
ComponentPropsWithoutRef,
useEffect,
} from 'react'
import React from 'react'

Expand All @@ -14,7 +15,7 @@ type WrappedProps = ComponentPropsWithoutRef<'input'>
type alternateProps = {
onInputChunk?: (value: string) => void
onChangeInputting?: (inputting: boolean) => void
value: string
value?: string
}

type excludeProps =
Expand All @@ -25,7 +26,7 @@ type excludeProps =
| 'value'
| 'type'

type WrapperProps = Omit<WrappedProps, excludeProps> & alternateProps
export type WrapperProps = Omit<WrappedProps, excludeProps> & alternateProps

export const TextField = forwardRef<HTMLInputElement, WrapperProps>(
(props, ref) => {
Expand All @@ -40,6 +41,10 @@ export const TextField = forwardRef<HTMLInputElement, WrapperProps>(
delete propsExcludedWrapperProps.onInputChunk
delete propsExcludedWrapperProps.onChangeInputting

useEffect(() => {
setInternalValue(props.value ?? '')
}, [props.value])

const handleChange = useCallback(() => {
const text = innerRef.current.value
setInternalValue(text)
Expand Down
5 changes: 5 additions & 0 deletions packages/textfield/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export { TextField } from './TextField'
export { TextArea } from './TextArea'
export {
ClearButton,
ClearButtonProvider,
TextFieldWithClear,
} from './ClearButtonProvider'
70 changes: 70 additions & 0 deletions packages/textfield/src/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'

import {
TextFieldWithClear,
TextAreaWithClear,
ClearButtonProvider,
ClearButton,
} from './ClearButtonProvider'
import { TextArea } from './TextArea'
import { TextField } from './TextField'

Expand Down Expand Up @@ -73,3 +79,67 @@ test('render TextField', async () => {
expect(element).toHaveProperty('value', firstMessage + secondMessage)
expect(handleChange).toBeCalledTimes(secondMessage.length)
})

test('render TextField with ClearButton', async () => {
const firstMessage = 'Hello'
const secondMessage = ', World'
const handleInputChunk = jest.fn()

// initial render
render(
<ClearButtonProvider>
<TextFieldWithClear
value={firstMessage}
onInputChunk={handleInputChunk}
/>
<ClearButton>Clear</ClearButton>
</ClearButtonProvider>
)
const element = screen.getByDisplayValue(firstMessage)
expect(element).toBeInstanceOf(HTMLInputElement)
expect(element).toHaveProperty('value', firstMessage)

// click and type
await userEvent.click(element)
await userEvent.type(element, secondMessage)
expect(element).toHaveProperty('value', firstMessage + secondMessage)
expect(handleInputChunk).toBeCalledTimes(secondMessage.length)

// click reset button
const resetButton = screen.getByText('Clear')
await userEvent.click(resetButton)
expect(element).toHaveProperty('value', '')
// handleInputChunk is also called when reset button is clicked.
expect(handleInputChunk).toBeCalledTimes(secondMessage.length + 1)
})

test('render TextArea with ClearButton', async () => {
const firstMessage = 'Hello'
const secondMessage = ', World'
const handleInputChunk = jest.fn()

// initial render
render(
<ClearButtonProvider>
<TextAreaWithClear value={firstMessage} onInputChunk={handleInputChunk} />
<ClearButton>Clear</ClearButton>
</ClearButtonProvider>
)

const element = screen.getByText(firstMessage)
expect(element).toBeInstanceOf(HTMLTextAreaElement)
expect(element).toHaveProperty('value', firstMessage)

// click and type
await userEvent.click(element)
await userEvent.type(element, secondMessage)
expect(element).toHaveProperty('value', firstMessage + secondMessage)
expect(handleInputChunk).toBeCalledTimes(secondMessage.length)

// click reset button
const resetButton = screen.getByText('Clear')
await userEvent.click(resetButton)
expect(element).toHaveProperty('value', '')
// handleInputChunk is also called when reset button is clicked.
expect(handleInputChunk).toBeCalledTimes(secondMessage.length + 1)
})
Loading