From c9ee2f5a232e27b88a8525de0e25cab07238cc52 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 11 Apr 2024 20:37:43 +1000 Subject: [PATCH] [core] Change to * imports and support shorthand render prop --- .../mui-base/src/Checkbox/Checkbox.test.tsx | 50 ++++++++--------- .../mui-base/src/Checkbox/Checkbox.types.ts | 2 +- .../src/Checkbox/CheckboxIndicator.test.tsx | 26 ++++----- .../src/Checkbox/CheckboxIndicator.tsx | 3 +- .../{Checkbox.tsx => CheckboxRoot.tsx} | 17 +++--- packages/mui-base/src/Checkbox/index.ts | 9 +--- .../src/NumberField/NumberField.test.tsx | 11 ++-- .../src/NumberField/NumberField.types.ts | 2 +- .../NumberField/NumberFieldDecrement.test.tsx | 42 +++++++-------- .../src/NumberField/NumberFieldDecrement.tsx | 3 +- .../src/NumberField/NumberFieldGroup.test.tsx | 6 +-- .../src/NumberField/NumberFieldGroup.tsx | 3 +- .../NumberField/NumberFieldIncrement.test.tsx | 42 +++++++-------- .../src/NumberField/NumberFieldIncrement.tsx | 3 +- .../src/NumberField/NumberFieldInput.test.tsx | 54 +++++++++---------- .../src/NumberField/NumberFieldInput.tsx | 3 +- .../{NumberField.tsx => NumberFieldRoot.tsx} | 13 ++--- .../NumberField/NumberFieldScrubArea.test.tsx | 18 +++---- .../src/NumberField/NumberFieldScrubArea.tsx | 3 +- .../NumberFieldScrubAreaCursor.test.tsx | 6 +-- .../NumberFieldScrubAreaCursor.tsx | 6 ++- packages/mui-base/src/NumberField/index.ts | 25 +++------ packages/mui-base/src/Switch/Switch.test.tsx | 44 +++++++-------- packages/mui-base/src/Switch/Switch.types.ts | 2 +- .../src/Switch/{Switch.tsx => SwitchRoot.tsx} | 10 ++-- .../mui-base/src/Switch/SwitchThumb.test.tsx | 2 +- packages/mui-base/src/Switch/SwitchThumb.tsx | 3 +- packages/mui-base/src/Switch/index.ts | 9 +--- packages/mui-base/src/utils/BaseUI.types.ts | 4 +- .../mui-base/src/utils/evaluateRenderProp.ts | 12 +++++ 30 files changed, 220 insertions(+), 213 deletions(-) rename packages/mui-base/src/Checkbox/{Checkbox.tsx => CheckboxRoot.tsx} (90%) rename packages/mui-base/src/NumberField/{NumberField.tsx => NumberFieldRoot.tsx} (93%) rename packages/mui-base/src/Switch/{Switch.tsx => SwitchRoot.tsx} (94%) create mode 100644 packages/mui-base/src/utils/evaluateRenderProp.ts diff --git a/packages/mui-base/src/Checkbox/Checkbox.test.tsx b/packages/mui-base/src/Checkbox/Checkbox.test.tsx index e2e0471167..e40b4b14cc 100644 --- a/packages/mui-base/src/Checkbox/Checkbox.test.tsx +++ b/packages/mui-base/src/Checkbox/Checkbox.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { createRenderer, act } from '@mui/internal-test-utils'; -import { Checkbox } from '.'; +import * as Checkbox from '@base_ui/react/Checkbox'; import { describeConformance } from '../../test/describeConformance'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); -describe('', () => { +describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ inheritComponent: 'button', refInstanceof: window.HTMLButtonElement, render, @@ -18,13 +18,13 @@ describe('', () => { describe('extra props', () => { it('can override the built-in attributes', () => { - const { container } = render(); + const { container } = render(); expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); }); }); it('should change its state when clicked', () => { - const { getAllByRole, container } = render(); + const { getAllByRole, container } = render(); const [checkbox] = getAllByRole('checkbox'); const input = container.querySelector('input[type=checkbox]') as HTMLInputElement; @@ -52,7 +52,7 @@ describe('', () => { return (
- ; + ;
); } @@ -77,7 +77,7 @@ describe('', () => { it('should call onChange when clicked', () => { const handleChange = spy(); - const { getAllByRole, container } = render(); + const { getAllByRole, container } = render(); const [checkbox] = getAllByRole('checkbox'); const input = container.querySelector('input[type=checkbox]') as HTMLInputElement; @@ -91,17 +91,17 @@ describe('', () => { describe('prop: disabled', () => { it('should have the `aria-disabled` attribute', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-disabled', 'true'); }); it('should not have the aria attribute when `disabled` is not set', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).not.to.have.attribute('aria-disabled'); }); it('should not change its state when clicked', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const [checkbox] = getAllByRole('checkbox'); expect(checkbox).to.have.attribute('aria-checked', 'false'); @@ -116,17 +116,17 @@ describe('', () => { describe('prop: readOnly', () => { it('should have the `aria-readonly` attribute', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-readonly', 'true'); }); it('should not have the aria attribute when `readOnly` is not set', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).not.to.have.attribute('aria-readonly'); }); it('should not change its state when clicked', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const [checkbox] = getAllByRole('checkbox'); expect(checkbox).to.have.attribute('aria-checked', 'false'); @@ -141,12 +141,12 @@ describe('', () => { describe('prop: indeterminate', () => { it('should set the `aria-checked` attribute as "mixed"', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-checked', 'mixed'); }); it('should not change its state when clicked', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const [checkbox] = getAllByRole('checkbox'); expect(checkbox).to.have.attribute('aria-checked', 'mixed'); @@ -159,23 +159,23 @@ describe('', () => { }); it('should not set the `data-indeterminate` attribute', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).to.not.have.attribute('data-indeterminate', 'true'); }); it('should not have the aria attribute when `indeterminate` is not set', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).not.to.have.attribute('aria-checked', 'mixed'); }); it('should not be overridden by `checked` prop', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-checked', 'mixed'); }); }); it('should update its state if the underlying input is toggled', () => { - const { getAllByRole, container } = render(); + const { getAllByRole, container } = render(); const [checkbox] = getAllByRole('checkbox'); const input = container.querySelector('input[type=checkbox]') as HTMLInputElement; @@ -188,9 +188,9 @@ describe('', () => { it('should place the style hooks on the root and the indicator', () => { const { getAllByRole } = render( - + - , + , ); const [checkbox] = getAllByRole('checkbox'); @@ -208,7 +208,7 @@ describe('', () => { }); it('should set the name attribute on the input', () => { - const { container } = render(); + const { container } = render(); const input = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; expect(input).to.have.attribute('name', 'checkbox-name'); @@ -223,7 +223,7 @@ describe('', () => { const { getByTestId, getAllByRole } = render( , ); @@ -251,7 +251,7 @@ describe('', () => { - + , ); @@ -284,7 +284,7 @@ describe('', () => { stringifiedFormData = new URLSearchParams(formData as any).toString(); }} > - + , ); diff --git a/packages/mui-base/src/Checkbox/Checkbox.types.ts b/packages/mui-base/src/Checkbox/Checkbox.types.ts index 58aa3c6f63..c286b82ced 100644 --- a/packages/mui-base/src/Checkbox/Checkbox.types.ts +++ b/packages/mui-base/src/Checkbox/Checkbox.types.ts @@ -9,7 +9,7 @@ export type CheckboxOwnerState = { indeterminate: boolean; }; -export interface CheckboxProps +export interface CheckboxRootProps extends UseCheckboxParameters, Omit, 'onChange'> {} diff --git a/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx b/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx index 5f2441dd89..c2ee36e168 100644 --- a/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx +++ b/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { createRenderer } from '@mui/internal-test-utils'; import { describeConformance } from '../../test/describeConformance'; -import { Checkbox } from '.'; +import * as Checkbox from '@base_ui/react/Checkbox'; import { CheckboxContext } from './CheckboxContext'; const testContext = { @@ -29,9 +29,9 @@ describe('', () => { it('should not render indicator by default', () => { const { container } = render( - + - , + , ); const indicator = container.querySelector('span'); expect(indicator).to.equal(null); @@ -39,9 +39,9 @@ describe('', () => { it('should render indicator when checked', () => { const { container } = render( - + - , + , ); const indicator = container.querySelector('span'); expect(indicator).not.to.equal(null); @@ -49,9 +49,9 @@ describe('', () => { it('should spread extra props', () => { const { container } = render( - + - , + , ); const indicator = container.querySelector('span'); expect(indicator).to.have.attribute('data-extra-prop', 'Lorem ipsum'); @@ -60,9 +60,9 @@ describe('', () => { describe('keepMounted prop', () => { it('should keep indicator mounted when unchecked', () => { const { container } = render( - + - , + , ); const indicator = container.querySelector('span'); expect(indicator).not.to.equal(null); @@ -70,9 +70,9 @@ describe('', () => { it('should keep indicator mounted when checked', () => { const { container } = render( - + - , + , ); const indicator = container.querySelector('span'); expect(indicator).not.to.equal(null); @@ -80,9 +80,9 @@ describe('', () => { it('should keep indicator mounted when indeterminate', () => { const { container } = render( - + - , + , ); const indicator = container.querySelector('span'); expect(indicator).not.to.equal(null); diff --git a/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx b/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx index 587d81f746..6996db5123 100644 --- a/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx +++ b/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx @@ -4,6 +4,7 @@ import type { CheckboxIndicatorProps } from './Checkbox.types'; import { CheckboxContext } from './CheckboxContext'; import { resolveClassName } from '../utils/resolveClassName'; import { useCheckboxStyleHooks } from './utils'; +import { evaluateRenderProp } from '../utils/evaluateRenderProp'; function defaultRender(props: React.ComponentPropsWithRef<'span'>) { return ; @@ -45,7 +46,7 @@ const CheckboxIndicator = React.forwardRef(function CheckboxIndicator( ...otherProps, }; - return render(elementProps, ownerState); + return evaluateRenderProp(render, elementProps, ownerState); }); CheckboxIndicator.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Checkbox/Checkbox.tsx b/packages/mui-base/src/Checkbox/CheckboxRoot.tsx similarity index 90% rename from packages/mui-base/src/Checkbox/Checkbox.tsx rename to packages/mui-base/src/Checkbox/CheckboxRoot.tsx index 0cbf27e590..f44973fd0f 100644 --- a/packages/mui-base/src/Checkbox/Checkbox.tsx +++ b/packages/mui-base/src/Checkbox/CheckboxRoot.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import type { CheckboxOwnerState, CheckboxProps } from './Checkbox.types'; +import type { CheckboxOwnerState, CheckboxRootProps } from './Checkbox.types'; import { resolveClassName } from '../utils/resolveClassName'; import { CheckboxContext } from './CheckboxContext'; import { useCheckbox } from '../useCheckbox'; import { useCheckboxStyleHooks } from './utils'; +import { evaluateRenderProp } from '../utils/evaluateRenderProp'; function defaultRender(props: React.ComponentPropsWithRef<'button'>) { return - ; + ; ); } @@ -58,7 +58,7 @@ describe('', () => { }); it('should update its state if the underlying input is toggled', () => { - const { getByRole, container } = render(); + const { getByRole, container } = render(); const switchElement = getByRole('switch'); const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; @@ -72,7 +72,7 @@ describe('', () => { describe('extra props', () => { it('should override the built-in attributes', () => { - const { container } = render(); + const { container } = render(); expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'checkbox'); expect(container.firstElementChild as HTMLElement).to.have.attribute('data-state', 'checked'); }); @@ -81,7 +81,7 @@ describe('', () => { describe('prop: onChange', () => { it('should call onChange when clicked', () => { const handleChange = spy(); - const { getByRole, container } = render(); + const { getByRole, container } = render(); const switchElement = getByRole('switch'); const internalInput = container.querySelector('input[type="checkbox"]')!; @@ -97,7 +97,7 @@ describe('', () => { describe('prop: onClick', () => { it('should call onClick when clicked', () => { const handleClick = spy(); - const { getByRole } = render(); + const { getByRole } = render(); const switchElement = getByRole('switch'); act(() => { @@ -110,17 +110,17 @@ describe('', () => { describe('prop: disabled', () => { it('should have the `aria-disabled` attribute', () => { - const { getByRole } = render(); + const { getByRole } = render(); expect(getByRole('switch')).to.have.attribute('aria-disabled', 'true'); }); it('should not have the aria attribute when `disabled` is not set', () => { - const { getByRole } = render(); + const { getByRole } = render(); expect(getByRole('switch')).not.to.have.attribute('aria-disabled'); }); it('should not change its state when clicked', () => { - const { getByRole } = render(); + const { getByRole } = render(); const switchElement = getByRole('switch'); expect(switchElement).to.have.attribute('aria-checked', 'false'); @@ -135,17 +135,17 @@ describe('', () => { describe('prop: readOnly', () => { it('should have the `aria-readonly` attribute', () => { - const { getByRole } = render(); + const { getByRole } = render(); expect(getByRole('switch')).to.have.attribute('aria-readonly', 'true'); }); it('should not have the aria attribute when `readOnly` is not set', () => { - const { getByRole } = render(); + const { getByRole } = render(); expect(getByRole('switch')).not.to.have.attribute('aria-readonly'); }); it('should not change its state when clicked', () => { - const { getByRole } = render(); + const { getByRole } = render(); const switchElement = getByRole('switch'); expect(switchElement).to.have.attribute('aria-checked', 'false'); @@ -161,7 +161,7 @@ describe('', () => { describe('prop: inputRef', () => { it('should be able to access the native input', () => { const inputRef = React.createRef(); - const { container } = render(); + const { container } = render(); const internalInput = container.querySelector('input[type="checkbox"]')!; expect(inputRef.current).to.equal(internalInput); @@ -172,7 +172,7 @@ describe('', () => { it('should toggle the switch when a parent label is clicked', () => { const { getByTestId, getByRole } = render( , ); @@ -195,7 +195,7 @@ describe('', () => { - + , ); @@ -227,7 +227,7 @@ describe('', () => { stringifiedFormData = new URLSearchParams(formData as any).toString(); }} > - + , ); @@ -251,9 +251,9 @@ describe('', () => { it('should place the style hooks on the root and the thumb', () => { const { getByRole } = render( - + - , + , ); const switchElement = getByRole('switch'); @@ -271,7 +271,7 @@ describe('', () => { }); it('should set the name attribute on the input', () => { - const { container } = render(); + const { container } = render(); const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; expect(internalInput).to.have.attribute('name', 'switch-name'); diff --git a/packages/mui-base/src/Switch/Switch.types.ts b/packages/mui-base/src/Switch/Switch.types.ts index 7c20bfc925..f7cd90a973 100644 --- a/packages/mui-base/src/Switch/Switch.types.ts +++ b/packages/mui-base/src/Switch/Switch.types.ts @@ -8,7 +8,7 @@ export type SwitchOwnerState = { required: boolean; }; -export interface SwitchProps +export interface SwitchRootProps extends UseSwitchParameters, Omit, 'onChange'> {} diff --git a/packages/mui-base/src/Switch/Switch.tsx b/packages/mui-base/src/Switch/SwitchRoot.tsx similarity index 94% rename from packages/mui-base/src/Switch/Switch.tsx rename to packages/mui-base/src/Switch/SwitchRoot.tsx index 817a6bc941..893c3fdcb4 100644 --- a/packages/mui-base/src/Switch/Switch.tsx +++ b/packages/mui-base/src/Switch/SwitchRoot.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import refType from '@mui/utils/refType'; import { useSwitch } from '../useSwitch'; -import type { SwitchProps, SwitchOwnerState } from './Switch.types'; +import type { SwitchRootProps, SwitchOwnerState } from './Switch.types'; import { resolveClassName } from '../utils/resolveClassName'; import { SwitchContext } from './SwitchContext'; import { useSwitchStyleHooks } from './useSwitchStyleHooks'; @@ -23,8 +23,8 @@ function defaultRender(props: React.ComponentPropsWithRef<'button'>) { * * - [Switch API](https://mui.com/base-ui/react-switch/components-api/#switch) */ -const Switch = React.forwardRef(function Switch( - props: SwitchProps, +const SwitchRoot = React.forwardRef(function SwitchRoot( + props: SwitchRootProps, forwardedRef: React.ForwardedRef, ) { const { @@ -72,7 +72,7 @@ const Switch = React.forwardRef(function Switch( ); }); -Switch.propTypes /* remove-proptypes */ = { +SwitchRoot.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -134,4 +134,4 @@ Switch.propTypes /* remove-proptypes */ = { required: PropTypes.bool, } as any; -export { Switch }; +export { SwitchRoot }; diff --git a/packages/mui-base/src/Switch/SwitchThumb.test.tsx b/packages/mui-base/src/Switch/SwitchThumb.test.tsx index ddeac1b6ae..2179affe02 100644 --- a/packages/mui-base/src/Switch/SwitchThumb.test.tsx +++ b/packages/mui-base/src/Switch/SwitchThumb.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { createRenderer } from '@mui/internal-test-utils'; -import { Switch } from '@base_ui/react/Switch'; +import * as Switch from '@base_ui/react/Switch'; import { describeConformance } from '../../test/describeConformance'; import { SwitchContext } from './SwitchContext'; diff --git a/packages/mui-base/src/Switch/SwitchThumb.tsx b/packages/mui-base/src/Switch/SwitchThumb.tsx index 401972106f..d2cd9861a4 100644 --- a/packages/mui-base/src/Switch/SwitchThumb.tsx +++ b/packages/mui-base/src/Switch/SwitchThumb.tsx @@ -4,6 +4,7 @@ import { SwitchThumbProps } from './Switch.types'; import { SwitchContext } from './SwitchContext'; import { resolveClassName } from '../utils/resolveClassName'; import { useSwitchStyleHooks } from './useSwitchStyleHooks'; +import { evaluateRenderProp } from '../utils/evaluateRenderProp'; function defaultRender(props: React.ComponentPropsWithRef<'span'>) { return ; @@ -31,7 +32,7 @@ const SwitchThumb = React.forwardRef(function SwitchThumb( ...other, }; - return render(elementProps, ownerState); + return evaluateRenderProp(render, elementProps, ownerState); }); SwitchThumb.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Switch/index.ts b/packages/mui-base/src/Switch/index.ts index 5dad8a63c3..4f424d807c 100644 --- a/packages/mui-base/src/Switch/index.ts +++ b/packages/mui-base/src/Switch/index.ts @@ -1,10 +1,5 @@ 'use client'; -import { combineComponentExports } from '../utils/combineComponentExports'; -import { Switch as SwitchRoot } from './Switch'; -import { SwitchThumb } from './SwitchThumb'; +export { SwitchRoot as Root } from './SwitchRoot'; +export { SwitchThumb as Thumb } from './SwitchThumb'; export * from './Switch.types'; - -export const Switch = combineComponentExports(SwitchRoot, { - Thumb: SwitchThumb, -}); diff --git a/packages/mui-base/src/utils/BaseUI.types.ts b/packages/mui-base/src/utils/BaseUI.types.ts index 01882aad5f..c95d40e135 100644 --- a/packages/mui-base/src/utils/BaseUI.types.ts +++ b/packages/mui-base/src/utils/BaseUI.types.ts @@ -40,5 +40,7 @@ export type BaseUIComponentProps, OwnerState>; + render?: + | ComponentRenderFn, OwnerState> + | React.ReactElement; }; diff --git a/packages/mui-base/src/utils/evaluateRenderProp.ts b/packages/mui-base/src/utils/evaluateRenderProp.ts new file mode 100644 index 0000000000..e23b3a67ed --- /dev/null +++ b/packages/mui-base/src/utils/evaluateRenderProp.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { BaseUIComponentProps } from './BaseUI.types'; + +export function evaluateRenderProp( + render: BaseUIComponentProps['render'], + props: React.HTMLAttributes, + ownerState: OwnerState, +) { + return typeof render === 'function' + ? render(props, ownerState) + : React.cloneElement(render, props); +}