-
Notifications
You must be signed in to change notification settings - Fork 69
useControlledValue updates [LG-3608] #1953
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
Closed
Closed
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
753ba16
Extends `useControlledValue` to accept any type
TheSonOfThomp eec25b7
Merge branch 'main' into adam/generic-useControlledValue
TheSonOfThomp c565f26
Adds more explicit tests
TheSonOfThomp b612835
adds test component tests
TheSonOfThomp f4e1c61
adds value tests
TheSonOfThomp bbb1832
isControlled never changes from initial render
TheSonOfThomp af6cc48
adds synthetic events
TheSonOfThomp f6751e8
adds initialValue & warning
TheSonOfThomp 4f37e4b
Merge branch 'main' into adam/generic-useControlledValue
TheSonOfThomp 0fd6d6b
Update useControlledValue.spec.tsx
TheSonOfThomp 405229a
resolves circular dependency in lib
TheSonOfThomp 152db57
expect no console
TheSonOfThomp 3d0abb5
Merge branch 'main' into adam/generic-useControlledValue
TheSonOfThomp 50a45ae
Adds NextLink legacyBehavior test
TheSonOfThomp d2377e7
Merge branch 'fix-button-test-next' into adam/generic-useControlledValue
TheSonOfThomp 80dc30c
rm lib from dev dep
TheSonOfThomp a1c34e9
Merge branch 'main' into adam/generic-useControlledValue
TheSonOfThomp a697215
Update useControlledValue.spec.tsx
TheSonOfThomp 7a833ae
updates useControlled value to be more flexible
TheSonOfThomp cf61a27
add warnings
TheSonOfThomp d4af1bd
Update soft-berries-obey.md
TheSonOfThomp 799a672
Merge branch 'main' into adam/generic-useControlledValue
TheSonOfThomp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@leafygreen-ui/hooks': major | ||
--- | ||
|
||
- Extends `useControlledValue` to accept any type. | ||
- Adds `initialValue` argument. Used for setting the initial value for uncontrolled components. Without this we may encounter a React error for switching between controlled/uncontrolled inputs | ||
- Changes signature of `onChange` argument to be a simple setter function (`(value: T) => void`) | ||
- Changes return object to include only `isControlled`, `value` and an `updateValue` setter function |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 0 additions & 43 deletions
43
packages/hooks/src/useControlledValue/useControlledValue.spec.ts
This file was deleted.
Oops, something went wrong.
264 changes: 264 additions & 0 deletions
264
packages/hooks/src/useControlledValue/useControlledValue.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import React from 'react'; | ||
import { ChangeEventHandler } from 'react'; | ||
import { render } from '@testing-library/react'; | ||
import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
import { useControlledValue } from './useControlledValue'; | ||
|
||
const errorSpy = jest.spyOn(console, 'error'); | ||
|
||
const renderUseControlledValueHook = <T extends any>( | ||
...[valueProp, callback, initial]: Parameters<typeof useControlledValue<T>> | ||
): RenderHookResult<T, ReturnType<typeof useControlledValue<T>>> => { | ||
const result = renderHook(v => useControlledValue(v, callback, initial), { | ||
initialProps: valueProp, | ||
}); | ||
|
||
return { ...result }; | ||
}; | ||
|
||
describe('packages/hooks/useControlledValue', () => { | ||
beforeEach(() => { | ||
errorSpy.mockImplementation(() => {}); | ||
}); | ||
|
||
afterEach(() => { | ||
errorSpy.mockReset(); | ||
}); | ||
|
||
test('rendering without any arguments sets hook to uncontrolled', () => { | ||
const { result } = renderUseControlledValueHook(); | ||
expect(result.current.isControlled).toEqual(false); | ||
}); | ||
|
||
describe('accepts various value types', () => { | ||
test('accepts number values', () => { | ||
const { result } = renderUseControlledValueHook(5); | ||
expect(result.current.value).toBe(5); | ||
}); | ||
|
||
test('accepts boolean values', () => { | ||
const { result } = renderUseControlledValueHook(false); | ||
expect(result.current.value).toBe(false); | ||
}); | ||
|
||
test('accepts array values', () => { | ||
const arr = ['foo', 'bar']; | ||
const { result } = renderUseControlledValueHook(arr); | ||
expect(result.current.value).toBe(arr); | ||
}); | ||
|
||
test('accepts object values', () => { | ||
const obj = { foo: 'foo', bar: 'bar' }; | ||
const { result } = renderUseControlledValueHook(obj); | ||
expect(result.current.value).toBe(obj); | ||
}); | ||
|
||
test('accepts date values', () => { | ||
const date = new Date('2023-08-23'); | ||
const { result } = renderUseControlledValueHook(date); | ||
expect(result.current.value).toBe(date); | ||
}); | ||
|
||
test('accepts multiple/union types', () => { | ||
const { result, rerender } = renderUseControlledValueHook< | ||
string | number | ||
>(5); | ||
expect(result.current.value).toBe(5); | ||
rerender('foo'); | ||
expect(result.current.value).toBe('foo'); | ||
}); | ||
}); | ||
|
||
describe('Controlled', () => { | ||
test('rendering with a value sets value and isControlled', () => { | ||
const { result } = renderUseControlledValueHook('apple'); | ||
expect(result.current.isControlled).toBe(true); | ||
expect(result.current.value).toBe('apple'); | ||
}); | ||
|
||
test('rerendering from initial undefined sets value and isControlled', async () => { | ||
const { rerender, result } = renderUseControlledValueHook(); | ||
rerender('apple'); | ||
expect(result.current.isControlled).toBe(true); | ||
expect(result.current.value).toEqual('apple'); | ||
}); | ||
|
||
test('rerendering with a new value changes the value', () => { | ||
const { rerender, result } = renderUseControlledValueHook('apple'); | ||
expect(result.current.value).toBe('apple'); | ||
rerender('banana'); | ||
expect(result.current.value).toBe('banana'); | ||
}); | ||
|
||
test('provided handler is called within `updateValue`', () => { | ||
const handler = jest.fn(); | ||
const { result } = renderUseControlledValueHook<string>('apple', handler); | ||
result.current.updateValue('banana'); | ||
expect(handler).toHaveBeenCalledWith('banana'); | ||
}); | ||
|
||
test('hook value does not change when `updateValue` is called', () => { | ||
const { result } = renderUseControlledValueHook<string>('apple'); | ||
result.current.updateValue('banana'); | ||
// value doesn't change unless we explicitly change it | ||
expect(result.current.value).toBe('apple'); | ||
}); | ||
|
||
test('setting value to undefined should keep the component controlled', () => { | ||
const { rerender, result } = renderUseControlledValueHook('apple'); | ||
expect(result.current.isControlled).toBe(true); | ||
rerender(undefined); | ||
expect(result.current.isControlled).toBe(true); | ||
}); | ||
|
||
test('initial value is ignored when controlled', () => { | ||
const { result } = renderUseControlledValueHook<string>( | ||
'apple', | ||
() => {}, | ||
'banana', | ||
); | ||
expect(result.current.value).toBe('apple'); | ||
}); | ||
}); | ||
|
||
describe('Uncontrolled', () => { | ||
test('calling without a value sets value to `initialValue`', () => { | ||
const { | ||
result: { current }, | ||
} = renderUseControlledValueHook(undefined, () => {}, 'apple'); | ||
|
||
expect(current.isControlled).toBe(false); | ||
expect(current.value).toBe('apple'); | ||
}); | ||
|
||
test('provided handler is called within `updateValue`', () => { | ||
const handler = jest.fn(); | ||
const { | ||
result: { current }, | ||
} = renderUseControlledValueHook(undefined, handler); | ||
|
||
current.updateValue('apple'); | ||
expect(handler).toHaveBeenCalledWith('apple'); | ||
}); | ||
|
||
test('updateValue updates the value', () => { | ||
const { result } = renderUseControlledValueHook<string>(undefined); | ||
result.current.updateValue('banana'); | ||
expect(result.current.value).toBe('banana'); | ||
}); | ||
}); | ||
|
||
describe('Within test component', () => { | ||
const TestComponent = ({ | ||
valueProp, | ||
handlerProp, | ||
}: { | ||
valueProp?: string; | ||
handlerProp?: (val?: string) => void; | ||
}) => { | ||
const initialVal = ''; | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
const { value, updateValue } = useControlledValue( | ||
valueProp, | ||
handlerProp, | ||
initialVal, | ||
); | ||
|
||
const handleChange: ChangeEventHandler<HTMLInputElement> = e => { | ||
updateValue(e.target.value); | ||
}; | ||
|
||
return ( | ||
<> | ||
<input | ||
data-testid="test-input" | ||
value={value} | ||
onChange={handleChange} | ||
/> | ||
<button | ||
data-testid="test-button" | ||
onClick={() => updateValue('carrot')} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
describe('Controlled', () => { | ||
test('initially renders with a value', () => { | ||
const result = render(<TestComponent valueProp="apple" />); | ||
const input = result.getByTestId('test-input'); | ||
expect(input).toHaveValue('apple'); | ||
}); | ||
|
||
test('responds to value changes', () => { | ||
const result = render(<TestComponent valueProp="apple" />); | ||
const input = result.getByTestId('test-input'); | ||
result.rerender(<TestComponent valueProp="banana" />); | ||
expect(input).toHaveValue('banana'); | ||
}); | ||
|
||
test('user interaction triggers handler', () => { | ||
const handler = jest.fn(); | ||
const result = render( | ||
<TestComponent valueProp="apple" handlerProp={handler} />, | ||
); | ||
const input = result.getByTestId('test-input'); | ||
userEvent.type(input, 'b'); | ||
expect(handler).toHaveBeenCalledWith(expect.stringContaining('b')); | ||
}); | ||
|
||
test('user interaction does not change the element value', () => { | ||
const result = render(<TestComponent valueProp="apple" />); | ||
const input = result.getByTestId('test-input'); | ||
userEvent.type(input, 'b'); | ||
expect(input).toHaveValue('apple'); | ||
}); | ||
|
||
// eslint-disable-next-line jest/no-disabled-tests | ||
test.skip('user interaction does not change the element value when initially undefined', () => { | ||
const handler = jest.fn(); | ||
const result = render( | ||
<TestComponent valueProp={undefined} handlerProp={handler} />, | ||
); | ||
const input = result.getByTestId('test-input'); | ||
userEvent.type(input, 'b'); | ||
expect(input).toHaveValue('apple'); | ||
}); | ||
}); | ||
|
||
describe('Uncontrolled', () => { | ||
test('initially renders without a value', () => { | ||
const result = render(<TestComponent />); | ||
const input = result.getByTestId('test-input'); | ||
expect(input).toHaveValue(''); | ||
expect(errorSpy).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('user interaction triggers handler', () => { | ||
const handler = jest.fn(); | ||
const result = render(<TestComponent handlerProp={handler} />); | ||
const input = result.getByTestId('test-input'); | ||
userEvent.type(input, 'b'); | ||
expect(handler).toHaveBeenCalled(); | ||
}); | ||
|
||
test('user interaction does not change the element value', () => { | ||
TheSonOfThomp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const handler = jest.fn(); | ||
const result = render(<TestComponent handlerProp={handler} />); | ||
const input = result.getByTestId('test-input'); | ||
userEvent.type(input, 'banana'); | ||
expect(input).toHaveValue('banana'); | ||
}); | ||
|
||
test('clicking the button updates the value', () => { | ||
const result = render(<TestComponent />); | ||
const input = result.getByTestId('test-input'); | ||
const button = result.getByTestId('test-button'); | ||
userEvent.click(button); | ||
expect(input).toHaveValue('carrot'); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want this case to log an error?