Skip to content

Commit

Permalink
chore(react-composition): migrate elements utilities to ui-react-core (
Browse files Browse the repository at this point in the history
…#5382)

- migrate elements utilities to ui-react-core
- add ui-react-core/elements export subpath
- remove useElement
- update withBaseElementProps defaultProps behavior 
- add keys to ElementDisplayName
  • Loading branch information
calebpollman authored Jul 11, 2024
1 parent defb285 commit 8b4703e
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 32 deletions.
2 changes: 1 addition & 1 deletion packages/react-core/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Amplify UI React Core

`@aws-amplify/ui-react-core` is a React platform agnostic utility library for Amplify UI internal usage in the `@aws-amplify/ui-react` and `@aws-amplify/ui-react-native` packages.
`@aws-amplify/ui-react-core` is a React platform agnostic utility library for Amplify UI internal usage in `@aws-amplify/ui-react*` and `@aws-amplify/ui-react-native*` namespaced packages.
7 changes: 7 additions & 0 deletions packages/react-core/elements/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@aws-amplify/ui-react-core/elements",
"main": "../dist/elements.js",
"module": "../dist/esm/elements/index.mjs",
"sideEffects": false,
"types": "../dist/types/elements/index.d.ts"
}
6 changes: 6 additions & 0 deletions packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
"require": "./dist/index.js",
"types": "./dist/types/index.d.ts"
},
"./elements": {
"import": "./dist/esm/elements/index.mjs",
"require": "./dist/elements.js",
"types": "./dist/types/elements/index.d.ts",
"react-native": "./src/elements/index.ts"
},
"./package.json": "./package.json"
},
"types": "dist/types/index.d.ts",
Expand Down
4 changes: 3 additions & 1 deletion packages/react-core/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import typescript from '@rollup/plugin-typescript';
import externals from 'rollup-plugin-node-externals';

// common config settings
const input = ['src/index.ts'];

// { OUTPUT_PATH: INPUT_PATH }
const input = { index: 'src/index.ts', elements: 'src/elements/index.ts' };
const sourceMap = false;
const tsconfig = 'tsconfig.dist.json';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React from 'react';
import { ElementDisplayName } from './types';

/**
* @internal @unstable
*/
export interface Elements
extends Partial<Record<ElementDisplayName, React.ComponentType>> {}

const ElementsContext = React.createContext<Elements | undefined>(undefined);
export const ElementsContext = React.createContext<Elements | undefined>(
undefined
);

