-
Notifications
You must be signed in to change notification settings - Fork 257
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui-react-core): add createContextUtilities (#4331)
Co-authored-by: Ioana Brooks <68251134+ioanabrooks@users.noreply.github.com>
- Loading branch information
1 parent
2ccbb88
commit 54d884d
Showing
12 changed files
with
266 additions
and
26 deletions.
There are no files selected for viewing
This file contains 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,7 @@ | ||
--- | ||
"@aws-amplify/ui-react-core": patch | ||
"@aws-amplify/ui-react": patch | ||
"@aws-amplify/ui": patch | ||
--- | ||
|
||
feat(ui-react-core): add createContextUtilities |
This file contains 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
This file contains 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
This file contains 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 @@ | ||
export type MergeProps<C, P> = C & Omit<P, keyof C>; |
71 changes: 71 additions & 0 deletions
71
packages/react-core/src/utils/__tests__/createContextUtilities.spec.tsx
This file contains 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,71 @@ | ||
import React from 'react'; | ||
import { renderHook } from '@testing-library/react-hooks'; | ||
|
||
import createContextUtilities, { | ||
INVALID_OPTIONS_MESSAGE, | ||
} from '../createContextUtilities'; | ||
|
||
interface Stuff { | ||
items: Record<string, number> | undefined; | ||
things: number | undefined; | ||
} | ||
|
||
const contextName = 'Stuff'; | ||
const errorMessage = '`useStuff` must be used in a `StuffProvider`'; | ||
|
||
describe('createContextUtilities', () => { | ||
it('utility hook exposes the expected values of a defined defaultValue', () => { | ||
const defaultValue: Stuff = { items: { '1': 1, '2': 2 }, things: 1 }; | ||
|
||
const { useContext: useStuff } = createContextUtilities({ | ||
contextName, | ||
defaultValue, | ||
}); | ||
|
||
const { result } = renderHook(useStuff); | ||
|
||
expect(result.current).toStrictEqual(defaultValue); | ||
}); | ||
|
||
it('throws an error when defaultValue is undefined and no errorMessage is provided', () => { | ||
const defaultValue = undefined as unknown as {}; | ||
|
||
expect(() => createContextUtilities({ contextName, defaultValue })).toThrow( | ||
INVALID_OPTIONS_MESSAGE | ||
); | ||
}); | ||
|
||
it('throws an error when no options are provided', () => { | ||
// @ts-expect-error | ||
expect(() => createContextUtilities()).toThrow(INVALID_OPTIONS_MESSAGE); | ||
}); | ||
|
||
it('utility hook exposes expected values without a defaultValue provided', () => { | ||
const { Provider: StuffProvider, useContext: useStuff } = | ||
createContextUtilities<Stuff>({ | ||
contextName, | ||
errorMessage, | ||
}); | ||
|
||
const { result } = renderHook(useStuff, { | ||
wrapper: (props: { children: React.ReactNode }) => ( | ||
<StuffProvider {...props} /> | ||
), | ||
}); | ||
|
||
expect(result.current).toStrictEqual({}); | ||
}); | ||
|
||
it('utility hook throws an error when no defaultValue is provided and used outside its context', () => { | ||
const errorMessage = 'Must be used in a `StuffProvider`'; | ||
|
||
const { useContext: useStuff } = createContextUtilities<Stuff>({ | ||
contextName, | ||
errorMessage, | ||
}); | ||
|
||
const { result } = renderHook(useStuff); | ||
|
||
expect(result.error?.message).toStrictEqual(errorMessage); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
111 changes: 111 additions & 0 deletions
111
packages/react-core/src/utils/createContextUtilities.tsx
This file contains 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,111 @@ | ||
import React from 'react'; | ||
|
||
import { isUndefined, isString } from '@aws-amplify/ui'; | ||
|
||
export const INVALID_OPTIONS_MESSAGE = | ||
'an `errorMessage` or a `defaultValue` must be provided in `options`'; | ||
|
||
type OptionsWithDefaultValue<ContextType extends {}> = { | ||
contextName: string; | ||
defaultValue: ContextType; | ||
}; | ||
|
||
type OptionsWithErrorMessage = { | ||
contextName: string; | ||
errorMessage: string; | ||
}; | ||
|
||
type ContextOptions<ContextType extends {}> = | ||
| OptionsWithDefaultValue<ContextType> | ||
| OptionsWithErrorMessage; | ||
|
||
/** | ||
* Use a `ContextType` generic and `options` to create: | ||
* - `Context`: React Context of type `ContextType` | ||
* - `Provider`: React Context `Provider` component exposing the `ContextType` | ||
* as optional props | ||
* - `useContext`: Utility Hook exposing the values of `ContextType` | ||
* | ||
* @template ContextType Type definition of the Context. | ||
* > For most use cases the keys of `ContextType` should not be optional in | ||
* preference of explicit `undefined` to avoid having optional types on the | ||
* Utility Hook return | ||
* | ||
* @param options Context utility options. Requires a `contextName`, and either | ||
* a `defaultValue` of `ContextType` or `errorMessage` allowing for differing | ||
* behaviors of the Utility Hook when used outside a parent `Provider`: | ||
* | ||
* - `defaultValue`: Ensures the Utility Hook returns a default value for | ||
* scenarios **where the missing context values should not impact usage** | ||
* - `errorMessage`: Ensures the Utility Hook throws an error for | ||
* scenarios **where the missing context values should prevent** usage | ||
* | ||
* @returns `Context`, `Provider` Component and `useContext` Utility Hook | ||
* | ||
* @usage | ||
* ```ts | ||
* interface StuffContextType { | ||
* things: number; | ||
* } | ||
* | ||
* // with `defaultValue` | ||
* const [StuffProvider, useStuff] = createContextUtilities<StuffContextType>({ | ||
* contextName: 'Stuff', | ||
* defaultValue: { things: 7 } | ||
* }); | ||
* | ||
* // with `errorMessage` | ||
* const [StuffProvider, useStuff] = createContextUtilities<StuffContextType>({ | ||
* contextName: 'Stuff', | ||
* errorMessage: '`useStuff` must be used in a `StuffProvider`' | ||
* }); | ||
* ``` | ||
*/ | ||
export default function createContextUtilities<ContextType extends {}>( | ||
options: ContextOptions<ContextType> | ||
): { | ||
Provider: React.ComponentType<React.PropsWithChildren<Partial<ContextType>>>; | ||
useContext: () => ContextType; | ||
Context: React.Context<ContextType | undefined>; | ||
} { | ||
const { contextName, defaultValue, errorMessage } = | ||
(options as { | ||
defaultValue: ContextType; | ||
errorMessage: string; | ||
contextName: string; | ||
}) ?? {}; | ||
|
||
if (isUndefined(defaultValue) && !isString(errorMessage)) { | ||
throw new Error(INVALID_OPTIONS_MESSAGE); | ||
} | ||
|
||
const Context = React.createContext<ContextType | undefined>(defaultValue); | ||
|
||
function Provider(props: React.PropsWithChildren<Partial<ContextType>>) { | ||
const { children, ...context } = props; | ||
const value = React.useMemo( | ||
() => context, | ||
// Unpack `context` for the dep array; using `[context]` results in | ||
// evaluation on every render | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
Object.values(context) | ||
) as ContextType; | ||
return <Context.Provider value={value}>{children}</Context.Provider>; | ||
} | ||
|
||
Provider.displayName = `${contextName}Provider`; | ||
|
||
return { | ||
Context, | ||
Provider, | ||
useContext: function () { | ||
const context = React.useContext(Context); | ||
|
||
if (isUndefined(context)) { | ||
throw new Error(errorMessage); | ||
} | ||
|
||
return context; | ||
}, | ||
}; | ||
} |
This file contains 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 |
---|---|---|
@@ -1,11 +1 @@ | ||
import { isString } from '@aws-amplify/ui'; | ||
|
||
export function templateJoin( | ||
values: string[], | ||
template: (value: string) => string | ||
): string { | ||
return values.reduce( | ||
(acc, curr) => `${acc}${isString(curr) ? template(curr) : ''}`, | ||
'' | ||
); | ||
} | ||
export { default as createContextUtilities } from './createContextUtilities'; |
This file contains 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
This file contains 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
This file contains 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
This file contains 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