-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react-composition): refactor ElementsContext (#5373)
Co-authored-by: Heather Buchel <[email protected]>
- Loading branch information
1 parent
e688f5d
commit 1783fe8
Showing
11 changed files
with
414 additions
and
170 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,73 @@ | ||
import React from 'react'; | ||
import { ElementDisplayName } from './types'; | ||
|
||
export interface Elements | ||
extends Partial<Record<ElementDisplayName, React.ComponentType>> {} | ||
|
||
const ElementsContext = React.createContext<Elements | undefined>(undefined); | ||
|
||
/** | ||
* `ElementsProvider` and its coresponding `useElement` hook provide | ||
* access to the values of the nearest ancestral `ElementsContext` | ||
* value. | ||
* | ||
* In most use cases, there is no need to directly invoke `useElement`; | ||
* `ElementsContext` lookup is handled directly by `BaseElement` | ||
* components returned by `defineBaseElement`. | ||
* | ||
* @example | ||
* | ||
* Add `ElementsContext` aware `BaseElement` components to a | ||
* Connected Component | ||
* | ||
* ```tsx | ||
* // `BaseElement`, renders custom or default element defintion | ||
* const ViewElement = defineBaseElement({ | ||
* displayName: "View", | ||
* type: "div", | ||
* }); | ||
* | ||
* // `BaseElement` components to be provided through `ElementsContext` | ||
* interface ConnectedComponentElements { | ||
* View: typeof ViewElement; | ||
* } | ||
* | ||
* function createConnectedComponent<T extends ConnectedComponentElements>( | ||
* elements?: T | ||
* ) { | ||
* const Provider = ({ children }: { children?: React.ReactNode }) => ( | ||
* <ElementsProvider elements={elements}> | ||
* <Children /> | ||
* </ElementsProvider> | ||
* ); | ||
* | ||
* function ConnectedComponent() { | ||
* return ( | ||
* <Provider> | ||
* <ConnectedComponentContent /> | ||
* </Provider> | ||
* ); | ||
* } | ||
* | ||
* ConnectedComponent.Provider = Provider; | ||
* | ||
* return ConnectedComponent; | ||
* } | ||
* ``` | ||
*/ | ||
export function ElementsProvider<T extends Elements>({ | ||
elements, | ||
...props | ||
}: { | ||
children?: React.ReactNode; | ||
elements?: T; | ||
}): React.JSX.Element { | ||
return <ElementsContext.Provider {...props} value={elements} />; | ||
} | ||
|
||
export const useElement = <T extends keyof Elements>( | ||
name: T | ||
): Elements[T] | undefined => { | ||
const context = React.useContext(ElementsContext); | ||
return context?.[name]; | ||
}; |
42 changes: 42 additions & 0 deletions
42
packages/react/src/context/elements/__tests__/ElementsContext.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,42 @@ | ||
import React from 'react'; | ||
import { renderHook } from '@testing-library/react-hooks'; | ||
|
||
import { ElementsProvider, useElement } from '../ElementsContext'; | ||
|
||
const ButtonElement = () => <button />; | ||
const ViewElement = () => <button />; | ||
|
||
const elements = { Button: ButtonElement, View: ViewElement }; | ||
|
||
describe('ElementsContext', () => { | ||
it('`useElement` reads `BaseElement` values passed to `ElementsProvider`', () => { | ||
const wrapper = ({ children }: { children?: React.ReactNode }) => ( | ||
<ElementsProvider elements={elements}>{children}</ElementsProvider> | ||
); | ||
|
||
const { | ||
result: { current: Button }, | ||
} = renderHook(() => useElement('Button'), { wrapper }); | ||
expect(Button).toBe(ButtonElement); | ||
|
||
const { | ||
result: { current: View }, | ||
} = renderHook(() => useElement('View'), { wrapper }); | ||
|
||
expect(View).toBe(ViewElement); | ||
}); | ||
|
||
it('`useElement` returns `undefined` when lookup fails', () => { | ||
const wrapper = ({ children }: { children?: React.ReactNode }) => ( | ||
<ElementsProvider elements={elements}>{children}</ElementsProvider> | ||
); | ||
|
||
const invalidElementName = 'Not a Button'; | ||
|
||
const { | ||
result: { current: Element }, | ||
// @ts-expect-error | ||
} = renderHook(() => useElement(invalidElementName), { wrapper }); | ||
expect(Element).toBeUndefined(); | ||
}); | ||
}); |
49 changes: 0 additions & 49 deletions
49
packages/react/src/context/elements/__tests__/createElementsContext.spec.tsx
This file was deleted.
Oops, something went wrong.
43 changes: 43 additions & 0 deletions
43
packages/react/src/context/elements/__tests__/defineBaseElement.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,43 @@ | ||
import React from 'react'; | ||
import { render } from '@testing-library/react'; | ||
import { ElementsProvider } from '../ElementsContext'; | ||
import defineBaseElement from '../defineBaseElement'; | ||
|
||
const displayName = 'Input'; | ||
const type = 'input'; | ||
|
||
describe('defineBaseElement', () => { | ||
it('renders a `BaseElement` of the provided element `type` and `displayName`', () => { | ||
const InputElement = defineBaseElement({ type, displayName }); | ||
|
||
expect(InputElement).toBeDefined(); | ||
expect(InputElement.displayName).toBe(displayName); | ||
|
||
const className = 'input-classname'; | ||
|
||
const { container } = render(<InputElement className={className} />); | ||
|
||
const element = container.querySelector('input'); | ||
|
||
expect(element).toBeDefined(); | ||
|
||
expect(element?.className).toBe(className); | ||
}); | ||
|
||
it('rendered `BaseElement` returns the value of `displayName` in `ElementsContext` if any', () => { | ||
const InputElement = defineBaseElement({ type, displayName }); | ||
const OverrideElement = () => <input type="checkbox" />; | ||
|
||
const { container } = render( | ||
<ElementsProvider elements={{ Input: OverrideElement }}> | ||
<InputElement /> | ||
</ElementsProvider> | ||
); | ||
|
||
const element = container.querySelector('input'); | ||
|
||
expect(element).toBeDefined(); | ||
|
||
expect(element?.type).toBe('checkbox'); | ||
}); | ||
}); |
101 changes: 101 additions & 0 deletions
101
packages/react/src/context/elements/__tests__/withBaseElementProps.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,101 @@ | ||
import React from 'react'; | ||
import { render } from '@testing-library/react'; | ||
import defineBaseElement from '../defineBaseElement'; | ||
import withBaseElementProps from '../withBaseElementProps'; | ||
|
||
const displayName = 'Input'; | ||
const type = 'input'; | ||
|
||
describe('withBaseElementProps', () => { | ||
it('applies a `defaultProps` object to a `Target` element', () => { | ||
const InputElement = defineBaseElement<'input', 'type'>({ | ||
type, | ||
displayName, | ||
}); | ||
|
||
const { container } = render(<InputElement />); | ||
|
||
const element = container.querySelector('input'); | ||
|
||
expect(element).toBeDefined(); | ||
|
||
expect(element?.type).toBe('text'); | ||
|
||
const defaultProps = { type: 'checkbox' }; | ||
|
||
const WrappedInputElement = withBaseElementProps( | ||
InputElement, | ||
defaultProps | ||
); | ||
|
||
const { container: wrappedContainer } = render(<WrappedInputElement />); | ||
|
||
const wrappedElement = wrappedContainer.querySelector('input'); | ||
|
||
expect(wrappedElement).toBeDefined(); | ||
|
||
expect(wrappedElement?.type).toBe('checkbox'); | ||
}); | ||
|
||
it('resolves and applies a `defaultProps` callback to a `Target` element', () => { | ||
const InputElement = defineBaseElement<'input', 'type'>({ | ||
type, | ||
displayName, | ||
}); | ||
|
||
const { container } = render(<InputElement />); | ||
|
||
const element = container.querySelector('input'); | ||
|
||
expect(element).toBeDefined(); | ||
|
||
expect(element?.type).toBe('text'); | ||
|
||
const defaultProps = { type: 'checkbox' }; | ||
|
||
const WrappedInputElement = withBaseElementProps( | ||
InputElement, | ||
() => defaultProps | ||
); | ||
|
||
const { container: wrappedContainer } = render(<WrappedInputElement />); | ||
|
||
const wrappedElement = wrappedContainer.querySelector('input'); | ||
|
||
expect(wrappedElement).toBeDefined(); | ||
|
||
expect(wrappedElement?.type).toBe('checkbox'); | ||
}); | ||
|
||
it('`defaultProps` are overriden by `props` passed to wrapped `BaseElement`', () => { | ||
const InputElement = defineBaseElement<'input', 'type'>({ | ||
type, | ||
displayName, | ||
}); | ||
|
||
const defaultProps = { type: 'checkbox' }; | ||
|
||
const WrappedInputElement = withBaseElementProps( | ||
InputElement, | ||
() => defaultProps | ||
); | ||
|
||
const { container } = render(<WrappedInputElement />); | ||
|
||
const element = container.querySelector('input'); | ||
|
||
expect(element).toBeDefined(); | ||
|
||
expect(element?.type).toBe('checkbox'); | ||
|
||
const { container: nextContainer } = render( | ||
<WrappedInputElement type="image" /> | ||
); | ||
|
||
const nextElement = nextContainer.querySelector('input'); | ||
|
||
expect(nextElement).toBeDefined(); | ||
|
||
expect(nextElement?.type).toBe('image'); | ||
}); | ||
}); |
27 changes: 0 additions & 27 deletions
27
packages/react/src/context/elements/createElementsContext.tsx
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.