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

fix: Allow setting Textbox "value" dynamically #1606

Merged
merged 8 commits into from
Sep 5, 2022
60 changes: 56 additions & 4 deletions ui/src/textbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { fireEvent, render } from '@testing-library/react'
import { fireEvent, render, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import { Textbox, XTextbox } from './textbox'
import { wave } from './ui'

const name = 'textbox'
const textboxProps: Textbox = { name }
let textboxProps: Textbox = { name }

describe('Textbox.tsx', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.useFakeTimers()
wave.args[name] = null
textboxProps = { name }
aalencar marked this conversation as resolved.
Show resolved Hide resolved
})

it('Renders data-test attr', () => {
Expand All @@ -51,7 +53,7 @@ describe('Textbox.tsx', () => {
it('Sets args on input', () => {
const { getByTestId } = render(<XTextbox model={textboxProps} />)
fireEvent.change(getByTestId(name), { target: { value: 'text' } })
jest.runOnlyPendingTimers()
act(() => { jest.runOnlyPendingTimers() })
aalencar marked this conversation as resolved.
Show resolved Hide resolved

expect(wave.args[name]).toBe('text')
})
Expand Down Expand Up @@ -79,10 +81,26 @@ describe('Textbox.tsx', () => {
fireEvent.change(getByTestId(name), { target: { value: 'aaa' } })

expect(pushMock).not.toBeCalled() // Not called immediately, but after specified timeout.
jest.runOnlyPendingTimers()
act(() => { jest.advanceTimersByTime(250) })
expect(pushMock).not.toBeCalled()
act(() => { jest.advanceTimersByTime(250) })
aalencar marked this conversation as resolved.
Show resolved Hide resolved
expect(pushMock).toBeCalled()
})

it('Debounces wave push', () => {
const { getByTestId } = render(<XTextbox model={{...textboxProps, trigger: true}} />)
const pushMock = jest.fn()
wave.push = pushMock

userEvent.type(getByTestId(name), 'a')
userEvent.type(getByTestId(name), 'a')
userEvent.type(getByTestId(name), 'a')
userEvent.type(getByTestId(name), 'a')
act(() => { jest.runOnlyPendingTimers() })

expect(pushMock).toBeCalledTimes(1)
})

it('Does not call sync on change - trigger not specified', () => {
const { getByTestId } = render(<XTextbox model={textboxProps} />)

Expand All @@ -93,4 +111,38 @@ describe('Textbox.tsx', () => {

expect(pushMock).not.toBeCalled()
})

it('Display new value when "value" prop changes', () => {
const { getByTestId, rerender } = render(<XTextbox model={{...textboxProps, value: 'A'}} />)
expect(getByTestId(name)).toHaveValue('A')

rerender(<XTextbox model={{...textboxProps, value: 'B'}} />)
expect(getByTestId(name)).toHaveValue('B')
})

it('Types value and then display new value when "value" prop changes', () => {
const { getByTestId, rerender } = render(<XTextbox model={textboxProps} />)
userEvent.type(getByTestId(name), '{backspace}A{Enter}')
expect(getByTestId(name)).toHaveValue('A')

rerender(<XTextbox model={{...textboxProps, value: 'B'}} />)
expect(getByTestId(name)).toHaveValue('B')
})

it('Display new value when "value" prop changes (masked)', () => {
const { getByTestId, rerender } = render(<XTextbox model={{...textboxProps, value: '123', mask: '(999)'}} />)
expect(getByTestId(name)).toHaveValue('(123)')

rerender(<XTextbox model={{...textboxProps, value: '456', mask: '(999)'}} />)
expect(getByTestId(name)).toHaveValue('(456)')
})

it('Types value and then display new value when "value" prop changes (masked)', () => {
const { getByTestId, rerender } = render(<XTextbox model={{...textboxProps, mask: '(999)'}} />)
userEvent.type(getByTestId(name), '{backspace}123{Enter}')
expect(getByTestId(name)).toHaveValue('(123)')

rerender(<XTextbox model={{...textboxProps, value: '456', mask: '(999)'}} />)
expect(getByTestId(name)).toHaveValue('(456)')
})
})
13 changes: 9 additions & 4 deletions ui/src/textbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,25 @@ const DEBOUNCE_TIMEOUT = 500
export const
XTextbox = ({ model: m }: { model: Textbox }) => {
const
[value, setValue] = React.useState(m.value ?? ''),
debounceRef = React.useRef(debounce(DEBOUNCE_TIMEOUT, () => wave.push())),
mturoci marked this conversation as resolved.
Show resolved Hide resolved
onChange = ({ target }: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, v?: S) => {
v = v || (target as HTMLInputElement).value

wave.args[m.name] = v ?? (m.value || '')
if (m.trigger) wave.push()
if (m.trigger) debounceRef.current()
setValue(v)
m.value = v
},
textFieldProps: Fluent.ITextFieldProps & { 'data-test': S } = {
'data-test': m.name,
label: m.label,
value,
errorMessage: m.error,
required: m.required,
disabled: m.disabled,
readOnly: m.readonly,
onChange: m.trigger ? debounce(DEBOUNCE_TIMEOUT, onChange) : onChange,
onChange,
iconProps: m.icon ? { iconName: m.icon } : undefined,
placeholder: m.placeholder,
prefix: m.prefix,
Expand All @@ -96,9 +101,10 @@ export const

// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = m.value || '' }, [])
React.useEffect(() => { setValue(m.value ?? '')}, [m.value])

return m.mask
? <Fluent.MaskedTextField mask={m.mask} {...textFieldProps} value={m.value} />
? <Fluent.MaskedTextField mask={m.mask} {...textFieldProps} />
: (
<Fluent.TextField
styles={
Expand All @@ -107,7 +113,6 @@ export const
: undefined
}
{...textFieldProps}
defaultValue={m.value}
/>
)
}