/**
* @internal @unstable
*
* `ElementsProvider` and its coresponding `useElement` hook provide
* access to the values of the nearest ancestral `ElementsContext`
* value.
Expand Down Expand Up @@ -64,10 +71,3 @@ export function ElementsProvider<T extends Elements>({
}): 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];
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';

import { ElementsProvider, useElement } from '../ElementsContext';
import { ElementsProvider, ElementsContext } from '../ElementsContext';

const ButtonElement = () => <button />;
const ViewElement = () => <button />;
Expand All @@ -16,27 +16,17 @@ describe('ElementsContext', () => {

const {
result: { current: Button },
} = renderHook(() => useElement('Button'), { wrapper });
} = renderHook(() => React.useContext(ElementsContext)?.['Button'], {
wrapper,
});
expect(Button).toBe(ButtonElement);

const {
result: { current: View },
} = renderHook(() => useElement('View'), { wrapper });
} = renderHook(() => React.useContext(ElementsContext)?.['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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,41 @@ describe('withBaseElementProps', () => {

expect(nextElement?.type).toBe('image');
});

it('provides `props` passed to wrapped `BaseElement` to `defaultProps` callback functions', () => {
const InputElement = defineBaseElement<'input', 'disabled' | 'type'>({
type,
displayName,
});

const defaultProps = { type: 'checkbox' };

const WrappedInputElement = withBaseElementProps(
InputElement,
({ disabled }) => ({
...defaultProps,
className: disabled ? 'input input--disabled' : 'input',
})
);

const { container } = render(<WrappedInputElement />);

const element = container.querySelector('input');

expect(element).toBeDefined();

expect(element?.type).toBe('checkbox');
expect(element?.className).toBe('input');

const { container: nextContainer } = render(
<WrappedInputElement disabled type="image" />
);

const nextElement = nextContainer.querySelector('input');

expect(nextElement).toBeDefined();

expect(nextElement?.type).toBe('image');
expect(nextElement?.className).toBe('input input--disabled');
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { useElement } from './ElementsContext';
import { ElementsContext } from './ElementsContext';
import {
BaseElement,
BaseElementProps,
Expand All @@ -9,6 +9,9 @@ import {
ReactElementType,
} from './types';

/**
* * @internal @unstable
*/
export interface DefineBaseElementInput<T> {
/**
* `BaseElement` display name in React dev tools and stack traces
Expand All @@ -22,6 +25,8 @@ export interface DefineBaseElementInput<T> {
}

/**
* @internal @unstable
*
* Defines a `ElementsContext` aware `BaseElement` UI component of the
* provided `type` with an assigned `displayName`.
*
Expand Down Expand Up @@ -51,7 +56,7 @@ export default function defineBaseElement<

const Element = React.forwardRef<ElementRefType<P>, P>(
({ variant, ...props }, ref) => {
const Element = useElement(displayName);
const Element = React.useContext(ElementsContext)?.[displayName];

if (Element) {
// only pass `variant` to provided `Element` values
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';

/**
* @internal @unstable
*
* Base type definition of `BaseElement` components available through
* `ElementsContext`. The definitions define a contract between a
* Connected Component and the `elements` that can be provided as
Expand All @@ -15,11 +17,37 @@ export type BaseElement<T = {}, K = {}> = React.ForwardRefExoticComponent<
React.PropsWithoutRef<T> & React.RefAttributes<K>
>;

type ListElementSubType = 'Ordered' | 'Unordered';
type ListElementDisplayName = 'List' | `${ListElementSubType}List`;

type TableElementSubType = 'Body' | 'Data' | 'Row' | 'Head' | 'Header';
type TableElementDisplayName = 'Table' | `Table${TableElementSubType}`;

/**
* @internal @unstable
*
* allowed values of `displayName` of `BaseElement` and `ElemebtsContext` keys
*/
export type ElementDisplayName = 'Button' | 'View' | 'Icon' | 'Input' | 'Span';
export type ElementDisplayName =
| 'Button'
| 'Divider'
| 'Heading' // h1, h2, etc
| 'Icon'
| 'Input'
| 'Label'
| 'ListItem'
| 'Nav'
| 'ProgressBar'
| 'Span'
| 'Text'
| 'Title'
| 'View'
| ListElementDisplayName
| TableElementDisplayName;

/**
* @internal @unstable
*/
export type ElementRefType<T> = T extends {
ref?:
| React.LegacyRef<infer K>
Expand All @@ -29,15 +57,27 @@ export type ElementRefType<T> = T extends {
? K
: never;

/**
* @internal @unstable
*/
export type ReactElementType = keyof React.JSX.IntrinsicElements;

/**
* @internal @unstable
*/
export type ReactElementProps<T extends ReactElementType> =
React.JSX.IntrinsicElements[T];

/**
* @internal @unstable
*
* key of `props` always available on `BaseElement` definitions
*/
type ElementPropKey<T> = T | 'children' | 'className' | 'style';

/**
* @internal @unstable
*/
export type BaseElementProps<
T extends keyof K,
V = string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import { BaseElement, ElementRefType } from './types';

/**
* @internal @unstable
*
* Extend target `BaseElement` with `defaultProps`. `defaultProps`
* are overidden by `props` provided to returned `BaseElement`.
*
Expand Down Expand Up @@ -30,14 +32,19 @@ import { BaseElement, ElementRefType } from './types';
* @param defaultProps `defaultProps` to apply to `Target`, accepts object or callback
* @returns extended `BaseElement` with `defaultProps`
*/
export default function withBaseElementProps<T, K extends T | (() => T)>(
export default function withBaseElementProps<
T,
K extends T | ((input: T) => T),
>(
Target: React.ForwardRefExoticComponent<T>,
defaultProps: K
): BaseElement<T, ElementRefType<T>> {
const Component = React.forwardRef<ElementRefType<T>, T>((props, ref) => (
<Target
{...{
...(typeof defaultProps === 'function' ? defaultProps() : defaultProps),
...(typeof defaultProps === 'function'
? defaultProps(props)
: defaultProps),
...props,
}}
ref={ref}
Expand Down

0 comments on commit 8b4703e

Please sign in to comment.