From c3c9b970e544c9a97387db8bdc2bf160f0dfba66 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 1 Feb 2023 16:55:40 +0800 Subject: [PATCH 01/70] [base] Add useNumberInput --- .../UnstyledNumberInputIntroduction.js | 70 ++++++ .../UnstyledNumberInputIntroduction.tsx | 75 ++++++ ...nstyledNumberInputIntroduction.tsx.preview | 1 + .../components/number-input/number-input.md | 33 +++ docs/data/base/pages.ts | 1 + docs/data/base/pagesApi.js | 4 + .../base-ui/api/number-input-unstyled.json | 22 ++ docs/pages/base-ui/api/use-number-input.json | 37 +++ docs/pages/base-ui/react-number-input.js | 7 + .../react-number-input/[docsTab]/index.js | 40 ++++ .../pages/base-ui/react-number-input/index.js | 13 + .../number-input-unstyled.json | 9 + .../number-input-unstyled.json | 9 + .../use-number-input/use-number-input.json | 10 + docs/translations/translations.json | 1 + .../mui-base/src/FormControl/FormControl.tsx | 9 + .../NumberInputUnstyled.test.tsx | 35 +++ .../NumberInputUnstyled.tsx | 89 +++++++ .../NumberInputUnstyled.types.ts | 52 ++++ .../src/NumberInputUnstyled/clamp.test.ts | 23 ++ .../mui-base/src/NumberInputUnstyled/clamp.ts | 30 +++ .../mui-base/src/NumberInputUnstyled/index.ts | 7 + .../useNumberInput.test.tsx | 59 +++++ .../src/NumberInputUnstyled/useNumberInput.ts | 225 ++++++++++++++++++ .../useNumberInput.types.ts | 55 +++++ packages/mui-base/src/index.d.ts | 3 + packages/mui-base/src/index.js | 3 + 27 files changed, 922 insertions(+) create mode 100644 docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js create mode 100644 docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx create mode 100644 docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx.preview create mode 100644 docs/data/base/components/number-input/number-input.md create mode 100644 docs/pages/base-ui/api/number-input-unstyled.json create mode 100644 docs/pages/base-ui/api/use-number-input.json create mode 100644 docs/pages/base-ui/react-number-input.js create mode 100644 docs/pages/base-ui/react-number-input/[docsTab]/index.js create mode 100644 docs/pages/base-ui/react-number-input/index.js create mode 100644 docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json create mode 100644 docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json create mode 100644 docs/translations/api-docs/use-number-input/use-number-input.json create mode 100644 packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx create mode 100644 packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx create mode 100644 packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts create mode 100644 packages/mui-base/src/NumberInputUnstyled/clamp.test.ts create mode 100644 packages/mui-base/src/NumberInputUnstyled/clamp.ts create mode 100644 packages/mui-base/src/NumberInputUnstyled/index.ts create mode 100644 packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx create mode 100644 packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts create mode 100644 packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js new file mode 100644 index 00000000000000..72eee5960b8715 --- /dev/null +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js @@ -0,0 +1,70 @@ +import * as React from 'react'; +import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; +import { styled } from '@mui/system'; + +const blue = { + 100: '#DAECFF', + 200: '#b6daff', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', +}; + +const StyledInputElement = styled('input')( + ({ theme }) => ` + width: 320px; + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + padding: 12px; + border-radius: 12px; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 4px 30px ${theme.palette.mode === 'dark' ? grey[900] : grey[200]}; + + &:hover { + border-color: ${blue[400]}; + } + + &:focus { + border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + // firefox + &:focus-visible { + outline: 0; + } +`, +); + +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + return ( + + ); +}); + +export default function UnstyledNumberInputIntroduction() { + return ( + + ); +} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx new file mode 100644 index 00000000000000..11f3be865d0716 --- /dev/null +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import NumberInputUnstyled, { + NumberInputUnstyledProps, +} from '@mui/base/NumberInputUnstyled'; +import { styled } from '@mui/system'; + +const blue = { + 100: '#DAECFF', + 200: '#b6daff', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', +}; + +const StyledInputElement = styled('input')( + ({ theme }) => ` + width: 320px; + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + padding: 12px; + border-radius: 12px; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 4px 30px ${theme.palette.mode === 'dark' ? grey[900] : grey[200]}; + + &:hover { + border-color: ${blue[400]}; + } + + &:focus { + border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + // firefox + &:focus-visible { + outline: 0; + } +`, +); + +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: NumberInputUnstyledProps, + ref: React.ForwardedRef, +) { + return ( + + ); +}); + +export default function UnstyledNumberInputIntroduction() { + return ( + + ); +} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx.preview b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx.preview new file mode 100644 index 00000000000000..fa6c5c5dc81d85 --- /dev/null +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md new file mode 100644 index 00000000000000..674444d1ce4f68 --- /dev/null +++ b/docs/data/base/components/number-input/number-input.md @@ -0,0 +1,33 @@ +--- +product: base +title: Unstyled React Number Input component and hook +components: NumberInputUnstyled +# hooks: useNumberInput +githubLabel: 'component: NumberInput' +--- + +# Unstyled Number Input + +

The Unstyled Number Input component provides users with a field for integer values, and buttons to increment or decrement the value.

+ +## Introduction + +🚧 + +{{"demo": "UnstyledNumberInputIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} + +{{"component": "modules/components/ComponentLinkHeader.js", "design": false}} + +## Component + +### Usage + +After [installation](/base/getting-started/installation/), you can start building with this component using the following basic elements: + +```jsx +import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; + +export default function MyApp() { + return ; +} +``` diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts index ac8a24e1875487..07c34afea6ba4f 100644 --- a/docs/data/base/pages.ts +++ b/docs/data/base/pages.ts @@ -25,6 +25,7 @@ const pages: readonly MuiPage[] = [ { pathname: '/base-ui/react-button', title: 'Button' }, { pathname: '/base-ui/react-checkbox', title: 'Checkbox', planned: true }, { pathname: '/base-ui/react-input', title: 'Input' }, + { pathname: '/base-ui/react-number-input', title: 'Number Input' }, { pathname: '/base-ui/react-radio-button', title: 'Radio Button', planned: true }, { pathname: '/base-ui/react-rating', title: 'Rating', planned: true }, { pathname: '/base-ui/react-select', title: 'Select' }, diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 87fb37f2e44ef1..adc2ebbb5078ac 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -24,6 +24,10 @@ module.exports = [ { pathname: '/base-ui/react-menu/components-api/#menu-item', title: 'MenuItem' }, { pathname: '/base-ui/react-modal/components-api/#modal', title: 'Modal' }, { pathname: '/base-ui/react-no-ssr/components-api/#no-ssr', title: 'NoSsr' }, + { + pathname: '/base-ui/react-number-input/components-api/#number-input-unstyled', + title: 'NumberInputUnstyled', + }, { pathname: '/base-ui/react-select/components-api/#option', title: 'Option' }, { pathname: '/base-ui/react-select/components-api/#option-group', diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json new file mode 100644 index 00000000000000..d937ca8161e907 --- /dev/null +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -0,0 +1,22 @@ +{ + "props": { + "id": { "type": { "name": "string" } }, + "slotProps": { + "type": { "name": "shape", "description": "{ input?: func
| object }" }, + "default": "{}" + }, + "slots": { + "type": { "name": "shape", "description": "{ input?: elementType }" }, + "default": "{}" + } + }, + "name": "NumberInputUnstyled", + "styles": { "classes": [], "globalClasses": {}, "name": null }, + "spread": true, + "themeDefaultProps": null, + "muiName": "MuiNumberInputUnstyled", + "filename": "/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json new file mode 100644 index 00000000000000..7edf41f8c7f27b --- /dev/null +++ b/docs/pages/base-ui/api/use-number-input.json @@ -0,0 +1,37 @@ +{ + "parameters": { + "defaultValue": { "type": { "name": "unknown", "description": "unknown" } }, + "disabled": { "type": { "name": "boolean", "description": "boolean" } }, + "error": { "type": { "name": "boolean", "description": "boolean" } }, + "inputRef": { + "type": { + "name": "React.Ref<HTMLInputElement>", + "description": "React.Ref<HTMLInputElement>" + } + }, + "max": { "type": { "name": "number", "description": "number" } }, + "min": { "type": { "name": "number", "description": "number" } }, + "onBlur": { + "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } + }, + "onChange": { + "type": { + "name": "UseNumberInputChangeHandler", + "description": "UseNumberInputChangeHandler" + } + }, + "onClick": { + "type": { "name": "React.MouseEventHandler", "description": "React.MouseEventHandler" } + }, + "onFocus": { + "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } + }, + "required": { "type": { "name": "boolean", "description": "boolean" } }, + "step": { "type": { "name": "number", "description": "number" } }, + "value": { "type": { "name": "unknown", "description": "unknown" } } + }, + "returnValue": {}, + "name": "useNumberInput", + "filename": "/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts", + "demos": "
    " +} diff --git a/docs/pages/base-ui/react-number-input.js b/docs/pages/base-ui/react-number-input.js new file mode 100644 index 00000000000000..01bef2bfa30c7d --- /dev/null +++ b/docs/pages/base-ui/react-number-input.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docs/data/base/components/number-input/number-input.md?@mui/markdown'; + +export default function Page() { + return ; +} diff --git a/docs/pages/base-ui/react-number-input/[docsTab]/index.js b/docs/pages/base-ui/react-number-input/[docsTab]/index.js new file mode 100644 index 00000000000000..0f7112e16e6d40 --- /dev/null +++ b/docs/pages/base-ui/react-number-input/[docsTab]/index.js @@ -0,0 +1,40 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs/data/base/components/number-input/number-input.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import NumberInputUnstyledApiJsonPageContent from '../../api/number-input-unstyled.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const NumberInputUnstyledApiReq = require.context( + 'docs/translations/api-docs-base/number-input-unstyled', + false, + /number-input-unstyled.*.json$/, + ); + const NumberInputUnstyledApiDescriptions = mapApiPageTranslations(NumberInputUnstyledApiReq); + + return { + props: { + componentsApiDescriptions: { NumberInputUnstyled: NumberInputUnstyledApiDescriptions }, + componentsApiPageContents: { NumberInputUnstyled: NumberInputUnstyledApiJsonPageContent }, + hooksApiDescriptions: {}, + hooksApiPageContents: {}, + }, + }; +}; diff --git a/docs/pages/base-ui/react-number-input/index.js b/docs/pages/base-ui/react-number-input/index.js new file mode 100644 index 00000000000000..124fb94411844c --- /dev/null +++ b/docs/pages/base-ui/react-number-input/index.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs/data/base/components/number-input/number-input.md?@mui/markdown'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; diff --git a/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json new file mode 100644 index 00000000000000..d271db44327172 --- /dev/null +++ b/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "id": "The id of the input element.", + "slotProps": "The props used for each slot inside the Input.", + "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details." + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json new file mode 100644 index 00000000000000..d271db44327172 --- /dev/null +++ b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "id": "The id of the input element.", + "slotProps": "The props used for each slot inside the Input.", + "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details." + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json new file mode 100644 index 00000000000000..73535952943abe --- /dev/null +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -0,0 +1,10 @@ +{ + "hookDescription": "", + "parametersDescriptions": { + "defaultValue": "The default value. Use when the component is not controlled.", + "disabled": "If true, the component is disabled.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", + "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", + "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component." + }, + "returnValueDescriptions": {} +} diff --git a/docs/translations/translations.json b/docs/translations/translations.json index e4cdd4d1e8ef6f..66a53dc18c9e3c 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -237,6 +237,7 @@ "/base-ui/react-button": "Button", "/base-ui/react-checkbox": "Checkbox", "/base-ui/react-input": "Input", + "/base-ui/react-number-input": "Number Input", "/base-ui/react-radio-button": "Radio Button", "/base-ui/react-rating": "Rating", "/base-ui/react-select": "Select", diff --git a/packages/mui-base/src/FormControl/FormControl.tsx b/packages/mui-base/src/FormControl/FormControl.tsx index 771ad6cade02c8..ffb7d4d89403c9 100644 --- a/packages/mui-base/src/FormControl/FormControl.tsx +++ b/packages/mui-base/src/FormControl/FormControl.tsx @@ -127,6 +127,15 @@ const FormControl = React.forwardRef(function FormControl< setValue(event.target.value); onChange?.(event); }, + // TODO: make it like this? to work with SelectUnstyled as well + // onChange: (event: React.ChangeEvent, valueTwo?: unknown) => { + // console.group('FormControlUnstyledContext onChange'); + // console.log(event); + // console.log('valueTwo', valueTwo); + // console.groupEnd(); + // setValue(valueTwo ?? event.target.value); + // onChange?.(event, valueTwo); + // }, onFocus: () => { setFocused(true); }, diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx new file mode 100644 index 00000000000000..19b6c06d501a72 --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { /* createMount, */ createRenderer /* , describeConformanceUnstyled */ } from 'test/utils'; +import { expect } from 'chai'; +import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; + +describe('', () => { + // const mount = createMount(); + const { render } = createRenderer(); + + // TODO: wow this looks complicated + // describeConformanceUnstyled(, () => ({ + // inheritComponent: 'div', + // render, + // mount, + // refInstanceof: window.HTMLDivElement, + // testComponentPropWith: 'div', + // muiName: 'MuiInput', + // slots: { + // root: { + // expectedClassName: '', + // }, + // input: { + // expectedClassName: '', + // testWithElement: 'input', + // }, + // }, + // })); + + it('should be able to attach input ref passed through props', () => { + const inputRef = React.createRef(); + const { getByRole } = render(); + + expect(inputRef.current).to.deep.equal(getByRole('spinbutton')); + }); +}); diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx new file mode 100644 index 00000000000000..38343d6f80412c --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import useNumberInput from './useNumberInput'; +import { + NumberInputUnstyledProps, + NumberInputUnstyledOwnerState, +} from './NumberInputUnstyled.types'; +import { EventHandlers, useSlotProps } from '../utils'; +/** + * + * Demos: + * + * - [hooks: useNumberInput](https://mui.com/base/react-number-input/) + * + * API: + * + * - [NumberInputUnstyled API](https://mui.com/base/react-number-input/components-api/#number-input-unstyled) + */ +const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( + props: NumberInputUnstyledProps, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + forwardedRef: // TODO: this is for the root slot later + React.ForwardedRef, +) { + // set up ALL the props + const { id, slotProps = {}, slots = {} } = props; + + const { getInputProps } = useNumberInput(props); + + const ownerState: NumberInputUnstyledOwnerState = { + ...props, + type: 'text', + }; + + const propsToForward = { + id, + }; + + // define the root slot + + // root -> useSlotProps + + // define the input slot + const Input = slots.input ?? 'input'; + + const inputProps = useSlotProps({ + elementType: Input, + getSlotProps: (otherHandlers: EventHandlers) => + getInputProps({ ...otherHandlers, ...propsToForward }), + externalSlotProps: slotProps.input, + additionalProps: {}, + ownerState, + }); + + // input -> useSlotProps + return ; +}); + +NumberInputUnstyled.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, + /** + * The id of the `input` element. + */ + id: PropTypes.string, + /** + * The props used for each slot inside the Input. + * @default {} + */ + slotProps: PropTypes.shape({ + input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + }), + /** + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. + * @default {} + */ + slots: PropTypes.shape({ + input: PropTypes.elementType, + }), +} as any; + +export default NumberInputUnstyled; diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts new file mode 100644 index 00000000000000..5f0822a84cc2d1 --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts @@ -0,0 +1,52 @@ +import { OverrideProps, Simplify } from '@mui/types'; +import { UseNumberInputParameters } from './useNumberInput.types'; +import { SlotComponentProps } from '../utils'; + +export interface NumberInputUnstyledComponentsPropsOverrides {} + +export type NumberInputUnstyledOwnProps = {} & Omit & { + /** + * The id of the `input` element. + */ + id?: string; + /** + * The props used for each slot inside the Input. + * @default {} + */ + slotProps?: { + input?: SlotComponentProps< + 'input', + NumberInputUnstyledComponentsPropsOverrides, + NumberInputUnstyledOwnerState + >; + }; + /** + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. + * @default {} + */ + slots?: { + // root?: React.ElementType; + input?: React.ElementType; + }; + }; + +export interface NumberInputUnstyledTypeMap

    { + props: P & NumberInputUnstyledOwnProps; + defaultComponent: D; +} + +export type NumberInputUnstyledProps< + D extends React.ElementType = NumberInputUnstyledTypeMap['defaultComponent'], + P = {}, +> = OverrideProps, D> & { + component?: D; +}; + +export type NumberInputUnstyledOwnerState = Simplify< + Omit & { + // formControlContext: FormControlUnstyledState | undefined; + // focused: boolean; + type: React.InputHTMLAttributes['type'] | undefined; + } +>; diff --git a/packages/mui-base/src/NumberInputUnstyled/clamp.test.ts b/packages/mui-base/src/NumberInputUnstyled/clamp.test.ts new file mode 100644 index 00000000000000..7c9781b863eb01 --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/clamp.test.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai'; +import clamp from './clamp'; + +describe('clamp', () => { + it('clamps a value based on min and max', () => { + expect(clamp(1, 2, 4)).to.equal(2); + expect(clamp(5, 2, 4)).to.equal(4); + expect(clamp(-5, -1, 5)).to.equal(-1); + }); + + it('clamps a value between min and max and on a valid step', () => { + expect(clamp(2, -15, 15, 3)).to.equal(3); + expect(clamp(-1, -15, 15, 3)).to.equal(0); + expect(clamp(5, -15, 15, 3)).to.equal(6); + expect(clamp(-5, -15, 15, 3)).to.equal(-6); + expect(clamp(-55, -15, 15, 3)).to.equal(-15); + expect(clamp(57, -15, 15, 3)).to.equal(15); + expect(clamp(3, -20, 20, 5)).to.equal(5); + expect(clamp(2, -20, 20, 5)).to.equal(0); + expect(clamp(8, -20, 20, 5)).to.equal(10); + expect(clamp(-7, -20, 20, 5)).to.equal(-5); + }); +}); diff --git a/packages/mui-base/src/NumberInputUnstyled/clamp.ts b/packages/mui-base/src/NumberInputUnstyled/clamp.ts new file mode 100644 index 00000000000000..c96c80eddcad2c --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/clamp.ts @@ -0,0 +1,30 @@ +function simpleClamp( + val: number, + min: number = Number.MIN_SAFE_INTEGER, + max: number = Number.MAX_SAFE_INTEGER, +): number { + return Math.max(min, Math.min(val, max)); +} + +export default function clamp( + val: number, + min: number = Number.MIN_SAFE_INTEGER, + max: number = Number.MAX_SAFE_INTEGER, + stepProp: number = NaN, +): number { + if (Number.isNaN(stepProp)) { + return simpleClamp(val, min, max); + } + + const step = stepProp || 1; + + const remainder = val % step; + + const positivity = Math.sign(remainder); + + if (Math.abs(remainder) > step / 2) { + return simpleClamp(val + positivity * (step - Math.abs(remainder)), min, max); + } + + return simpleClamp(val - positivity * Math.abs(remainder), min, max); +} diff --git a/packages/mui-base/src/NumberInputUnstyled/index.ts b/packages/mui-base/src/NumberInputUnstyled/index.ts new file mode 100644 index 00000000000000..0fc14b5ffc9d2e --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/index.ts @@ -0,0 +1,7 @@ +export { default } from './NumberInputUnstyled'; + +export * from './NumberInputUnstyled.types'; + +export { default as useNumberInput } from './useNumberInput'; + +export * from './useNumberInput.types'; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx new file mode 100644 index 00000000000000..8740cf9d7c363e --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { spy } from 'sinon'; +import * as React from 'react'; +import { createRenderer, screen, act, fireEvent } from 'test/utils'; +import { useNumberInput, UseNumberInputParameters } from './index'; + +describe('useNumberInput', () => { + const { render } = createRenderer(); + const invokeUseNumberInput = (props: UseNumberInputParameters) => { + const ref = React.createRef>(); + function TestComponent() { + const numberInputDefinition = useNumberInput(props); + React.useImperativeHandle(ref, () => numberInputDefinition, [numberInputDefinition]); + return null; + } + + render(); + + return ref.current!; + }; + + describe('getInputProps', () => { + it('should include the incoming uncontrolled props in the output', () => { + const props: UseNumberInputParameters = { + defaultValue: 100, + disabled: true, + required: true, + }; + + const { getInputProps } = invokeUseNumberInput(props); + const inputProps = getInputProps(); + + expect(inputProps.defaultValue).to.equal(100); + expect(inputProps.required).to.equal(true); + }); + + it('should call onChange if a change event is fired', () => { + const handleChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ onChange: handleChange }); + + // TODO: how to make accept my custom onChange ?! + // @ts-ignore + return ; + } + render(); + + const input = screen.getByRole('spinbutton'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement!, { target: { value: 2 } }); + input.blur(); + }); + + expect(handleChange.callCount).to.equal(1); + }); + }); +}); diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts new file mode 100644 index 00000000000000..25e186fd958b84 --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -0,0 +1,225 @@ +import * as React from 'react'; +import MuiError from '@mui/utils/macros/MuiError.macro'; +import { + // unstable_useControlled as useControlled, // TODO: do I need this? + unstable_useForkRef as useForkRef, +} from '@mui/utils'; +import { FormControlState, useFormControlContext } from '../FormControl'; +import { + UseNumberInputParameters, + UseNumberInputInputSlotProps, + UseNumberInputChangeHandler, +} from './useNumberInput.types'; +import clamp from './clamp'; +import extractEventHandlers from '../utils/extractEventHandlers'; + +type EventHandlers = { + onBlur?: React.FocusEventHandler; + onChange?: UseNumberInputChangeHandler; + onFocus?: React.FocusEventHandler; +}; + +// TODO +// 1 - make a proper parser +// 2 - accept a parser (func) prop +const parseInput = (v: string): string => { + return v ? String(v.trim()) : String(v); +}; +/** + * + * API: + * + * - [useNumberInput API](https://mui.com/base/api/use-number-input/) + */ +export default function useNumberInput(parameters: UseNumberInputParameters) { + const { + // number + min, + max, + step, + // + defaultValue: defaultValueProp, + disabled: disabledProp = false, + error: errorProp = false, + onFocus, + onChange, + onBlur, + required: requiredProp = false, + value: valueProp, + inputRef: inputRefProp, + } = parameters; + + // TODO: make it work with FormControl + const formControlContext: FormControlState | undefined = useFormControlContext(); + + const { current: isControlled } = React.useRef(valueProp != null); + + const handleInputRefWarning = React.useCallback((instance: HTMLElement) => { + if (process.env.NODE_ENV !== 'production') { + if (instance && instance.nodeName !== 'INPUT' && !instance.focus) { + console.error( + [ + 'MUI: You have provided a `slots.input` to the input component', + 'that does not correctly handle the `ref` prop.', + 'Make sure the `ref` prop is called with a HTMLInputElement.', + ].join('\n'), + ); + } + } + }, []); + + const inputRef = React.useRef(null); + const handleInputRef = useForkRef(inputRef, inputRefProp, handleInputRefWarning); + + const [focused, setFocused] = React.useState(false); + + // the "final" value + const [value, setValue] = React.useState(valueProp ?? defaultValueProp); + // the (potentially) dirty or invalid input value + const [inputValue, setInputValue] = React.useState(undefined); + + React.useEffect(() => { + if (!formControlContext && disabledProp && focused) { + setFocused(false); + + // @ts-ignore + onBlur?.(); + } + }, [formControlContext, disabledProp, focused, onBlur]); + + const handleFocus = + (otherHandlers: EventHandlers) => (event: React.FocusEvent) => { + // Fix a bug with IE11 where the focus/blur events are triggered + // while the component is disabled. + if (formControlContext && formControlContext?.disabled) { + event.stopPropagation(); + return; + } + + otherHandlers.onFocus?.(event); + + if (formControlContext && formControlContext.onFocus) { + formControlContext?.onFocus?.(); + } + setFocused(true); + }; + + const handleChange = + (otherHandlers: EventHandlers) => + (event: React.FocusEvent, val: number | undefined) => { + // 1. clamp the number + // 2. setInputValue(clamped_value) + // 3. call onChange(event, returnValue) + + // console.log('handleChange', val); + + let newValue; + + if (val === undefined) { + newValue = val; + setInputValue(''); + } else { + newValue = clamp(val, min, max, step); + setInputValue(String(newValue)); + } + + setValue(newValue); + + formControlContext?.onChange?.(event /* newValue */); + // TODO: pass an (optional) "newValue" to formControlContext.onChange, this will make FormControl work with Select too + + // @ts-ignore + otherHandlers.onChange?.(event, newValue); + }; + + const handleInputChange = () => (event: React.KeyboardEvent) => { + if (!isControlled) { + const element = event.target || inputRef.current; + if (element == null) { + throw new MuiError( + 'MUI: Expected valid input target. ' + + 'Did you use a custom `slots.input` and forget to forward refs? ' + + 'See https://mui.com/r/input-component-ref-interface for more info.', + ); + } + } + + const val = parseInput(event.currentTarget.value); + + if (val === '' || val === '-') { + setInputValue(val); + setValue(undefined); + } + + if (val.match(/^-?\d+?$/)) { + setInputValue(val); + setValue(parseInt(val, 10)); + } + }; + + const handleBlur = + (otherHandlers: EventHandlers) => (event: React.FocusEvent) => { + const val = parseInput(event.currentTarget.value); + + if (val === '' || val === '-') { + handleChange(otherHandlers)(event, undefined); + } else { + handleChange(otherHandlers)(event, parseInt(val, 10)); + } + + otherHandlers.onBlur?.(event); + + if (formControlContext && formControlContext.onBlur) { + formControlContext.onBlur(); + } + + setFocused(false); + }; + + const getInputProps = = {}>( + externalProps: TOther = {} as TOther, + ): UseNumberInputInputSlotProps => { + const propsEventHandlers: EventHandlers = { + onBlur, + onChange, + onFocus, + }; + + const externalEventHandlers = { ...propsEventHandlers, ...extractEventHandlers(externalProps) }; + + const mergedEventHandlers = { + ...externalProps, + ...externalEventHandlers, + onFocus: handleFocus(externalEventHandlers), + // TODO: will I ever need the other handlers? + onChange: handleInputChange(/* externalEventHandlers */), + onBlur: handleBlur(externalEventHandlers), + }; + + return { + ...mergedEventHandlers, + // TODO: check to see if SR support is still weird + role: 'spinbutton', + defaultValue: defaultValueProp as number | undefined, + ref: handleInputRef, + value: ((focused ? inputValue : value) ?? '') as number | undefined, + required: requiredProp, + disabled: disabledProp, + }; + }; + + return { + disabled: disabledProp, + error: errorProp, + focused, + formControlContext, + getInputProps, + // getIncrementButtonProps, + // getDecrementButtonProps, + // getRootProps, + required: requiredProp, + value: focused ? inputValue : value, + // private and could be thrown out later + inputValue, + }; +} diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts new file mode 100644 index 00000000000000..172870c2e55a0e --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts @@ -0,0 +1,55 @@ +import * as React from 'react'; + +export type UseNumberInputChangeHandler = ( + e: React.KeyboardEvent, + value: number | null, +) => void; + +export interface UseNumberInputParameters { + // props for number specific features + min?: number; + max?: number; + step?: number; + /** + * The default value. Use when the component is not controlled. + */ + defaultValue?: unknown; + /** + * If `true`, the component is disabled. + * The prop defaults to the value (`false`) inherited from the parent FormControl component. + */ + disabled?: boolean; + /** + * If `true`, the `input` will indicate an error by setting the `aria-invalid` attribute. + * The prop defaults to the value (`false`) inherited from the parent FormControl component. + */ + error?: boolean; + onBlur?: React.FocusEventHandler; + onClick?: React.MouseEventHandler; + onChange?: UseNumberInputChangeHandler; + onFocus?: React.FocusEventHandler; + inputRef?: React.Ref; + /** + * If `true`, the `input` element is required. + * The prop defaults to the value (`false`) inherited from the parent FormControl component. + */ + required?: boolean; + value?: unknown; +} + +export interface UseNumberInputInputSlotOwnProps { + defaultValue: number | undefined; + ref: React.Ref; + value: number | undefined; + onBlur: React.FocusEventHandler; + onChange: UseNumberInputChangeHandler; + onFocus: React.FocusEventHandler; + required: boolean; + disabled: boolean; +} + +export type UseNumberInputInputSlotProps = Omit< + TOther, + keyof UseNumberInputInputSlotOwnProps +> & + UseNumberInputInputSlotOwnProps; diff --git a/packages/mui-base/src/index.d.ts b/packages/mui-base/src/index.d.ts index ea9553b3d37094..efaab51e968c41 100644 --- a/packages/mui-base/src/index.d.ts +++ b/packages/mui-base/src/index.d.ts @@ -38,6 +38,9 @@ export * from './Modal'; export { default as NoSsr } from './NoSsr'; +export { default as NumberInputUnstyled } from './NumberInputUnstyled'; +export * from './NumberInputUnstyled'; + export { default as OptionGroup } from './OptionGroup'; export * from './OptionGroup'; diff --git a/packages/mui-base/src/index.js b/packages/mui-base/src/index.js index 4535ace2218e14..049e6be0233135 100644 --- a/packages/mui-base/src/index.js +++ b/packages/mui-base/src/index.js @@ -35,6 +35,9 @@ export * from './Modal'; export { default as NoSsr } from './NoSsr'; +export { default as NumberInputUnstyled } from './NumberInputUnstyled'; +export * from './NumberInputUnstyled'; + export { default as OptionGroup } from './OptionGroup'; export * from './OptionGroup'; From a8418f0c0ac34d9f8215c5678eaf2a44a4dbb4aa Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 27 Feb 2023 16:34:36 +0800 Subject: [PATCH 02/70] Add aria attributes, more types. --- docs/pages/base-ui/api/use-number-input.json | 49 +++++++++++++++- .../use-number-input/use-number-input.json | 12 +++- .../src/NumberInputUnstyled/useNumberInput.ts | 22 +++++-- .../useNumberInput.types.ts | 57 +++++++++++++++++++ 4 files changed, 134 insertions(+), 6 deletions(-) diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 7edf41f8c7f27b..7c6a3c2757a905 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -30,7 +30,54 @@ "step": { "type": { "name": "number", "description": "number" } }, "value": { "type": { "name": "unknown", "description": "unknown" } } }, - "returnValue": {}, + "returnValue": { + "disabled": { + "type": { "name": "boolean", "description": "boolean" }, + "default": "false", + "required": true + }, + "error": { + "type": { "name": "boolean", "description": "boolean" }, + "default": "false", + "required": true + }, + "focused": { + "type": { "name": "boolean", "description": "boolean" }, + "default": "false", + "required": true + }, + "formControlContext": { + "type": { + "name": "FormControlUnstyledState | undefined", + "description": "FormControlUnstyledState | undefined" + }, + "required": true + }, + "getInputProps": { + "type": { + "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>", + "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>" + }, + "required": true + }, + "getRootProps": { + "type": { + "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputRootSlotProps<TOther>", + "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputRootSlotProps<TOther>" + }, + "required": true + }, + "inputValue": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + }, + "required": { + "type": { "name": "boolean", "description": "boolean" }, + "default": "false", + "required": true + }, + "value": { "type": { "name": "unknown", "description": "unknown" }, "required": true } + }, "name": "useNumberInput", "filename": "/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts", "demos": "

      " diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index 73535952943abe..6627884d2f8b30 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -6,5 +6,15 @@ "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component." }, - "returnValueDescriptions": {} + "returnValueDescriptions": { + "disabled": "If true, the component will be disabled.", + "error": "If true, the input will indicate an error by setting the aria-invalid attribute.", + "focused": "If true, the input will be focused.", + "formControlContext": "Return value from the useFormControlUnstyledContext hook.", + "getInputProps": "Resolver for the input slot's props.", + "getRootProps": "Resolver for the root slot's props.", + "inputValue": "The dirty value of the input element when it is in focus.", + "required": "If true, the input will indicate that it's required.", + "value": "The clamped value of the input element." + } } diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index 25e186fd958b84..68779bf1d4ef46 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -9,6 +9,7 @@ import { UseNumberInputParameters, UseNumberInputInputSlotProps, UseNumberInputChangeHandler, + UseNumberInputReturnValue, } from './useNumberInput.types'; import clamp from './clamp'; import extractEventHandlers from '../utils/extractEventHandlers'; @@ -31,7 +32,9 @@ const parseInput = (v: string): string => { * * - [useNumberInput API](https://mui.com/base/api/use-number-input/) */ -export default function useNumberInput(parameters: UseNumberInputParameters) { +export default function useNumberInput( + parameters: UseNumberInputParameters, +): UseNumberInputReturnValue { const { // number min, @@ -41,9 +44,9 @@ export default function useNumberInput(parameters: UseNumberInputParameters) { defaultValue: defaultValueProp, disabled: disabledProp = false, error: errorProp = false, - onFocus, - onChange, onBlur, + onChange, + onFocus, required: requiredProp = false, value: valueProp, inputRef: inputRefProp, @@ -196,14 +199,25 @@ export default function useNumberInput(parameters: UseNumberInputParameters) { onBlur: handleBlur(externalEventHandlers), }; + const displayValue = (focused ? inputValue : value) ?? ''; + return { ...mergedEventHandlers, + type: 'text', // TODO: check to see if SR support is still weird role: 'spinbutton', + 'aria-invalid': errorProp || undefined, defaultValue: defaultValueProp as number | undefined, ref: handleInputRef, - value: ((focused ? inputValue : value) ?? '') as number | undefined, + value: displayValue as number | undefined, + 'aria-valuenow': displayValue as number | undefined, + 'aria-valuetext': String(displayValue), + 'aria-valuemin': min, + 'aria-valuemax': max, + autoComplete: 'off', + autoCorrect: 'off', required: requiredProp, + 'aria-disabled': disabledProp, disabled: disabledProp, }; }; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts index 172870c2e55a0e..0f6cd0f9d9c00c 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { FormControlUnstyledState } from '../FormControlUnstyled'; export type UseNumberInputChangeHandler = ( e: React.KeyboardEvent, @@ -41,6 +42,11 @@ export interface UseNumberInputInputSlotOwnProps { defaultValue: number | undefined; ref: React.Ref; value: number | undefined; + 'aria-disabled': React.AriaAttributes['aria-disabled']; + 'aria-valuemax': React.AriaAttributes['aria-valuemax']; + 'aria-valuemin': React.AriaAttributes['aria-valuemin']; + 'aria-valuenow': React.AriaAttributes['aria-valuenow']; + 'aria-valuetext': React.AriaAttributes['aria-valuetext']; onBlur: React.FocusEventHandler; onChange: UseNumberInputChangeHandler; onFocus: React.FocusEventHandler; @@ -53,3 +59,54 @@ export type UseNumberInputInputSlotProps = Omit< keyof UseNumberInputInputSlotOwnProps > & UseNumberInputInputSlotOwnProps; + +export interface UseNumberInputReturnValue { + /** + * If `true`, the component will be disabled. + * @default false + */ + disabled: boolean; + /** + * If `true`, the `input` will indicate an error by setting the `aria-invalid` attribute. + * @default false + */ + error: boolean; + /** + * If `true`, the `input` will be focused. + * @default false + */ + focused: boolean; + /** + * Return value from the `useFormControlUnstyledContext` hook. + */ + formControlContext: FormControlUnstyledState | undefined; + /** + * Resolver for the input slot's props. + * @param externalProps props for the input slot + * @returns props that should be spread on the input slot + */ + getInputProps: = {}>( + externalProps?: TOther, + ) => UseNumberInputInputSlotProps; + /** + * Resolver for the root slot's props. + * @param externalProps props for the root slot + * @returns props that should be spread on the root slot + */ + getRootProps: = {}>( + externalProps?: TOther, + ) => UseNumberInputRootSlotProps; + /** + * If `true`, the `input` will indicate that it's required. + * @default false + */ + required: boolean; + /** + * The clamped `value` of the `input` element. + */ + value: unknown; + /** + * The dirty `value` of the `input` element when it is in focus. + */ + inputValue: string | undefined; +} From c5f9b59480da50a98bd1a9a45e3a7a00e193dba0 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 23 Feb 2023 13:51:29 +0800 Subject: [PATCH 03/70] Add NumberInputUnstyled --- .../base-ui/api/number-input-unstyled.json | 14 +- docs/pages/base-ui/api/use-number-input.json | 12 +- .../number-input-unstyled.json | 7 +- .../number-input-unstyled.json | 7 +- .../use-number-input/use-number-input.json | 2 +- .../NumberInputUnstyled.test.tsx | 43 +++-- .../NumberInputUnstyled.tsx | 164 ++++++++++++++++-- .../NumberInputUnstyled.types.ts | 88 ++++++---- .../mui-base/src/NumberInputUnstyled/index.ts | 3 + .../numberInputUnstyledClasses.ts | 43 +++++ .../src/NumberInputUnstyled/useNumberInput.ts | 36 +++- .../useNumberInput.types.ts | 16 +- 12 files changed, 344 insertions(+), 91 deletions(-) create mode 100644 packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index d937ca8161e907..6ea950414aaacd 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -1,20 +1,28 @@ { "props": { + "component": { "type": { "name": "elementType" } }, + "defaultValue": { "type": { "name": "any" } }, + "disabled": { "type": { "name": "bool" } }, + "error": { "type": { "name": "bool" } }, "id": { "type": { "name": "string" } }, + "required": { "type": { "name": "bool" } }, "slotProps": { - "type": { "name": "shape", "description": "{ input?: func
      | object }" }, + "type": { + "name": "shape", + "description": "{ input?: func
      | object, root?: func
      | object }" + }, "default": "{}" }, "slots": { - "type": { "name": "shape", "description": "{ input?: elementType }" }, + "type": { "name": "shape", "description": "{ input?: elementType, root?: elementType }" }, "default": "{}" } }, "name": "NumberInputUnstyled", "styles": { "classes": [], "globalClasses": {}, "name": null }, "spread": true, - "themeDefaultProps": null, "muiName": "MuiNumberInputUnstyled", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 7c6a3c2757a905..5e6dea9fc33915 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -48,22 +48,22 @@ }, "formControlContext": { "type": { - "name": "FormControlUnstyledState | undefined", - "description": "FormControlUnstyledState | undefined" + "name": "FormControlState | undefined", + "description": "FormControlState | undefined" }, "required": true }, "getInputProps": { "type": { - "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>", - "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>" + "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>", + "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>" }, "required": true }, "getRootProps": { "type": { - "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputRootSlotProps<TOther>", - "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputRootSlotProps<TOther>" + "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputRootSlotProps<TOther>", + "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputRootSlotProps<TOther>" }, "required": true }, diff --git a/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json index d271db44327172..f47ddfa8884ad4 100644 --- a/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json +++ b/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json @@ -1,8 +1,13 @@ { "componentDescription": "", "propDescriptions": { + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultValue": "The default value. Use when the component is not controlled.", + "disabled": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component.", + "error": "If true, the input will indicate an error by setting the aria-invalid attribute on the input and the Mui-error class on the root element.", "id": "The id of the input element.", - "slotProps": "The props used for each slot inside the Input.", + "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", + "slotProps": "The props used for each slot inside the NumberInput.", "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details." }, "classDescriptions": {} diff --git a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json index d271db44327172..f47ddfa8884ad4 100644 --- a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json +++ b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json @@ -1,8 +1,13 @@ { "componentDescription": "", "propDescriptions": { + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultValue": "The default value. Use when the component is not controlled.", + "disabled": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component.", + "error": "If true, the input will indicate an error by setting the aria-invalid attribute on the input and the Mui-error class on the root element.", "id": "The id of the input element.", - "slotProps": "The props used for each slot inside the Input.", + "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", + "slotProps": "The props used for each slot inside the NumberInput.", "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details." }, "classDescriptions": {} diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index 6627884d2f8b30..cd6e0fe6800fe8 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -10,7 +10,7 @@ "disabled": "If true, the component will be disabled.", "error": "If true, the input will indicate an error by setting the aria-invalid attribute.", "focused": "If true, the input will be focused.", - "formControlContext": "Return value from the useFormControlUnstyledContext hook.", + "formControlContext": "Return value from the useFormControlContext hook.", "getInputProps": "Resolver for the input slot's props.", "getRootProps": "Resolver for the root slot's props.", "inputValue": "The dirty value of the input element when it is in focus.", diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx index 19b6c06d501a72..0e955af21841d9 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.test.tsx @@ -1,30 +1,29 @@ import * as React from 'react'; -import { /* createMount, */ createRenderer /* , describeConformanceUnstyled */ } from 'test/utils'; +import { createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; import { expect } from 'chai'; -import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; +import NumberInputUnstyled, { numberInputUnstyledClasses } from '@mui/base/NumberInputUnstyled'; -describe('', () => { - // const mount = createMount(); +describe('', () => { + const mount = createMount(); const { render } = createRenderer(); - // TODO: wow this looks complicated - // describeConformanceUnstyled(, () => ({ - // inheritComponent: 'div', - // render, - // mount, - // refInstanceof: window.HTMLDivElement, - // testComponentPropWith: 'div', - // muiName: 'MuiInput', - // slots: { - // root: { - // expectedClassName: '', - // }, - // input: { - // expectedClassName: '', - // testWithElement: 'input', - // }, - // }, - // })); + describeConformanceUnstyled(, () => ({ + inheritComponent: 'div', + render, + mount, + refInstanceof: window.HTMLDivElement, + testComponentPropWith: 'div', + muiName: 'MuiNumberInput', + slots: { + root: { + expectedClassName: numberInputUnstyledClasses.root, + }, + input: { + expectedClassName: numberInputUnstyledClasses.input, + testWithElement: 'input', + }, + }, + })); it('should be able to attach input ref passed through props', () => { const inputRef = React.createRef(); diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index 38343d6f80412c..0d92bcc94a7fef 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -1,11 +1,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import { OverridableComponent } from '@mui/types'; +import classes from './numberInputUnstyledClasses'; import useNumberInput from './useNumberInput'; import { - NumberInputUnstyledProps, NumberInputUnstyledOwnerState, + NumberInputUnstyledProps, + NumberInputUnstyledRootSlotProps, + NumberInputUnstyledInputSlotProps, + NumberInputUnstyledTypeMap, } from './NumberInputUnstyled.types'; -import { EventHandlers, useSlotProps } from '../utils'; +import { EventHandlers, useSlotProps, WithOptionalOwnerState } from '../utils'; /** * * Demos: @@ -18,43 +23,105 @@ import { EventHandlers, useSlotProps } from '../utils'; */ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( props: NumberInputUnstyledProps, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - forwardedRef: // TODO: this is for the root slot later - React.ForwardedRef, + forwardedRef: React.ForwardedRef, ) { - // set up ALL the props - const { id, slotProps = {}, slots = {} } = props; + const { + className, + component, + defaultValue, + disabled, + error, + id, + max, + min, + onBlur, + onChange, + onFocus, + placeholder, + required, + step, + value, + slotProps = {}, + slots = {}, + ...rest + } = props; - const { getInputProps } = useNumberInput(props); + const { + getRootProps, + getInputProps, + focused, + error: errorState, + disabled: disabledState, + formControlContext, + } = useNumberInput({ + min, + max, + step, + defaultValue, + disabled, + error, + onFocus, + onChange, + onBlur, + required, + value, + }); const ownerState: NumberInputUnstyledOwnerState = { ...props, - type: 'text', + disabled: disabledState, + error: errorState, + focused, + formControlContext: undefined, + }; + + const rootStateClasses = { + [classes.disabled]: disabledState, + [classes.error]: errorState, + [classes.focused]: focused, + [classes.formControl]: Boolean(formControlContext), + }; + + const inputStateClasses = { + [classes.disabled]: disabledState, }; const propsToForward = { id, + placeholder, }; - // define the root slot - - // root -> useSlotProps + const Root = component ?? slots.root ?? 'div'; + const rootProps: WithOptionalOwnerState = useSlotProps({ + elementType: Root, + getSlotProps: getRootProps, + externalSlotProps: slotProps.root, + externalForwardedProps: rest, + additionalProps: { + ref: forwardedRef, + }, + ownerState, + className: [classes.root, rootStateClasses, className], + }); - // define the input slot const Input = slots.input ?? 'input'; - const inputProps = useSlotProps({ + const inputProps: WithOptionalOwnerState = useSlotProps({ elementType: Input, getSlotProps: (otherHandlers: EventHandlers) => getInputProps({ ...otherHandlers, ...propsToForward }), externalSlotProps: slotProps.input, additionalProps: {}, ownerState, + className: [classes.input, inputStateClasses], }); - // input -> useSlotProps - return ; -}); + return ( + + + + ); +}) as OverridableComponent; NumberInputUnstyled.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- @@ -65,16 +132,68 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { * @ignore */ children: PropTypes.node, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * The default value. Use when the component is not controlled. + */ + defaultValue: PropTypes.any, + /** + * If `true`, the component is disabled. + * The prop defaults to the value (`false`) inherited from the parent FormControl component. + */ + disabled: PropTypes.bool, + /** + * If `true`, the `input` will indicate an error by setting the `aria-invalid` attribute on the input and the `Mui-error` class on the root element. + */ + error: PropTypes.bool, /** * The id of the `input` element. */ id: PropTypes.string, /** - * The props used for each slot inside the Input. + * @ignore + */ + max: PropTypes.number, + /** + * @ignore + */ + min: PropTypes.number, + /** + * @ignore + */ + onBlur: PropTypes.func, + /** + * @ignore + */ + onChange: PropTypes.func, + /** + * @ignore + */ + onFocus: PropTypes.func, + /** + * @ignore + */ + placeholder: PropTypes.string, + /** + * If `true`, the `input` element is required. + * The prop defaults to the value (`false`) inherited from the parent FormControl component. + */ + required: PropTypes.bool, + /** + * The props used for each slot inside the NumberInput. * @default {} */ slotProps: PropTypes.shape({ input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), }), /** * The components used for each slot inside the InputBase. @@ -83,7 +202,16 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { */ slots: PropTypes.shape({ input: PropTypes.elementType, + root: PropTypes.elementType, }), + /** + * @ignore + */ + step: PropTypes.number, + /** + * @ignore + */ + value: PropTypes.any, } as any; export default NumberInputUnstyled; diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts index 5f0822a84cc2d1..3125cdcb6b9db3 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts @@ -1,35 +1,46 @@ import { OverrideProps, Simplify } from '@mui/types'; -import { UseNumberInputParameters } from './useNumberInput.types'; +import { FormControlState } from '../FormControl'; +import { UseNumberInputParameters, UseNumberInputRootSlotProps } from './useNumberInput.types'; import { SlotComponentProps } from '../utils'; -export interface NumberInputUnstyledComponentsPropsOverrides {} +export interface NumberInputUnstyledRootSlotPropsOverrides {} +export interface NumberInputUnstyledInputSlotPropsOverrides {} -export type NumberInputUnstyledOwnProps = {} & Omit & { - /** - * The id of the `input` element. - */ - id?: string; - /** - * The props used for each slot inside the Input. - * @default {} - */ - slotProps?: { - input?: SlotComponentProps< - 'input', - NumberInputUnstyledComponentsPropsOverrides, - NumberInputUnstyledOwnerState - >; - }; - /** - * The components used for each slot inside the InputBase. - * Either a string to use a HTML element or a component. - * @default {} - */ - slots?: { - // root?: React.ElementType; - input?: React.ElementType; - }; +export type NumberInputUnstyledOwnProps = Omit & { + /** + * If `true`, the `input` will indicate an error by setting the `aria-invalid` attribute on the input and the `Mui-error` class on the root element. + */ + error?: boolean; + /** + * The id of the `input` element. + */ + id?: string; + /** + * The props used for each slot inside the NumberInput. + * @default {} + */ + slotProps?: { + root?: SlotComponentProps< + 'div', + NumberInputUnstyledRootSlotPropsOverrides, + NumberInputUnstyledOwnerState + >; + input?: SlotComponentProps< + 'input', + NumberInputUnstyledInputSlotPropsOverrides, + NumberInputUnstyledOwnerState + >; }; + /** + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. + * @default {} + */ + slots?: { + root?: React.ElementType; + input?: React.ElementType; + }; +}; export interface NumberInputUnstyledTypeMap

      { props: P & NumberInputUnstyledOwnProps; @@ -45,8 +56,25 @@ export type NumberInputUnstyledProps< export type NumberInputUnstyledOwnerState = Simplify< Omit & { - // formControlContext: FormControlUnstyledState | undefined; - // focused: boolean; - type: React.InputHTMLAttributes['type'] | undefined; + formControlContext: FormControlState | undefined; + focused: boolean; + } +>; + +export type NumberInputUnstyledRootSlotProps = Simplify< + UseNumberInputRootSlotProps & { + ownerState: NumberInputUnstyledOwnerState; + className?: string; + children?: React.ReactNode; + ref?: React.Ref; + } +>; + +export type NumberInputUnstyledInputSlotProps = Simplify< + Omit & { + id: string | undefined; + ownerState: NumberInputUnstyledOwnerState; + placeholder: string | undefined; + ref: React.Ref; } >; diff --git a/packages/mui-base/src/NumberInputUnstyled/index.ts b/packages/mui-base/src/NumberInputUnstyled/index.ts index 0fc14b5ffc9d2e..ee449c6ec5411d 100644 --- a/packages/mui-base/src/NumberInputUnstyled/index.ts +++ b/packages/mui-base/src/NumberInputUnstyled/index.ts @@ -1,5 +1,8 @@ export { default } from './NumberInputUnstyled'; +export { default as numberInputUnstyledClasses } from './numberInputUnstyledClasses'; +export * from './numberInputUnstyledClasses'; + export * from './NumberInputUnstyled.types'; export { default as useNumberInput } from './useNumberInput'; diff --git a/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts b/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts new file mode 100644 index 00000000000000..def33268458582 --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts @@ -0,0 +1,43 @@ +import generateUtilityClass from '../generateUtilityClass'; +import generateUtilityClasses from '../generateUtilityClasses'; + +export interface NumberInputUnstyledClasses { + /** Class name applied to the root element. */ + root: string; + /** Class name applied to the root element if the component is a descendant of `FormControl`. */ + formControl: string; + /** Class name applied to the root element if `startAdornment` is provided. */ + // TODO: adornedStart: string; + /** Class name applied to the root element if `endAdornment` is provided. */ + // TODO: adornedEnd: string; + /** Class name applied to the root element if the component is focused. */ + focused: string; + /** Class name applied to the root element if `disabled={true}`. */ + disabled: string; + /** State class applied to the root element if `error={true}`. */ + error: string; + /** Class name applied to the input element. */ + input: string; +} + +export type NumberInputUnstyledClassKey = keyof NumberInputUnstyledClasses; + +export function getNumberInputUnstyledUtilityClass(slot: string): string { + return generateUtilityClass('MuiNumberInput', slot); +} + +const numberInputUnstyledClasses: NumberInputUnstyledClasses = generateUtilityClasses( + 'MuiNumberInput', + [ + 'root', + 'formControl', + 'focused', + 'disabled', + 'error', + 'input', + // 'adornedStart', + // 'adornedEnd', + ], +); + +export default numberInputUnstyledClasses; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index 68779bf1d4ef46..c56a9de19b426f 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -7,6 +7,7 @@ import { import { FormControlState, useFormControlContext } from '../FormControl'; import { UseNumberInputParameters, + UseNumberInputRootSlotProps, UseNumberInputInputSlotProps, UseNumberInputChangeHandler, UseNumberInputReturnValue, @@ -14,7 +15,7 @@ import { import clamp from './clamp'; import extractEventHandlers from '../utils/extractEventHandlers'; -type EventHandlers = { +type UseNumberInputEventHandlers = { onBlur?: React.FocusEventHandler; onChange?: UseNumberInputChangeHandler; onFocus?: React.FocusEventHandler; @@ -91,7 +92,7 @@ export default function useNumberInput( }, [formControlContext, disabledProp, focused, onBlur]); const handleFocus = - (otherHandlers: EventHandlers) => (event: React.FocusEvent) => { + (otherHandlers: UseNumberInputEventHandlers) => (event: React.FocusEvent) => { // Fix a bug with IE11 where the focus/blur events are triggered // while the component is disabled. if (formControlContext && formControlContext?.disabled) { @@ -108,7 +109,7 @@ export default function useNumberInput( }; const handleChange = - (otherHandlers: EventHandlers) => + (otherHandlers: UseNumberInputEventHandlers) => (event: React.FocusEvent, val: number | undefined) => { // 1. clamp the number // 2. setInputValue(clamped_value) @@ -161,7 +162,7 @@ export default function useNumberInput( }; const handleBlur = - (otherHandlers: EventHandlers) => (event: React.FocusEvent) => { + (otherHandlers: UseNumberInputEventHandlers) => (event: React.FocusEvent) => { const val = parseInput(event.currentTarget.value); if (val === '' || val === '-') { @@ -179,10 +180,33 @@ export default function useNumberInput( setFocused(false); }; + const handleClick = + (otherHandlers: Record>) => + (event: React.MouseEvent) => { + if (inputRef.current && event.currentTarget === event.target) { + inputRef.current.focus(); + } + + otherHandlers.onClick?.(event); + }; + + const getRootProps = = {}>( + externalProps: TOther = {} as TOther, + ): UseNumberInputRootSlotProps => { + const propsEventHandlers = extractEventHandlers(parameters, ['onBlur', 'onChange', 'onFocus']); + const externalEventHandlers = { ...propsEventHandlers, ...extractEventHandlers(externalProps) }; + + return { + ...externalProps, + ...externalEventHandlers, + onClick: handleClick(externalEventHandlers), + }; + }; + const getInputProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputInputSlotProps => { - const propsEventHandlers: EventHandlers = { + const propsEventHandlers: UseNumberInputEventHandlers = { onBlur, onChange, onFocus, @@ -230,7 +254,7 @@ export default function useNumberInput( getInputProps, // getIncrementButtonProps, // getDecrementButtonProps, - // getRootProps, + getRootProps, required: requiredProp, value: focused ? inputValue : value, // private and could be thrown out later diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts index 0f6cd0f9d9c00c..c50fdac6a12563 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FormControlUnstyledState } from '../FormControlUnstyled'; +import { FormControlState } from '../FormControl'; export type UseNumberInputChangeHandler = ( e: React.KeyboardEvent, @@ -38,6 +38,16 @@ export interface UseNumberInputParameters { value?: unknown; } +export interface UseNumberInputRootSlotOwnProps { + onClick: React.MouseEventHandler | undefined; +} + +export type UseNumberInputRootSlotProps = Omit< + TOther, + keyof UseNumberInputRootSlotOwnProps | 'onBlur' | 'onChange' | 'onFocus' +> & + UseNumberInputRootSlotOwnProps; + export interface UseNumberInputInputSlotOwnProps { defaultValue: number | undefined; ref: React.Ref; @@ -77,9 +87,9 @@ export interface UseNumberInputReturnValue { */ focused: boolean; /** - * Return value from the `useFormControlUnstyledContext` hook. + * Return value from the `useFormControlContext` hook. */ - formControlContext: FormControlUnstyledState | undefined; + formControlContext: FormControlState | undefined; /** * Resolver for the input slot's props. * @param externalProps props for the input slot From 3a9b8f9c0ca64e8202234c0c828fc5db2079ca7c Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 28 Feb 2023 16:34:33 +0800 Subject: [PATCH 04/70] Redo change handlers --- docs/pages/base-ui/api/use-number-input.json | 12 +- .../NumberInputUnstyled.tsx | 10 +- .../useNumberInput.test.tsx | 1 - .../src/NumberInputUnstyled/useNumberInput.ts | 109 ++++++++++-------- .../useNumberInput.types.ts | 7 +- 5 files changed, 81 insertions(+), 58 deletions(-) diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 5e6dea9fc33915..3b742dd7846a04 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -12,12 +12,15 @@ "max": { "type": { "name": "number", "description": "number" } }, "min": { "type": { "name": "number", "description": "number" } }, "onBlur": { - "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } + "type": { + "name": "React.FocusEventHandler<HTMLInputElement>", + "description": "React.FocusEventHandler<HTMLInputElement>" + } }, "onChange": { "type": { - "name": "UseNumberInputChangeHandler", - "description": "UseNumberInputChangeHandler" + "name": "React.ChangeEventHandler<HTMLInputElement>", + "description": "React.ChangeEventHandler<HTMLInputElement>" } }, "onClick": { @@ -26,6 +29,9 @@ "onFocus": { "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } }, + "onValueChange": { + "type": { "name": "(value: number) => void", "description": "(value: number) => void" } + }, "required": { "type": { "name": "boolean", "description": "boolean" } }, "step": { "type": { "name": "number", "description": "number" } }, "value": { "type": { "name": "unknown", "description": "unknown" } } diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index 0d92bcc94a7fef..0c4a4bff341be9 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -37,6 +37,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( onBlur, onChange, onFocus, + onValueChange, placeholder, required, step, @@ -63,6 +64,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( onFocus, onChange, onBlur, + onValueChange, required, value, }); @@ -86,7 +88,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( [classes.disabled]: disabledState, }; - const propsToForward = { + const propsForwardedToInputSlot = { id, placeholder, }; @@ -109,7 +111,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( const inputProps: WithOptionalOwnerState = useSlotProps({ elementType: Input, getSlotProps: (otherHandlers: EventHandlers) => - getInputProps({ ...otherHandlers, ...propsToForward }), + getInputProps({ ...otherHandlers, ...propsForwardedToInputSlot }), externalSlotProps: slotProps.input, additionalProps: {}, ownerState, @@ -178,6 +180,10 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { * @ignore */ onFocus: PropTypes.func, + /** + * @ignore + */ + onValueChange: PropTypes.func, /** * @ignore */ diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx index 8740cf9d7c363e..bd11e437c5fc10 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx @@ -50,7 +50,6 @@ describe('useNumberInput', () => { act(() => { input.focus(); fireEvent.change(document.activeElement!, { target: { value: 2 } }); - input.blur(); }); expect(handleChange.callCount).to.equal(1); diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index c56a9de19b426f..1b460ee6c43da1 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -9,18 +9,11 @@ import { UseNumberInputParameters, UseNumberInputRootSlotProps, UseNumberInputInputSlotProps, - UseNumberInputChangeHandler, UseNumberInputReturnValue, } from './useNumberInput.types'; import clamp from './clamp'; import extractEventHandlers from '../utils/extractEventHandlers'; -type UseNumberInputEventHandlers = { - onBlur?: React.FocusEventHandler; - onChange?: UseNumberInputChangeHandler; - onFocus?: React.FocusEventHandler; -}; - // TODO // 1 - make a proper parser // 2 - accept a parser (func) prop @@ -48,6 +41,7 @@ export default function useNumberInput( onBlur, onChange, onFocus, + onValueChange, required: requiredProp = false, value: valueProp, inputRef: inputRefProp, @@ -92,7 +86,8 @@ export default function useNumberInput( }, [formControlContext, disabledProp, focused, onBlur]); const handleFocus = - (otherHandlers: UseNumberInputEventHandlers) => (event: React.FocusEvent) => { + (otherHandlers: Record | undefined>) => + (event: React.FocusEvent) => { // Fix a bug with IE11 where the focus/blur events are triggered // while the component is disabled. if (formControlContext && formControlContext?.disabled) { @@ -102,21 +97,21 @@ export default function useNumberInput( otherHandlers.onFocus?.(event); + if (event.defaultPrevented) { + return; + } + if (formControlContext && formControlContext.onFocus) { formControlContext?.onFocus?.(); } setFocused(true); }; - const handleChange = - (otherHandlers: UseNumberInputEventHandlers) => - (event: React.FocusEvent, val: number | undefined) => { + const handleValueChange = + () => (event: React.FocusEvent, val: number | undefined) => { // 1. clamp the number // 2. setInputValue(clamped_value) - // 3. call onChange(event, returnValue) - - // console.log('handleChange', val); - + // 3. call onValueChange(newValue) let newValue; if (val === undefined) { @@ -128,51 +123,62 @@ export default function useNumberInput( } setValue(newValue); + // TODO: integration with formControlContext + // OR: (event, newValue) similar to SelectUnstyled + // formControlContext?.onValueChange?.(newValue); - formControlContext?.onChange?.(event /* newValue */); - // TODO: pass an (optional) "newValue" to formControlContext.onChange, this will make FormControl work with Select too - - // @ts-ignore - otherHandlers.onChange?.(event, newValue); + if (newValue) { + onValueChange?.(newValue); + } }; - const handleInputChange = () => (event: React.KeyboardEvent) => { - if (!isControlled) { - const element = event.target || inputRef.current; - if (element == null) { - throw new MuiError( - 'MUI: Expected valid input target. ' + - 'Did you use a custom `slots.input` and forget to forward refs? ' + - 'See https://mui.com/r/input-component-ref-interface for more info.', - ); + const handleInputChange = + (otherHandlers: Record | undefined>) => + (event: React.ChangeEvent) => { + if (!isControlled) { + const element = event.target || inputRef.current; + if (element == null) { + throw new MuiError( + 'MUI: Expected valid input target. ' + + 'Did you use a custom `slots.input` and forget to forward refs? ' + + 'See https://mui.com/r/input-component-ref-interface for more info.', + ); + } } - } - const val = parseInput(event.currentTarget.value); + const val = parseInput(event.currentTarget.value); - if (val === '' || val === '-') { - setInputValue(val); - setValue(undefined); - } + if (val === '' || val === '-') { + setInputValue(val); + setValue(undefined); + } - if (val.match(/^-?\d+?$/)) { - setInputValue(val); - setValue(parseInt(val, 10)); - } - }; + if (val.match(/^-?\d+?$/)) { + setInputValue(val); + setValue(parseInt(val, 10)); + } + + // TODO: + // 1 - move up to allow developers to skip? + // 2 - preventDefault if val contains an invalid char? + formControlContext?.onChange?.(event); + + otherHandlers.onChange?.(event); + }; const handleBlur = - (otherHandlers: UseNumberInputEventHandlers) => (event: React.FocusEvent) => { + (otherHandlers: Record | undefined>) => + (event: React.FocusEvent) => { const val = parseInput(event.currentTarget.value); + otherHandlers.onBlur?.(event); + if (val === '' || val === '-') { - handleChange(otherHandlers)(event, undefined); + handleValueChange()(event, undefined); } else { - handleChange(otherHandlers)(event, parseInt(val, 10)); + handleValueChange()(event, parseInt(val, 10)); } - otherHandlers.onBlur?.(event); - if (formControlContext && formControlContext.onBlur) { formControlContext.onBlur(); } @@ -193,7 +199,13 @@ export default function useNumberInput( const getRootProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputRootSlotProps => { - const propsEventHandlers = extractEventHandlers(parameters, ['onBlur', 'onChange', 'onFocus']); + const propsEventHandlers = extractEventHandlers(parameters, [ + 'onBlur', + 'onChange', + 'onFocus', + 'onValueChange', + ]); + const externalEventHandlers = { ...propsEventHandlers, ...extractEventHandlers(externalProps) }; return { @@ -206,7 +218,7 @@ export default function useNumberInput( const getInputProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputInputSlotProps => { - const propsEventHandlers: UseNumberInputEventHandlers = { + const propsEventHandlers: Record | undefined> = { onBlur, onChange, onFocus, @@ -218,8 +230,7 @@ export default function useNumberInput( ...externalProps, ...externalEventHandlers, onFocus: handleFocus(externalEventHandlers), - // TODO: will I ever need the other handlers? - onChange: handleInputChange(/* externalEventHandlers */), + onChange: handleInputChange(externalEventHandlers), onBlur: handleBlur(externalEventHandlers), }; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts index c50fdac6a12563..4da31efc9710c9 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts @@ -25,10 +25,11 @@ export interface UseNumberInputParameters { * The prop defaults to the value (`false`) inherited from the parent FormControl component. */ error?: boolean; - onBlur?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; onClick?: React.MouseEventHandler; - onChange?: UseNumberInputChangeHandler; + onChange?: React.ChangeEventHandler; onFocus?: React.FocusEventHandler; + onValueChange?: (value: number) => void; inputRef?: React.Ref; /** * If `true`, the `input` element is required. @@ -58,7 +59,7 @@ export interface UseNumberInputInputSlotOwnProps { 'aria-valuenow': React.AriaAttributes['aria-valuenow']; 'aria-valuetext': React.AriaAttributes['aria-valuetext']; onBlur: React.FocusEventHandler; - onChange: UseNumberInputChangeHandler; + onChange: React.ChangeEventHandler; onFocus: React.FocusEventHandler; required: boolean; disabled: boolean; From fb52a63039ed149d9babb002a73aa64282e9129e Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 1 Mar 2023 16:16:44 +0800 Subject: [PATCH 05/70] Add increment and decrement buttons --- .../base-ui/api/number-input-unstyled.json | 7 +- docs/pages/base-ui/api/use-number-input.json | 29 +++++++- .../use-number-input/use-number-input.json | 4 ++ .../NumberInputUnstyled.tsx | 46 +++++++++++- .../NumberInputUnstyled.types.ts | 33 ++++++++- .../numberInputUnstyledClasses.ts | 6 ++ .../src/NumberInputUnstyled/useNumberInput.ts | 71 +++++++++++++++++-- .../useNumberInput.types.ts | 52 +++++++++++++- 8 files changed, 235 insertions(+), 13 deletions(-) diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index 6ea950414aaacd..883a239d0e2525 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -9,12 +9,15 @@ "slotProps": { "type": { "name": "shape", - "description": "{ input?: func
      | object, root?: func
      | object }" + "description": "{ decrementButton?: func
      | object, incrementButton?: func
      | object, input?: func
      | object, root?: func
      | object }" }, "default": "{}" }, "slots": { - "type": { "name": "shape", "description": "{ input?: elementType, root?: elementType }" }, + "type": { + "name": "shape", + "description": "{ decrementButton?: elementType, incrementButton?: elementType, input?: elementType, root?: elementType }" + }, "default": "{}" } }, diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 3b742dd7846a04..28ce9d35273138 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -30,7 +30,10 @@ "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } }, "onValueChange": { - "type": { "name": "(value: number) => void", "description": "(value: number) => void" } + "type": { + "name": "(value: number | undefined) => void", + "description": "(value: number | undefined) => void" + } }, "required": { "type": { "name": "boolean", "description": "boolean" } }, "step": { "type": { "name": "number", "description": "number" } }, @@ -59,6 +62,20 @@ }, "required": true }, + "getDecrementButtonProps": { + "type": { + "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputDecrementButtonSlotProps<TOther>", + "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputDecrementButtonSlotProps<TOther>" + }, + "required": true + }, + "getIncrementButtonProps": { + "type": { + "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputIncrementButtonSlotProps<TOther>", + "description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputIncrementButtonSlotProps<TOther>" + }, + "required": true + }, "getInputProps": { "type": { "name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseNumberInputInputSlotProps<TOther>", @@ -77,6 +94,16 @@ "type": { "name": "string | undefined", "description": "string | undefined" }, "required": true }, + "isDecrementDisabled": { + "type": { "name": "boolean", "description": "boolean" }, + "default": "false", + "required": true + }, + "isIncrementDisabled": { + "type": { "name": "boolean", "description": "boolean" }, + "default": "false", + "required": true + }, "required": { "type": { "name": "boolean", "description": "boolean" }, "default": "false", diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index cd6e0fe6800fe8..4849a8f3d4485c 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -11,9 +11,13 @@ "error": "If true, the input will indicate an error by setting the aria-invalid attribute.", "focused": "If true, the input will be focused.", "formControlContext": "Return value from the useFormControlContext hook.", + "getDecrementButtonProps": "Resolver for the decrement button slot's props.", + "getIncrementButtonProps": "Resolver for the increment button slot's props.", "getInputProps": "Resolver for the input slot's props.", "getRootProps": "Resolver for the root slot's props.", "inputValue": "The dirty value of the input element when it is in focus.", + "isDecrementDisabled": "If true, the decrement button will be disabled.\ne.g. when the value is already at min", + "isIncrementDisabled": "If true, the increment button will be disabled.\ne.g. when the value is already at max", "required": "If true, the input will indicate that it's required.", "value": "The clamped value of the input element." } diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index 0c4a4bff341be9..50a659f40c4b9f 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -8,6 +8,8 @@ import { NumberInputUnstyledProps, NumberInputUnstyledRootSlotProps, NumberInputUnstyledInputSlotProps, + NumberInputUnstyledIncrementButtonSlotProps, + NumberInputUnstyledDecrementButtonSlotProps, NumberInputUnstyledTypeMap, } from './NumberInputUnstyled.types'; import { EventHandlers, useSlotProps, WithOptionalOwnerState } from '../utils'; @@ -50,10 +52,14 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( const { getRootProps, getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, focused, error: errorState, disabled: disabledState, formControlContext, + isIncrementDisabled, + isDecrementDisabled, } = useNumberInput({ min, max, @@ -88,6 +94,16 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( [classes.disabled]: disabledState, }; + const incrementButtonStateClasses = { + [classes.disabled]: isIncrementDisabled, + // TODO: focusable if input is readonly + }; + + const decrementButtonStateClasses = { + [classes.disabled]: isDecrementDisabled, + // TODO: focusable if input is readonly + }; + const propsForwardedToInputSlot = { id, placeholder, @@ -107,20 +123,42 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( }); const Input = slots.input ?? 'input'; - const inputProps: WithOptionalOwnerState = useSlotProps({ elementType: Input, getSlotProps: (otherHandlers: EventHandlers) => getInputProps({ ...otherHandlers, ...propsForwardedToInputSlot }), externalSlotProps: slotProps.input, - additionalProps: {}, + // additionalProps: {}, ownerState, className: [classes.input, inputStateClasses], }); + const IncrementButton = slots.incrementButton ?? 'button'; + const incrementButtonProps: WithOptionalOwnerState = + useSlotProps({ + elementType: IncrementButton, + getSlotProps: getIncrementButtonProps, + externalSlotProps: slotProps.incrementButton, + ownerState, + className: [classes.incrementButton, incrementButtonStateClasses], + }); + + const DecrementButton = slots.decrementButton ?? 'button'; + const decrementButtonProps: WithOptionalOwnerState = + useSlotProps({ + elementType: DecrementButton, + getSlotProps: getDecrementButtonProps, + externalSlotProps: slotProps.decrementButton, + // additionalProps: {}, + ownerState, + className: [classes.decrementButton, decrementButtonStateClasses], + }); + return ( + - + + ); }) as OverridableComponent; @@ -198,6 +236,8 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { * @default {} */ slotProps: PropTypes.shape({ + decrementButton: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + incrementButton: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), }), @@ -207,6 +247,8 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { * @default {} */ slots: PropTypes.shape({ + decrementButton: PropTypes.elementType, + incrementButton: PropTypes.elementType, input: PropTypes.elementType, root: PropTypes.elementType, }), diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts index 3125cdcb6b9db3..3e83d38598c29b 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts @@ -1,10 +1,17 @@ import { OverrideProps, Simplify } from '@mui/types'; import { FormControlState } from '../FormControl'; -import { UseNumberInputParameters, UseNumberInputRootSlotProps } from './useNumberInput.types'; +import { + UseNumberInputParameters, + UseNumberInputRootSlotProps, + UseNumberInputIncrementButtonSlotProps, + UseNumberInputDecrementButtonSlotProps, +} from './useNumberInput.types'; import { SlotComponentProps } from '../utils'; export interface NumberInputUnstyledRootSlotPropsOverrides {} export interface NumberInputUnstyledInputSlotPropsOverrides {} +export interface NumberInputUnstyledIncrementButtonSlotPropsOverrides {} +export interface NumberInputUnstyledDecrementButtonSlotPropsOverrides {} export type NumberInputUnstyledOwnProps = Omit & { /** @@ -30,6 +37,16 @@ export type NumberInputUnstyledOwnProps = Omit; + incrementButton?: SlotComponentProps< + 'button', + NumberInputUnstyledIncrementButtonSlotPropsOverrides, + NumberInputUnstyledOwnerState + >; + decrementButton?: SlotComponentProps< + 'button', + NumberInputUnstyledDecrementButtonSlotPropsOverrides, + NumberInputUnstyledOwnerState + >; }; /** * The components used for each slot inside the InputBase. @@ -39,6 +56,8 @@ export type NumberInputUnstyledOwnProps = Omit; } >; + +export type NumberInputUnstyledIncrementButtonSlotProps = Simplify< + UseNumberInputIncrementButtonSlotProps & { + ownerState: NumberInputUnstyledOwnerState; + } +>; + +export type NumberInputUnstyledDecrementButtonSlotProps = Simplify< + UseNumberInputDecrementButtonSlotProps & { + ownerState: NumberInputUnstyledOwnerState; + } +>; diff --git a/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts b/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts index def33268458582..0c6e18f45120ed 100644 --- a/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts +++ b/packages/mui-base/src/NumberInputUnstyled/numberInputUnstyledClasses.ts @@ -18,6 +18,10 @@ export interface NumberInputUnstyledClasses { error: string; /** Class name applied to the input element. */ input: string; + /** Class name applied to the increment button element. */ + incrementButton: string; + /** Class name applied to the decrement button element. */ + decrementButton: string; } export type NumberInputUnstyledClassKey = keyof NumberInputUnstyledClasses; @@ -35,6 +39,8 @@ const numberInputUnstyledClasses: NumberInputUnstyledClasses = generateUtilityCl 'disabled', 'error', 'input', + 'incrementButton', + 'decrementButton', // 'adornedStart', // 'adornedEnd', ], diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index 1b460ee6c43da1..16bafa0249d0a0 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -9,6 +9,8 @@ import { UseNumberInputParameters, UseNumberInputRootSlotProps, UseNumberInputInputSlotProps, + UseNumberInputIncrementButtonSlotProps, + UseNumberInputDecrementButtonSlotProps, UseNumberInputReturnValue, } from './useNumberInput.types'; import clamp from './clamp'; @@ -30,11 +32,9 @@ export default function useNumberInput( parameters: UseNumberInputParameters, ): UseNumberInputReturnValue { const { - // number min, max, step, - // defaultValue: defaultValueProp, disabled: disabledProp = false, error: errorProp = false, @@ -108,7 +108,8 @@ export default function useNumberInput( }; const handleValueChange = - () => (event: React.FocusEvent, val: number | undefined) => { + () => + (event: React.FocusEvent | React.PointerEvent, val: number | undefined) => { // 1. clamp the number // 2. setInputValue(clamped_value) // 3. call onValueChange(newValue) @@ -127,8 +128,10 @@ export default function useNumberInput( // OR: (event, newValue) similar to SelectUnstyled // formControlContext?.onValueChange?.(newValue); - if (newValue) { + if (typeof newValue === 'number' && !Number.isNaN(newValue)) { onValueChange?.(newValue); + } else { + onValueChange?.(undefined); } }; @@ -196,6 +199,28 @@ export default function useNumberInput( otherHandlers.onClick?.(event); }; + const handleStep = + (direction: 'up' | 'down') => + ( + event: React.PointerEvent, // TODO: this could also be a keyboard event: arrow up/down or enter on the button + ) => { + let newValue; + + if (typeof value === 'number') { + newValue = { + up: value + (step ?? 1), + down: value - (step ?? 1), + }[direction]; + } else { + // no value + newValue = { + up: min ?? 0, + down: max ?? 0, + }[direction]; + } + handleValueChange()(event, newValue); + }; + const getRootProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputRootSlotProps => { @@ -257,17 +282,51 @@ export default function useNumberInput( }; }; + const isIncrementDisabled = + typeof value === 'number' ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false; + + const getIncrementButtonProps = = {}>( + externalProps: TOther = {} as TOther, + ): UseNumberInputIncrementButtonSlotProps => { + return { + ...externalProps, + // the button should be tab-able if the input is readonly + tabIndex: -1, + disabled: isIncrementDisabled, + 'aria-disabled': isIncrementDisabled, + onClick: handleStep('up'), + }; + }; + + const isDecrementDisabled = + typeof value === 'number' ? value <= (min ?? Number.MIN_SAFE_INTEGER) : false; + + const getDecrementButtonProps = = {}>( + externalProps: TOther = {} as TOther, + ): UseNumberInputDecrementButtonSlotProps => { + return { + ...externalProps, + // the button should be tab-able if the input is readonly + tabIndex: -1, + disabled: isDecrementDisabled, + 'aria-disabled': isDecrementDisabled, + onClick: handleStep('down'), + }; + }; + return { disabled: disabledProp, error: errorProp, focused, formControlContext, getInputProps, - // getIncrementButtonProps, - // getDecrementButtonProps, + getIncrementButtonProps, + getDecrementButtonProps, getRootProps, required: requiredProp, value: focused ? inputValue : value, + isIncrementDisabled, + isDecrementDisabled, // private and could be thrown out later inputValue, }; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts index 4da31efc9710c9..03f263a545ff99 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts @@ -29,7 +29,7 @@ export interface UseNumberInputParameters { onClick?: React.MouseEventHandler; onChange?: React.ChangeEventHandler; onFocus?: React.FocusEventHandler; - onValueChange?: (value: number) => void; + onValueChange?: (value: number | undefined) => void; inputRef?: React.Ref; /** * If `true`, the `input` element is required. @@ -71,6 +71,28 @@ export type UseNumberInputInputSlotProps = Omit< > & UseNumberInputInputSlotOwnProps; +export interface UseNumberInputIncrementButtonSlotOwnProps { + 'aria-disabled': React.AriaAttributes['aria-disabled']; + disabled: boolean; +} + +export type UseNumberInputIncrementButtonSlotProps = Omit< + TOther, + keyof UseNumberInputIncrementButtonSlotOwnProps +> & + UseNumberInputIncrementButtonSlotOwnProps; + +export interface UseNumberInputDecrementButtonSlotOwnProps { + 'aria-disabled': React.AriaAttributes['aria-disabled']; + disabled: boolean; +} + +export type UseNumberInputDecrementButtonSlotProps = Omit< + TOther, + keyof UseNumberInputDecrementButtonSlotOwnProps +> & + UseNumberInputDecrementButtonSlotOwnProps; + export interface UseNumberInputReturnValue { /** * If `true`, the component will be disabled. @@ -91,6 +113,22 @@ export interface UseNumberInputReturnValue { * Return value from the `useFormControlContext` hook. */ formControlContext: FormControlState | undefined; + /** + * Resolver for the decrement button slot's props. + * @param externalProps props for the decrement button slot + * @returns props that should be spread on the decrement button slot + */ + getDecrementButtonProps: = {}>( + externalProps?: TOther, + ) => UseNumberInputDecrementButtonSlotProps; + /** + * Resolver for the increment button slot's props. + * @param externalProps props for the increment button slot + * @returns props that should be spread on the increment button slot + */ + getIncrementButtonProps: = {}>( + externalProps?: TOther, + ) => UseNumberInputIncrementButtonSlotProps; /** * Resolver for the input slot's props. * @param externalProps props for the input slot @@ -120,4 +158,16 @@ export interface UseNumberInputReturnValue { * The dirty `value` of the `input` element when it is in focus. */ inputValue: string | undefined; + /** + * If `true`, the increment button will be disabled. + * e.g. when the `value` is already at `max` + * @default false + */ + isIncrementDisabled: boolean; + /** + * If `true`, the decrement button will be disabled. + * e.g. when the `value` is already at `min` + * @default false + */ + isDecrementDisabled: boolean; } From 3e19da1b6cde571aa28767c1339612773daabd1f Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 2 Mar 2023 18:21:47 +0800 Subject: [PATCH 06/70] Keyboard handlers and shift multiplier --- docs/pages/base-ui/api/use-number-input.json | 1 + .../use-number-input/use-number-input.json | 3 +- .../NumberInputUnstyled.tsx | 4 +- .../src/NumberInputUnstyled/useNumberInput.ts | 54 ++++++++++++++++--- .../useNumberInput.types.ts | 5 ++ 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 28ce9d35273138..59ef1cf395a1df 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -36,6 +36,7 @@ } }, "required": { "type": { "name": "boolean", "description": "boolean" } }, + "shiftMultiplier": { "type": { "name": "number", "description": "number" } }, "step": { "type": { "name": "number", "description": "number" } }, "value": { "type": { "name": "unknown", "description": "unknown" } } }, diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index 4849a8f3d4485c..008e52b7d05172 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -4,7 +4,8 @@ "defaultValue": "The default value. Use when the component is not controlled.", "disabled": "If true, the component is disabled.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", - "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component." + "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", + "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10." }, "returnValueDescriptions": { "disabled": "If true, the component will be disabled.", diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index 50a659f40c4b9f..97ab5ace7bce63 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -156,9 +156,9 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( return ( - - + - + + ); }) as OverridableComponent; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index 16bafa0249d0a0..840f5633aa64c0 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -16,6 +16,8 @@ import { import clamp from './clamp'; import extractEventHandlers from '../utils/extractEventHandlers'; +type StepDirection = 'up' | 'down'; + // TODO // 1 - make a proper parser // 2 - accept a parser (func) prop @@ -35,6 +37,7 @@ export default function useNumberInput( min, max, step, + shiftMultiplier = 10, defaultValue: defaultValueProp, disabled: disabledProp = false, error: errorProp = false, @@ -109,7 +112,10 @@ export default function useNumberInput( const handleValueChange = () => - (event: React.FocusEvent | React.PointerEvent, val: number | undefined) => { + ( + event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent, + val: number | undefined, + ) => { // 1. clamp the number // 2. setInputValue(clamped_value) // 3. call onValueChange(newValue) @@ -200,16 +206,20 @@ export default function useNumberInput( }; const handleStep = - (direction: 'up' | 'down') => - ( - event: React.PointerEvent, // TODO: this could also be a keyboard event: arrow up/down or enter on the button - ) => { + (direction: StepDirection) => (event: React.PointerEvent | React.KeyboardEvent) => { let newValue; if (typeof value === 'number') { + const multiplier = + event.shiftKey || + (event.nativeEvent instanceof KeyboardEvent && + ((event as React.KeyboardEvent).key === 'PageUp' || + (event as React.KeyboardEvent).key === 'PageDown')) + ? shiftMultiplier + : 1; newValue = { - up: value + (step ?? 1), - down: value - (step ?? 1), + up: value + (step ?? 1) * multiplier, + down: value - (step ?? 1) * multiplier, }[direction]; } else { // no value @@ -221,6 +231,35 @@ export default function useNumberInput( handleValueChange()(event, newValue); }; + const handleKeyDown = + (otherHandlers: Record | undefined>) => + (event: React.KeyboardEvent) => { + otherHandlers.onKeyDown?.(event); + + if (event.defaultPrevented) { + return; + } + + if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'].includes(event.key)) { + const direction = { + ArrowUp: 'up', + ArrowDown: 'down', + PageUp: 'up', + PageDown: 'down', + }[event.key] as StepDirection; + + handleStep(direction)(event); + } + + if (event.key === 'Home' && typeof max === 'number') { + handleValueChange()(event, max); + } + + if (event.key === 'End' && typeof min === 'number') { + handleValueChange()(event, min); + } + }; + const getRootProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputRootSlotProps => { @@ -257,6 +296,7 @@ export default function useNumberInput( onFocus: handleFocus(externalEventHandlers), onChange: handleInputChange(externalEventHandlers), onBlur: handleBlur(externalEventHandlers), + onKeyDown: handleKeyDown(externalEventHandlers), }; const displayValue = (focused ? inputValue : value) ?? ''; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts index 03f263a545ff99..d99954574f331a 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts @@ -11,6 +11,11 @@ export interface UseNumberInputParameters { min?: number; max?: number; step?: number; + /** + * Multiplier applied to `step` if the shift key is held while incrementing + * or decrementing the value. Defaults to `10`. + */ + shiftMultiplier?: number; /** * The default value. Use when the component is not controlled. */ From 40bee99007c0369d39a83f9f40476fa1e31df682 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 3 Mar 2023 13:38:10 +0800 Subject: [PATCH 07/70] Apply code review changes --- .../src/NumberInputUnstyled/useNumberInput.ts | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index 840f5633aa64c0..97e70a0886d4f5 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -77,7 +77,7 @@ export default function useNumberInput( // the "final" value const [value, setValue] = React.useState(valueProp ?? defaultValueProp); // the (potentially) dirty or invalid input value - const [inputValue, setInputValue] = React.useState(undefined); + const [dirtyValue, setInputValue] = React.useState(undefined); React.useEffect(() => { if (!formControlContext && disabledProp && focused) { @@ -144,17 +144,18 @@ export default function useNumberInput( const handleInputChange = (otherHandlers: Record | undefined>) => (event: React.ChangeEvent) => { - if (!isControlled) { - const element = event.target || inputRef.current; - if (element == null) { - throw new MuiError( - 'MUI: Expected valid input target. ' + - 'Did you use a custom `slots.input` and forget to forward refs? ' + - 'See https://mui.com/r/input-component-ref-interface for more info.', - ); - } + if (!isControlled && event.target === null) { + throw new MuiError( + 'MUI: Expected valid input target. ' + + 'Did you use a custom `slots.input` and forget to forward refs? ' + + 'See https://mui.com/r/input-component-ref-interface for more info.', + ); } + formControlContext?.onChange?.(event); + + otherHandlers.onChange?.(event); + const val = parseInput(event.currentTarget.value); if (val === '' || val === '-') { @@ -166,13 +167,6 @@ export default function useNumberInput( setInputValue(val); setValue(parseInt(val, 10)); } - - // TODO: - // 1 - move up to allow developers to skip? - // 2 - preventDefault if val contains an invalid char? - formControlContext?.onChange?.(event); - - otherHandlers.onChange?.(event); }; const handleBlur = @@ -299,7 +293,7 @@ export default function useNumberInput( onKeyDown: handleKeyDown(externalEventHandlers), }; - const displayValue = (focused ? inputValue : value) ?? ''; + const displayValue = (focused ? dirtyValue : value) ?? ''; return { ...mergedEventHandlers, @@ -364,10 +358,10 @@ export default function useNumberInput( getDecrementButtonProps, getRootProps, required: requiredProp, - value: focused ? inputValue : value, + value: focused ? dirtyValue : value, isIncrementDisabled, isDecrementDisabled, // private and could be thrown out later - inputValue, + inputValue: dirtyValue, }; } From df5b8e8c5031e179057d0d106bf0f6ed89c506a3 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 3 Mar 2023 14:17:58 +0800 Subject: [PATCH 08/70] Extract number check util --- .../src/NumberInputUnstyled/clamp.test.ts | 23 --------- .../src/NumberInputUnstyled/useNumberInput.ts | 17 +++---- .../src/NumberInputUnstyled/utils.test.ts | 50 +++++++++++++++++++ .../{clamp.ts => utils.ts} | 6 ++- 4 files changed, 63 insertions(+), 33 deletions(-) delete mode 100644 packages/mui-base/src/NumberInputUnstyled/clamp.test.ts create mode 100644 packages/mui-base/src/NumberInputUnstyled/utils.test.ts rename packages/mui-base/src/NumberInputUnstyled/{clamp.ts => utils.ts} (81%) diff --git a/packages/mui-base/src/NumberInputUnstyled/clamp.test.ts b/packages/mui-base/src/NumberInputUnstyled/clamp.test.ts deleted file mode 100644 index 7c9781b863eb01..00000000000000 --- a/packages/mui-base/src/NumberInputUnstyled/clamp.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect } from 'chai'; -import clamp from './clamp'; - -describe('clamp', () => { - it('clamps a value based on min and max', () => { - expect(clamp(1, 2, 4)).to.equal(2); - expect(clamp(5, 2, 4)).to.equal(4); - expect(clamp(-5, -1, 5)).to.equal(-1); - }); - - it('clamps a value between min and max and on a valid step', () => { - expect(clamp(2, -15, 15, 3)).to.equal(3); - expect(clamp(-1, -15, 15, 3)).to.equal(0); - expect(clamp(5, -15, 15, 3)).to.equal(6); - expect(clamp(-5, -15, 15, 3)).to.equal(-6); - expect(clamp(-55, -15, 15, 3)).to.equal(-15); - expect(clamp(57, -15, 15, 3)).to.equal(15); - expect(clamp(3, -20, 20, 5)).to.equal(5); - expect(clamp(2, -20, 20, 5)).to.equal(0); - expect(clamp(8, -20, 20, 5)).to.equal(10); - expect(clamp(-7, -20, 20, 5)).to.equal(-5); - }); -}); diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts index 97e70a0886d4f5..31fe22416bdef7 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts +++ b/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts @@ -13,7 +13,7 @@ import { UseNumberInputDecrementButtonSlotProps, UseNumberInputReturnValue, } from './useNumberInput.types'; -import clamp from './clamp'; +import { clamp, isNumber } from './utils'; import extractEventHandlers from '../utils/extractEventHandlers'; type StepDirection = 'up' | 'down'; @@ -24,6 +24,7 @@ type StepDirection = 'up' | 'down'; const parseInput = (v: string): string => { return v ? String(v.trim()) : String(v); }; + /** * * API: @@ -134,7 +135,7 @@ export default function useNumberInput( // OR: (event, newValue) similar to SelectUnstyled // formControlContext?.onValueChange?.(newValue); - if (typeof newValue === 'number' && !Number.isNaN(newValue)) { + if (isNumber(newValue)) { onValueChange?.(newValue); } else { onValueChange?.(undefined); @@ -203,7 +204,7 @@ export default function useNumberInput( (direction: StepDirection) => (event: React.PointerEvent | React.KeyboardEvent) => { let newValue; - if (typeof value === 'number') { + if (isNumber(value)) { const multiplier = event.shiftKey || (event.nativeEvent instanceof KeyboardEvent && @@ -245,11 +246,11 @@ export default function useNumberInput( handleStep(direction)(event); } - if (event.key === 'Home' && typeof max === 'number') { + if (event.key === 'Home' && isNumber(max)) { handleValueChange()(event, max); } - if (event.key === 'End' && typeof min === 'number') { + if (event.key === 'End' && isNumber(min)) { handleValueChange()(event, min); } }; @@ -316,8 +317,7 @@ export default function useNumberInput( }; }; - const isIncrementDisabled = - typeof value === 'number' ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false; + const isIncrementDisabled = isNumber(value) ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false; const getIncrementButtonProps = = {}>( externalProps: TOther = {} as TOther, @@ -332,8 +332,7 @@ export default function useNumberInput( }; }; - const isDecrementDisabled = - typeof value === 'number' ? value <= (min ?? Number.MIN_SAFE_INTEGER) : false; + const isDecrementDisabled = isNumber(value) ? value <= (min ?? Number.MIN_SAFE_INTEGER) : false; const getDecrementButtonProps = = {}>( externalProps: TOther = {} as TOther, diff --git a/packages/mui-base/src/NumberInputUnstyled/utils.test.ts b/packages/mui-base/src/NumberInputUnstyled/utils.test.ts new file mode 100644 index 00000000000000..213294e133bf95 --- /dev/null +++ b/packages/mui-base/src/NumberInputUnstyled/utils.test.ts @@ -0,0 +1,50 @@ +import { expect } from 'chai'; +import { clamp, isNumber } from './utils'; + +describe('utils', () => { + it('clamp: clamps a value based on min and max', () => { + expect(clamp(1, 2, 4)).to.equal(2); + expect(clamp(5, 2, 4)).to.equal(4); + expect(clamp(-5, -1, 5)).to.equal(-1); + }); + + it('clamp: clamps a value between min and max and on a valid step', () => { + expect(clamp(2, -15, 15, 3)).to.equal(3); + expect(clamp(-1, -15, 15, 3)).to.equal(0); + expect(clamp(5, -15, 15, 3)).to.equal(6); + expect(clamp(-5, -15, 15, 3)).to.equal(-6); + expect(clamp(-55, -15, 15, 3)).to.equal(-15); + expect(clamp(57, -15, 15, 3)).to.equal(15); + expect(clamp(3, -20, 20, 5)).to.equal(5); + expect(clamp(2, -20, 20, 5)).to.equal(0); + expect(clamp(8, -20, 20, 5)).to.equal(10); + expect(clamp(-7, -20, 20, 5)).to.equal(-5); + }); + + it('isNumber: rejects NaN', () => { + expect(isNumber(NaN)).to.equal(false); + }); + + it('isNumber: rejects Infinity', () => { + expect(isNumber(Infinity)).to.equal(false); + expect(isNumber(-Infinity)).to.equal(false); + }); + + it('isNumber: rejects falsy values', () => { + expect(isNumber('')).to.equal(false); + expect(isNumber(undefined)).to.equal(false); + expect(isNumber(null)).to.equal(false); + }); + + it('isNumber: accepts positive and negative integers', () => { + expect(isNumber(10)).to.equal(true); + expect(isNumber(7)).to.equal(true); + expect(isNumber(-20)).to.equal(true); + expect(isNumber(-333)).to.equal(true); + }); + + it('isNumber: accepts 0', () => { + expect(isNumber(0)).to.equal(true); + expect(isNumber(-0)).to.equal(true); + }); +}); diff --git a/packages/mui-base/src/NumberInputUnstyled/clamp.ts b/packages/mui-base/src/NumberInputUnstyled/utils.ts similarity index 81% rename from packages/mui-base/src/NumberInputUnstyled/clamp.ts rename to packages/mui-base/src/NumberInputUnstyled/utils.ts index c96c80eddcad2c..c20907be5b683a 100644 --- a/packages/mui-base/src/NumberInputUnstyled/clamp.ts +++ b/packages/mui-base/src/NumberInputUnstyled/utils.ts @@ -6,7 +6,7 @@ function simpleClamp( return Math.max(min, Math.min(val, max)); } -export default function clamp( +export function clamp( val: number, min: number = Number.MIN_SAFE_INTEGER, max: number = Number.MAX_SAFE_INTEGER, @@ -28,3 +28,7 @@ export default function clamp( return simpleClamp(val - positivity * Math.abs(remainder), min, max); } + +export function isNumber(val: unknown): val is number { + return typeof val === 'number' && !Number.isNaN(val) && Number.isFinite(val); +} From de0cb54b57c1414eff0967042ed7b214a5e14ab8 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 3 Mar 2023 14:35:35 +0800 Subject: [PATCH 09/70] Separate directories --- docs/pages/base-ui/api/use-number-input.json | 2 +- .../mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx | 2 +- .../src/NumberInputUnstyled/NumberInputUnstyled.types.ts | 2 +- packages/mui-base/src/NumberInputUnstyled/index.ts | 4 ---- packages/mui-base/src/index.d.ts | 3 +++ packages/mui-base/src/index.js | 3 +++ packages/mui-base/src/useNumberInput/index.ts | 3 +++ .../useNumberInput.test.tsx | 2 +- .../{NumberInputUnstyled => useNumberInput}/useNumberInput.ts | 0 .../useNumberInput.types.ts | 0 .../src/{NumberInputUnstyled => useNumberInput}/utils.test.ts | 0 .../src/{NumberInputUnstyled => useNumberInput}/utils.ts | 0 12 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 packages/mui-base/src/useNumberInput/index.ts rename packages/mui-base/src/{NumberInputUnstyled => useNumberInput}/useNumberInput.test.tsx (96%) rename packages/mui-base/src/{NumberInputUnstyled => useNumberInput}/useNumberInput.ts (100%) rename packages/mui-base/src/{NumberInputUnstyled => useNumberInput}/useNumberInput.types.ts (100%) rename packages/mui-base/src/{NumberInputUnstyled => useNumberInput}/utils.test.ts (100%) rename packages/mui-base/src/{NumberInputUnstyled => useNumberInput}/utils.ts (100%) diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 59ef1cf395a1df..a75004caa3f44b 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -113,6 +113,6 @@ "value": { "type": { "name": "unknown", "description": "unknown" }, "required": true } }, "name": "useNumberInput", - "filename": "/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts", + "filename": "/packages/mui-base/src/useNumberInput/useNumberInput.ts", "demos": "

        " } diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index 97ab5ace7bce63..aa19e6b8be7823 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; import classes from './numberInputUnstyledClasses'; -import useNumberInput from './useNumberInput'; +import useNumberInput from '../useNumberInput'; import { NumberInputUnstyledOwnerState, NumberInputUnstyledProps, diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts index 3e83d38598c29b..eb1c6d8f74c50e 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts @@ -5,7 +5,7 @@ import { UseNumberInputRootSlotProps, UseNumberInputIncrementButtonSlotProps, UseNumberInputDecrementButtonSlotProps, -} from './useNumberInput.types'; +} from '../useNumberInput/useNumberInput.types'; import { SlotComponentProps } from '../utils'; export interface NumberInputUnstyledRootSlotPropsOverrides {} diff --git a/packages/mui-base/src/NumberInputUnstyled/index.ts b/packages/mui-base/src/NumberInputUnstyled/index.ts index ee449c6ec5411d..0251f46e627939 100644 --- a/packages/mui-base/src/NumberInputUnstyled/index.ts +++ b/packages/mui-base/src/NumberInputUnstyled/index.ts @@ -4,7 +4,3 @@ export { default as numberInputUnstyledClasses } from './numberInputUnstyledClas export * from './numberInputUnstyledClasses'; export * from './NumberInputUnstyled.types'; - -export { default as useNumberInput } from './useNumberInput'; - -export * from './useNumberInput.types'; diff --git a/packages/mui-base/src/index.d.ts b/packages/mui-base/src/index.d.ts index efaab51e968c41..2f80f47b34299d 100644 --- a/packages/mui-base/src/index.d.ts +++ b/packages/mui-base/src/index.d.ts @@ -107,6 +107,9 @@ export * from './useMenuButton'; export { default as useMenuItem } from './useMenuItem'; export * from './useMenuItem'; +export { default as useNumberInput } from './useNumberInput'; +export * from './useNumberInput'; + export { default as useOption } from './useOption'; export * from './useOption'; diff --git a/packages/mui-base/src/index.js b/packages/mui-base/src/index.js index 049e6be0233135..c5297cc777baf2 100644 --- a/packages/mui-base/src/index.js +++ b/packages/mui-base/src/index.js @@ -100,6 +100,9 @@ export * from './useMenuButton'; export { default as useMenuItem } from './useMenuItem'; export * from './useMenuItem'; +export { default as useNumberInput } from './useNumberInput'; +export * from './useNumberInput'; + export { default as useOption } from './useOption'; export * from './useOption'; diff --git a/packages/mui-base/src/useNumberInput/index.ts b/packages/mui-base/src/useNumberInput/index.ts new file mode 100644 index 00000000000000..4a0c6ad81a44e5 --- /dev/null +++ b/packages/mui-base/src/useNumberInput/index.ts @@ -0,0 +1,3 @@ +export { default } from './useNumberInput'; + +export * from './useNumberInput.types'; diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx similarity index 96% rename from packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx rename to packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index bd11e437c5fc10..3a5725ae5a11fe 100644 --- a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import * as React from 'react'; import { createRenderer, screen, act, fireEvent } from 'test/utils'; -import { useNumberInput, UseNumberInputParameters } from './index'; +import useNumberInput, { UseNumberInputParameters } from './index'; describe('useNumberInput', () => { const { render } = createRenderer(); diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts similarity index 100% rename from packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts rename to packages/mui-base/src/useNumberInput/useNumberInput.ts diff --git a/packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts similarity index 100% rename from packages/mui-base/src/NumberInputUnstyled/useNumberInput.types.ts rename to packages/mui-base/src/useNumberInput/useNumberInput.types.ts diff --git a/packages/mui-base/src/NumberInputUnstyled/utils.test.ts b/packages/mui-base/src/useNumberInput/utils.test.ts similarity index 100% rename from packages/mui-base/src/NumberInputUnstyled/utils.test.ts rename to packages/mui-base/src/useNumberInput/utils.test.ts diff --git a/packages/mui-base/src/NumberInputUnstyled/utils.ts b/packages/mui-base/src/useNumberInput/utils.ts similarity index 100% rename from packages/mui-base/src/NumberInputUnstyled/utils.ts rename to packages/mui-base/src/useNumberInput/utils.ts From 39c1e2cf56e4c136ccd731ea412346b03d0ae110 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 3 Mar 2023 15:01:06 +0800 Subject: [PATCH 10/70] Update docs --- .../components/number-input/number-input.md | 2 +- .../base-ui/api/number-input-unstyled.json | 9 +++++++-- docs/pages/base-ui/api/use-number-input.json | 2 +- .../number-input-unstyled.json | 7 ++++++- .../use-number-input/use-number-input.json | 7 ++++++- .../NumberInputUnstyled/NumberInputUnstyled.tsx | 13 +++++++------ .../src/useNumberInput/useNumberInput.ts | 4 ++++ .../src/useNumberInput/useNumberInput.types.ts | 17 ++++++++++++++++- 8 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 674444d1ce4f68..23f4205518fde7 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -2,7 +2,7 @@ product: base title: Unstyled React Number Input component and hook components: NumberInputUnstyled -# hooks: useNumberInput +hooks: useNumberInput githubLabel: 'component: NumberInput' --- diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index 883a239d0e2525..173eece873015a 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -5,6 +5,9 @@ "disabled": { "type": { "name": "bool" } }, "error": { "type": { "name": "bool" } }, "id": { "type": { "name": "string" } }, + "max": { "type": { "name": "number" } }, + "min": { "type": { "name": "number" } }, + "onValueChange": { "type": { "name": "func" } }, "required": { "type": { "name": "bool" } }, "slotProps": { "type": { @@ -19,7 +22,9 @@ "description": "{ decrementButton?: elementType, incrementButton?: elementType, input?: elementType, root?: elementType }" }, "default": "{}" - } + }, + "step": { "type": { "name": "number" } }, + "value": { "type": { "name": "any" } } }, "name": "NumberInputUnstyled", "styles": { "classes": [], "globalClasses": {}, "name": null }, @@ -28,6 +33,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index a75004caa3f44b..0459d7feecccd3 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -114,5 +114,5 @@ }, "name": "useNumberInput", "filename": "/packages/mui-base/src/useNumberInput/useNumberInput.ts", - "demos": "
          " + "demos": "" } diff --git a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json index f47ddfa8884ad4..4d8c3b82671253 100644 --- a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json +++ b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json @@ -6,9 +6,14 @@ "disabled": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component.", "error": "If true, the input will indicate an error by setting the aria-invalid attribute on the input and the Mui-error class on the root element.", "id": "The id of the input element.", + "max": "The maximum value.", + "min": "The minimum value.", + "onValueChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", "slotProps": "The props used for each slot inside the NumberInput.", - "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details." + "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details.", + "step": "The amount that the value changes on each increment or decrement.", + "value": "The current value. Use when the component is controlled." }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index 008e52b7d05172..a4b27afa9fbd13 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -4,8 +4,13 @@ "defaultValue": "The default value. Use when the component is not controlled.", "disabled": "If true, the component is disabled.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", + "max": "The maximum value.", + "min": "The minimum value.", + "onValueChange": "Callback fired after the value is clamped and changes.\nCalled with undefined when the value is unset.", "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", - "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10." + "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10.", + "step": "The amount that the value changes on each increment or decrement.", + "value": "The current value. Use when the component is controlled." }, "returnValueDescriptions": { "disabled": "If true, the component will be disabled.", diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index aa19e6b8be7823..c6f47fc34a1138 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -17,7 +17,7 @@ import { EventHandlers, useSlotProps, WithOptionalOwnerState } from '../utils'; * * Demos: * - * - [hooks: useNumberInput](https://mui.com/base/react-number-input/) + * - [Unstyled Number Input](https://mui.com/base/react-number-input/) * * API: * @@ -199,11 +199,11 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { */ id: PropTypes.string, /** - * @ignore + * The maximum value. */ max: PropTypes.number, /** - * @ignore + * The minimum value. */ min: PropTypes.number, /** @@ -219,7 +219,8 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { */ onFocus: PropTypes.func, /** - * @ignore + * Callback fired after the value is clamped and changes. + * Called with `undefined` when the value is unset. */ onValueChange: PropTypes.func, /** @@ -253,11 +254,11 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { root: PropTypes.elementType, }), /** - * @ignore + * The amount that the value changes on each increment or decrement. */ step: PropTypes.number, /** - * @ignore + * The current value. Use when the component is controlled. */ value: PropTypes.any, } as any; diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index 31fe22416bdef7..45a203ff56d27e 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -26,6 +26,10 @@ const parseInput = (v: string): string => { }; /** + * + * Demos: + * + * - [Unstyled Number Input](https://mui.com/base/react-number-input/#hook) * * API: * diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index d99954574f331a..d771fce2baeaaf 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -7,9 +7,17 @@ export type UseNumberInputChangeHandler = ( ) => void; export interface UseNumberInputParameters { - // props for number specific features + /** + * The minimum value. + */ min?: number; + /** + * The maximum value. + */ max?: number; + /** + * The amount that the value changes on each increment or decrement. + */ step?: number; /** * Multiplier applied to `step` if the shift key is held while incrementing @@ -34,6 +42,10 @@ export interface UseNumberInputParameters { onClick?: React.MouseEventHandler; onChange?: React.ChangeEventHandler; onFocus?: React.FocusEventHandler; + /** + * Callback fired after the value is clamped and changes. + * Called with `undefined` when the value is unset. + */ onValueChange?: (value: number | undefined) => void; inputRef?: React.Ref; /** @@ -41,6 +53,9 @@ export interface UseNumberInputParameters { * The prop defaults to the value (`false`) inherited from the parent FormControl component. */ required?: boolean; + /** + * The current value. Use when the component is controlled. + */ value?: unknown; } From 7fc512220de612f17dc8e487d0a61648bca61f9d Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 3 Mar 2023 23:53:34 +0800 Subject: [PATCH 11/70] NumberInput introduction demo --- .../UnstyledNumberInputIntroduction.js | 94 ++++++++++++++++--- .../UnstyledNumberInputIntroduction.tsx | 91 +++++++++++++++--- 2 files changed, 162 insertions(+), 23 deletions(-) diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js index 72eee5960b8715..01c65a6b61c9ff 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js @@ -1,5 +1,7 @@ import * as React from 'react'; -import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; +import NumberInputUnstyled, { + numberInputUnstyledClasses, +} from '@mui/base/NumberInputUnstyled'; import { styled } from '@mui/system'; const blue = { @@ -23,27 +25,28 @@ const grey = { 900: '#24292f', }; -const StyledInputElement = styled('input')( +const StyledInputRoot = styled('div')( ({ theme }) => ` - width: 320px; font-family: IBM Plex Sans, sans-serif; - font-size: 0.875rem; font-weight: 400; - line-height: 1.5; - padding: 12px; border-radius: 12px; - color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 4px 30px ${theme.palette.mode === 'dark' ? grey[900] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + display: grid; + grid-template-columns: 1fr 24px; + grid-template-rows: 1fr 1fr; + overflow: hidden; - &:hover { + + &.${numberInputUnstyledClasses.focused} { border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; } - &:focus { + &:hover { border-color: ${blue[400]}; - box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; } // firefox @@ -53,10 +56,77 @@ const StyledInputElement = styled('input')( `, ); +const StyledInputElement = styled('input')( + ({ theme }) => ` + font-size: 0.875rem; + font-family: inherit; + font-weight: 400; + line-height: 1.5; + grid-column: 1/2; + grid-row: 1/3; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: inherit; + border: none; + border-radius: inherit; + padding: 12px 12px; + outline: 0; +`, +); + +const StyledButton = styled('button')( + ({ theme }) => ` + width: 24px; + height: 24px; + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 0; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + cursor: pointer; + } + + &.${numberInputUnstyledClasses.incrementButton} { + grid-column: 2/3; + grid-row: 1/2; + } + + &.${numberInputUnstyledClasses.decrementButton} { + grid-column: 2/3; + grid-row: 2/3; + transform: rotate(180deg); + } +`, +); + const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { return ( diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx index 11f3be865d0716..e71b254f6e280e 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import NumberInputUnstyled, { NumberInputUnstyledProps, + numberInputUnstyledClasses, } from '@mui/base/NumberInputUnstyled'; import { styled } from '@mui/system'; @@ -25,27 +26,28 @@ const grey = { 900: '#24292f', }; -const StyledInputElement = styled('input')( +const StyledInputRoot = styled('div')( ({ theme }) => ` - width: 320px; font-family: IBM Plex Sans, sans-serif; - font-size: 0.875rem; font-weight: 400; - line-height: 1.5; - padding: 12px; border-radius: 12px; - color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 4px 30px ${theme.palette.mode === 'dark' ? grey[900] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + display: grid; + grid-template-columns: 1fr 24px; + grid-template-rows: 1fr 1fr; + overflow: hidden; - &:hover { + + &.${numberInputUnstyledClasses.focused} { border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; } - &:focus { + &:hover { border-color: ${blue[400]}; - box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; } // firefox @@ -55,13 +57,80 @@ const StyledInputElement = styled('input')( `, ); +const StyledInputElement = styled('input')( + ({ theme }) => ` + font-size: 0.875rem; + font-family: inherit; + font-weight: 400; + line-height: 1.5; + grid-column: 1/2; + grid-row: 1/3; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: inherit; + border: none; + border-radius: inherit; + padding: 12px 12px; + outline: 0; +`, +); + +const StyledButton = styled('button')( + ({ theme }) => ` + width: 24px; + height: 24px; + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 0; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + cursor: pointer; + } + + &.${numberInputUnstyledClasses.incrementButton} { + grid-column: 2/3; + grid-row: 1/2; + } + + &.${numberInputUnstyledClasses.decrementButton} { + grid-column: 2/3; + grid-row: 2/3; + transform: rotate(180deg); + } +`, +); + const CustomNumberInput = React.forwardRef(function CustomNumberInput( props: NumberInputUnstyledProps, ref: React.ForwardedRef, ) { return ( From c508ec38057717d07b182425c2410eaa51cc0c3a Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 9 Mar 2023 09:52:19 +0100 Subject: [PATCH 12/70] Store quantity picker demo --- .../components/number-input/QuantityInput.js | 141 +++++++++++++++++ .../components/number-input/QuantityInput.tsx | 146 ++++++++++++++++++ .../number-input/QuantityInput.tsx.preview | 1 + .../components/number-input/number-input.md | 12 ++ 4 files changed, 300 insertions(+) create mode 100644 docs/data/base/components/number-input/QuantityInput.js create mode 100644 docs/data/base/components/number-input/QuantityInput.tsx create mode 100644 docs/data/base/components/number-input/QuantityInput.tsx.preview diff --git a/docs/data/base/components/number-input/QuantityInput.js b/docs/data/base/components/number-input/QuantityInput.js new file mode 100644 index 00000000000000..6f7338782cb6d2 --- /dev/null +++ b/docs/data/base/components/number-input/QuantityInput.js @@ -0,0 +1,141 @@ +import * as React from 'react'; +import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; +import { styled } from '@mui/system'; +import RemoveIcon from '@mui/icons-material/Remove'; +import AddIcon from '@mui/icons-material/Add'; + +const blue = { + 100: '#DAECFF', + 200: '#b6daff', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', +}; + +const StyledInputRoot = styled('div')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-weight: 400; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + &:hover { + border-color: ${blue[400]}; + } + + // firefox + &:focus-visible { + outline: 0; + } +`, +); + +const StyledInput = styled('input')( + ({ theme }) => ` + font-size: 0.875rem; + font-family: inherit; + font-weight: 400; + line-height: 1.375; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + border-radius: 4px; + margin: 0 4px; + padding: 10px 12px; + outline: 0; + min-width: 0; + width: 4rem; + text-align: center; + + &:hover { + border-color: ${blue[400]}; + } + + &:focus { + border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + &:focus-visible { + outline: 0; + } +`, +); + +const StyledButton = styled('button')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + line-height: 1.5; + border: 0; + border-radius: 999px; + color: ${theme.palette.mode === 'dark' ? blue[400] : blue[600]}; + background: transparent; + + width: 40px; + height: 40px; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : blue[100]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + cursor: pointer; + } + + &:focus-visible { + outline: 0; + } +`, +); + +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + return ( + , + }, + decrementButton: { + children: , + }, + }} + {...props} + ref={ref} + /> + ); +}); + +export default function QuantityInput() { + return ; +} diff --git a/docs/data/base/components/number-input/QuantityInput.tsx b/docs/data/base/components/number-input/QuantityInput.tsx new file mode 100644 index 00000000000000..7c402fb0fc55a4 --- /dev/null +++ b/docs/data/base/components/number-input/QuantityInput.tsx @@ -0,0 +1,146 @@ +import * as React from 'react'; +import NumberInputUnstyled, { + NumberInputUnstyledProps, +} from '@mui/base/NumberInputUnstyled'; +import { styled } from '@mui/system'; +import RemoveIcon from '@mui/icons-material/Remove'; +import AddIcon from '@mui/icons-material/Add'; + +const blue = { + 100: '#DAECFF', + 200: '#b6daff', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', +}; + +const StyledInputRoot = styled('div')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-weight: 400; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + &:hover { + border-color: ${blue[400]}; + } + + // firefox + &:focus-visible { + outline: 0; + } +`, +); + +const StyledInput = styled('input')( + ({ theme }) => ` + font-size: 0.875rem; + font-family: inherit; + font-weight: 400; + line-height: 1.375; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + border-radius: 4px; + margin: 0 4px; + padding: 10px 12px; + outline: 0; + min-width: 0; + width: 4rem; + text-align: center; + + &:hover { + border-color: ${blue[400]}; + } + + &:focus { + border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + &:focus-visible { + outline: 0; + } +`, +); + +const StyledButton = styled('button')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + line-height: 1.5; + border: 0; + border-radius: 999px; + color: ${theme.palette.mode === 'dark' ? blue[400] : blue[600]}; + background: transparent; + + width: 40px; + height: 40px; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : blue[100]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + cursor: pointer; + } + + &:focus-visible { + outline: 0; + } +`, +); + +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: NumberInputUnstyledProps, + ref: React.ForwardedRef, +) { + return ( + , + }, + decrementButton: { + children: , + }, + }} + {...props} + ref={ref} + /> + ); +}); + +export default function QuantityInput() { + return ; +} diff --git a/docs/data/base/components/number-input/QuantityInput.tsx.preview b/docs/data/base/components/number-input/QuantityInput.tsx.preview new file mode 100644 index 00000000000000..d28010b2207aa6 --- /dev/null +++ b/docs/data/base/components/number-input/QuantityInput.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 23f4205518fde7..065674e2e4fb06 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -31,3 +31,15 @@ export default function MyApp() { return ; } ``` + +## Hook + +```js +import useNumberInput from '@mui/base/useNumberInput'; +``` + +## Customization + +### Quantity Input + +{{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} From fdf42c9ca12b8811ed0329ee86c6a511230c2d52 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 22 Mar 2023 18:21:32 +0800 Subject: [PATCH 13/70] Add a demo using useNumberInput --- .../components/number-input/UseNumberInput.js | 171 +++++++++++++++++ .../number-input/UseNumberInput.tsx | 174 ++++++++++++++++++ .../number-input/UseNumberInput.tsx.preview | 1 + .../components/number-input/number-input.md | 4 + 4 files changed, 350 insertions(+) create mode 100644 docs/data/base/components/number-input/UseNumberInput.js create mode 100644 docs/data/base/components/number-input/UseNumberInput.tsx create mode 100644 docs/data/base/components/number-input/UseNumberInput.tsx.preview diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js new file mode 100644 index 00000000000000..bf608ec76a5b2c --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -0,0 +1,171 @@ +import * as React from 'react'; +import useNumberInput from '@mui/base/useNumberInput'; +import { styled } from '@mui/system'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; + +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + focused, + } = useNumberInput(props); + + const inputProps = getInputProps(); + + // Make sure that both the forwarded ref and the ref returned from the getInputProps are applied on the input element + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( + + + + + + + + + + + + + + + + ); +}); + +export default function UseNumberInput() { + return ( + + ); +} + +const blue = { + 100: '#DAECFF', + 200: '#80BFFF', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', +}; + +const StyledInputRoot = styled('div')( + ({ theme, focused }) => ` + width: 320px; + + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + + display: grid; + grid-template-columns: 24px 1fr; + grid-template-rows: 1fr 1fr; + column-gap: 8px; + padding: 6px; + + border-radius: 6px; + border-style: solid; + border-width: 1px; + + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + ${ + // can be massively simplified with has:() once firefox supports it + // ETA first half 2023 - https://connect.mozilla.org/t5/discussions/when-is-has-css-selector-going-to-be-fully-implemented-in/m-p/27010/highlight/true#M10711 + focused + ? ` + border-color: ${blue[400]}; + box-shadow: + inset 0 0 0 1px ${blue[400]}, + 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + & button:hover { + background: ${blue[400]}; + } + ` + : ` + border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 2px 2px ${ + theme.palette.mode === 'dark' ? grey[900] : grey[50] + }; + ` + } + `, +); + +const StyledInputElement = styled('input')` + grid-column: 2/3; + grid-row: 1/3; + + background: none; + border: 0; + outline: 0; + padding: 0; +`; + +const StyledStepperButton = styled('button')` + width: 1.5rem; + height: 1rem; + + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + font-size: 0.875rem; + box-sizing: border-box; + border: 0; + color: inherit; + + &:hover { + cursor: pointer; + background: ${blue[400]}; + color: ${grey[50]}; + } + + &.increment { + grid-column: 1/2; + grid-row: 1/2; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + + &.decrement { + grid-column: 1/2; + grid-row: 2/3; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } +`; diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx new file mode 100644 index 00000000000000..d57249d3fe4591 --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -0,0 +1,174 @@ +import * as React from 'react'; +import useNumberInput, { UseNumberInputParameters } from '@mui/base/useNumberInput'; +import { styled, Theme } from '@mui/system'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; + +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: UseNumberInputParameters & React.InputHTMLAttributes, + ref: React.ForwardedRef, +) { + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + focused, + } = useNumberInput(props); + + const inputProps = getInputProps(); + + // Make sure that both the forwarded ref and the ref returned from the getInputProps are applied on the input element + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( + + + + + + + + + + + + + + + + ); +}); + +export default function UseNumberInput() { + return ( + + ); +} + +const blue = { + 100: '#DAECFF', + 200: '#80BFFF', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', +}; + +const StyledInputRoot: React.ElementType = styled('div')( + ({ theme, focused }: { theme: Theme; focused: boolean }) => ` + width: 320px; + + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + + display: grid; + grid-template-columns: 24px 1fr; + grid-template-rows: 1fr 1fr; + column-gap: 8px; + padding: 6px; + + border-radius: 6px; + border-style: solid; + border-width: 1px; + + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + ${ + // can be massively simplified with has:() once firefox supports it + // ETA first half 2023 - https://connect.mozilla.org/t5/discussions/when-is-has-css-selector-going-to-be-fully-implemented-in/m-p/27010/highlight/true#M10711 + focused + ? ` + border-color: ${blue[400]}; + box-shadow: + inset 0 0 0 1px ${blue[400]}, + 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + & button:hover { + background: ${blue[400]}; + } + ` + : ` + border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 2px 2px ${ + theme.palette.mode === 'dark' ? grey[900] : grey[50] + }; + ` + } + `, +); + +const StyledInputElement = styled('input')` + grid-column: 2/3; + grid-row: 1/3; + + background: none; + border: 0; + outline: 0; + padding: 0; +`; + +const StyledStepperButton = styled('button')` + width: 1.5rem; + height: 1rem; + + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + font-size: 0.875rem; + box-sizing: border-box; + border: 0; + color: inherit; + + &:hover { + cursor: pointer; + background: ${blue[400]}; + color: ${grey[50]}; + } + + &.increment { + grid-column: 1/2; + grid-row: 1/2; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + + &.decrement { + grid-column: 1/2; + grid-row: 2/3; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } +`; diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx.preview b/docs/data/base/components/number-input/UseNumberInput.tsx.preview new file mode 100644 index 00000000000000..fa6c5c5dc81d85 --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInput.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 065674e2e4fb06..04e24fb4877d4d 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -38,6 +38,10 @@ export default function MyApp() { import useNumberInput from '@mui/base/useNumberInput'; ``` +Here's an example of a component built using the hook alone: + +{{"demo": "UseNumberInput.js", "defaultCodeOpen": false, "bg": "gradient"}} + ## Customization ### Quantity Input From 073476406183c645a1881576ea162e58e06c0bf5 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 23 Mar 2023 03:21:52 +0800 Subject: [PATCH 14/70] Update NumberInputUnstyled to support useClassNamesOverride --- .../components/number-input/UseNumberInput.js | 3 - .../number-input/UseNumberInput.tsx | 3 - .../NumberInputUnstyled.tsx | 58 ++++++++++--------- .../NumberInputUnstyled.types.ts | 2 + .../src/composeClasses/composeClasses.ts | 2 +- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index bf608ec76a5b2c..13ac914821f4a7 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -128,7 +128,6 @@ const StyledInputRoot = styled('div')( const StyledInputElement = styled('input')` grid-column: 2/3; grid-row: 1/3; - background: none; border: 0; outline: 0; @@ -138,12 +137,10 @@ const StyledInputElement = styled('input')` const StyledStepperButton = styled('button')` width: 1.5rem; height: 1rem; - display: flex; flex-flow: row nowrap; justify-content: center; align-items: center; - font-size: 0.875rem; box-sizing: border-box; border: 0; diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index d57249d3fe4591..68470bdaf3baa1 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -131,7 +131,6 @@ const StyledInputRoot: React.ElementType = styled('div')( const StyledInputElement = styled('input')` grid-column: 2/3; grid-row: 1/3; - background: none; border: 0; outline: 0; @@ -141,12 +140,10 @@ const StyledInputElement = styled('input')` const StyledStepperButton = styled('button')` width: 1.5rem; height: 1rem; - display: flex; flex-flow: row nowrap; justify-content: center; align-items: center; - font-size: 0.875rem; box-sizing: border-box; border: 0; diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index c6f47fc34a1138..63bde95849ed1b 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; -import classes from './numberInputUnstyledClasses'; +import { getNumberInputUnstyledUtilityClass } from './numberInputUnstyledClasses'; import useNumberInput from '../useNumberInput'; import { NumberInputUnstyledOwnerState, @@ -12,7 +12,30 @@ import { NumberInputUnstyledDecrementButtonSlotProps, NumberInputUnstyledTypeMap, } from './NumberInputUnstyled.types'; +import composeClasses from '../composeClasses'; import { EventHandlers, useSlotProps, WithOptionalOwnerState } from '../utils'; +import { useClassNamesOverride } from '../utils/ClassNameConfigurator'; + +const useUtilityClasses = (ownerState: NumberInputUnstyledOwnerState) => { + const { disabled, error, focused, formControlContext, isIncrementDisabled, isDecrementDisabled } = + ownerState; + + const slots = { + root: [ + 'root', + disabled && 'disabled', + error && 'error', + focused && 'focused', + Boolean(formControlContext) && 'formControl', + ], + input: ['input', disabled && 'disabled'], + incrementButton: ['incrementButton', isIncrementDisabled && 'disabled'], + decrementButton: ['decrementButton', isDecrementDisabled && 'disabled'], + }; + + return composeClasses(slots, useClassNamesOverride(getNumberInputUnstyledUtilityClass)); +}; + /** * * Demos: @@ -80,29 +103,12 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( disabled: disabledState, error: errorState, focused, - formControlContext: undefined, - }; - - const rootStateClasses = { - [classes.disabled]: disabledState, - [classes.error]: errorState, - [classes.focused]: focused, - [classes.formControl]: Boolean(formControlContext), - }; - - const inputStateClasses = { - [classes.disabled]: disabledState, - }; - - const incrementButtonStateClasses = { - [classes.disabled]: isIncrementDisabled, - // TODO: focusable if input is readonly + formControlContext, + isIncrementDisabled, + isDecrementDisabled, }; - const decrementButtonStateClasses = { - [classes.disabled]: isDecrementDisabled, - // TODO: focusable if input is readonly - }; + const classes = useUtilityClasses(ownerState); const propsForwardedToInputSlot = { id, @@ -119,7 +125,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( ref: forwardedRef, }, ownerState, - className: [classes.root, rootStateClasses, className], + className: [classes.root, className], }); const Input = slots.input ?? 'input'; @@ -130,7 +136,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( externalSlotProps: slotProps.input, // additionalProps: {}, ownerState, - className: [classes.input, inputStateClasses], + className: classes.input, }); const IncrementButton = slots.incrementButton ?? 'button'; @@ -140,7 +146,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( getSlotProps: getIncrementButtonProps, externalSlotProps: slotProps.incrementButton, ownerState, - className: [classes.incrementButton, incrementButtonStateClasses], + className: classes.incrementButton, }); const DecrementButton = slots.decrementButton ?? 'button'; @@ -151,7 +157,7 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled( externalSlotProps: slotProps.decrementButton, // additionalProps: {}, ownerState, - className: [classes.decrementButton, decrementButtonStateClasses], + className: classes.decrementButton, }); return ( diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts index eb1c6d8f74c50e..d20104acd37a28 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.types.ts @@ -77,6 +77,8 @@ export type NumberInputUnstyledOwnerState = Simplify< Omit & { formControlContext: FormControlState | undefined; focused: boolean; + isIncrementDisabled: boolean; + isDecrementDisabled: boolean; } >; diff --git a/packages/mui-utils/src/composeClasses/composeClasses.ts b/packages/mui-utils/src/composeClasses/composeClasses.ts index cf049e73c5d15f..259ed7a05534c3 100644 --- a/packages/mui-utils/src/composeClasses/composeClasses.ts +++ b/packages/mui-utils/src/composeClasses/composeClasses.ts @@ -6,7 +6,7 @@ export default function composeClasses( const output: Record = {} as any; Object.keys(slots).forEach( - // `Objet.keys(slots)` can't be wider than `T` because we infer `T` from `slots`. + // `Object.keys(slots)` can't be wider than `T` because we infer `T` from `slots`. // @ts-expect-error https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208 (slot: ClassKey) => { output[slot] = slots[slot] From e95f643400480c9be1974236845313407ae60306 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 23 Mar 2023 12:09:06 +0800 Subject: [PATCH 15/70] Add second hook demo --- .../number-input/UseNumberInputCompact.js | 171 +++++++++++++++++ .../number-input/UseNumberInputCompact.tsx | 175 ++++++++++++++++++ .../UseNumberInputCompact.tsx.preview | 10 + .../components/number-input/number-input.md | 8 + docs/data/base/pagesApi.js | 4 + .../react-number-input/[docsTab]/index.js | 12 +- .../number-input-unstyled.json | 7 +- docs/translations/translations.json | 2 + .../src/useNumberInput/useNumberInput.ts | 2 +- 9 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 docs/data/base/components/number-input/UseNumberInputCompact.js create mode 100644 docs/data/base/components/number-input/UseNumberInputCompact.tsx create mode 100644 docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.js b/docs/data/base/components/number-input/UseNumberInputCompact.js new file mode 100644 index 00000000000000..e439d4c196f930 --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact.js @@ -0,0 +1,171 @@ +import * as React from 'react'; +import useNumberInput from '@mui/base/useNumberInput'; +import { styled } from '@mui/system'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; + +const CompactNumberInput = React.forwardRef(function CompactNumberInput(props, ref) { + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + } = useNumberInput(props); + + const inputProps = getInputProps(); + + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( + + + + + + + + + + + + + + + + ); +}); + +export default function UseNumberInputCompact() { + const [value, setValue] = React.useState(); + + return ( + + setValue(val)} + /> +
          Current value: {value ?? ' '}
          +
          + ); +} + +const blue = { + 100: '#DAECFF', + 200: '#80BFFF', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', +}; + +const StyledInputRoot = styled('div')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + display: grid; + grid-template-columns: 2.5rem; + grid-template-rows: 2rem 2rem; + row-gap: 2px; + border-radius: 0.5rem; + border-style: solid; + border-width: 2px; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[600] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[500] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + + &:hover { + border-color: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; + box-shadow: + 0 0 0 1px ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}, + 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + } + `, +); + +const HiddenInput = styled('input')` + visibility: hidden; + position: absolute; +`; + +const StyledStepperButton = styled('button')( + ({ theme }) => ` + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + font-size: 0.875rem; + box-sizing: border-box; + border: 0; + color: inherit; + + &:hover { + cursor: pointer; + background: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; + color: ${grey[50]}; + } + + &:focus-visible { + outline: 0; + background: ${theme.palette.mode === 'dark' ? blue[400] : blue[200]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[50]}; + } + + &.increment { + border-top-left-radius: 0.35rem; + border-top-right-radius: 0.35rem; + } + + &.decrement { + border-bottom-left-radius: 0.35rem; + border-bottom-right-radius: 0.35rem; + } +`, +); + +const Layout = styled('div')` + display: flex; + flex-flow: row nowrap; + align-items: center; + column-gap: 2rem; +`; diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx b/docs/data/base/components/number-input/UseNumberInputCompact.tsx new file mode 100644 index 00000000000000..bde43e3b77e3f6 --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; +import useNumberInput, { UseNumberInputParameters } from '@mui/base/useNumberInput'; +import { styled } from '@mui/system'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; + +const CompactNumberInput = React.forwardRef(function CompactNumberInput( + props: UseNumberInputParameters & React.InputHTMLAttributes, + ref: React.ForwardedRef, +) { + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + } = useNumberInput(props); + + const inputProps = getInputProps(); + + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( + + + + + + + + + + + + + + + + ); +}); + +export default function UseNumberInputCompact() { + const [value, setValue] = React.useState(); + + return ( + + setValue(val)} + /> + +
          Current value: {value ?? ' '}
          +
          + ); +} + +const blue = { + 100: '#DAECFF', + 200: '#80BFFF', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', +}; + +const StyledInputRoot = styled('div')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + display: grid; + grid-template-columns: 2.5rem; + grid-template-rows: 2rem 2rem; + row-gap: 2px; + border-radius: 0.5rem; + border-style: solid; + border-width: 2px; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: ${theme.palette.mode === 'dark' ? grey[600] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[500] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + + &:hover { + border-color: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; + box-shadow: + 0 0 0 1px ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}, + 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + } + `, +); + +const HiddenInput = styled('input')` + visibility: hidden; + position: absolute; +`; + +const StyledStepperButton = styled('button')( + ({ theme }) => ` + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + + font-size: 0.875rem; + box-sizing: border-box; + border: 0; + color: inherit; + + &:hover { + cursor: pointer; + background: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; + color: ${grey[50]}; + } + + &:focus-visible { + outline: 0; + background: ${theme.palette.mode === 'dark' ? blue[400] : blue[200]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[50]}; + } + + &.increment { + border-top-left-radius: 0.35rem; + border-top-right-radius: 0.35rem; + } + + &.decrement { + border-bottom-left-radius: 0.35rem; + border-bottom-right-radius: 0.35rem; + } +`, +); + +const Layout = styled('div')` + display: flex; + flex-flow: row nowrap; + align-items: center; + column-gap: 2rem; +`; diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview b/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview new file mode 100644 index 00000000000000..b0d36ab08ca07b --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview @@ -0,0 +1,10 @@ + + setValue(val)} + /> + +
          Current value: {value ?? ' '}
          +
          \ No newline at end of file diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 04e24fb4877d4d..53b3b184f659c9 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -46,4 +46,12 @@ Here's an example of a component built using the hook alone: ### Quantity Input +The "purchase quantity" input component from the MUI store: + {{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} + +### Steppers only + +A compact stepper-only component using the hook: + +{{"demo": "UseNumberInputCompact.js", "defaultCodeOpen": false, "bg": "gradient"}} diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index adc2ebbb5078ac..c32591c6708721 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -72,6 +72,10 @@ module.exports = [ title: 'useMenuButton', }, { pathname: '/base-ui/react-menu/hooks-api/#use-menu-item', title: 'useMenuItem' }, + { + pathname: '/base-ui/react-number-input/hooks-api/#use-number-input', + title: 'useNumberInput', + }, { pathname: '/base-ui/react-select/hooks-api/#use-option', title: 'useOption' }, { pathname: '/base-ui/react-select/hooks-api/#use-select', title: 'useSelect' }, { pathname: '/base-ui/react-slider/hooks-api/#use-slider', title: 'useSlider' }, diff --git a/docs/pages/base-ui/react-number-input/[docsTab]/index.js b/docs/pages/base-ui/react-number-input/[docsTab]/index.js index 0f7112e16e6d40..b7fe29bff5c846 100644 --- a/docs/pages/base-ui/react-number-input/[docsTab]/index.js +++ b/docs/pages/base-ui/react-number-input/[docsTab]/index.js @@ -4,6 +4,7 @@ import AppFrame from 'docs/src/modules/components/AppFrame'; import * as pageProps from 'docs/data/base/components/number-input/number-input.md?@mui/markdown'; import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; import NumberInputUnstyledApiJsonPageContent from '../../api/number-input-unstyled.json'; +import useNumberInputApiJsonPageContent from '../../api/use-number-input.json'; export default function Page(props) { const { userLanguage, ...other } = props; @@ -29,12 +30,19 @@ export const getStaticProps = () => { ); const NumberInputUnstyledApiDescriptions = mapApiPageTranslations(NumberInputUnstyledApiReq); + const useNumberInputApiReq = require.context( + 'docs/translations/api-docs/use-number-input', + false, + /use-number-input.*.json$/, + ); + const useNumberInputApiDescriptions = mapApiPageTranslations(useNumberInputApiReq); + return { props: { componentsApiDescriptions: { NumberInputUnstyled: NumberInputUnstyledApiDescriptions }, componentsApiPageContents: { NumberInputUnstyled: NumberInputUnstyledApiJsonPageContent }, - hooksApiDescriptions: {}, - hooksApiPageContents: {}, + hooksApiDescriptions: { useNumberInput: useNumberInputApiDescriptions }, + hooksApiPageContents: { useNumberInput: useNumberInputApiJsonPageContent }, }, }; }; diff --git a/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json index f47ddfa8884ad4..4d8c3b82671253 100644 --- a/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json +++ b/docs/translations/api-docs-base/number-input-unstyled/number-input-unstyled.json @@ -6,9 +6,14 @@ "disabled": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component.", "error": "If true, the input will indicate an error by setting the aria-invalid attribute on the input and the Mui-error class on the root element.", "id": "The id of the input element.", + "max": "The maximum value.", + "min": "The minimum value.", + "onValueChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", "slotProps": "The props used for each slot inside the NumberInput.", - "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details." + "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details.", + "step": "The amount that the value changes on each increment or decrement.", + "value": "The current value. Use when the component is controlled." }, "classDescriptions": {} } diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 66a53dc18c9e3c..a262477aa33338 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -278,6 +278,7 @@ "/base-ui/react-menu/components-api/#menu-item": "MenuItem", "/base-ui/react-modal/components-api/#modal": "Modal", "/base-ui/react-no-ssr/components-api/#no-ssr": "NoSsr", + "/base-ui/react-number-input/components-api/#number-input-unstyled": "NumberInputUnstyled", "/base-ui/react-select/components-api/#option": "Option", "/base-ui/react-select/components-api/#option-group": "OptionGroup", "/base-ui/react-popper/components-api/#popper": "Popper", @@ -301,6 +302,7 @@ "/base-ui/react-menu/hooks-api/#use-menu": "useMenu", "/base-ui/react-menu/hooks-api/#use-menu-button": "useMenuButton", "/base-ui/react-menu/hooks-api/#use-menu-item": "useMenuItem", + "/base-ui/react-number-input/hooks-api/#use-number-input": "useNumberInput", "/base-ui/react-select/hooks-api/#use-option": "useOption", "/base-ui/react-select/hooks-api/#use-select": "useSelect", "/base-ui/react-slider/hooks-api/#use-slider": "useSlider", diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index 45a203ff56d27e..e38da7c10de9c2 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -33,7 +33,7 @@ const parseInput = (v: string): string => { * * API: * - * - [useNumberInput API](https://mui.com/base/api/use-number-input/) + * - [useNumberInput API](https://mui.com/base/react-number-input/hooks-api/#use-number-input) */ export default function useNumberInput( parameters: UseNumberInputParameters, From b0ea58c4c7b9ac1fea5eb1a387a78279f4292bbc Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 12 Apr 2023 17:42:41 +0800 Subject: [PATCH 16/70] Fix compatibility with ComponentPageTabs --- docs/data/base/components/number-input/number-input.md | 8 +++++--- docs/pages/base-ui/react-number-input.js | 7 ------- 2 files changed, 5 insertions(+), 10 deletions(-) delete mode 100644 docs/pages/base-ui/react-number-input.js diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 53b3b184f659c9..fd3f066e9e045a 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -10,14 +10,16 @@ githubLabel: 'component: NumberInput'

          The Unstyled Number Input component provides users with a field for integer values, and buttons to increment or decrement the value.

          +{{"component": "modules/components/ComponentLinkHeader.js", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + ## Introduction -🚧 +Lorem ipsum etc etc {{"demo": "UnstyledNumberInputIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} -{{"component": "modules/components/ComponentLinkHeader.js", "design": false}} - ## Component ### Usage diff --git a/docs/pages/base-ui/react-number-input.js b/docs/pages/base-ui/react-number-input.js deleted file mode 100644 index 01bef2bfa30c7d..00000000000000 --- a/docs/pages/base-ui/react-number-input.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; -import * as pageProps from 'docs/data/base/components/number-input/number-input.md?@mui/markdown'; - -export default function Page() { - return ; -} From 23d9d361d1898211c798e1dcbf6904c920167847 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 12 Apr 2023 18:52:51 +0800 Subject: [PATCH 17/70] Write docs page --- .../number-input/UnstyledNumberInputBasic.js | 146 +++++++++++++++++ .../number-input/UnstyledNumberInputBasic.tsx | 150 ++++++++++++++++++ .../UnstyledNumberInputBasic.tsx.preview | 6 + .../components/number-input/number-input.md | 81 ++++++++-- 4 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 docs/data/base/components/number-input/UnstyledNumberInputBasic.js create mode 100644 docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx create mode 100644 docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js new file mode 100644 index 00000000000000..8b72121a7486ff --- /dev/null +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js @@ -0,0 +1,146 @@ +import * as React from 'react'; +import NumberInputUnstyled, { + numberInputUnstyledClasses, +} from '@mui/base/NumberInputUnstyled'; +import { styled } from '@mui/system'; + +const blue = { + 100: '#DAECFF', + 200: '#b6daff', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', +}; + +const StyledInputRoot = styled('div')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-weight: 400; + border-radius: 12px; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + display: grid; + grid-template-columns: 1fr 24px; + grid-template-rows: 1fr 1fr; + overflow: hidden; + + + &.${numberInputUnstyledClasses.focused} { + border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + &:hover { + border-color: ${blue[400]}; + } + + // firefox + &:focus-visible { + outline: 0; + } +`, +); + +const StyledInputElement = styled('input')( + ({ theme }) => ` + font-size: 0.875rem; + font-family: inherit; + font-weight: 400; + line-height: 1.5; + grid-column: 1/2; + grid-row: 1/3; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: inherit; + border: none; + border-radius: inherit; + padding: 12px 12px; + outline: 0; +`, +); + +const StyledButton = styled('button')( + ({ theme }) => ` + width: 24px; + height: 24px; + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 0; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + cursor: pointer; + } + + &.${numberInputUnstyledClasses.incrementButton} { + grid-column: 2/3; + grid-row: 1/2; + } + + &.${numberInputUnstyledClasses.decrementButton} { + grid-column: 2/3; + grid-row: 2/3; + transform: rotate(180deg); + } +`, +); + +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + return ( + + ); +}); + +export default function UnstyledNumberInputBasic() { + const [value, setValue] = React.useState(); + return ( + setValue(val)} + /> + ); +} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx new file mode 100644 index 00000000000000..74180e92172b2e --- /dev/null +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import NumberInputUnstyled, { + NumberInputUnstyledProps, + numberInputUnstyledClasses, +} from '@mui/base/NumberInputUnstyled'; +import { styled } from '@mui/system'; + +const blue = { + 100: '#DAECFF', + 200: '#b6daff', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', +}; + +const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', +}; + +const StyledInputRoot = styled('div')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-weight: 400; + border-radius: 12px; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + display: grid; + grid-template-columns: 1fr 24px; + grid-template-rows: 1fr 1fr; + overflow: hidden; + + + &.${numberInputUnstyledClasses.focused} { + border-color: ${blue[400]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + &:hover { + border-color: ${blue[400]}; + } + + // firefox + &:focus-visible { + outline: 0; + } +`, +); + +const StyledInputElement = styled('input')( + ({ theme }) => ` + font-size: 0.875rem; + font-family: inherit; + font-weight: 400; + line-height: 1.5; + grid-column: 1/2; + grid-row: 1/3; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + background: inherit; + border: none; + border-radius: inherit; + padding: 12px 12px; + outline: 0; +`, +); + +const StyledButton = styled('button')( + ({ theme }) => ` + width: 24px; + height: 24px; + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 0; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + cursor: pointer; + } + + &.${numberInputUnstyledClasses.incrementButton} { + grid-column: 2/3; + grid-row: 1/2; + } + + &.${numberInputUnstyledClasses.decrementButton} { + grid-column: 2/3; + grid-row: 2/3; + transform: rotate(180deg); + } +`, +); + +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: NumberInputUnstyledProps, + ref: React.ForwardedRef, +) { + return ( + + ); +}); + +export default function UnstyledNumberInputBasic() { + const [value, setValue] = React.useState(); + return ( + setValue(val)} + /> + ); +} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview new file mode 100644 index 00000000000000..86506a8a509b24 --- /dev/null +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview @@ -0,0 +1,6 @@ + setValue(val)} +/> \ No newline at end of file diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index fd3f066e9e045a..2cf70804e3292f 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -16,7 +16,8 @@ githubLabel: 'component: NumberInput' ## Introduction -Lorem ipsum etc etc +A number input is a UI element that accepts numeric values from the user. +The Unstyled Number Input component is a customizable replacement for the native HTML `` that solves common usability [issues](/material-ui/react-text-field/#type-quot-number-quot) of the native counterpart. {{"demo": "UnstyledNumberInputIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} @@ -34,26 +35,86 @@ export default function MyApp() { } ``` +### Basics + +The following demo shows how to create a number input component, apply some styling, and write the latest value to a state variable using the `onValueChange` prop: + +{{"demo": "UnstyledNumberInputBasic.js"}} + +Here's another demo of a Unstyled Number Input with fully customized styles: + +{{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} + +### Anatomy + +The Unstyled Number Input component consists of a root `
          ` that contains one interior `` slot, and two `
          +``` + +### Slot props + +:::info +The following props are available on all non-utility Base components. +See [Usage](/base/getting-started/usage/) for full details. +::: + +Use the `slots` prop to override the root slot or any interior slots: + +```jsx + +``` + +Use the `slotProps` prop to pass custom props to internal slots. +The following code snippet: + +- applies a CSS class called `my-num-input` to the input slot, +- and passes a `direction` prop to the `CustomButton` components in the increment and decrement button slots + +```jsx + +``` + ## Hook ```js import useNumberInput from '@mui/base/useNumberInput'; ``` -Here's an example of a component built using the hook alone: - -{{"demo": "UseNumberInput.js", "defaultCodeOpen": false, "bg": "gradient"}} +The `useNumberInput` hook lets you apply the functionality of a number input to a fully custom component. +It returns props to be placed on the custom component, along with fields representing the component's internal state. -## Customization +Hooks _do not_ support [slot props](#slot-props), but they do support [customization props](#customization). -### Quantity Input +:::info +Hooks give you the most room for customization, but require more work to implement. +With hooks, you can take full control over how your component is rendered, and define all the custom props and CSS classes you need. -The "purchase quantity" input component from the MUI store: +You may not need to use hooks unless you find that you're limited by the customization options of their component counterparts—for instance, if your component requires significantly different [structure](#anatomy). +::: -{{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} +Here's an example of a custom component built using the `useNumberInput` hook with all the required props: -### Steppers only +{{"demo": "UseNumberInput.js", "defaultCodeOpen": false, "bg": "gradient"}} -A compact stepper-only component using the hook: +Here's an example of a "compact" number input component using the hook that only consists of the stepper buttons. +In this demo, `onValueChange` is used to write the latest value of the component to a state variable. {{"demo": "UseNumberInputCompact.js", "defaultCodeOpen": false, "bg": "gradient"}} From ce96976514c07e1e128ab7d9d8538bde4a4f4f4c Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 12 Apr 2023 19:36:42 +0800 Subject: [PATCH 18/70] Add explanation of min, max, step props --- .../components/number-input/number-input.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 2cf70804e3292f..287aa5a4785666 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -41,6 +41,24 @@ The following demo shows how to create a number input component, apply some styl {{"demo": "UnstyledNumberInputBasic.js"}} +The `min` and `max` props can be used to define a range of accepted values. You can pass only one of them to define an open-ended range. + +```tsx + + + // Open-ended + +``` + +The `step` prop can be used to defined the granularity of the change in value when incrementing or decrementing. For example, if `min={0}` and `step={2}`, valid values for the component would be 0, 2, 4… since the value can only be changed in increments of 2. + +```tsx +// valid values: 0, 2, 4, 6, 8... + +``` + +When the input field is in focus, you can enter values that fall outside the valid range. The value will be clamped based on `min`, `max` and `step` once the input field is blurred. + Here's another demo of a Unstyled Number Input with fully customized styles: {{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} From e187960f5de5ebc2e878acceeb76249327561296 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 13 Apr 2023 18:40:57 +0800 Subject: [PATCH 19/70] Add explanation of shiftMultiplier prop --- docs/data/base/components/number-input/number-input.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 287aa5a4785666..0d691338853d2c 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -59,6 +59,14 @@ The `step` prop can be used to defined the granularity of the change in value wh When the input field is in focus, you can enter values that fall outside the valid range. The value will be clamped based on `min`, `max` and `step` once the input field is blurred. +Holding down the Shift key when interacting with the stepper buttons applies a multipler (default 10x) to the value change of each step. + +This can be customized with the `shiftMultiplier` prop. In the following snippet, if Shift is held when clicking the increment button, the value will change from 0 to 5, then to 10 etc. + +```tsx + +``` + Here's another demo of a Unstyled Number Input with fully customized styles: {{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} From 7b733c53fb23cbfc2e07b95a732d474e84b0c1b1 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 17 Apr 2023 17:59:14 +0800 Subject: [PATCH 20/70] Simplify hook demo --- .../components/number-input/UseNumberInput.js | 36 +++++++----------- .../number-input/UseNumberInput.tsx | 38 ++++++++----------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index 13ac914821f4a7..8c6cd348814846 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -18,7 +18,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref inputProps.ref = useForkRef(inputProps.ref, ref); return ( - + ` + ({ theme }) => ` width: 320px; font-family: IBM Plex Sans, sans-serif; @@ -101,26 +101,18 @@ const StyledInputRoot = styled('div')( color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; - ${ - // can be massively simplified with has:() once firefox supports it - // ETA first half 2023 - https://connect.mozilla.org/t5/discussions/when-is-has-css-selector-going-to-be-fully-implemented-in/m-p/27010/highlight/true#M10711 - focused - ? ` - border-color: ${blue[400]}; - box-shadow: - inset 0 0 0 1px ${blue[400]}, - 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; - - & button:hover { - background: ${blue[400]}; - } - ` - : ` - border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 2px 2px ${ - theme.palette.mode === 'dark' ? grey[900] : grey[50] - }; - ` + border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + &.focused { + border-color: ${blue[400]}; + box-shadow: + inset 0 0 0 1px ${blue[400]}, + 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + & button:hover { + background: ${blue[400]}; + } } `, ); diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index 68470bdaf3baa1..cd35d17eb2e141 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import useNumberInput, { UseNumberInputParameters } from '@mui/base/useNumberInput'; -import { styled, Theme } from '@mui/system'; +import { styled } from '@mui/system'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; const CustomNumberInput = React.forwardRef(function CustomNumberInput( @@ -21,7 +21,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( inputProps.ref = useForkRef(inputProps.ref, ref); return ( - + ` + ({ theme }) => ` width: 320px; font-family: IBM Plex Sans, sans-serif; @@ -104,26 +104,18 @@ const StyledInputRoot: React.ElementType = styled('div')( color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; - ${ - // can be massively simplified with has:() once firefox supports it - // ETA first half 2023 - https://connect.mozilla.org/t5/discussions/when-is-has-css-selector-going-to-be-fully-implemented-in/m-p/27010/highlight/true#M10711 - focused - ? ` - border-color: ${blue[400]}; - box-shadow: - inset 0 0 0 1px ${blue[400]}, - 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; - - & button:hover { - background: ${blue[400]}; - } - ` - : ` - border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 2px 2px ${ - theme.palette.mode === 'dark' ? grey[900] : grey[50] - }; - ` + border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + &.focused { + border-color: ${blue[400]}; + box-shadow: + inset 0 0 0 1px ${blue[400]}, + 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + + & button:hover { + background: ${blue[400]}; + } } `, ); From e9267ff921f13bd10db866b29295a976bfe60dfb Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 18 Apr 2023 23:12:19 +0800 Subject: [PATCH 21/70] Design revisions --- .../components/number-input/QuantityInput.js | 15 ++++++------ .../components/number-input/QuantityInput.tsx | 15 ++++++------ .../number-input/UseNumberInputCompact.js | 24 +++++++------------ .../number-input/UseNumberInputCompact.tsx | 24 +++++++------------ .../components/number-input/number-input.md | 6 ++--- 5 files changed, 37 insertions(+), 47 deletions(-) diff --git a/docs/data/base/components/number-input/QuantityInput.js b/docs/data/base/components/number-input/QuantityInput.js index 6f7338782cb6d2..69095541949128 100644 --- a/docs/data/base/components/number-input/QuantityInput.js +++ b/docs/data/base/components/number-input/QuantityInput.js @@ -5,11 +5,13 @@ import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; const blue = { - 100: '#DAECFF', + 100: '#daecff', 200: '#b6daff', - 400: '#3399FF', - 500: '#007FFF', - 600: '#0072E5', + 300: '#66b2ff', + 400: '#3399ff', + 500: '#007fff', + 600: '#0072e5', + 800: '#004c99', }; const grey = { @@ -87,7 +89,7 @@ const StyledButton = styled('button')( line-height: 1.5; border: 0; border-radius: 999px; - color: ${theme.palette.mode === 'dark' ? blue[400] : blue[600]}; + color: ${theme.palette.mode === 'dark' ? blue[300] : blue[600]}; background: transparent; width: 40px; @@ -102,8 +104,7 @@ const StyledButton = styled('button')( transition-duration: 120ms; &:hover { - background: ${theme.palette.mode === 'dark' ? grey[800] : blue[100]}; - border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + background: ${theme.palette.mode === 'dark' ? blue[800] : blue[100]}; cursor: pointer; } diff --git a/docs/data/base/components/number-input/QuantityInput.tsx b/docs/data/base/components/number-input/QuantityInput.tsx index 7c402fb0fc55a4..e2d7187f9189b3 100644 --- a/docs/data/base/components/number-input/QuantityInput.tsx +++ b/docs/data/base/components/number-input/QuantityInput.tsx @@ -7,11 +7,13 @@ import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; const blue = { - 100: '#DAECFF', + 100: '#daecff', 200: '#b6daff', - 400: '#3399FF', - 500: '#007FFF', - 600: '#0072E5', + 300: '#66b2ff', + 400: '#3399ff', + 500: '#007fff', + 600: '#0072e5', + 800: '#004c99', }; const grey = { @@ -89,7 +91,7 @@ const StyledButton = styled('button')( line-height: 1.5; border: 0; border-radius: 999px; - color: ${theme.palette.mode === 'dark' ? blue[400] : blue[600]}; + color: ${theme.palette.mode === 'dark' ? blue[300] : blue[600]}; background: transparent; width: 40px; @@ -104,8 +106,7 @@ const StyledButton = styled('button')( transition-duration: 120ms; &:hover { - background: ${theme.palette.mode === 'dark' ? grey[800] : blue[100]}; - border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + background: ${theme.palette.mode === 'dark' ? blue[800] : blue[100]}; cursor: pointer; } diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.js b/docs/data/base/components/number-input/UseNumberInputCompact.js index e439d4c196f930..0de481551a251c 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.js +++ b/docs/data/base/components/number-input/UseNumberInputCompact.js @@ -97,27 +97,20 @@ const grey = { const StyledInputRoot = styled('div')( ({ theme }) => ` - font-family: IBM Plex Sans, sans-serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.5; display: grid; grid-template-columns: 2.5rem; grid-template-rows: 2rem 2rem; - row-gap: 2px; + row-gap: 1px; border-radius: 0.5rem; border-style: solid; - border-width: 2px; + border-width: 1px; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; - background: ${theme.palette.mode === 'dark' ? grey[600] : grey[50]}; - border-color: ${theme.palette.mode === 'dark' ? grey[500] : grey[200]}; - box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; + border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; + overflow: auto; &:hover { - border-color: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; - box-shadow: - 0 0 0 1px ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}, - 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border-color: ${blue[500]}; } `, ); @@ -138,16 +131,17 @@ const StyledStepperButton = styled('button')( box-sizing: border-box; border: 0; color: inherit; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; &:hover { cursor: pointer; - background: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; + background: ${blue[500]}; color: ${grey[50]}; } &:focus-visible { outline: 0; - background: ${theme.palette.mode === 'dark' ? blue[400] : blue[200]}; + background: ${blue[500]}; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[50]}; } diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx b/docs/data/base/components/number-input/UseNumberInputCompact.tsx index bde43e3b77e3f6..ed6349f506d69a 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.tsx +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx @@ -101,27 +101,20 @@ const grey = { const StyledInputRoot = styled('div')( ({ theme }) => ` - font-family: IBM Plex Sans, sans-serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.5; display: grid; grid-template-columns: 2.5rem; grid-template-rows: 2rem 2rem; - row-gap: 2px; + row-gap: 1px; border-radius: 0.5rem; border-style: solid; - border-width: 2px; + border-width: 1px; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; - background: ${theme.palette.mode === 'dark' ? grey[600] : grey[50]}; - border-color: ${theme.palette.mode === 'dark' ? grey[500] : grey[200]}; - box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; + border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; + overflow: auto; &:hover { - border-color: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; - box-shadow: - 0 0 0 1px ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}, - 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border-color: ${blue[500]}; } `, ); @@ -142,16 +135,17 @@ const StyledStepperButton = styled('button')( box-sizing: border-box; border: 0; color: inherit; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; &:hover { cursor: pointer; - background: ${theme.palette.mode === 'dark' ? blue[600] : blue[400]}; + background: ${blue[500]}; color: ${grey[50]}; } &:focus-visible { outline: 0; - background: ${theme.palette.mode === 'dark' ? blue[400] : blue[200]}; + background: ${blue[500]}; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[50]}; } diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 0d691338853d2c..431491ed54e5f7 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -69,7 +69,7 @@ This can be customized with the `shiftMultiplier` prop. In the following snippet Here's another demo of a Unstyled Number Input with fully customized styles: -{{"demo": "QuantityInput.js", "defaultCodeOpen": false, "bg": "gradient"}} +{{"demo": "QuantityInput.js", "defaultCodeOpen": false}} ### Anatomy @@ -138,9 +138,9 @@ You may not need to use hooks unless you find that you're limited by the customi Here's an example of a custom component built using the `useNumberInput` hook with all the required props: -{{"demo": "UseNumberInput.js", "defaultCodeOpen": false, "bg": "gradient"}} +{{"demo": "UseNumberInput.js", "defaultCodeOpen": false}} Here's an example of a "compact" number input component using the hook that only consists of the stepper buttons. In this demo, `onValueChange` is used to write the latest value of the component to a state variable. -{{"demo": "UseNumberInputCompact.js", "defaultCodeOpen": false, "bg": "gradient"}} +{{"demo": "UseNumberInputCompact.js", "defaultCodeOpen": false}} From e42485866bff68a04de7bd088e79f7486359baa9 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 18 Apr 2023 23:18:36 +0800 Subject: [PATCH 22/70] Format demos --- .../components/number-input/QuantityInput.js | 54 ++++++------- .../components/number-input/QuantityInput.tsx | 60 +++++++------- .../number-input/UnstyledNumberInputBasic.js | 74 ++++++++--------- .../number-input/UnstyledNumberInputBasic.tsx | 80 +++++++++---------- .../UnstyledNumberInputIntroduction.js | 62 +++++++------- .../UnstyledNumberInputIntroduction.tsx | 68 ++++++++-------- 6 files changed, 199 insertions(+), 199 deletions(-) diff --git a/docs/data/base/components/number-input/QuantityInput.js b/docs/data/base/components/number-input/QuantityInput.js index 69095541949128..f9fc6ed88c93d9 100644 --- a/docs/data/base/components/number-input/QuantityInput.js +++ b/docs/data/base/components/number-input/QuantityInput.js @@ -4,6 +4,33 @@ import { styled } from '@mui/system'; import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + return ( + , + }, + decrementButton: { + children: , + }, + }} + {...props} + ref={ref} + /> + ); +}); + +export default function QuantityInput() { + return ; +} + const blue = { 100: '#daecff', 200: '#b6daff', @@ -113,30 +140,3 @@ const StyledButton = styled('button')( } `, ); - -const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { - return ( - , - }, - decrementButton: { - children: , - }, - }} - {...props} - ref={ref} - /> - ); -}); - -export default function QuantityInput() { - return ; -} diff --git a/docs/data/base/components/number-input/QuantityInput.tsx b/docs/data/base/components/number-input/QuantityInput.tsx index e2d7187f9189b3..8febde40ff27d9 100644 --- a/docs/data/base/components/number-input/QuantityInput.tsx +++ b/docs/data/base/components/number-input/QuantityInput.tsx @@ -6,6 +6,36 @@ import { styled } from '@mui/system'; import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: NumberInputUnstyledProps, + ref: React.ForwardedRef, +) { + return ( + , + }, + decrementButton: { + children: , + }, + }} + {...props} + ref={ref} + /> + ); +}); + +export default function QuantityInput() { + return ; +} + const blue = { 100: '#daecff', 200: '#b6daff', @@ -115,33 +145,3 @@ const StyledButton = styled('button')( } `, ); - -const CustomNumberInput = React.forwardRef(function CustomNumberInput( - props: NumberInputUnstyledProps, - ref: React.ForwardedRef, -) { - return ( - , - }, - decrementButton: { - children: , - }, - }} - {...props} - ref={ref} - /> - ); -}); - -export default function QuantityInput() { - return ; -} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js index 8b72121a7486ff..72077718cec61e 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js @@ -4,6 +4,43 @@ import NumberInputUnstyled, { } from '@mui/base/NumberInputUnstyled'; import { styled } from '@mui/system'; +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + return ( + + ); +}); + +export default function UnstyledNumberInputBasic() { + const [value, setValue] = React.useState(); + return ( + setValue(val)} + /> + ); +} + const blue = { 100: '#DAECFF', 200: '#b6daff', @@ -107,40 +144,3 @@ const StyledButton = styled('button')( } `, ); - -const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { - return ( - - ); -}); - -export default function UnstyledNumberInputBasic() { - const [value, setValue] = React.useState(); - return ( - setValue(val)} - /> - ); -} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx index 74180e92172b2e..a5dd73d8b945ec 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx @@ -5,6 +5,46 @@ import NumberInputUnstyled, { } from '@mui/base/NumberInputUnstyled'; import { styled } from '@mui/system'; +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: NumberInputUnstyledProps, + ref: React.ForwardedRef, +) { + return ( + + ); +}); + +export default function UnstyledNumberInputBasic() { + const [value, setValue] = React.useState(); + return ( + setValue(val)} + /> + ); +} + const blue = { 100: '#DAECFF', 200: '#b6daff', @@ -108,43 +148,3 @@ const StyledButton = styled('button')( } `, ); - -const CustomNumberInput = React.forwardRef(function CustomNumberInput( - props: NumberInputUnstyledProps, - ref: React.ForwardedRef, -) { - return ( - - ); -}); - -export default function UnstyledNumberInputBasic() { - const [value, setValue] = React.useState(); - return ( - setValue(val)} - /> - ); -} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js index 01c65a6b61c9ff..d36592fdc6fa8e 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js @@ -4,6 +4,37 @@ import NumberInputUnstyled, { } from '@mui/base/NumberInputUnstyled'; import { styled } from '@mui/system'; +const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { + return ( + + ); +}); + +export default function UnstyledNumberInputIntroduction() { + return ( + + ); +} + const blue = { 100: '#DAECFF', 200: '#b6daff', @@ -107,34 +138,3 @@ const StyledButton = styled('button')( } `, ); - -const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { - return ( - - ); -}); - -export default function UnstyledNumberInputIntroduction() { - return ( - - ); -} diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx index e71b254f6e280e..28092a884b8f5e 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx @@ -5,6 +5,40 @@ import NumberInputUnstyled, { } from '@mui/base/NumberInputUnstyled'; import { styled } from '@mui/system'; +const CustomNumberInput = React.forwardRef(function CustomNumberInput( + props: NumberInputUnstyledProps, + ref: React.ForwardedRef, +) { + return ( + + ); +}); + +export default function UnstyledNumberInputIntroduction() { + return ( + + ); +} + const blue = { 100: '#DAECFF', 200: '#b6daff', @@ -108,37 +142,3 @@ const StyledButton = styled('button')( } `, ); - -const CustomNumberInput = React.forwardRef(function CustomNumberInput( - props: NumberInputUnstyledProps, - ref: React.ForwardedRef, -) { - return ( - - ); -}); - -export default function UnstyledNumberInputIntroduction() { - return ( - - ); -} From 1a670685d9d570707610cabec152e0f4b185714a Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 19 Apr 2023 12:19:21 +0800 Subject: [PATCH 23/70] Expand introduction paragraph --- docs/data/base/components/number-input/number-input.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 431491ed54e5f7..3fe5e5a23532fb 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -17,7 +17,13 @@ githubLabel: 'component: NumberInput' ## Introduction A number input is a UI element that accepts numeric values from the user. -The Unstyled Number Input component is a customizable replacement for the native HTML `` that solves common usability [issues](/material-ui/react-text-field/#type-quot-number-quot) of the native counterpart. +The Unstyled Number Input component is a customizable replacement for the native HTML `` that solves common usability issues of the native counterpart, such as: + +- Inconsistencies across browsers in the appearance and behavior of the stepper buttons +- Allowing certain non-numeric characters ('e', '+', '-', '.') and silently discarding others +- Incompatibilities with assistive technologies and limited accessibility features + +See [this article](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/) by the GOV.UK Design System team for a more detailed explanation. {{"demo": "UnstyledNumberInputIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} From 1f8162f50f5697182141c4c9629edbb3055154df Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 19 Apr 2023 19:01:24 +0800 Subject: [PATCH 24/70] Update onValueChange signature --- .../number-input/UnstyledNumberInputBasic.js | 2 +- .../number-input/UnstyledNumberInputBasic.tsx | 2 +- .../number-input/UnstyledNumberInputBasic.tsx.preview | 2 +- .../components/number-input/UseNumberInputCompact.js | 2 +- .../components/number-input/UseNumberInputCompact.tsx | 2 +- .../number-input/UseNumberInputCompact.tsx.preview | 2 +- docs/pages/base-ui/api/number-input-unstyled.json | 1 + docs/pages/base-ui/api/use-number-input.json | 4 ++-- .../number-input-unstyled/number-input-unstyled.json | 1 + .../api-docs/use-number-input/use-number-input.json | 1 + .../src/NumberInputUnstyled/NumberInputUnstyled.tsx | 4 +++- packages/mui-base/src/useNumberInput/useNumberInput.ts | 4 ++-- .../src/useNumberInput/useNumberInput.types.ts | 10 +++++++++- 13 files changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js index 72077718cec61e..c13f107d495bc2 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js @@ -36,7 +36,7 @@ export default function UnstyledNumberInputBasic() { aria-label="Demo number input" placeholder="Type a number…" value={value} - onValueChange={(val) => setValue(val)} + onValueChange={(event, val) => setValue(val)} /> ); } diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx index a5dd73d8b945ec..c5b2244d5dbdb6 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx @@ -40,7 +40,7 @@ export default function UnstyledNumberInputBasic() { aria-label="Demo number input" placeholder="Type a number…" value={value} - onValueChange={(val) => setValue(val)} + onValueChange={(event, val) => setValue(val)} /> ); } diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview index 86506a8a509b24..0d8503aef8796c 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview @@ -2,5 +2,5 @@ aria-label="Demo number input" placeholder="Type a number…" value={value} - onValueChange={(val) => setValue(val)} + onValueChange={(event, val) => setValue(val)} /> \ No newline at end of file diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.js b/docs/data/base/components/number-input/UseNumberInputCompact.js index 0de481551a251c..1e65c986dcd0e8 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.js +++ b/docs/data/base/components/number-input/UseNumberInputCompact.js @@ -67,7 +67,7 @@ export default function UseNumberInputCompact() { aria-label="Compact number input" placeholder="Type a number…" value={value} - onValueChange={(val) => setValue(val)} + onValueChange={(event, val) => setValue(val)} />
          Current value: {value ?? ' '}
          diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx b/docs/data/base/components/number-input/UseNumberInputCompact.tsx index ed6349f506d69a..0e4ba9032f05ef 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.tsx +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx @@ -70,7 +70,7 @@ export default function UseNumberInputCompact() { aria-label="Compact number input" placeholder="Type a number…" value={value} - onValueChange={(val) => setValue(val)} + onValueChange={(event, val) => setValue(val)} />
          Current value: {value ?? ' '}
          diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview b/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview index b0d36ab08ca07b..9f7ef7b2eb0ca2 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview @@ -3,7 +3,7 @@ aria-label="Compact number input" placeholder="Type a number…" value={value} - onValueChange={(val) => setValue(val)} + onValueChange={(event, val) => setValue(val)} />
          Current value: {value ?? ' '}
          diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index 173eece873015a..b937735c543a81 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -7,6 +7,7 @@ "id": { "type": { "name": "string" } }, "max": { "type": { "name": "number" } }, "min": { "type": { "name": "number" } }, + "onChange": { "type": { "name": "func" } }, "onValueChange": { "type": { "name": "func" } }, "required": { "type": { "name": "bool" } }, "slotProps": { diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 0459d7feecccd3..7bf9e31c1a9bcb 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -31,8 +31,8 @@ }, "onValueChange": { "type": { - "name": "(value: number | undefined) => void", - "description": "(value: number | undefined) => void" + "name": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void", + "description": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void" } }, "required": { "type": { "name": "boolean", "description": "boolean" } }, diff --git a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json index 4d8c3b82671253..aebd3dc8f153a4 100644 --- a/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json +++ b/docs/translations/api-docs/number-input-unstyled/number-input-unstyled.json @@ -8,6 +8,7 @@ "id": "The id of the input element.", "max": "The maximum value.", "min": "The minimum value.", + "onChange": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", "onValueChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", "slotProps": "The props used for each slot inside the NumberInput.", diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index a4b27afa9fbd13..ba913c2e248c92 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -6,6 +6,7 @@ "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "max": "The maximum value.", "min": "The minimum value.", + "onChange": "Callback fired when the value changes, before clamping is applied. Note that\nevent.target.value may contain values that fall outside of min and max or\nare otherwise \"invalid\".", "onValueChange": "Callback fired after the value is clamped and changes.\nCalled with undefined when the value is unset.", "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10.", diff --git a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx index 63bde95849ed1b..67a631871d2887 100644 --- a/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx +++ b/packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx @@ -217,7 +217,9 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = { */ onBlur: PropTypes.func, /** - * @ignore + * Callback fired when the value changes, before clamping is applied. Note that + * `event.target.value` may contain values that fall outside of `min` and `max` or + * are otherwise "invalid". */ onChange: PropTypes.func, /** diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index e38da7c10de9c2..c94f0487d4a22a 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -140,9 +140,9 @@ export default function useNumberInput( // formControlContext?.onValueChange?.(newValue); if (isNumber(newValue)) { - onValueChange?.(newValue); + onValueChange?.(event, newValue); } else { - onValueChange?.(undefined); + onValueChange?.(event, undefined); } }; diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index d771fce2baeaaf..0d51695c29f109 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -40,13 +40,21 @@ export interface UseNumberInputParameters { error?: boolean; onBlur?: React.FocusEventHandler; onClick?: React.MouseEventHandler; + /** + * Callback fired when the value changes, before clamping is applied. Note that + * `event.target.value` may contain values that fall outside of `min` and `max` or + * are otherwise "invalid". + */ onChange?: React.ChangeEventHandler; onFocus?: React.FocusEventHandler; /** * Callback fired after the value is clamped and changes. * Called with `undefined` when the value is unset. */ - onValueChange?: (value: number | undefined) => void; + onValueChange?: ( + event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent, + value: number | undefined, + ) => void; inputRef?: React.Ref; /** * If `true`, the `input` element is required. From 11bff8107e04b59e51be647e978e2790e90c269b Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 21 Apr 2023 14:50:11 +0800 Subject: [PATCH 25/70] Align dirty value updater naming --- .../mui-base/src/useNumberInput/useNumberInput.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index c94f0487d4a22a..5246a6868e58d3 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -82,7 +82,7 @@ export default function useNumberInput( // the "final" value const [value, setValue] = React.useState(valueProp ?? defaultValueProp); // the (potentially) dirty or invalid input value - const [dirtyValue, setInputValue] = React.useState(undefined); + const [dirtyValue, setDirtyValue] = React.useState(undefined); React.useEffect(() => { if (!formControlContext && disabledProp && focused) { @@ -122,16 +122,16 @@ export default function useNumberInput( val: number | undefined, ) => { // 1. clamp the number - // 2. setInputValue(clamped_value) - // 3. call onValueChange(newValue) + // 2. setDirtyValue(clamped_value) + // 3. call onValueChange(event, newValue) let newValue; if (val === undefined) { newValue = val; - setInputValue(''); + setDirtyValue(''); } else { newValue = clamp(val, min, max, step); - setInputValue(String(newValue)); + setDirtyValue(String(newValue)); } setValue(newValue); @@ -164,12 +164,12 @@ export default function useNumberInput( const val = parseInput(event.currentTarget.value); if (val === '' || val === '-') { - setInputValue(val); + setDirtyValue(val); setValue(undefined); } if (val.match(/^-?\d+?$/)) { - setInputValue(val); + setDirtyValue(val); setValue(parseInt(val, 10)); } }; From 188e02ed048ee169eb34a60f6587c204a2500022 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 28 Apr 2023 16:43:29 +0800 Subject: [PATCH 26/70] Remove Unstyled suffix --- .../components/number-input/QuantityInput.js | 4 +- .../components/number-input/QuantityInput.tsx | 8 +-- .../number-input/UnstyledNumberInputBasic.js | 14 ++--- .../number-input/UnstyledNumberInputBasic.tsx | 20 +++---- .../UnstyledNumberInputIntroduction.js | 14 ++--- .../UnstyledNumberInputIntroduction.tsx | 20 +++---- .../components/number-input/number-input.md | 30 +++++----- docs/data/base/pagesApi.js | 4 +- .../base-ui/api/number-input-unstyled.json | 8 +-- docs/pages/base-ui/api/number-input.json | 39 ++++++++++++ docs/pages/base-ui/api/use-number-input.json | 2 +- .../react-number-input/[docsTab]/index.js | 14 ++--- .../number-input/number-input.json} | 0 docs/translations/translations.json | 2 +- .../NumberInput.test.tsx} | 12 ++-- .../NumberInput.tsx} | 46 +++++++------- .../NumberInput.types.ts} | 60 ++++++++----------- packages/mui-base/src/NumberInput/index.ts | 6 ++ .../numberInputClasses.ts} | 35 +++++------ .../mui-base/src/NumberInputUnstyled/index.ts | 6 -- packages/mui-base/src/index.d.ts | 4 +- packages/mui-base/src/index.js | 4 +- .../src/useNumberInput/useNumberInput.ts | 2 +- 23 files changed, 188 insertions(+), 166 deletions(-) create mode 100644 docs/pages/base-ui/api/number-input.json rename docs/translations/{api-docs/number-input-unstyled/number-input-unstyled.json => api-docs-base/number-input/number-input.json} (100%) rename packages/mui-base/src/{NumberInputUnstyled/NumberInputUnstyled.test.tsx => NumberInput/NumberInput.test.tsx} (62%) rename packages/mui-base/src/{NumberInputUnstyled/NumberInputUnstyled.tsx => NumberInput/NumberInput.tsx} (84%) rename packages/mui-base/src/{NumberInputUnstyled/NumberInputUnstyled.types.ts => NumberInput/NumberInput.types.ts} (50%) create mode 100644 packages/mui-base/src/NumberInput/index.ts rename packages/mui-base/src/{NumberInputUnstyled/numberInputUnstyledClasses.ts => NumberInput/numberInputClasses.ts} (67%) delete mode 100644 packages/mui-base/src/NumberInputUnstyled/index.ts diff --git a/docs/data/base/components/number-input/QuantityInput.js b/docs/data/base/components/number-input/QuantityInput.js index f9fc6ed88c93d9..a867e7c3766eb6 100644 --- a/docs/data/base/components/number-input/QuantityInput.js +++ b/docs/data/base/components/number-input/QuantityInput.js @@ -1,12 +1,12 @@ import * as React from 'react'; -import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; +import NumberInput from '@mui/base/NumberInput'; import { styled } from '@mui/system'; import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { return ( - , ) { return ( - , ) { return ( - (); return ( ); @@ -71,7 +69,7 @@ const StyledInputRoot = styled('div')( overflow: hidden; - &.${numberInputUnstyledClasses.focused} { + &.${numberInputClasses.focused} { border-color: ${blue[400]}; box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; } @@ -126,12 +124,12 @@ const StyledButton = styled('button')( cursor: pointer; } - &.${numberInputUnstyledClasses.incrementButton} { + &.${numberInputClasses.incrementButton} { grid-column: 2/3; grid-row: 1/2; } - &.${numberInputUnstyledClasses.decrementButton} { + &.${numberInputClasses.decrementButton} { grid-column: 2/3; grid-row: 2/3; transform: rotate(180deg); diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx index 28092a884b8f5e..097a1538be2e99 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; -import NumberInputUnstyled, { - NumberInputUnstyledProps, - numberInputUnstyledClasses, -} from '@mui/base/NumberInputUnstyled'; +import NumberInput, { + NumberInputProps, + numberInputClasses, +} from '@mui/base/NumberInput'; import { styled } from '@mui/system'; const CustomNumberInput = React.forwardRef(function CustomNumberInput( - props: NumberInputUnstyledProps, + props: NumberInputProps, ref: React.ForwardedRef, ) { return ( - ); @@ -75,7 +75,7 @@ const StyledInputRoot = styled('div')( overflow: hidden; - &.${numberInputUnstyledClasses.focused} { + &.${numberInputClasses.focused} { border-color: ${blue[400]}; box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; } @@ -130,12 +130,12 @@ const StyledButton = styled('button')( cursor: pointer; } - &.${numberInputUnstyledClasses.incrementButton} { + &.${numberInputClasses.incrementButton} { grid-column: 2/3; grid-row: 1/2; } - &.${numberInputUnstyledClasses.decrementButton} { + &.${numberInputClasses.decrementButton} { grid-column: 2/3; grid-row: 2/3; transform: rotate(180deg); diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 3fe5e5a23532fb..aa5946e8f1dcda 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -1,14 +1,14 @@ --- product: base -title: Unstyled React Number Input component and hook -components: NumberInputUnstyled +title: React Number Input component and hook +components: NumberInput hooks: useNumberInput githubLabel: 'component: NumberInput' --- -# Unstyled Number Input +# Number Input -

          The Unstyled Number Input component provides users with a field for integer values, and buttons to increment or decrement the value.

          +

          The Number Input component provides users with a field for integer values, and buttons to increment or decrement the value.

          {{"component": "modules/components/ComponentLinkHeader.js", "design": false}} @@ -17,7 +17,7 @@ githubLabel: 'component: NumberInput' ## Introduction A number input is a UI element that accepts numeric values from the user. -The Unstyled Number Input component is a customizable replacement for the native HTML `` that solves common usability issues of the native counterpart, such as: +Base UI's Number Input component is a customizable replacement for the native HTML `` that solves common usability issues of the native counterpart, such as: - Inconsistencies across browsers in the appearance and behavior of the stepper buttons - Allowing certain non-numeric characters ('e', '+', '-', '.') and silently discarding others @@ -34,10 +34,10 @@ See [this article](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-desi After [installation](/base/getting-started/installation/), you can start building with this component using the following basic elements: ```jsx -import NumberInputUnstyled from '@mui/base/NumberInputUnstyled'; +import NumberInput from '@mui/base/NumberInput'; export default function MyApp() { - return ; + return ; } ``` @@ -50,17 +50,17 @@ The following demo shows how to create a number input component, apply some styl The `min` and `max` props can be used to define a range of accepted values. You can pass only one of them to define an open-ended range. ```tsx - + // Open-ended - + ``` The `step` prop can be used to defined the granularity of the change in value when incrementing or decrementing. For example, if `min={0}` and `step={2}`, valid values for the component would be 0, 2, 4… since the value can only be changed in increments of 2. ```tsx // valid values: 0, 2, 4, 6, 8... - + ``` When the input field is in focus, you can enter values that fall outside the valid range. The value will be clamped based on `min`, `max` and `step` once the input field is blurred. @@ -70,16 +70,16 @@ Holding down the Shift key when interacting with the stepper buttons This can be customized with the `shiftMultiplier` prop. In the following snippet, if Shift is held when clicking the increment button, the value will change from 0 to 5, then to 10 etc. ```tsx - + ``` -Here's another demo of a Unstyled Number Input with fully customized styles: +Here's another demo of a Number Input with fully customized styles: {{"demo": "QuantityInput.js", "defaultCodeOpen": false}} ### Anatomy -The Unstyled Number Input component consists of a root `
          ` that contains one interior `` slot, and two `
          ``` diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index fd7548c3d840b8..d16461ca96bd6c 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -9,6 +9,7 @@ "min": { "type": { "name": "number" } }, "onChange": { "type": { "name": "func" } }, "onValueChange": { "type": { "name": "func" } }, + "readOnly": { "type": { "name": "bool" } }, "required": { "type": { "name": "bool" } }, "slotProps": { "type": { diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 01240378c96bcf..38d72354d431e5 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -35,6 +35,7 @@ "description": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void" } }, + "readOnly": { "type": { "name": "boolean", "description": "boolean" } }, "required": { "type": { "name": "boolean", "description": "boolean" } }, "shiftMultiplier": { "type": { "name": "number", "description": "number" } }, "step": { "type": { "name": "number", "description": "number" } }, diff --git a/docs/translations/api-docs-base/number-input/number-input.json b/docs/translations/api-docs-base/number-input/number-input.json index aebd3dc8f153a4..328d6733289045 100644 --- a/docs/translations/api-docs-base/number-input/number-input.json +++ b/docs/translations/api-docs-base/number-input/number-input.json @@ -10,6 +10,7 @@ "min": "The minimum value.", "onChange": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", "onValueChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", + "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active, with the addition that they are now keyboard focusable.", "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", "slotProps": "The props used for each slot inside the NumberInput.", "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details.", diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index ba913c2e248c92..b58eadd86a2265 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -8,6 +8,7 @@ "min": "The minimum value.", "onChange": "Callback fired when the value changes, before clamping is applied. Note that\nevent.target.value may contain values that fall outside of min and max or\nare otherwise \"invalid\".", "onValueChange": "Callback fired after the value is clamped and changes.\nCalled with undefined when the value is unset.", + "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active,\nwith the addition that they are now keyboard focusable.", "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10.", "step": "The amount that the value changes on each increment or decrement.", diff --git a/packages/mui-base/src/NumberInput/NumberInput.tsx b/packages/mui-base/src/NumberInput/NumberInput.tsx index 830419c70a85cf..18a6b9f6578f6f 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.tsx @@ -65,6 +65,7 @@ const NumberInput = React.forwardRef(function NumberInput( onValueChange, placeholder, required, + readOnly, step, value, slotProps = {}, @@ -95,6 +96,7 @@ const NumberInput = React.forwardRef(function NumberInput( onBlur, onValueChange, required, + readOnly, value, }); @@ -163,8 +165,8 @@ const NumberInput = React.forwardRef(function NumberInput( return ( - + ); }) as OverridableComponent; @@ -235,6 +237,11 @@ NumberInput.propTypes /* remove-proptypes */ = { * @ignore */ placeholder: PropTypes.string, + /** + * If `true`, the `input` element becomes read-only. The stepper buttons remain active, + * with the addition that they are now keyboard focusable. + */ + readOnly: PropTypes.bool, /** * If `true`, the `input` element is required. * The prop defaults to the value (`false`) inherited from the parent FormControl component. diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index 9ec0368ec948f5..4f6a401d17a251 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -55,6 +55,7 @@ export default function useNumberInput( onFocus, onValueChange, required: requiredProp = false, + readOnly: readOnlyProp = false, value: valueProp, inputRef: inputRefProp, } = parameters; @@ -324,6 +325,8 @@ export default function useNumberInput( autoComplete: 'off', autoCorrect: 'off', required: requiredProp, + readOnly: readOnlyProp, + tabIndex: readOnlyProp ? -1 : 0, 'aria-disabled': disabledProp, disabled: disabledProp, }; @@ -344,8 +347,7 @@ export default function useNumberInput( ): UseNumberInputIncrementButtonSlotProps => { return { ...externalProps, - // the button should be tab-able if the input is readonly - tabIndex: -1, + tabIndex: readOnlyProp ? 0 : -1, disabled: isIncrementDisabled, 'aria-disabled': isIncrementDisabled, onMouseDown: handleStepperButtonMouseDown, @@ -360,8 +362,7 @@ export default function useNumberInput( ): UseNumberInputDecrementButtonSlotProps => { return { ...externalProps, - // the button should be tab-able if the input is readonly - tabIndex: -1, + tabIndex: readOnlyProp ? 0 : -1, disabled: isDecrementDisabled, 'aria-disabled': isDecrementDisabled, onMouseDown: handleStepperButtonMouseDown, diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index 0d51695c29f109..49247d80399eea 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -61,6 +61,11 @@ export interface UseNumberInputParameters { * The prop defaults to the value (`false`) inherited from the parent FormControl component. */ required?: boolean; + /** + * If `true`, the `input` element becomes read-only. The stepper buttons remain active, + * with the addition that they are now keyboard focusable. + */ + readOnly?: boolean; /** * The current value. Use when the component is controlled. */ From 1b9ed63687648833a61e009fb027e2441020ad25 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 11 May 2023 20:35:20 +0800 Subject: [PATCH 29/70] Align with latest API changes --- docs/data/base/components/number-input/number-input.md | 2 +- packages/mui-base/src/NumberInput/NumberInput.tsx | 2 +- packages/mui-base/src/useNumberInput/useNumberInput.types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 739739f13caf24..cc80b019735ef8 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -31,7 +31,7 @@ See [this article](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-desi ### Usage -After [installation](/base/getting-started/installation/), you can start building with this component using the following basic elements: +After [installation](/base/getting-started/quickstart/#installation), you can start building with this component using the following basic elements: ```jsx import NumberInput from '@mui/base/NumberInput'; diff --git a/packages/mui-base/src/NumberInput/NumberInput.tsx b/packages/mui-base/src/NumberInput/NumberInput.tsx index 18a6b9f6578f6f..8d727cf1a4a831 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.tsx @@ -48,7 +48,7 @@ const useUtilityClasses = (ownerState: NumberInputOwnerState) => { */ const NumberInput = React.forwardRef(function NumberInput( props: NumberInputProps, - forwardedRef: React.ForwardedRef, + forwardedRef: React.ForwardedRef, ) { const { className, diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index 49247d80399eea..1ffd399853c6d5 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -84,7 +84,7 @@ export type UseNumberInputRootSlotProps = Omit< export interface UseNumberInputInputSlotOwnProps { defaultValue: number | undefined; - ref: React.Ref; + ref: React.RefCallback | null; value: number | undefined; 'aria-disabled': React.AriaAttributes['aria-disabled']; 'aria-valuemax': React.AriaAttributes['aria-valuemax']; From dc6f1eefc329d6df444c048c768192dffdea9f98 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 15 May 2023 17:36:01 +0800 Subject: [PATCH 30/70] Add tests --- .../src/NumberInput/NumberInput.test.tsx | 46 ++++++++++++++++++- .../useNumberInput/useNumberInput.test.tsx | 21 ++++++++- .../useNumberInput/useNumberInput.types.ts | 2 + 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index 8044e31f48f35d..cee0f710366e80 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; import { expect } from 'chai'; -import NumberInput, { numberInputClasses } from '@mui/base/NumberInput'; +import NumberInput, { numberInputClasses, NumberInputOwnerState } from '@mui/base/NumberInput'; describe('', () => { const mount = createMount(); @@ -31,4 +31,48 @@ describe('', () => { expect(inputRef.current).to.deep.equal(getByRole('spinbutton')); }); + + it('passes ownerState to all the slots', () => { + interface SlotProps { + ownerState: NumberInputOwnerState; + children?: React.ReactNode; + } + + const CustomComponent = React.forwardRef( + ({ ownerState, children }: SlotProps, ref: React.Ref) => { + return ( +
          + {children} +
          + ); + }, + ); + + const slots = { + root: CustomComponent, + input: CustomComponent, + decrementButton: CustomComponent, + incrementButton: CustomComponent, + }; + + const { getAllByTestId } = render(); + const renderedComponents = getAllByTestId('custom'); + + expect(renderedComponents.length).to.equal(4); + for (let i = 0; i < renderedComponents.length; i += 1) { + expect(renderedComponents[i]).to.have.attribute('data-disabled', 'true'); + expect(renderedComponents[i]).to.have.attribute('data-focused', 'false'); + expect(renderedComponents[i]).to.have.attribute('data-readonly', 'true'); + expect(renderedComponents[i]).to.have.attribute('data-decrementdisabled', 'false'); + expect(renderedComponents[i]).to.have.attribute('data-incrementdisabled', 'false'); + } + }); }); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 3a5725ae5a11fe..92e6dd32ce930c 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -20,6 +20,25 @@ describe('useNumberInput', () => { }; describe('getInputProps', () => { + it('should return correct ARIA attributes', () => { + const props: UseNumberInputParameters = { + value: 50, + min: 10, + max: 100, + disabled: true, + }; + + const { getInputProps } = invokeUseNumberInput(props); + const inputProps = getInputProps(); + + expect(inputProps.role).to.equal('spinbutton'); + expect(inputProps['aria-valuenow']).to.equal(50); + expect(inputProps['aria-valuemin']).to.equal(10); + expect(inputProps['aria-valuemax']).to.equal(100); + expect(inputProps['aria-disabled']).to.equal(true); + expect(inputProps.tabIndex).to.equal(0); + }); + it('should include the incoming uncontrolled props in the output', () => { const props: UseNumberInputParameters = { defaultValue: 100, @@ -39,8 +58,6 @@ describe('useNumberInput', () => { function NumberInput() { const { getInputProps } = useNumberInput({ onChange: handleChange }); - // TODO: how to make accept my custom onChange ?! - // @ts-ignore return ; } render(); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index 1ffd399853c6d5..3f82304f1e289c 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -86,11 +86,13 @@ export interface UseNumberInputInputSlotOwnProps { defaultValue: number | undefined; ref: React.RefCallback | null; value: number | undefined; + role?: React.AriaRole; 'aria-disabled': React.AriaAttributes['aria-disabled']; 'aria-valuemax': React.AriaAttributes['aria-valuemax']; 'aria-valuemin': React.AriaAttributes['aria-valuemin']; 'aria-valuenow': React.AriaAttributes['aria-valuenow']; 'aria-valuetext': React.AriaAttributes['aria-valuetext']; + tabIndex?: number; onBlur: React.FocusEventHandler; onChange: React.ChangeEventHandler; onFocus: React.FocusEventHandler; From 8c8e8a80c083934ff6e88f233fa1c6900be4c845 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 15 May 2023 17:59:09 +0800 Subject: [PATCH 31/70] Add onValueChange test --- .../useNumberInput/useNumberInput.test.tsx | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 92e6dd32ce930c..7ad6c1619c5b4a 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -71,5 +71,114 @@ describe('useNumberInput', () => { expect(handleChange.callCount).to.equal(1); }); + + it('should call onValueChange when the input is blurred', () => { + const handleValueChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ onValueChange: handleValueChange }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement!, { target: { value: 3 } }); + input.blur(); + }); + + expect(handleValueChange.callCount).to.equal(1); + }); + + it('should not call onValueChange when the input has focus', () => { + const handleValueChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ onValueChange: handleValueChange }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement!, { target: { value: 4 } }); + }); + + expect(handleValueChange.callCount).to.equal(0); + }); + + it('should call onValueChange with a value within max', () => { + const handleValueChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ + onValueChange: handleValueChange, + max: 5, + }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement!, { target: { value: 9 } }); + input.blur(); + }); + + expect(handleValueChange.args[0][1]).to.equal(5); + }); + + it('should call onValueChange with a value within min', () => { + const handleValueChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ + onValueChange: handleValueChange, + min: 5, + }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement!, { target: { value: -9 } }); + input.blur(); + }); + + expect(handleValueChange.args[0][1]).to.equal(5); + }); + + it('should call onValueChange with a value based on a custom step', () => { + const handleValueChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ + onValueChange: handleValueChange, + min: 0, + step: 5, + }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement!, { target: { value: 4 } }); + input.blur(); + }); + + expect(handleValueChange.args[0][1]).to.equal(5); + }); }); }); From 7a774be2c3ea0a85c64dc5a16d6338c2df5a13f7 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 15 May 2023 21:00:22 +0800 Subject: [PATCH 32/70] Add keyboard interaction tests --- .../base-ui/api/number-input-unstyled.json | 1 + .../number-input/number-input.json | 1 + .../src/NumberInput/NumberInput.test.tsx | 142 +++++++++++++++++- .../mui-base/src/NumberInput/NumberInput.tsx | 7 + .../src/NumberInput/NumberInput.types.ts | 2 +- .../useNumberInput/useNumberInput.test.tsx | 4 +- .../src/useNumberInput/useNumberInput.ts | 11 +- 7 files changed, 158 insertions(+), 10 deletions(-) diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index d16461ca96bd6c..e6abde845d704b 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -11,6 +11,7 @@ "onValueChange": { "type": { "name": "func" } }, "readOnly": { "type": { "name": "bool" } }, "required": { "type": { "name": "bool" } }, + "shiftMultiplier": { "type": { "name": "number" } }, "slotProps": { "type": { "name": "shape", diff --git a/docs/translations/api-docs-base/number-input/number-input.json b/docs/translations/api-docs-base/number-input/number-input.json index 328d6733289045..47c20be82c99c4 100644 --- a/docs/translations/api-docs-base/number-input/number-input.json +++ b/docs/translations/api-docs-base/number-input/number-input.json @@ -12,6 +12,7 @@ "onValueChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active, with the addition that they are now keyboard focusable.", "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", + "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing or decrementing the value. Defaults to 10.", "slotProps": "The props used for each slot inside the NumberInput.", "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details.", "step": "The amount that the value changes on each increment or decrement.", diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index cee0f710366e80..c2c7166742008b 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -1,7 +1,14 @@ import * as React from 'react'; -import { createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; import { expect } from 'chai'; +import { spy } from 'sinon'; import NumberInput, { numberInputClasses, NumberInputOwnerState } from '@mui/base/NumberInput'; +import { + act, + createMount, + createRenderer, + describeConformanceUnstyled, + fireEvent, +} from 'test/utils'; describe('', () => { const mount = createMount(); @@ -75,4 +82,137 @@ describe('', () => { expect(renderedComponents[i]).to.have.attribute('data-incrementdisabled', 'false'); } }); + + describe('keyboard interaction', () => { + it('ArrowUp and ArrowDown changes the value', () => { + const handleValueChange = spy(); + + const { getByRole } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'ArrowUp' }); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + expect(handleValueChange.args[1][1]).to.equal(12); + expect(input.value).to.equal('12'); + + fireEvent.keyDown(input, { key: 'Down' }); + expect(handleValueChange.args[2][1]).to.equal(11); + expect(handleValueChange.callCount).to.equal(3); + }); + + it('ArrowUp and ArrowDown changes the value based on a custom step', () => { + const handleValueChange = spy(); + + const { getByRole } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'ArrowUp' }); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + expect(handleValueChange.args[1][1]).to.equal(20); + expect(input.value).to.equal('20'); + + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(handleValueChange.args[2][1]).to.equal(15); + expect(handleValueChange.callCount).to.equal(3); + expect(input.value).to.equal('15'); + }); + + it('ArrowUp and ArrowDown changes the value based on shiftMultiplier if the Shift key is held', () => { + const handleValueChange = spy(); + + const { getByRole } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'ArrowUp', shiftKey: true }); + expect(handleValueChange.args[0][1]).to.equal(25); + expect(input.value).to.equal('25'); + + fireEvent.keyDown(input, { key: 'ArrowDown', shiftKey: true }); + fireEvent.keyDown(input, { key: 'ArrowDown', shiftKey: true }); + expect(handleValueChange.args[2][1]).to.equal(15); + expect(handleValueChange.callCount).to.equal(3); + expect(input.value).to.equal('15'); + }); + + it('PageUp and PageDown changes the value based on shiftMultiplier', () => { + const handleValueChange = spy(); + + const { getByRole } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'PageUp' }); + expect(handleValueChange.args[0][1]).to.equal(25); + expect(input.value).to.equal('25'); + + fireEvent.keyDown(input, { key: 'PageDown' }); + fireEvent.keyDown(input, { key: 'PageDown' }); + expect(handleValueChange.args[2][1]).to.equal(15); + expect(handleValueChange.callCount).to.equal(3); + expect(input.value).to.equal('15'); + }); + + it('sets value to max when Home is pressed', () => { + const handleValueChange = spy(); + + const { getByRole } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'Home', shiftKey: true }); + expect(handleValueChange.args[0][1]).to.equal(50); + expect(input.value).to.equal('50'); + }); + + it('sets value to min when End is pressed', () => { + const handleValueChange = spy(); + + const { getByRole } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'End', shiftKey: true }); + expect(handleValueChange.args[0][1]).to.equal(1); + expect(input.value).to.equal('1'); + }); + }); }); diff --git a/packages/mui-base/src/NumberInput/NumberInput.tsx b/packages/mui-base/src/NumberInput/NumberInput.tsx index 8d727cf1a4a831..abc2e5cc4f916d 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.tsx @@ -66,6 +66,7 @@ const NumberInput = React.forwardRef(function NumberInput( placeholder, required, readOnly, + shiftMultiplier, step, value, slotProps = {}, @@ -88,6 +89,7 @@ const NumberInput = React.forwardRef(function NumberInput( min, max, step, + shiftMultiplier, defaultValue, disabled, error, @@ -247,6 +249,11 @@ NumberInput.propTypes /* remove-proptypes */ = { * The prop defaults to the value (`false`) inherited from the parent FormControl component. */ required: PropTypes.bool, + /** + * Multiplier applied to `step` if the shift key is held while incrementing + * or decrementing the value. Defaults to `10`. + */ + shiftMultiplier: PropTypes.number, /** * The props used for each slot inside the NumberInput. * @default {} diff --git a/packages/mui-base/src/NumberInput/NumberInput.types.ts b/packages/mui-base/src/NumberInput/NumberInput.types.ts index 287c2c1c281ba6..e72676edfac7bb 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.types.ts +++ b/packages/mui-base/src/NumberInput/NumberInput.types.ts @@ -79,7 +79,7 @@ export type NumberInputRootSlotProps = Simplify< ownerState: NumberInputOwnerState; className?: string; children?: React.ReactNode; - ref?: React.Ref; + ref?: React.Ref; } >; diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 7ad6c1619c5b4a..6a0d7d6e29ab94 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -39,7 +39,7 @@ describe('useNumberInput', () => { expect(inputProps.tabIndex).to.equal(0); }); - it('should include the incoming uncontrolled props in the output', () => { + it('should accept defaultValue in uncontrolled mode', () => { const props: UseNumberInputParameters = { defaultValue: 100, disabled: true, @@ -49,7 +49,7 @@ describe('useNumberInput', () => { const { getInputProps } = invokeUseNumberInput(props); const inputProps = getInputProps(); - expect(inputProps.defaultValue).to.equal(100); + expect(inputProps.value).to.equal(100); expect(inputProps.required).to.equal(true); }); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index 4f6a401d17a251..c293969e328ebd 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -87,7 +87,7 @@ export default function useNumberInput( // the "final" value const [value, setValue] = React.useState(valueProp ?? defaultValueProp); // the (potentially) dirty or invalid input value - const [dirtyValue, setDirtyValue] = React.useState(undefined); + const [dirtyValue, setDirtyValue] = React.useState(String(value)); React.useEffect(() => { if (!formControlContext && disabledProp && focused) { @@ -215,10 +215,9 @@ export default function useNumberInput( if (isNumber(value)) { const multiplier = - event.shiftKey || - (event.nativeEvent instanceof KeyboardEvent && - ((event as React.KeyboardEvent).key === 'PageUp' || - (event as React.KeyboardEvent).key === 'PageDown')) + event.shiftKey /* event.nativeEvent instanceof KeyboardEvent && */ || + (event as React.KeyboardEvent).key === 'PageUp' || + (event as React.KeyboardEvent).key === 'PageDown' ? shiftMultiplier : 1; newValue = { @@ -315,7 +314,7 @@ export default function useNumberInput( // TODO: check to see if SR support is still weird role: 'spinbutton', 'aria-invalid': errorProp || undefined, - defaultValue: defaultValueProp as number | undefined, + defaultValue: undefined, ref: handleInputRef, value: displayValue as number | undefined, 'aria-valuenow': displayValue as number | undefined, From e4a73d9ac84fb1e4bbe35298a8f344292fb36c02 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 16 May 2023 19:07:09 +0800 Subject: [PATCH 33/70] Add stepper button tests --- .../src/NumberInput/NumberInput.test.tsx | 77 +++++++++++++++++++ .../src/NumberInput/NumberInput.types.ts | 9 ++- .../useNumberInput/useNumberInput.test.tsx | 33 ++++++++ .../src/useNumberInput/useNumberInput.ts | 2 +- .../useNumberInput/useNumberInput.types.ts | 2 + 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index c2c7166742008b..181c340dfe91ac 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -83,6 +83,83 @@ describe('', () => { } }); + describe('step buttons', () => { + it('clicking the increment and decrement buttons changes the value', () => { + const handleValueChange = spy(); + + const { getByRole, getByTestId } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + const incrementButton = getByTestId('increment-btn'); + const decrementButton = getByTestId('decrement-btn'); + + act(() => { + input.focus(); + }); + + fireEvent.click(incrementButton); + expect(handleValueChange.args[0][1]).to.equal(11); + expect(input.value).to.equal('11'); + + fireEvent.click(decrementButton); + fireEvent.click(decrementButton); + expect(handleValueChange.callCount).to.equal(3); + expect(handleValueChange.args[2][1]).to.equal(9); + expect(input.value).to.equal('9'); + }); + + it('clicking the increment and decrement buttons changes the value based on shiftMultiplier if the Shift key is held', () => { + const handleValueChange = spy(); + + const { getByRole, getByTestId } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + const incrementButton = getByTestId('increment-btn'); + const decrementButton = getByTestId('decrement-btn'); + + act(() => { + input.focus(); + }); + + fireEvent.click(incrementButton, { shiftKey: true }); + fireEvent.click(incrementButton, { shiftKey: true }); + expect(handleValueChange.args[1][1]).to.equal(30); + expect(input.value).to.equal('30'); + + fireEvent.click(decrementButton, { shiftKey: true }); + expect(handleValueChange.args[2][1]).to.equal(25); + expect(handleValueChange.callCount).to.equal(3); + expect(input.value).to.equal('25'); + }); + }); + describe('keyboard interaction', () => { it('ArrowUp and ArrowDown changes the value', () => { const handleValueChange = spy(); diff --git a/packages/mui-base/src/NumberInput/NumberInput.types.ts b/packages/mui-base/src/NumberInput/NumberInput.types.ts index e72676edfac7bb..a7a7da129ca2e3 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.types.ts +++ b/packages/mui-base/src/NumberInput/NumberInput.types.ts @@ -10,8 +10,9 @@ import { SlotComponentProps } from '../utils'; export interface NumberInputRootSlotPropsOverrides {} export interface NumberInputInputSlotPropsOverrides {} -export interface NumberInputIncrementButtonSlotPropsOverrides {} -export interface NumberInputDecrementButtonSlotPropsOverrides {} +export interface NumberInputStepperButtonSlotPropsOverrides { + 'data-testid'?: string; +} export type NumberInputOwnProps = Omit & { /** @@ -31,12 +32,12 @@ export type NumberInputOwnProps = Omit & { input?: SlotComponentProps<'input', NumberInputInputSlotPropsOverrides, NumberInputOwnerState>; incrementButton?: SlotComponentProps< 'button', - NumberInputIncrementButtonSlotPropsOverrides, + NumberInputStepperButtonSlotPropsOverrides, NumberInputOwnerState >; decrementButton?: SlotComponentProps< 'button', - NumberInputDecrementButtonSlotPropsOverrides, + NumberInputStepperButtonSlotPropsOverrides, NumberInputOwnerState >; }; diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 6a0d7d6e29ab94..bf246384dc2cff 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -181,4 +181,37 @@ describe('useNumberInput', () => { expect(handleValueChange.args[0][1]).to.equal(5); }); }); + + describe('prop: readOnly', () => { + it('stepper buttons should not be in the tab order when readOnly is false', () => { + const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = + invokeUseNumberInput({ + defaultValue: 10, + }); + + const inputProps = getInputProps(); + const incrementButtonProps = getIncrementButtonProps(); + const decrementButtonProps = getDecrementButtonProps(); + + expect(inputProps.tabIndex).to.equal(0); + expect(incrementButtonProps.tabIndex).to.equal(-1); + expect(decrementButtonProps.tabIndex).to.equal(-1); + }); + + it('stepper buttons should be in the tab order when readOnly is true', () => { + const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = + invokeUseNumberInput({ + defaultValue: 10, + readOnly: true, + }); + + const inputProps = getInputProps(); + const incrementButtonProps = getIncrementButtonProps(); + const decrementButtonProps = getDecrementButtonProps(); + + expect(inputProps.tabIndex).to.equal(-1); + expect(incrementButtonProps.tabIndex).to.equal(0); + expect(decrementButtonProps.tabIndex).to.equal(0); + }); + }); }); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index c293969e328ebd..db547ec402e567 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -215,7 +215,7 @@ export default function useNumberInput( if (isNumber(value)) { const multiplier = - event.shiftKey /* event.nativeEvent instanceof KeyboardEvent && */ || + event.shiftKey || (event as React.KeyboardEvent).key === 'PageUp' || (event as React.KeyboardEvent).key === 'PageDown' ? shiftMultiplier diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index 3f82304f1e289c..7a7ec146213a0b 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -109,6 +109,7 @@ export type UseNumberInputInputSlotProps = Omit< export interface UseNumberInputIncrementButtonSlotOwnProps { 'aria-disabled': React.AriaAttributes['aria-disabled']; disabled: boolean; + tabIndex?: number; } export type UseNumberInputIncrementButtonSlotProps = Omit< @@ -120,6 +121,7 @@ export type UseNumberInputIncrementButtonSlotProps = Omit< export interface UseNumberInputDecrementButtonSlotOwnProps { 'aria-disabled': React.AriaAttributes['aria-disabled']; disabled: boolean; + tabIndex?: number; } export type UseNumberInputDecrementButtonSlotProps = Omit< From 18868e42b1a9ccf4e7e4f6c316cc9582c69b9ba3 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 17 May 2023 18:58:49 +0800 Subject: [PATCH 34/70] Test out @testing-library/user-event --- package.json | 1 + .../useNumberInput/useNumberInput.test.tsx | 28 +++++++++++++++++++ .../src/useNumberInput/useNumberInput.ts | 4 ++- yarn.lock | 5 ++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f6e28ca994976c..8433004895e053 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@slack/bolt": "^3.13.2", "@testing-library/dom": "^9.3.1", "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", "@types/chai": "^4.3.5", "@types/chai-dom": "^1.11.0", "@types/enzyme": "^3.10.13", diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index bf246384dc2cff..56e518457e2b69 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import * as React from 'react'; +import userEvent from '@testing-library/user-event'; import { createRenderer, screen, act, fireEvent } from 'test/utils'; import useNumberInput, { UseNumberInputParameters } from './index'; @@ -52,7 +53,9 @@ describe('useNumberInput', () => { expect(inputProps.value).to.equal(100); expect(inputProps.required).to.equal(true); }); + }); + describe('prop: onChange', () => { it('should call onChange if a change event is fired', () => { const handleChange = spy(); function NumberInput() { @@ -72,6 +75,31 @@ describe('useNumberInput', () => { expect(handleChange.callCount).to.equal(1); }); + it('DEBUG: test inputting characters one by one then backspacing them one by one in a plain ', async () => { + const handleChange = spy(); + + const user = userEvent.setup(); + + render(); + + const input = screen.getByTestId('test-input') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + await user.keyboard('abc'); + + expect(handleChange.callCount).to.equal(3); // works + expect(input.value).to.equal('abc'); // works + + await user.keyboard('[Backspace]'); + expect(handleChange.callCount).to.equal(4); // works + expect(input.value).to.equal('ab'); // works + }); + }); + + describe('prop: onValueChange', () => { it('should call onValueChange when the input is blurred', () => { const handleValueChange = spy(); function NumberInput() { diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index db547ec402e567..98b7b4530bc382 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -87,7 +87,9 @@ export default function useNumberInput( // the "final" value const [value, setValue] = React.useState(valueProp ?? defaultValueProp); // the (potentially) dirty or invalid input value - const [dirtyValue, setDirtyValue] = React.useState(String(value)); + const [dirtyValue, setDirtyValue] = React.useState( + value ? String(value) : undefined, + ); React.useEffect(() => { if (!formControlContext && disabledProp && focused) { diff --git a/yarn.lock b/yarn.lock index 70af7bdbad1711..7e9ae9acc9ce15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2999,6 +2999,11 @@ "@testing-library/dom" "^9.0.0" "@types/react-dom" "^18.0.0" +"@testing-library/user-event@^14.4.3": + version "14.4.3" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" + integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== + "@theme-ui/color-modes@^0.16.0": version "0.16.0" resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.16.0.tgz#7e137b7b17be56a4620e90d9a68d6c32cc97e92e" From b3161e660559ec53a7b04dd4c18cfa87ba95fbf8 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 17 May 2023 20:59:08 +0800 Subject: [PATCH 35/70] Update keyboard interaction tests --- .../useNumberInput/useNumberInput.test.tsx | 143 +++++++++++++++--- 1 file changed, 120 insertions(+), 23 deletions(-) diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 56e518457e2b69..e2022b3e4f745f 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import * as React from 'react'; import userEvent from '@testing-library/user-event'; -import { createRenderer, screen, act, fireEvent } from 'test/utils'; +import { createRenderer, screen, act } from 'test/utils'; import useNumberInput, { UseNumberInputParameters } from './index'; describe('useNumberInput', () => { @@ -56,7 +56,8 @@ describe('useNumberInput', () => { }); describe('prop: onChange', () => { - it('should call onChange if a change event is fired', () => { + it('should call onChange accordingly when inputting valid characters', async () => { + const user = userEvent.setup(); const handleChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ onChange: handleChange }); @@ -65,14 +66,39 @@ describe('useNumberInput', () => { } render(); - const input = screen.getByRole('spinbutton'); + const input = screen.getByRole('spinbutton') as HTMLInputElement; act(() => { input.focus(); - fireEvent.change(document.activeElement!, { target: { value: 2 } }); }); - expect(handleChange.callCount).to.equal(1); + await user.keyboard('-12'); + + expect(handleChange.callCount).to.equal(3); + expect(handleChange.args[2][0].target.value).to.equal('-12'); + expect(input.value).to.equal('-12'); + }); + + it('should not change the input value when inputting invalid characters', async () => { + const user = userEvent.setup(); + const handleChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ onChange: handleChange }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + await user.keyboard('-5a'); + + expect(handleChange.callCount).to.equal(3); + expect(input.value).to.equal('-5'); }); it('DEBUG: test inputting characters one by one then backspacing them one by one in a plain ', async () => { @@ -100,7 +126,8 @@ describe('useNumberInput', () => { }); describe('prop: onValueChange', () => { - it('should call onValueChange when the input is blurred', () => { + it('should call onValueChange when the input is blurred', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ onValueChange: handleValueChange }); @@ -113,17 +140,27 @@ describe('useNumberInput', () => { act(() => { input.focus(); - fireEvent.change(document.activeElement!, { target: { value: 3 } }); + }); + + await user.keyboard('34'); + + expect(handleValueChange.callCount).to.equal(0); + + act(() => { input.blur(); }); expect(handleValueChange.callCount).to.equal(1); }); - it('should not call onValueChange when the input has focus', () => { + it('should call onValueChange with a value within max', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); function NumberInput() { - const { getInputProps } = useNumberInput({ onValueChange: handleValueChange }); + const { getInputProps } = useNumberInput({ + onValueChange: handleValueChange, + max: 5, + }); return ; } @@ -133,18 +170,24 @@ describe('useNumberInput', () => { act(() => { input.focus(); - fireEvent.change(document.activeElement!, { target: { value: 4 } }); }); - expect(handleValueChange.callCount).to.equal(0); + await user.keyboard('9'); + + act(() => { + input.blur(); + }); + + expect(handleValueChange.args[0][1]).to.equal(5); }); - it('should call onValueChange with a value within max', () => { + it('should call onValueChange with a value within min', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ onValueChange: handleValueChange, - max: 5, + min: 5, }); return ; @@ -155,19 +198,25 @@ describe('useNumberInput', () => { act(() => { input.focus(); - fireEvent.change(document.activeElement!, { target: { value: 9 } }); + }); + + await user.keyboard('-9'); + + act(() => { input.blur(); }); expect(handleValueChange.args[0][1]).to.equal(5); }); - it('should call onValueChange with a value within min', () => { + it('should call onValueChange with a value based on a custom step', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ onValueChange: handleValueChange, - min: 5, + min: 0, + step: 5, }); return ; @@ -178,35 +227,83 @@ describe('useNumberInput', () => { act(() => { input.focus(); - fireEvent.change(document.activeElement!, { target: { value: -9 } }); + }); + + await user.keyboard('4'); + + act(() => { input.blur(); }); expect(handleValueChange.args[0][1]).to.equal(5); }); - it('should call onValueChange with a value based on a custom step', () => { + it('should call onValueChange with undefined when the value is cleared', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ onValueChange: handleValueChange, - min: 0, - step: 5, }); return ; } render(); - const input = screen.getByRole('spinbutton'); + const input = screen.getByRole('spinbutton') as HTMLInputElement; act(() => { input.focus(); - fireEvent.change(document.activeElement!, { target: { value: 4 } }); + }); + + await user.keyboard('9'); + + expect(input.value).to.equal('9'); + + await user.keyboard('[Backspace]'); + + expect(input.value).to.equal(''); + + act(() => { input.blur(); }); - expect(handleValueChange.args[0][1]).to.equal(5); + expect(handleValueChange.callCount).to.equal(1); + expect(handleValueChange.args[0][1]).to.equal(undefined); + }); + + it('should call onValueChange with undefined when input value is -', async () => { + const user = userEvent.setup(); + const handleValueChange = spy(); + function NumberInput() { + const { getInputProps } = useNumberInput({ + onValueChange: handleValueChange, + }); + + return ; + } + render(); + + const input = screen.getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + await user.keyboard('-5'); + + expect(input.value).to.equal('-5'); + + await user.keyboard('[Backspace]'); + + expect(input.value).to.equal('-'); + + act(() => { + input.blur(); + }); + + expect(handleValueChange.callCount).to.equal(1); + expect(handleValueChange.args[0][1]).to.equal(undefined); }); }); From bef43f28c2cd772a1da1e087802f363f6aa2f16b Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 17 May 2023 21:11:11 +0800 Subject: [PATCH 36/70] Add more stepper button tests --- .../src/NumberInput/NumberInput.test.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index 181c340dfe91ac..0a8b81adf74b62 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -269,7 +269,7 @@ describe('', () => { input.focus(); }); - fireEvent.keyDown(input, { key: 'Home', shiftKey: true }); + fireEvent.keyDown(input, { key: 'Home' }); expect(handleValueChange.args[0][1]).to.equal(50); expect(input.value).to.equal('50'); }); @@ -287,9 +287,41 @@ describe('', () => { input.focus(); }); - fireEvent.keyDown(input, { key: 'End', shiftKey: true }); + fireEvent.keyDown(input, { key: 'End' }); expect(handleValueChange.args[0][1]).to.equal(1); expect(input.value).to.equal('1'); }); + + it('sets value to min when the input has no value and ArrowUp is pressed', () => { + const handleValueChange = spy(); + + const { getByRole } = render(); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'ArrowUp' }); + expect(handleValueChange.args[0][1]).to.equal(5); + expect(input.value).to.equal('5'); + }); + + it('sets value to max when the input has no value and ArrowDown is pressed', () => { + const handleValueChange = spy(); + + const { getByRole } = render(); + + const input = getByRole('spinbutton') as HTMLInputElement; + + act(() => { + input.focus(); + }); + + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(handleValueChange.args[0][1]).to.equal(9); + expect(input.value).to.equal('9'); + }); }); }); From 64d459f0b9f8dbc9b646a7678b9d51fb0a96010c Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 17 May 2023 22:03:31 +0800 Subject: [PATCH 37/70] Refactor tests using user-event --- .../src/NumberInput/NumberInput.test.tsx | 165 ++++++++++-------- .../useNumberInput/useNumberInput.test.tsx | 82 ++------- 2 files changed, 112 insertions(+), 135 deletions(-) diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index 0a8b81adf74b62..d05973818fb1f0 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -1,14 +1,9 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; +import userEvent from '@testing-library/user-event'; import NumberInput, { numberInputClasses, NumberInputOwnerState } from '@mui/base/NumberInput'; -import { - act, - createMount, - createRenderer, - describeConformanceUnstyled, - fireEvent, -} from 'test/utils'; +import { act, createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; describe('', () => { const mount = createMount(); @@ -84,7 +79,8 @@ describe('', () => { }); describe('step buttons', () => { - it('clicking the increment and decrement buttons changes the value', () => { + it('clicking the increment and decrement buttons changes the value', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole, getByTestId } = render( @@ -106,22 +102,19 @@ describe('', () => { const incrementButton = getByTestId('increment-btn'); const decrementButton = getByTestId('decrement-btn'); - act(() => { - input.focus(); - }); - - fireEvent.click(incrementButton); + await user.click(incrementButton); expect(handleValueChange.args[0][1]).to.equal(11); expect(input.value).to.equal('11'); - fireEvent.click(decrementButton); - fireEvent.click(decrementButton); + await user.click(decrementButton); + await user.click(decrementButton); expect(handleValueChange.callCount).to.equal(3); expect(handleValueChange.args[2][1]).to.equal(9); expect(input.value).to.equal('9'); }); - it('clicking the increment and decrement buttons changes the value based on shiftMultiplier if the Shift key is held', () => { + it('clicking the increment and decrement buttons changes the value based on shiftMultiplier if the Shift key is held', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole, getByTestId } = render( @@ -144,24 +137,60 @@ describe('', () => { const incrementButton = getByTestId('increment-btn'); const decrementButton = getByTestId('decrement-btn'); - act(() => { - input.focus(); - }); - - fireEvent.click(incrementButton, { shiftKey: true }); - fireEvent.click(incrementButton, { shiftKey: true }); + await user.keyboard('{Shift>}'); + await user.click(incrementButton); + await user.click(incrementButton); expect(handleValueChange.args[1][1]).to.equal(30); expect(input.value).to.equal('30'); - fireEvent.click(decrementButton, { shiftKey: true }); + await user.click(decrementButton); expect(handleValueChange.args[2][1]).to.equal(25); expect(handleValueChange.callCount).to.equal(3); expect(input.value).to.equal('25'); }); + + it('clicking on the stepper buttons will focus the input', async () => { + const user = userEvent.setup(); + + const { getByRole, getByTestId } = render( + , + ); + + const input = getByRole('spinbutton') as HTMLInputElement; + const incrementButton = getByTestId('increment-btn'); + const decrementButton = getByTestId('decrement-btn'); + + expect(document.activeElement).to.equal(document.body); + + await user.click(incrementButton); + + expect(document.activeElement).to.equal(input); + + act(() => { + input.blur(); + }); + + expect(document.activeElement).to.equal(document.body); + + await user.click(decrementButton); + + expect(document.activeElement).to.equal(input); + }); }); describe('keyboard interaction', () => { - it('ArrowUp and ArrowDown changes the value', () => { + it('ArrowUp and ArrowDown changes the value', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render( @@ -170,21 +199,22 @@ describe('', () => { const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'ArrowUp' }); - fireEvent.keyDown(input, { key: 'ArrowUp' }); + await user.keyboard('[ArrowUp]'); + await user.keyboard('[ArrowUp]'); + expect(handleValueChange.callCount).to.equal(2); expect(handleValueChange.args[1][1]).to.equal(12); expect(input.value).to.equal('12'); - fireEvent.keyDown(input, { key: 'Down' }); - expect(handleValueChange.args[2][1]).to.equal(11); + await user.keyboard('[ArrowDown]'); expect(handleValueChange.callCount).to.equal(3); + expect(handleValueChange.args[2][1]).to.equal(11); + expect(input.value).to.equal('11'); }); - it('ArrowUp and ArrowDown changes the value based on a custom step', () => { + it('ArrowUp and ArrowDown changes the value based on a custom step', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render( @@ -193,22 +223,21 @@ describe('', () => { const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'ArrowUp' }); - fireEvent.keyDown(input, { key: 'ArrowUp' }); + await user.keyboard('[ArrowUp]'); + await user.keyboard('[ArrowUp]'); expect(handleValueChange.args[1][1]).to.equal(20); expect(input.value).to.equal('20'); - fireEvent.keyDown(input, { key: 'ArrowDown' }); + await user.keyboard('[ArrowDown]'); expect(handleValueChange.args[2][1]).to.equal(15); expect(handleValueChange.callCount).to.equal(3); expect(input.value).to.equal('15'); }); - it('ArrowUp and ArrowDown changes the value based on shiftMultiplier if the Shift key is held', () => { + it('ArrowUp and ArrowDown changes the value based on shiftMultiplier if the Shift key is held', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render( @@ -217,22 +246,21 @@ describe('', () => { const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'ArrowUp', shiftKey: true }); + await user.keyboard('{Shift>}[ArrowUp]/'); + expect(handleValueChange.callCount).to.equal(1); expect(handleValueChange.args[0][1]).to.equal(25); expect(input.value).to.equal('25'); - fireEvent.keyDown(input, { key: 'ArrowDown', shiftKey: true }); - fireEvent.keyDown(input, { key: 'ArrowDown', shiftKey: true }); + await user.keyboard('{Shift>}[ArrowDown][ArrowDown]{/Shift}'); expect(handleValueChange.args[2][1]).to.equal(15); expect(handleValueChange.callCount).to.equal(3); expect(input.value).to.equal('15'); }); - it('PageUp and PageDown changes the value based on shiftMultiplier', () => { + it('PageUp and PageDown changes the value based on shiftMultiplier', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render( @@ -241,22 +269,20 @@ describe('', () => { const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'PageUp' }); + await user.keyboard('[PageUp]'); expect(handleValueChange.args[0][1]).to.equal(25); expect(input.value).to.equal('25'); - fireEvent.keyDown(input, { key: 'PageDown' }); - fireEvent.keyDown(input, { key: 'PageDown' }); + await user.keyboard('[PageDown][PageDown]'); expect(handleValueChange.args[2][1]).to.equal(15); expect(handleValueChange.callCount).to.equal(3); expect(input.value).to.equal('15'); }); - it('sets value to max when Home is pressed', () => { + it('sets value to max when Home is pressed', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render( @@ -265,16 +291,15 @@ describe('', () => { const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'Home' }); + await user.keyboard('[Home]'); expect(handleValueChange.args[0][1]).to.equal(50); expect(input.value).to.equal('50'); }); - it('sets value to min when End is pressed', () => { + it('sets value to min when End is pressed', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render( @@ -283,43 +308,39 @@ describe('', () => { const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'End' }); + await user.keyboard('[End]'); expect(handleValueChange.args[0][1]).to.equal(1); expect(input.value).to.equal('1'); }); - it('sets value to min when the input has no value and ArrowUp is pressed', () => { + it('sets value to min when the input has no value and ArrowUp is pressed', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render(); const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'ArrowUp' }); + await user.keyboard('[ArrowUp]'); expect(handleValueChange.args[0][1]).to.equal(5); expect(input.value).to.equal('5'); }); - it('sets value to max when the input has no value and ArrowDown is pressed', () => { + it('sets value to max when the input has no value and ArrowDown is pressed', async () => { + const user = userEvent.setup(); const handleValueChange = spy(); const { getByRole } = render(); const input = getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); - fireEvent.keyDown(input, { key: 'ArrowDown' }); + await user.keyboard('[ArrowDown]'); expect(handleValueChange.args[0][1]).to.equal(9); expect(input.value).to.equal('9'); }); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index e2022b3e4f745f..8fca866e6fab15 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import * as React from 'react'; import userEvent from '@testing-library/user-event'; -import { createRenderer, screen, act } from 'test/utils'; +import { createRenderer, screen } from 'test/utils'; import useNumberInput, { UseNumberInputParameters } from './index'; describe('useNumberInput', () => { @@ -68,9 +68,7 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('-12'); @@ -91,38 +89,13 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('-5a'); expect(handleChange.callCount).to.equal(3); expect(input.value).to.equal('-5'); }); - - it('DEBUG: test inputting characters one by one then backspacing them one by one in a plain ', async () => { - const handleChange = spy(); - - const user = userEvent.setup(); - - render(); - - const input = screen.getByTestId('test-input') as HTMLInputElement; - - act(() => { - input.focus(); - }); - - await user.keyboard('abc'); - - expect(handleChange.callCount).to.equal(3); // works - expect(input.value).to.equal('abc'); // works - - await user.keyboard('[Backspace]'); - expect(handleChange.callCount).to.equal(4); // works - expect(input.value).to.equal('ab'); // works - }); }); describe('prop: onValueChange', () => { @@ -138,17 +111,14 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton'); - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('34'); expect(handleValueChange.callCount).to.equal(0); - act(() => { - input.blur(); - }); + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); expect(handleValueChange.callCount).to.equal(1); }); @@ -168,15 +138,12 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton'); - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('9'); - act(() => { - input.blur(); - }); + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); expect(handleValueChange.args[0][1]).to.equal(5); }); @@ -196,15 +163,12 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton'); - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('-9'); - act(() => { - input.blur(); - }); + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); expect(handleValueChange.args[0][1]).to.equal(5); }); @@ -225,15 +189,12 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton'); - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('4'); - act(() => { - input.blur(); - }); + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); expect(handleValueChange.args[0][1]).to.equal(5); }); @@ -252,9 +213,7 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('9'); @@ -264,9 +223,8 @@ describe('useNumberInput', () => { expect(input.value).to.equal(''); - act(() => { - input.blur(); - }); + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); expect(handleValueChange.callCount).to.equal(1); expect(handleValueChange.args[0][1]).to.equal(undefined); @@ -286,9 +244,7 @@ describe('useNumberInput', () => { const input = screen.getByRole('spinbutton') as HTMLInputElement; - act(() => { - input.focus(); - }); + await user.click(input); await user.keyboard('-5'); From 10e8c331489938b2670aeb2e8ee681535ea806fd Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 17 May 2023 22:23:02 +0800 Subject: [PATCH 38/70] Improve tab order tests --- .../src/NumberInput/NumberInput.test.tsx | 48 +++++++++++++++++++ .../useNumberInput/useNumberInput.test.tsx | 38 +-------------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index d05973818fb1f0..c390eaf42406a9 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -345,4 +345,52 @@ describe('', () => { expect(input.value).to.equal('9'); }); }); + + describe('prop: readOnly', () => { + it('stepper buttons should not be in the tab order when readOnly is false', async () => { + const user = userEvent.setup(); + + const { getByRole } = render(); + + const input = getByRole('spinbutton') as HTMLInputElement; + expect(document.activeElement).to.equal(document.body); + + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(input); + + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); + }); + + it('tab order should be increment button, decrement button when readOnly is true', async () => { + const user = userEvent.setup(); + + const { getByTestId } = render( + , + ); + + const incrementButton = getByTestId('increment-btn'); + const decrementButton = getByTestId('decrement-btn'); + expect(document.activeElement).to.equal(document.body); + + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(decrementButton); + + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(incrementButton); + + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); + }); + }); }); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 8fca866e6fab15..215be06865cc95 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -254,45 +254,11 @@ describe('useNumberInput', () => { expect(input.value).to.equal('-'); - act(() => { - input.blur(); - }); + await user.keyboard('[Tab]'); + expect(document.activeElement).to.equal(document.body); expect(handleValueChange.callCount).to.equal(1); expect(handleValueChange.args[0][1]).to.equal(undefined); }); }); - - describe('prop: readOnly', () => { - it('stepper buttons should not be in the tab order when readOnly is false', () => { - const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = - invokeUseNumberInput({ - defaultValue: 10, - }); - - const inputProps = getInputProps(); - const incrementButtonProps = getIncrementButtonProps(); - const decrementButtonProps = getDecrementButtonProps(); - - expect(inputProps.tabIndex).to.equal(0); - expect(incrementButtonProps.tabIndex).to.equal(-1); - expect(decrementButtonProps.tabIndex).to.equal(-1); - }); - - it('stepper buttons should be in the tab order when readOnly is true', () => { - const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = - invokeUseNumberInput({ - defaultValue: 10, - readOnly: true, - }); - - const inputProps = getInputProps(); - const incrementButtonProps = getIncrementButtonProps(); - const decrementButtonProps = getDecrementButtonProps(); - - expect(inputProps.tabIndex).to.equal(-1); - expect(incrementButtonProps.tabIndex).to.equal(0); - expect(decrementButtonProps.tabIndex).to.equal(0); - }); - }); }); From 0f2b80430681ca56935ffaf0a335dad2432be5f7 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 18 May 2023 17:31:24 +0800 Subject: [PATCH 39/70] Cleanup --- packages/mui-base/src/NumberInput/NumberInput.tsx | 2 -- .../mui-base/src/useNumberInput/useNumberInput.ts | 15 ++------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/mui-base/src/NumberInput/NumberInput.tsx b/packages/mui-base/src/NumberInput/NumberInput.tsx index abc2e5cc4f916d..2d224b7c13e492 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.tsx @@ -138,7 +138,6 @@ const NumberInput = React.forwardRef(function NumberInput( getSlotProps: (otherHandlers: EventHandlers) => getInputProps({ ...otherHandlers, ...propsForwardedToInputSlot }), externalSlotProps: slotProps.input, - // additionalProps: {}, ownerState, className: classes.input, }); @@ -159,7 +158,6 @@ const NumberInput = React.forwardRef(function NumberInput( elementType: DecrementButton, getSlotProps: getDecrementButtonProps, externalSlotProps: slotProps.decrementButton, - // additionalProps: {}, ownerState, className: classes.decrementButton, }); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index 98b7b4530bc382..f20a7ce127edce 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -22,12 +22,9 @@ const STEP_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown']; const SUPPORTED_KEYS = [...STEP_KEYS, 'Home', 'End']; -// TODO -// 1 - make a proper parser -// 2 - accept a parser (func) prop -const parseInput = (v: string): string => { +function parseInput(v: string): string { return v ? String(v.trim()) : String(v); -}; +} /** * @@ -128,9 +125,6 @@ export default function useNumberInput( event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent, val: number | undefined, ) => { - // 1. clamp the number - // 2. setDirtyValue(clamped_value) - // 3. call onValueChange(event, newValue) let newValue; if (val === undefined) { @@ -142,9 +136,6 @@ export default function useNumberInput( } setValue(newValue); - // TODO: integration with formControlContext - // OR: (event, newValue) similar to SelectUnstyled - // formControlContext?.onValueChange?.(newValue); if (isNumber(newValue)) { onValueChange?.(event, newValue); @@ -313,7 +304,6 @@ export default function useNumberInput( return { ...mergedEventHandlers, type: 'text', - // TODO: check to see if SR support is still weird role: 'spinbutton', 'aria-invalid': errorProp || undefined, defaultValue: undefined, @@ -384,7 +374,6 @@ export default function useNumberInput( value: focused ? dirtyValue : value, isIncrementDisabled, isDecrementDisabled, - // private and could be thrown out later inputValue: dirtyValue, }; } From e6c57a501e95c7c012e3a543d9470ef50d73612e Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 19 May 2023 16:31:18 +0800 Subject: [PATCH 40/70] Remove spinbutton role, add aria-controls --- docs/pages/base-ui/api/use-number-input.json | 1 + .../use-number-input/use-number-input.json | 2 + .../src/NumberInput/NumberInput.test.tsx | 197 ++++++++++++------ .../mui-base/src/NumberInput/NumberInput.tsx | 2 +- .../src/NumberInput/NumberInput.types.ts | 4 +- .../useNumberInput/useNumberInput.test.tsx | 103 +++++---- .../src/useNumberInput/useNumberInput.ts | 26 ++- .../useNumberInput/useNumberInput.types.ts | 10 + 8 files changed, 220 insertions(+), 125 deletions(-) diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 38d72354d431e5..e4052b0cbf4db5 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -3,6 +3,7 @@ "defaultValue": { "type": { "name": "unknown", "description": "unknown" } }, "disabled": { "type": { "name": "boolean", "description": "boolean" } }, "error": { "type": { "name": "boolean", "description": "boolean" } }, + "inputId": { "type": { "name": "string", "description": "string" } }, "inputRef": { "type": { "name": "React.Ref<HTMLInputElement>", diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index b58eadd86a2265..b505d32196b11a 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -4,6 +4,8 @@ "defaultValue": "The default value. Use when the component is not controlled.", "disabled": "If true, the component is disabled.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", + "inputId": "The id attribute of the input element.", + "inputRef": "The ref of the input element.", "max": "The maximum value.", "min": "The minimum value.", "onChange": "Callback fired when the value changes, before clamping is applied. Note that\nevent.target.value may contain values that fall outside of min and max or\nare otherwise \"invalid\".", diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index c390eaf42406a9..32654615f8cb14 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -29,9 +29,11 @@ describe('', () => { it('should be able to attach input ref passed through props', () => { const inputRef = React.createRef(); - const { getByRole } = render(); + const { getByTestId } = render( + , + ); - expect(inputRef.current).to.deep.equal(getByRole('spinbutton')); + expect(inputRef.current).to.deep.equal(getByTestId('input')); }); it('passes ownerState to all the slots', () => { @@ -73,8 +75,8 @@ describe('', () => { expect(renderedComponents[i]).to.have.attribute('data-disabled', 'true'); expect(renderedComponents[i]).to.have.attribute('data-focused', 'false'); expect(renderedComponents[i]).to.have.attribute('data-readonly', 'true'); - expect(renderedComponents[i]).to.have.attribute('data-decrementdisabled', 'false'); - expect(renderedComponents[i]).to.have.attribute('data-incrementdisabled', 'false'); + expect(renderedComponents[i]).to.have.attribute('data-decrementdisabled', 'true'); + expect(renderedComponents[i]).to.have.attribute('data-incrementdisabled', 'true'); } }); @@ -83,22 +85,27 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole, getByTestId } = render( + const { getByTestId } = render( , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; const incrementButton = getByTestId('increment-btn'); const decrementButton = getByTestId('decrement-btn'); @@ -117,23 +124,28 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole, getByTestId } = render( + const { getByTestId } = render( , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; const incrementButton = getByTestId('increment-btn'); const decrementButton = getByTestId('decrement-btn'); @@ -152,21 +164,26 @@ describe('', () => { it('clicking on the stepper buttons will focus the input', async () => { const user = userEvent.setup(); - const { getByRole, getByTestId } = render( + const { getByTestId } = render( , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; const incrementButton = getByTestId('increment-btn'); const decrementButton = getByTestId('decrement-btn'); @@ -193,11 +210,15 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render( - , + const { getByTestId } = render( + , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -217,11 +238,16 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render( - , + const { getByTestId } = render( + , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -240,11 +266,16 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render( - , + const { getByTestId } = render( + , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -263,11 +294,16 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render( - , + const { getByTestId } = render( + , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -285,11 +321,16 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render( - , + const { getByTestId } = render( + , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -302,11 +343,16 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render( - , + const { getByTestId } = render( + , ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -319,9 +365,15 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render(); + const { getByTestId } = render( + , + ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -334,9 +386,15 @@ describe('', () => { const user = userEvent.setup(); const handleValueChange = spy(); - const { getByRole } = render(); + const { getByTestId } = render( + , + ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; await user.click(input); @@ -350,9 +408,11 @@ describe('', () => { it('stepper buttons should not be in the tab order when readOnly is false', async () => { const user = userEvent.setup(); - const { getByRole } = render(); + const { getByTestId } = render( + , + ); - const input = getByRole('spinbutton') as HTMLInputElement; + const input = getByTestId('input') as HTMLInputElement; expect(document.activeElement).to.equal(document.body); await user.keyboard('[Tab]'); @@ -368,14 +428,19 @@ describe('', () => { const { getByTestId } = render( , ); diff --git a/packages/mui-base/src/NumberInput/NumberInput.tsx b/packages/mui-base/src/NumberInput/NumberInput.tsx index 2d224b7c13e492..66e48aece2337a 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.tsx @@ -100,6 +100,7 @@ const NumberInput = React.forwardRef(function NumberInput( required, readOnly, value, + inputId: id, }); const ownerState: NumberInputOwnerState = { @@ -115,7 +116,6 @@ const NumberInput = React.forwardRef(function NumberInput( const classes = useUtilityClasses(ownerState); const propsForwardedToInputSlot = { - id, placeholder, }; diff --git a/packages/mui-base/src/NumberInput/NumberInput.types.ts b/packages/mui-base/src/NumberInput/NumberInput.types.ts index a7a7da129ca2e3..71e756c5812d18 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.types.ts +++ b/packages/mui-base/src/NumberInput/NumberInput.types.ts @@ -10,9 +10,7 @@ import { SlotComponentProps } from '../utils'; export interface NumberInputRootSlotPropsOverrides {} export interface NumberInputInputSlotPropsOverrides {} -export interface NumberInputStepperButtonSlotPropsOverrides { - 'data-testid'?: string; -} +export interface NumberInputStepperButtonSlotPropsOverrides {} export type NumberInputOwnProps = Omit & { /** diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx index 215be06865cc95..2aacfcff3fee33 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx @@ -20,39 +20,50 @@ describe('useNumberInput', () => { return ref.current!; }; - describe('getInputProps', () => { - it('should return correct ARIA attributes', () => { - const props: UseNumberInputParameters = { - value: 50, - min: 10, - max: 100, - disabled: true, - }; - - const { getInputProps } = invokeUseNumberInput(props); - const inputProps = getInputProps(); - - expect(inputProps.role).to.equal('spinbutton'); - expect(inputProps['aria-valuenow']).to.equal(50); - expect(inputProps['aria-valuemin']).to.equal(10); - expect(inputProps['aria-valuemax']).to.equal(100); - expect(inputProps['aria-disabled']).to.equal(true); - expect(inputProps.tabIndex).to.equal(0); - }); + it('should return correct ARIA attributes', () => { + const INPUT_ID = 'TestInput'; + + const props: UseNumberInputParameters = { + inputId: INPUT_ID, + value: 50, + min: 10, + max: 100, + disabled: true, + }; + + const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = + invokeUseNumberInput(props); + const inputProps = getInputProps(); + const incrementButtonProps = getIncrementButtonProps(); + const decrementButtonProps = getDecrementButtonProps(); + + expect(inputProps['aria-valuenow']).to.equal(50); + expect(inputProps['aria-valuemin']).to.equal(10); + expect(inputProps['aria-valuemax']).to.equal(100); + expect(inputProps['aria-disabled']).to.equal(true); + expect(inputProps.tabIndex).to.equal(0); + + expect(decrementButtonProps.tabIndex).to.equal(-1); + expect(decrementButtonProps['aria-controls']).to.equal(INPUT_ID); + expect(decrementButtonProps['aria-disabled']).to.equal(true); + + expect(incrementButtonProps.tabIndex).to.equal(-1); + expect(incrementButtonProps['aria-controls']).to.equal(INPUT_ID); + expect(incrementButtonProps['aria-disabled']).to.equal(true); + }); - it('should accept defaultValue in uncontrolled mode', () => { - const props: UseNumberInputParameters = { - defaultValue: 100, - disabled: true, - required: true, - }; + it('should accept defaultValue in uncontrolled mode', () => { + const props: UseNumberInputParameters = { + defaultValue: 100, + disabled: true, + required: true, + }; - const { getInputProps } = invokeUseNumberInput(props); - const inputProps = getInputProps(); + const { getInputProps } = invokeUseNumberInput(props); + const inputProps = getInputProps(); - expect(inputProps.value).to.equal(100); - expect(inputProps.required).to.equal(true); - }); + expect(inputProps.value).to.equal(100); + expect(inputProps.required).to.equal(true); }); describe('prop: onChange', () => { @@ -62,11 +73,11 @@ describe('useNumberInput', () => { function NumberInput() { const { getInputProps } = useNumberInput({ onChange: handleChange }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton') as HTMLInputElement; + const input = screen.getByTestId('test-input') as HTMLInputElement; await user.click(input); @@ -83,11 +94,11 @@ describe('useNumberInput', () => { function NumberInput() { const { getInputProps } = useNumberInput({ onChange: handleChange }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton') as HTMLInputElement; + const input = screen.getByTestId('test-input') as HTMLInputElement; await user.click(input); @@ -105,11 +116,11 @@ describe('useNumberInput', () => { function NumberInput() { const { getInputProps } = useNumberInput({ onValueChange: handleValueChange }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton'); + const input = screen.getByTestId('test-input'); await user.click(input); @@ -132,11 +143,11 @@ describe('useNumberInput', () => { max: 5, }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton'); + const input = screen.getByTestId('test-input'); await user.click(input); @@ -157,11 +168,11 @@ describe('useNumberInput', () => { min: 5, }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton'); + const input = screen.getByTestId('test-input'); await user.click(input); @@ -183,11 +194,11 @@ describe('useNumberInput', () => { step: 5, }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton'); + const input = screen.getByTestId('test-input'); await user.click(input); @@ -207,11 +218,11 @@ describe('useNumberInput', () => { onValueChange: handleValueChange, }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton') as HTMLInputElement; + const input = screen.getByTestId('test-input') as HTMLInputElement; await user.click(input); @@ -238,11 +249,11 @@ describe('useNumberInput', () => { onValueChange: handleValueChange, }); - return ; + return ; } render(); - const input = screen.getByRole('spinbutton') as HTMLInputElement; + const input = screen.getByTestId('test-input') as HTMLInputElement; await user.click(input); diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/useNumberInput/useNumberInput.ts index f20a7ce127edce..4e563a8a19fc22 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.ts @@ -1,9 +1,6 @@ import * as React from 'react'; import MuiError from '@mui/utils/macros/MuiError.macro'; -import { - // unstable_useControlled as useControlled, // TODO: do I need this? - unstable_useForkRef as useForkRef, -} from '@mui/utils'; +import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils'; import { FormControlState, useFormControlContext } from '../FormControl'; import { UseNumberInputParameters, @@ -55,6 +52,7 @@ export default function useNumberInput( readOnly: readOnlyProp = false, value: valueProp, inputRef: inputRefProp, + inputId: inputIdProp, } = parameters; // TODO: make it work with FormControl @@ -79,6 +77,8 @@ export default function useNumberInput( const inputRef = React.useRef(null); const handleInputRef = useForkRef(inputRef, inputRefProp, handleInputRefWarning); + const inputId = useId(inputIdProp); + const [focused, setFocused] = React.useState(false); // the "final" value @@ -304,7 +304,7 @@ export default function useNumberInput( return { ...mergedEventHandlers, type: 'text', - role: 'spinbutton', + id: inputId, 'aria-invalid': errorProp || undefined, defaultValue: undefined, ref: handleInputRef, @@ -315,6 +315,7 @@ export default function useNumberInput( 'aria-valuemax': max, autoComplete: 'off', autoCorrect: 'off', + spellCheck: 'false', required: requiredProp, readOnly: readOnlyProp, tabIndex: readOnlyProp ? -1 : 0, @@ -331,14 +332,20 @@ export default function useNumberInput( } }; - const isIncrementDisabled = isNumber(value) ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false; + const stepperButtonCommonProps = { + 'aria-controls': inputId, + tabIndex: readOnlyProp ? 0 : -1, + }; + + const isIncrementDisabled = + disabledProp || (isNumber(value) ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false); const getIncrementButtonProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputIncrementButtonSlotProps => { return { ...externalProps, - tabIndex: readOnlyProp ? 0 : -1, + ...stepperButtonCommonProps, disabled: isIncrementDisabled, 'aria-disabled': isIncrementDisabled, onMouseDown: handleStepperButtonMouseDown, @@ -346,14 +353,15 @@ export default function useNumberInput( }; }; - const isDecrementDisabled = isNumber(value) ? value <= (min ?? Number.MIN_SAFE_INTEGER) : false; + const isDecrementDisabled = + disabledProp || (isNumber(value) ? value <= (min ?? Number.MIN_SAFE_INTEGER) : false); const getDecrementButtonProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputDecrementButtonSlotProps => { return { ...externalProps, - tabIndex: readOnlyProp ? 0 : -1, + ...stepperButtonCommonProps, disabled: isDecrementDisabled, 'aria-disabled': isDecrementDisabled, onMouseDown: handleStepperButtonMouseDown, diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts index 7a7ec146213a0b..f7528e4c23a162 100644 --- a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/useNumberInput/useNumberInput.types.ts @@ -55,6 +55,13 @@ export interface UseNumberInputParameters { event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent, value: number | undefined, ) => void; + /** + * The `id` attribute of the input element. + */ + inputId?: string; + /** + * The ref of the input element. + */ inputRef?: React.Ref; /** * If `true`, the `input` element is required. @@ -84,6 +91,7 @@ export type UseNumberInputRootSlotProps = Omit< export interface UseNumberInputInputSlotOwnProps { defaultValue: number | undefined; + id: string | undefined; ref: React.RefCallback | null; value: number | undefined; role?: React.AriaRole; @@ -107,6 +115,7 @@ export type UseNumberInputInputSlotProps = Omit< UseNumberInputInputSlotOwnProps; export interface UseNumberInputIncrementButtonSlotOwnProps { + 'aria-controls': React.AriaAttributes['aria-controls']; 'aria-disabled': React.AriaAttributes['aria-disabled']; disabled: boolean; tabIndex?: number; @@ -119,6 +128,7 @@ export type UseNumberInputIncrementButtonSlotProps = Omit< UseNumberInputIncrementButtonSlotOwnProps; export interface UseNumberInputDecrementButtonSlotOwnProps { + 'aria-controls': React.AriaAttributes['aria-controls']; 'aria-disabled': React.AriaAttributes['aria-disabled']; disabled: boolean; tabIndex?: number; From 68b2641bc3f2ef58b19d6a4a662c71b862713162 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 19 May 2023 18:27:42 +0800 Subject: [PATCH 41/70] Fix NumberInput test missing slots --- packages/mui-base/src/NumberInput/NumberInput.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/NumberInput/NumberInput.test.tsx index 32654615f8cb14..c3e8a9c79f1785 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/NumberInput/NumberInput.test.tsx @@ -24,6 +24,14 @@ describe('', () => { expectedClassName: numberInputClasses.input, testWithElement: 'input', }, + incrementButton: { + expectedClassName: numberInputClasses.incrementButton, + testWithElement: 'button', + }, + decrementButton: { + expectedClassName: numberInputClasses.decrementButton, + testWithElement: 'button', + }, }, })); From eb93cbb583e6cce2045e2a2c544f87f306734439 Mon Sep 17 00:00:00 2001 From: zanivan Date: Mon, 22 May 2023 15:24:38 -0300 Subject: [PATCH 42/70] A few visual tweaks on the Demos Mainly tweaks on dropShadow and colors to make the demos more consistent with the demos on the 'Input' page --- .../number-input/UnstyledNumberInputBasic.tsx | 24 +++++++++---------- .../UnstyledNumberInputIntroduction.tsx | 7 +++--- .../number-input/UseNumberInput.tsx | 22 ++++++++--------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx index 04cfae3c76853b..c386a40cdd38fe 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx @@ -47,23 +47,23 @@ export default function NumberInputBasic() { const blue = { 100: '#DAECFF', - 200: '#b6daff', + 200: '#80BFFF', 400: '#3399FF', 500: '#007FFF', 600: '#0072E5', }; const grey = { - 50: '#f6f8fa', - 100: '#eaeef2', - 200: '#d0d7de', - 300: '#afb8c1', - 400: '#8c959f', - 500: '#6e7781', - 600: '#57606a', - 700: '#424a53', - 800: '#32383f', - 900: '#24292f', + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', }; const StyledInputRoot = styled('div')( @@ -71,7 +71,7 @@ const StyledInputRoot = styled('div')( font-family: IBM Plex Sans, sans-serif; font-weight: 400; border-radius: 12px; - color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx index 097a1538be2e99..a12cf46e52c545 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx @@ -45,6 +45,7 @@ const blue = { 400: '#3399FF', 500: '#007FFF', 600: '#0072E5', + 900: '#003A75', }; const grey = { @@ -65,10 +66,10 @@ const StyledInputRoot = styled('div')( font-family: IBM Plex Sans, sans-serif; font-weight: 400; border-radius: 12px; - color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + box-shadow: 0px 2px 24px ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; display: grid; grid-template-columns: 1fr 24px; grid-template-rows: 1fr 1fr; @@ -77,7 +78,7 @@ const StyledInputRoot = styled('div')( &.${numberInputClasses.focused} { border-color: ${blue[400]}; - box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]}; } &:hover { diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index c820bf867c9726..e6e3fdb3c9fad8 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -63,23 +63,23 @@ export default function UseNumberInput() { const blue = { 100: '#DAECFF', - 200: '#80BFFF', + 200: '#b6daff', 400: '#3399FF', 500: '#007FFF', 600: '#0072E5', }; const grey = { - 50: '#F3F6F9', - 100: '#E7EBF0', - 200: '#E0E3E7', - 300: '#CDD2D7', - 400: '#B2BAC2', - 500: '#A0AAB4', - 600: '#6F7E8C', - 700: '#3E5060', - 800: '#2D3843', - 900: '#1A2027', + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', }; const StyledInputRoot: React.ElementType = styled('div')( From bcf5a675175344aad53b6b4127f00022876a00ea Mon Sep 17 00:00:00 2001 From: zanivan Date: Mon, 22 May 2023 15:27:28 -0300 Subject: [PATCH 43/70] yarn docs:typescript:formatted --- .../number-input/UnstyledNumberInputBasic.js | 24 +++++++++---------- .../UnstyledNumberInputIntroduction.js | 7 +++--- .../components/number-input/UseNumberInput.js | 22 ++++++++--------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js index 361ec60122e557..35740d4a3d820c 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js @@ -41,23 +41,23 @@ export default function NumberInputBasic() { const blue = { 100: '#DAECFF', - 200: '#b6daff', + 200: '#80BFFF', 400: '#3399FF', 500: '#007FFF', 600: '#0072E5', }; const grey = { - 50: '#f6f8fa', - 100: '#eaeef2', - 200: '#d0d7de', - 300: '#afb8c1', - 400: '#8c959f', - 500: '#6e7781', - 600: '#57606a', - 700: '#424a53', - 800: '#32383f', - 900: '#24292f', + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', }; const StyledInputRoot = styled('div')( @@ -65,7 +65,7 @@ const StyledInputRoot = styled('div')( font-family: IBM Plex Sans, sans-serif; font-weight: 400; border-radius: 12px; - color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js index baed973fe22f32..542c9fd8dc5084 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js @@ -39,6 +39,7 @@ const blue = { 400: '#3399FF', 500: '#007FFF', 600: '#0072E5', + 900: '#003A75', }; const grey = { @@ -59,10 +60,10 @@ const StyledInputRoot = styled('div')( font-family: IBM Plex Sans, sans-serif; font-weight: 400; border-radius: 12px; - color: ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + box-shadow: 0px 2px 24px ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; display: grid; grid-template-columns: 1fr 24px; grid-template-rows: 1fr 1fr; @@ -71,7 +72,7 @@ const StyledInputRoot = styled('div')( &.${numberInputClasses.focused} { border-color: ${blue[400]}; - box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]}; } &:hover { diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index e1b6dd09839d01..73d04ed8499b67 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -60,23 +60,23 @@ export default function UseNumberInput() { const blue = { 100: '#DAECFF', - 200: '#80BFFF', + 200: '#b6daff', 400: '#3399FF', 500: '#007FFF', 600: '#0072E5', }; const grey = { - 50: '#F3F6F9', - 100: '#E7EBF0', - 200: '#E0E3E7', - 300: '#CDD2D7', - 400: '#B2BAC2', - 500: '#A0AAB4', - 600: '#6F7E8C', - 700: '#3E5060', - 800: '#2D3843', - 900: '#1A2027', + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', }; const StyledInputRoot = styled('div')( From f5e84228e92ff15787befcbcfe7debf1afa23678 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 24 May 2023 18:03:07 +0800 Subject: [PATCH 44/70] Add unstable prefix --- docs/data/base/components/number-input/QuantityInput.js | 2 +- docs/data/base/components/number-input/QuantityInput.tsx | 2 +- .../components/number-input/UnstyledNumberInputBasic.js | 2 +- .../components/number-input/UnstyledNumberInputBasic.tsx | 2 +- .../number-input/UnstyledNumberInputIntroduction.js | 2 +- .../number-input/UnstyledNumberInputIntroduction.tsx | 2 +- docs/data/base/components/number-input/UseNumberInput.js | 2 +- docs/data/base/components/number-input/UseNumberInput.tsx | 4 +++- .../base/components/number-input/UseNumberInputCompact.js | 2 +- .../components/number-input/UseNumberInputCompact.tsx | 4 +++- docs/pages/base-ui/api/number-input-unstyled.json | 2 +- docs/pages/base-ui/api/use-number-input.json | 2 +- .../NumberInput.test.tsx | 5 ++++- .../{NumberInput => Unstable_NumberInput}/NumberInput.tsx | 2 +- .../NumberInput.types.ts | 2 +- .../src/{NumberInput => Unstable_NumberInput}/index.ts | 0 .../numberInputClasses.ts | 0 packages/mui-base/src/index.d.ts | 8 ++++---- packages/mui-base/src/index.js | 8 ++++---- .../{useNumberInput => unstable_useNumberInput}/index.ts | 0 .../useNumberInput.test.tsx | 0 .../useNumberInput.ts | 0 .../useNumberInput.types.ts | 0 .../utils.test.ts | 0 .../{useNumberInput => unstable_useNumberInput}/utils.ts | 0 25 files changed, 30 insertions(+), 23 deletions(-) rename packages/mui-base/src/{NumberInput => Unstable_NumberInput}/NumberInput.test.tsx (99%) rename packages/mui-base/src/{NumberInput => Unstable_NumberInput}/NumberInput.tsx (99%) rename packages/mui-base/src/{NumberInput => Unstable_NumberInput}/NumberInput.types.ts (98%) rename packages/mui-base/src/{NumberInput => Unstable_NumberInput}/index.ts (100%) rename packages/mui-base/src/{NumberInput => Unstable_NumberInput}/numberInputClasses.ts (100%) rename packages/mui-base/src/{useNumberInput => unstable_useNumberInput}/index.ts (100%) rename packages/mui-base/src/{useNumberInput => unstable_useNumberInput}/useNumberInput.test.tsx (100%) rename packages/mui-base/src/{useNumberInput => unstable_useNumberInput}/useNumberInput.ts (100%) rename packages/mui-base/src/{useNumberInput => unstable_useNumberInput}/useNumberInput.types.ts (100%) rename packages/mui-base/src/{useNumberInput => unstable_useNumberInput}/utils.test.ts (100%) rename packages/mui-base/src/{useNumberInput => unstable_useNumberInput}/utils.ts (100%) diff --git a/docs/data/base/components/number-input/QuantityInput.js b/docs/data/base/components/number-input/QuantityInput.js index 53cd024de2dca8..f8478c7abe7b0e 100644 --- a/docs/data/base/components/number-input/QuantityInput.js +++ b/docs/data/base/components/number-input/QuantityInput.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import NumberInput from '@mui/base/NumberInput'; +import NumberInput from '@mui/base/Unstable_NumberInput'; import { styled } from '@mui/system'; import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; diff --git a/docs/data/base/components/number-input/QuantityInput.tsx b/docs/data/base/components/number-input/QuantityInput.tsx index 5ec4e276a0bc3c..c5427ddc36ad6d 100644 --- a/docs/data/base/components/number-input/QuantityInput.tsx +++ b/docs/data/base/components/number-input/QuantityInput.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import NumberInput, { NumberInputProps } from '@mui/base/NumberInput'; +import NumberInput, { NumberInputProps } from '@mui/base/Unstable_NumberInput'; import { styled } from '@mui/system'; import RemoveIcon from '@mui/icons-material/Remove'; import AddIcon from '@mui/icons-material/Add'; diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js index 35740d4a3d820c..24c83f2902e1c1 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import NumberInput, { numberInputClasses } from '@mui/base/NumberInput'; +import NumberInput, { numberInputClasses } from '@mui/base/Unstable_NumberInput'; import { styled } from '@mui/system'; const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx index c386a40cdd38fe..176431e6ed449a 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import NumberInput, { NumberInputProps, numberInputClasses, -} from '@mui/base/NumberInput'; +} from '@mui/base/Unstable_NumberInput'; import { styled } from '@mui/system'; const CustomNumberInput = React.forwardRef(function CustomNumberInput( diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js index 542c9fd8dc5084..edee4ca7a03c34 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import NumberInput, { numberInputClasses } from '@mui/base/NumberInput'; +import NumberInput, { numberInputClasses } from '@mui/base/Unstable_NumberInput'; import { styled } from '@mui/system'; const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref) { diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx index a12cf46e52c545..d5d29fbb274564 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import NumberInput, { NumberInputProps, numberInputClasses, -} from '@mui/base/NumberInput'; +} from '@mui/base/Unstable_NumberInput'; import { styled } from '@mui/system'; const CustomNumberInput = React.forwardRef(function CustomNumberInput( diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index 73d04ed8499b67..a568df4deada20 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import useNumberInput from '@mui/base/useNumberInput'; +import useNumberInput from '@mui/base/unstable_useNumberInput'; import { styled } from '@mui/system'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index e6e3fdb3c9fad8..ff06120b198157 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; -import useNumberInput, { UseNumberInputParameters } from '@mui/base/useNumberInput'; +import useNumberInput, { + UseNumberInputParameters, +} from '@mui/base/unstable_useNumberInput'; import { styled } from '@mui/system'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.js b/docs/data/base/components/number-input/UseNumberInputCompact.js index eea0b136b37e47..275b649c3cdeec 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.js +++ b/docs/data/base/components/number-input/UseNumberInputCompact.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import useNumberInput from '@mui/base/useNumberInput'; +import useNumberInput from '@mui/base/unstable_useNumberInput'; import { styled } from '@mui/system'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx b/docs/data/base/components/number-input/UseNumberInputCompact.tsx index 7366c81aa727fb..d651de14814094 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.tsx +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; -import useNumberInput, { UseNumberInputParameters } from '@mui/base/useNumberInput'; +import useNumberInput, { + UseNumberInputParameters, +} from '@mui/base/unstable_useNumberInput'; import { styled } from '@mui/system'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index e6abde845d704b..fb5b14d1bcbebb 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -34,7 +34,7 @@ "spread": true, "muiName": "MuiNumberInput", "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/NumberInput/NumberInput.tsx", + "filename": "/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index e4052b0cbf4db5..8767834f75ef13 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -115,6 +115,6 @@ "value": { "type": { "name": "unknown", "description": "unknown" }, "required": true } }, "name": "useNumberInput", - "filename": "/packages/mui-base/src/useNumberInput/useNumberInput.ts", + "filename": "/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts", "demos": "" } diff --git a/packages/mui-base/src/NumberInput/NumberInput.test.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx similarity index 99% rename from packages/mui-base/src/NumberInput/NumberInput.test.tsx rename to packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx index c3e8a9c79f1785..44fa1bf45992a1 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx @@ -2,7 +2,10 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import userEvent from '@testing-library/user-event'; -import NumberInput, { numberInputClasses, NumberInputOwnerState } from '@mui/base/NumberInput'; +import NumberInput, { + numberInputClasses, + NumberInputOwnerState, +} from '@mui/base/Unstable_NumberInput'; import { act, createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; describe('', () => { diff --git a/packages/mui-base/src/NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx similarity index 99% rename from packages/mui-base/src/NumberInput/NumberInput.tsx rename to packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 66e48aece2337a..56407c8cc4e022 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; import { getNumberInputUtilityClass } from './numberInputClasses'; -import useNumberInput from '../useNumberInput'; +import useNumberInput from '../unstable_useNumberInput'; import { NumberInputOwnerState, NumberInputProps, diff --git a/packages/mui-base/src/NumberInput/NumberInput.types.ts b/packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts similarity index 98% rename from packages/mui-base/src/NumberInput/NumberInput.types.ts rename to packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts index 71e756c5812d18..b5a204b323616c 100644 --- a/packages/mui-base/src/NumberInput/NumberInput.types.ts +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts @@ -5,7 +5,7 @@ import { UseNumberInputRootSlotProps, UseNumberInputIncrementButtonSlotProps, UseNumberInputDecrementButtonSlotProps, -} from '../useNumberInput/useNumberInput.types'; +} from '../unstable_useNumberInput/useNumberInput.types'; import { SlotComponentProps } from '../utils'; export interface NumberInputRootSlotPropsOverrides {} diff --git a/packages/mui-base/src/NumberInput/index.ts b/packages/mui-base/src/Unstable_NumberInput/index.ts similarity index 100% rename from packages/mui-base/src/NumberInput/index.ts rename to packages/mui-base/src/Unstable_NumberInput/index.ts diff --git a/packages/mui-base/src/NumberInput/numberInputClasses.ts b/packages/mui-base/src/Unstable_NumberInput/numberInputClasses.ts similarity index 100% rename from packages/mui-base/src/NumberInput/numberInputClasses.ts rename to packages/mui-base/src/Unstable_NumberInput/numberInputClasses.ts diff --git a/packages/mui-base/src/index.d.ts b/packages/mui-base/src/index.d.ts index eea7c4a8cbd716..8be49ed6cf0f8c 100644 --- a/packages/mui-base/src/index.d.ts +++ b/packages/mui-base/src/index.d.ts @@ -38,8 +38,8 @@ export * from './Modal'; export { default as NoSsr } from './NoSsr'; -export { default as NumberInput } from './NumberInput'; -export * from './NumberInput'; +export { default as Unstable_NumberInput } from './Unstable_NumberInput'; +export * from './Unstable_NumberInput'; export { default as OptionGroup } from './OptionGroup'; export * from './OptionGroup'; @@ -107,8 +107,8 @@ export * from './useMenuButton'; export { default as useMenuItem } from './useMenuItem'; export * from './useMenuItem'; -export { default as useNumberInput } from './useNumberInput'; -export * from './useNumberInput'; +export { default as unstable_useNumberInput } from './unstable_useNumberInput'; +export * from './unstable_useNumberInput'; export { default as useOption } from './useOption'; export * from './useOption'; diff --git a/packages/mui-base/src/index.js b/packages/mui-base/src/index.js index ce33be4f4ae775..b7ad15d40d5940 100644 --- a/packages/mui-base/src/index.js +++ b/packages/mui-base/src/index.js @@ -35,8 +35,8 @@ export * from './Modal'; export { default as NoSsr } from './NoSsr'; -export { default as NumberInput } from './NumberInput'; -export * from './NumberInput'; +export { default as Unstable_NumberInput } from './Unstable_NumberInput'; +export * from './Unstable_NumberInput'; export { default as OptionGroup } from './OptionGroup'; export * from './OptionGroup'; @@ -100,8 +100,8 @@ export * from './useMenuButton'; export { default as useMenuItem } from './useMenuItem'; export * from './useMenuItem'; -export { default as useNumberInput } from './useNumberInput'; -export * from './useNumberInput'; +export { default as unstable_useNumberInput } from './unstable_useNumberInput'; +export * from './unstable_useNumberInput'; export { default as useOption } from './useOption'; export * from './useOption'; diff --git a/packages/mui-base/src/useNumberInput/index.ts b/packages/mui-base/src/unstable_useNumberInput/index.ts similarity index 100% rename from packages/mui-base/src/useNumberInput/index.ts rename to packages/mui-base/src/unstable_useNumberInput/index.ts diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx similarity index 100% rename from packages/mui-base/src/useNumberInput/useNumberInput.test.tsx rename to packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts similarity index 100% rename from packages/mui-base/src/useNumberInput/useNumberInput.ts rename to packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts diff --git a/packages/mui-base/src/useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts similarity index 100% rename from packages/mui-base/src/useNumberInput/useNumberInput.types.ts rename to packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts diff --git a/packages/mui-base/src/useNumberInput/utils.test.ts b/packages/mui-base/src/unstable_useNumberInput/utils.test.ts similarity index 100% rename from packages/mui-base/src/useNumberInput/utils.test.ts rename to packages/mui-base/src/unstable_useNumberInput/utils.test.ts diff --git a/packages/mui-base/src/useNumberInput/utils.ts b/packages/mui-base/src/unstable_useNumberInput/utils.ts similarity index 100% rename from packages/mui-base/src/useNumberInput/utils.ts rename to packages/mui-base/src/unstable_useNumberInput/utils.ts From db39c57cf667ff0c6d61dfe35b242dc19cead4b9 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 29 May 2023 16:29:06 +0800 Subject: [PATCH 45/70] Rename change handlers --- .../number-input/UnstyledNumberInputBasic.js | 2 +- .../number-input/UnstyledNumberInputBasic.tsx | 2 +- .../UnstyledNumberInputBasic.tsx.preview | 2 +- .../number-input/UseNumberInputCompact.js | 2 +- .../number-input/UseNumberInputCompact.tsx | 5 +- .../UseNumberInputCompact.tsx.preview | 2 +- .../components/number-input/number-input.md | 4 +- .../base-ui/api/number-input-unstyled.json | 2 +- docs/pages/base-ui/api/use-number-input.json | 10 +-- .../number-input/number-input.json | 4 +- .../use-number-input/use-number-input.json | 4 +- .../mui-base/src/FormControl/FormControl.tsx | 9 -- .../Unstable_NumberInput/NumberInput.test.tsx | 88 +++++++++---------- .../src/Unstable_NumberInput/NumberInput.tsx | 12 +-- .../useNumberInput.test.tsx | 74 ++++++++-------- .../unstable_useNumberInput/useNumberInput.ts | 22 +++-- .../useNumberInput.types.ts | 18 ++-- 17 files changed, 129 insertions(+), 133 deletions(-) diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js index 24c83f2902e1c1..7e7e104816d37a 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.js @@ -34,7 +34,7 @@ export default function NumberInputBasic() { aria-label="Demo number input" placeholder="Type a number…" value={value} - onValueChange={(event, val) => setValue(val)} + onChange={(event, val) => setValue(val)} /> ); } diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx index 176431e6ed449a..cf734aaa05d51b 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx @@ -40,7 +40,7 @@ export default function NumberInputBasic() { aria-label="Demo number input" placeholder="Type a number…" value={value} - onValueChange={(event, val) => setValue(val)} + onChange={(event, val) => setValue(val)} /> ); } diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview index 0d8503aef8796c..a2ecd626c6645c 100644 --- a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview +++ b/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview @@ -2,5 +2,5 @@ aria-label="Demo number input" placeholder="Type a number…" value={value} - onValueChange={(event, val) => setValue(val)} + onChange={(event, val) => setValue(val)} /> \ No newline at end of file diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.js b/docs/data/base/components/number-input/UseNumberInputCompact.js index 275b649c3cdeec..a0e913e98ab6ba 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.js +++ b/docs/data/base/components/number-input/UseNumberInputCompact.js @@ -60,7 +60,7 @@ export default function UseNumberInputCompact() { placeholder="Type a number…" readOnly value={value} - onValueChange={(event, val) => setValue(val)} + onChange={(event, val) => setValue(val)} />
          Current value: {value ?? ' '}
          diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx b/docs/data/base/components/number-input/UseNumberInputCompact.tsx index d651de14814094..a5710dd0211ef1 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.tsx +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx @@ -6,7 +6,8 @@ import { styled } from '@mui/system'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; const CompactNumberInput = React.forwardRef(function CompactNumberInput( - props: UseNumberInputParameters & React.InputHTMLAttributes, + props: Omit, 'onChange'> & + UseNumberInputParameters, ref: React.ForwardedRef, ) { const { @@ -65,7 +66,7 @@ export default function UseNumberInputCompact() { placeholder="Type a number…" readOnly value={value} - onValueChange={(event, val) => setValue(val)} + onChange={(event, val) => setValue(val)} />
          Current value: {value ?? ' '}
          diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview b/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview index 8220b8cfc47a36..bc55f0dc37f866 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview +++ b/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview @@ -4,7 +4,7 @@ placeholder="Type a number…" readOnly value={value} - onValueChange={(event, val) => setValue(val)} + onChange={(event, val) => setValue(val)} />
          Current value: {value ?? ' '}
          diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index cc80b019735ef8..69f03fe7163f74 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -43,7 +43,7 @@ export default function MyApp() { ### Basics -The following demo shows how to create a number input component, apply some styling, and write the latest value to a state variable using the `onValueChange` prop: +The following demo shows how to create a number input component, apply some styling, and write the latest value to a state variable using the `onChange` prop: {{"demo": "UnstyledNumberInputBasic.js"}} @@ -147,6 +147,6 @@ Here's an example of a custom component built using the `useNumberInput` hook wi {{"demo": "UseNumberInput.js", "defaultCodeOpen": false}} Here's an example of a "compact" number input component using the hook that only consists of the stepper buttons. -In this demo, `onValueChange` is used to write the latest value of the component to a state variable. +In this demo, `onChange` is used to write the latest value of the component to a state variable. {{"demo": "UseNumberInputCompact.js", "defaultCodeOpen": false}} diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json index fb5b14d1bcbebb..0b1bfd3603a676 100644 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ b/docs/pages/base-ui/api/number-input-unstyled.json @@ -8,7 +8,7 @@ "max": { "type": { "name": "number" } }, "min": { "type": { "name": "number" } }, "onChange": { "type": { "name": "func" } }, - "onValueChange": { "type": { "name": "func" } }, + "onInputChange": { "type": { "name": "func" } }, "readOnly": { "type": { "name": "bool" } }, "required": { "type": { "name": "bool" } }, "shiftMultiplier": { "type": { "name": "number" } }, diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 8767834f75ef13..9d1deecb0b96ff 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -20,8 +20,8 @@ }, "onChange": { "type": { - "name": "React.ChangeEventHandler<HTMLInputElement>", - "description": "React.ChangeEventHandler<HTMLInputElement>" + "name": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void", + "description": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void" } }, "onClick": { @@ -30,10 +30,10 @@ "onFocus": { "type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" } }, - "onValueChange": { + "onInputChange": { "type": { - "name": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void", - "description": "(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void" + "name": "React.ChangeEventHandler<HTMLInputElement>", + "description": "React.ChangeEventHandler<HTMLInputElement>" } }, "readOnly": { "type": { "name": "boolean", "description": "boolean" } }, diff --git a/docs/translations/api-docs-base/number-input/number-input.json b/docs/translations/api-docs-base/number-input/number-input.json index 47c20be82c99c4..ff207044f2fd6d 100644 --- a/docs/translations/api-docs-base/number-input/number-input.json +++ b/docs/translations/api-docs-base/number-input/number-input.json @@ -8,8 +8,8 @@ "id": "The id of the input element.", "max": "The maximum value.", "min": "The minimum value.", - "onChange": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", - "onValueChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", + "onInputChange": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", + "onChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active, with the addition that they are now keyboard focusable.", "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing or decrementing the value. Defaults to 10.", diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index b505d32196b11a..5c28f2249a7e87 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -8,8 +8,8 @@ "inputRef": "The ref of the input element.", "max": "The maximum value.", "min": "The minimum value.", - "onChange": "Callback fired when the value changes, before clamping is applied. Note that\nevent.target.value may contain values that fall outside of min and max or\nare otherwise \"invalid\".", - "onValueChange": "Callback fired after the value is clamped and changes.\nCalled with undefined when the value is unset.", + "onChange": "Callback fired after the value is clamped and changes - when the is blurred or when\nthe stepper buttons are triggered.\nCalled with undefined when the value is unset.", + "onInputChange": "Callback fired when the value changes after each keypress, before clamping is applied.\nNote that event.target.value may contain values that fall outside of min and max or\nare otherwise \"invalid\".", "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active,\nwith the addition that they are now keyboard focusable.", "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10.", diff --git a/packages/mui-base/src/FormControl/FormControl.tsx b/packages/mui-base/src/FormControl/FormControl.tsx index ffb7d4d89403c9..771ad6cade02c8 100644 --- a/packages/mui-base/src/FormControl/FormControl.tsx +++ b/packages/mui-base/src/FormControl/FormControl.tsx @@ -127,15 +127,6 @@ const FormControl = React.forwardRef(function FormControl< setValue(event.target.value); onChange?.(event); }, - // TODO: make it like this? to work with SelectUnstyled as well - // onChange: (event: React.ChangeEvent, valueTwo?: unknown) => { - // console.group('FormControlUnstyledContext onChange'); - // console.log(event); - // console.log('valueTwo', valueTwo); - // console.groupEnd(); - // setValue(valueTwo ?? event.target.value); - // onChange?.(event, valueTwo); - // }, onFocus: () => { setFocused(true); }, diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx index 44fa1bf45992a1..28c0a20e564882 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx @@ -94,12 +94,12 @@ describe('', () => { describe('step buttons', () => { it('clicking the increment and decrement buttons changes the value', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( ', () => { const decrementButton = getByTestId('decrement-btn'); await user.click(incrementButton); - expect(handleValueChange.args[0][1]).to.equal(11); + expect(handleChange.args[0][1]).to.equal(11); expect(input.value).to.equal('11'); await user.click(decrementButton); await user.click(decrementButton); - expect(handleValueChange.callCount).to.equal(3); - expect(handleValueChange.args[2][1]).to.equal(9); + expect(handleChange.callCount).to.equal(3); + expect(handleChange.args[2][1]).to.equal(9); expect(input.value).to.equal('9'); }); it('clicking the increment and decrement buttons changes the value based on shiftMultiplier if the Shift key is held', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( ', () => { await user.keyboard('{Shift>}'); await user.click(incrementButton); await user.click(incrementButton); - expect(handleValueChange.args[1][1]).to.equal(30); + expect(handleChange.args[1][1]).to.equal(30); expect(input.value).to.equal('30'); await user.click(decrementButton); - expect(handleValueChange.args[2][1]).to.equal(25); - expect(handleValueChange.callCount).to.equal(3); + expect(handleChange.args[2][1]).to.equal(25); + expect(handleChange.callCount).to.equal(3); expect(input.value).to.equal('25'); }); @@ -219,12 +219,12 @@ describe('', () => { describe('keyboard interaction', () => { it('ArrowUp and ArrowDown changes the value', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -235,25 +235,25 @@ describe('', () => { await user.keyboard('[ArrowUp]'); await user.keyboard('[ArrowUp]'); - expect(handleValueChange.callCount).to.equal(2); - expect(handleValueChange.args[1][1]).to.equal(12); + expect(handleChange.callCount).to.equal(2); + expect(handleChange.args[1][1]).to.equal(12); expect(input.value).to.equal('12'); await user.keyboard('[ArrowDown]'); - expect(handleValueChange.callCount).to.equal(3); - expect(handleValueChange.args[2][1]).to.equal(11); + expect(handleChange.callCount).to.equal(3); + expect(handleChange.args[2][1]).to.equal(11); expect(input.value).to.equal('11'); }); it('ArrowUp and ArrowDown changes the value based on a custom step', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -264,24 +264,24 @@ describe('', () => { await user.keyboard('[ArrowUp]'); await user.keyboard('[ArrowUp]'); - expect(handleValueChange.args[1][1]).to.equal(20); + expect(handleChange.args[1][1]).to.equal(20); expect(input.value).to.equal('20'); await user.keyboard('[ArrowDown]'); - expect(handleValueChange.args[2][1]).to.equal(15); - expect(handleValueChange.callCount).to.equal(3); + expect(handleChange.args[2][1]).to.equal(15); + expect(handleChange.callCount).to.equal(3); expect(input.value).to.equal('15'); }); it('ArrowUp and ArrowDown changes the value based on shiftMultiplier if the Shift key is held', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -291,25 +291,25 @@ describe('', () => { await user.click(input); await user.keyboard('{Shift>}[ArrowUp]/'); - expect(handleValueChange.callCount).to.equal(1); - expect(handleValueChange.args[0][1]).to.equal(25); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal(25); expect(input.value).to.equal('25'); await user.keyboard('{Shift>}[ArrowDown][ArrowDown]{/Shift}'); - expect(handleValueChange.args[2][1]).to.equal(15); - expect(handleValueChange.callCount).to.equal(3); + expect(handleChange.args[2][1]).to.equal(15); + expect(handleChange.callCount).to.equal(3); expect(input.value).to.equal('15'); }); it('PageUp and PageDown changes the value based on shiftMultiplier', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -319,24 +319,24 @@ describe('', () => { await user.click(input); await user.keyboard('[PageUp]'); - expect(handleValueChange.args[0][1]).to.equal(25); + expect(handleChange.args[0][1]).to.equal(25); expect(input.value).to.equal('25'); await user.keyboard('[PageDown][PageDown]'); - expect(handleValueChange.args[2][1]).to.equal(15); - expect(handleValueChange.callCount).to.equal(3); + expect(handleChange.args[2][1]).to.equal(15); + expect(handleChange.callCount).to.equal(3); expect(input.value).to.equal('15'); }); it('sets value to max when Home is pressed', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -346,19 +346,19 @@ describe('', () => { await user.click(input); await user.keyboard('[Home]'); - expect(handleValueChange.args[0][1]).to.equal(50); + expect(handleChange.args[0][1]).to.equal(50); expect(input.value).to.equal('50'); }); it('sets value to min when End is pressed', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -368,18 +368,18 @@ describe('', () => { await user.click(input); await user.keyboard('[End]'); - expect(handleValueChange.args[0][1]).to.equal(1); + expect(handleChange.args[0][1]).to.equal(1); expect(input.value).to.equal('1'); }); it('sets value to min when the input has no value and ArrowUp is pressed', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -389,18 +389,18 @@ describe('', () => { await user.click(input); await user.keyboard('[ArrowUp]'); - expect(handleValueChange.args[0][1]).to.equal(5); + expect(handleChange.args[0][1]).to.equal(5); expect(input.value).to.equal('5'); }); it('sets value to max when the input has no value and ArrowDown is pressed', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); const { getByTestId } = render( , ); @@ -410,7 +410,7 @@ describe('', () => { await user.click(input); await user.keyboard('[ArrowDown]'); - expect(handleValueChange.args[0][1]).to.equal(9); + expect(handleChange.args[0][1]).to.equal(9); expect(input.value).to.equal('9'); }); }); diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 56407c8cc4e022..fc89e7e5a81efa 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -60,9 +60,9 @@ const NumberInput = React.forwardRef(function NumberInput( max, min, onBlur, - onChange, + onInputChange, onFocus, - onValueChange, + onChange, placeholder, required, readOnly, @@ -94,9 +94,9 @@ const NumberInput = React.forwardRef(function NumberInput( disabled, error, onFocus, - onChange, + onInputChange, onBlur, - onValueChange, + onChange, required, readOnly, value, @@ -223,7 +223,7 @@ NumberInput.propTypes /* remove-proptypes */ = { * `event.target.value` may contain values that fall outside of `min` and `max` or * are otherwise "invalid". */ - onChange: PropTypes.func, + onInputChange: PropTypes.func, /** * @ignore */ @@ -232,7 +232,7 @@ NumberInput.propTypes /* remove-proptypes */ = { * Callback fired after the value is clamped and changes. * Called with `undefined` when the value is unset. */ - onValueChange: PropTypes.func, + onChange: PropTypes.func, /** * @ignore */ diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx index 2aacfcff3fee33..38d15510465e55 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx @@ -66,12 +66,12 @@ describe('useNumberInput', () => { expect(inputProps.required).to.equal(true); }); - describe('prop: onChange', () => { - it('should call onChange accordingly when inputting valid characters', async () => { + describe('prop: onInputChange', () => { + it('should call onInputChange accordingly when inputting valid characters', async () => { const user = userEvent.setup(); - const handleChange = spy(); + const handleInputChange = spy(); function NumberInput() { - const { getInputProps } = useNumberInput({ onChange: handleChange }); + const { getInputProps } = useNumberInput({ onInputChange: handleInputChange }); return ; } @@ -83,16 +83,16 @@ describe('useNumberInput', () => { await user.keyboard('-12'); - expect(handleChange.callCount).to.equal(3); - expect(handleChange.args[2][0].target.value).to.equal('-12'); + expect(handleInputChange.callCount).to.equal(3); + expect(handleInputChange.args[2][0].target.value).to.equal('-12'); expect(input.value).to.equal('-12'); }); it('should not change the input value when inputting invalid characters', async () => { const user = userEvent.setup(); - const handleChange = spy(); + const handleInputChange = spy(); function NumberInput() { - const { getInputProps } = useNumberInput({ onChange: handleChange }); + const { getInputProps } = useNumberInput({ onInputChange: handleInputChange }); return ; } @@ -104,17 +104,17 @@ describe('useNumberInput', () => { await user.keyboard('-5a'); - expect(handleChange.callCount).to.equal(3); + expect(handleInputChange.callCount).to.equal(3); expect(input.value).to.equal('-5'); }); }); - describe('prop: onValueChange', () => { - it('should call onValueChange when the input is blurred', async () => { + describe('prop: onChange', () => { + it('should call onChange when the input is blurred', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); function NumberInput() { - const { getInputProps } = useNumberInput({ onValueChange: handleValueChange }); + const { getInputProps } = useNumberInput({ onChange: handleChange }); return ; } @@ -126,20 +126,20 @@ describe('useNumberInput', () => { await user.keyboard('34'); - expect(handleValueChange.callCount).to.equal(0); + expect(handleChange.callCount).to.equal(0); await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); - expect(handleValueChange.callCount).to.equal(1); + expect(handleChange.callCount).to.equal(1); }); - it('should call onValueChange with a value within max', async () => { + it('should call onChange with a value within max', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ - onValueChange: handleValueChange, + onChange: handleChange, max: 5, }); @@ -156,15 +156,15 @@ describe('useNumberInput', () => { await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); - expect(handleValueChange.args[0][1]).to.equal(5); + expect(handleChange.args[0][1]).to.equal(5); }); - it('should call onValueChange with a value within min', async () => { + it('should call onChange with a value within min', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ - onValueChange: handleValueChange, + onChange: handleChange, min: 5, }); @@ -181,15 +181,15 @@ describe('useNumberInput', () => { await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); - expect(handleValueChange.args[0][1]).to.equal(5); + expect(handleChange.args[0][1]).to.equal(5); }); - it('should call onValueChange with a value based on a custom step', async () => { + it('should call onChange with a value based on a custom step', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ - onValueChange: handleValueChange, + onChange: handleChange, min: 0, step: 5, }); @@ -207,15 +207,15 @@ describe('useNumberInput', () => { await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); - expect(handleValueChange.args[0][1]).to.equal(5); + expect(handleChange.args[0][1]).to.equal(5); }); - it('should call onValueChange with undefined when the value is cleared', async () => { + it('should call onChange with undefined when the value is cleared', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ - onValueChange: handleValueChange, + onChange: handleChange, }); return ; @@ -237,16 +237,16 @@ describe('useNumberInput', () => { await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); - expect(handleValueChange.callCount).to.equal(1); - expect(handleValueChange.args[0][1]).to.equal(undefined); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal(undefined); }); - it('should call onValueChange with undefined when input value is -', async () => { + it('should call onChange with undefined when input value is -', async () => { const user = userEvent.setup(); - const handleValueChange = spy(); + const handleChange = spy(); function NumberInput() { const { getInputProps } = useNumberInput({ - onValueChange: handleValueChange, + onChange: handleChange, }); return ; @@ -268,8 +268,8 @@ describe('useNumberInput', () => { await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); - expect(handleValueChange.callCount).to.equal(1); - expect(handleValueChange.args[0][1]).to.equal(undefined); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal(undefined); }); }); }); diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index 4e563a8a19fc22..15d53e5b6d2cea 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -45,9 +45,9 @@ export default function useNumberInput( disabled: disabledProp = false, error: errorProp = false, onBlur, - onChange, + onInputChange, onFocus, - onValueChange, + onChange, required: requiredProp = false, readOnly: readOnlyProp = false, value: valueProp, @@ -138,9 +138,9 @@ export default function useNumberInput( setValue(newValue); if (isNumber(newValue)) { - onValueChange?.(event, newValue); + onChange?.(event, newValue); } else { - onValueChange?.(event, undefined); + onChange?.(event, undefined); } }; @@ -157,7 +157,7 @@ export default function useNumberInput( formControlContext?.onChange?.(event); - otherHandlers.onChange?.(event); + otherHandlers.onInputChange?.(event); const val = parseInput(event.currentTarget.value); @@ -265,9 +265,9 @@ export default function useNumberInput( ): UseNumberInputRootSlotProps => { const propsEventHandlers = extractEventHandlers(parameters, [ 'onBlur', - 'onChange', + 'onInputChange', 'onFocus', - 'onValueChange', + 'onChange', ]); const externalEventHandlers = { ...propsEventHandlers, ...extractEventHandlers(externalProps) }; @@ -282,19 +282,17 @@ export default function useNumberInput( const getInputProps = = {}>( externalProps: TOther = {} as TOther, ): UseNumberInputInputSlotProps => { - const propsEventHandlers: Record | undefined> = { + const externalEventHandlers = { onBlur, - onChange, onFocus, + ...extractEventHandlers(externalProps, ['onInputChange']), }; - const externalEventHandlers = { ...propsEventHandlers, ...extractEventHandlers(externalProps) }; - const mergedEventHandlers = { ...externalProps, ...externalEventHandlers, onFocus: handleFocus(externalEventHandlers), - onChange: handleInputChange(externalEventHandlers), + onChange: handleInputChange({ ...externalEventHandlers, onInputChange }), onBlur: handleBlur(externalEventHandlers), onKeyDown: handleKeyDown(externalEventHandlers), }; diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index f7528e4c23a162..22168c790f66a9 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -41,17 +41,23 @@ export interface UseNumberInputParameters { onBlur?: React.FocusEventHandler; onClick?: React.MouseEventHandler; /** - * Callback fired when the value changes, before clamping is applied. Note that - * `event.target.value` may contain values that fall outside of `min` and `max` or + * Callback fired when the value changes after each keypress, before clamping is applied. + * Note that `event.target.value` may contain values that fall outside of `min` and `max` or * are otherwise "invalid". + * + * @param {React.ChangeEvent} event The event source of the callback. */ - onChange?: React.ChangeEventHandler; + onInputChange?: React.ChangeEventHandler; onFocus?: React.FocusEventHandler; /** - * Callback fired after the value is clamped and changes. + * Callback fired after the value is clamped and changes - when the is blurred or when + * the stepper buttons are triggered. * Called with `undefined` when the value is unset. + * + * @param {React.FocusEvent|React.PointerEvent|React.KeyboardEvent} event The event source of the callback + * @param {number|undefined} value The new value of the component */ - onValueChange?: ( + onChange?: ( event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent, value: number | undefined, ) => void; @@ -85,7 +91,7 @@ export interface UseNumberInputRootSlotOwnProps { export type UseNumberInputRootSlotProps = Omit< TOther, - keyof UseNumberInputRootSlotOwnProps | 'onBlur' | 'onChange' | 'onFocus' + keyof UseNumberInputRootSlotOwnProps | 'onBlur' | 'onInputChange' | 'onFocus' > & UseNumberInputRootSlotOwnProps; From ccd319945edb1e9ddd20c465670d9dd8268c142a Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 26 Jul 2023 18:03:21 +0800 Subject: [PATCH 46/70] Add use-client --- packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx | 1 + packages/mui-base/src/Unstable_NumberInput/index.ts | 1 + packages/mui-base/src/unstable_useNumberInput/index.ts | 1 + packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index fc89e7e5a81efa..31f2985aebc19e 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; diff --git a/packages/mui-base/src/Unstable_NumberInput/index.ts b/packages/mui-base/src/Unstable_NumberInput/index.ts index 8c86220e0274da..b83be49446103e 100644 --- a/packages/mui-base/src/Unstable_NumberInput/index.ts +++ b/packages/mui-base/src/Unstable_NumberInput/index.ts @@ -1,3 +1,4 @@ +'use client'; export { default } from './NumberInput'; export { default as numberInputClasses } from './numberInputClasses'; diff --git a/packages/mui-base/src/unstable_useNumberInput/index.ts b/packages/mui-base/src/unstable_useNumberInput/index.ts index 4a0c6ad81a44e5..6984f2df6e29b2 100644 --- a/packages/mui-base/src/unstable_useNumberInput/index.ts +++ b/packages/mui-base/src/unstable_useNumberInput/index.ts @@ -1,3 +1,4 @@ +'use client'; export { default } from './useNumberInput'; export * from './useNumberInput.types'; diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index 15d53e5b6d2cea..837fff2a3f2184 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import MuiError from '@mui/utils/macros/MuiError.macro'; import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils'; From 63c0d445e25bdb5f2ae3e002f27c5b6c3e83b91e Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 26 Jul 2023 20:36:13 +0800 Subject: [PATCH 47/70] Docs fixes --- ...umberInputBasic.js => NumberInputBasic.js} | 0 ...berInputBasic.tsx => NumberInputBasic.tsx} | 0 ...x.preview => NumberInputBasic.tsx.preview} | 0 ...oduction.js => NumberInputIntroduction.js} | 0 ...uction.tsx => NumberInputIntroduction.tsx} | 0 ...ew => NumberInputIntroduction.tsx.preview} | 0 .../components/number-input/number-input.md | 19 ++++++++++++------- 7 files changed, 12 insertions(+), 7 deletions(-) rename docs/data/base/components/number-input/{UnstyledNumberInputBasic.js => NumberInputBasic.js} (100%) rename docs/data/base/components/number-input/{UnstyledNumberInputBasic.tsx => NumberInputBasic.tsx} (100%) rename docs/data/base/components/number-input/{UnstyledNumberInputBasic.tsx.preview => NumberInputBasic.tsx.preview} (100%) rename docs/data/base/components/number-input/{UnstyledNumberInputIntroduction.js => NumberInputIntroduction.js} (100%) rename docs/data/base/components/number-input/{UnstyledNumberInputIntroduction.tsx => NumberInputIntroduction.tsx} (100%) rename docs/data/base/components/number-input/{UnstyledNumberInputIntroduction.tsx.preview => NumberInputIntroduction.tsx.preview} (100%) diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.js b/docs/data/base/components/number-input/NumberInputBasic.js similarity index 100% rename from docs/data/base/components/number-input/UnstyledNumberInputBasic.js rename to docs/data/base/components/number-input/NumberInputBasic.js diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx b/docs/data/base/components/number-input/NumberInputBasic.tsx similarity index 100% rename from docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx rename to docs/data/base/components/number-input/NumberInputBasic.tsx diff --git a/docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview b/docs/data/base/components/number-input/NumberInputBasic.tsx.preview similarity index 100% rename from docs/data/base/components/number-input/UnstyledNumberInputBasic.tsx.preview rename to docs/data/base/components/number-input/NumberInputBasic.tsx.preview diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js b/docs/data/base/components/number-input/NumberInputIntroduction.js similarity index 100% rename from docs/data/base/components/number-input/UnstyledNumberInputIntroduction.js rename to docs/data/base/components/number-input/NumberInputIntroduction.js diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx b/docs/data/base/components/number-input/NumberInputIntroduction.tsx similarity index 100% rename from docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx rename to docs/data/base/components/number-input/NumberInputIntroduction.tsx diff --git a/docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx.preview b/docs/data/base/components/number-input/NumberInputIntroduction.tsx.preview similarity index 100% rename from docs/data/base/components/number-input/UnstyledNumberInputIntroduction.tsx.preview rename to docs/data/base/components/number-input/NumberInputIntroduction.tsx.preview diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 69f03fe7163f74..cbab15481acbcb 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -25,16 +25,16 @@ Base UI's Number Input component is a customizable replacement for the native HT See [this article](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/) by the GOV.UK Design System team for a more detailed explanation. -{{"demo": "UnstyledNumberInputIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} +{{"demo": "NumberInputIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} ## Component ### Usage -After [installation](/base/getting-started/quickstart/#installation), you can start building with this component using the following basic elements: +After [installation](/base-ui/getting-started/quickstart/#installation), you can start building with this component using the following basic elements: ```jsx -import NumberInput from '@mui/base/NumberInput'; +import NumberInput from '@mui/base/Unstable_NumberInput'; export default function MyApp() { return ; @@ -45,7 +45,7 @@ export default function MyApp() { The following demo shows how to create a number input component, apply some styling, and write the latest value to a state variable using the `onChange` prop: -{{"demo": "UnstyledNumberInputBasic.js"}} +{{"demo": "NumberInputBasic.js"}} The `min` and `max` props can be used to define a range of accepted values. You can pass only one of them to define an open-ended range. @@ -79,7 +79,12 @@ Here's another demo of a Number Input with fully customized styles: ### Anatomy -The Base UI Number Input component consists of a root `
          ` that contains one interior `` slot, and two ` + + +
          + ); +}); + +export default function UseNumberInputCompact() { + const [value, setValue] = React.useState(); + + return ( +
          + setValue(val)} + className="my-input" + /> +
          Current value: {value ?? ' '}
          + + +
          + ); +} + +const cyan = { + 50: '#E9F8FC', + 100: '#BDEBF4', + 200: '#99D8E5', + 300: '#66BACC', + 400: '#1F94AD', + 500: '#0D5463', + 600: '#094855', + 700: '#063C47', + 800: '#043039', + 900: '#022127', +}; + +const grey = { + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', +}; + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx new file mode 100644 index 00000000000000..198c29d0cba6fd --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import useNumberInput, { + UseNumberInputParameters, +} from '@mui/base/unstable_useNumberInput'; +import { useTheme } from '@mui/system'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; + +const CompactNumberInput = React.forwardRef(function CompactNumberInput( + props: Omit, 'onChange'> & + UseNumberInputParameters, + ref: React.ForwardedRef, +) { + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + } = useNumberInput(props); + + const inputProps = getInputProps(); + + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( +
          + + + +
          + ); +}); + +export default function UseNumberInputCompact() { + const [value, setValue] = React.useState(); + + return ( +
          + setValue(val)} + className="my-input" + /> + +
          Current value: {value ?? ' '}
          + + +
          + ); +} + +const cyan = { + 50: '#E9F8FC', + 100: '#BDEBF4', + 200: '#99D8E5', + 300: '#66BACC', + 400: '#1F94AD', + 500: '#0D5463', + 600: '#094855', + 700: '#063C47', + 800: '#043039', + 900: '#022127', +}; + +const grey = { + 50: '#F3F6F9', + 100: '#E7EBF0', + 200: '#E0E3E7', + 300: '#CDD2D7', + 400: '#B2BAC2', + 500: '#A0AAB4', + 600: '#6F7E8C', + 700: '#3E5060', + 800: '#2D3843', + 900: '#1A2027', +}; + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx.preview b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx.preview new file mode 100644 index 00000000000000..e586cf96949b12 --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx.preview @@ -0,0 +1,12 @@ + setValue(val)} + className="my-input" +/> + +
          Current value: {value ?? ' '}
          + + \ No newline at end of file diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.js b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.js similarity index 100% rename from docs/data/base/components/number-input/UseNumberInputCompact.js rename to docs/data/base/components/number-input/UseNumberInputCompact/system/index.js diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx similarity index 100% rename from docs/data/base/components/number-input/UseNumberInputCompact.tsx rename to docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx diff --git a/docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx.preview similarity index 100% rename from docs/data/base/components/number-input/UseNumberInputCompact.tsx.preview rename to docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx.preview diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.js b/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.js new file mode 100644 index 00000000000000..cbb316b0965f40 --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.js @@ -0,0 +1,88 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import useNumberInput from '@mui/base/unstable_useNumberInput'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; +import clsx from 'clsx'; + +const CompactNumberInput = React.forwardRef(function CompactNumberInput(props, ref) { + const { className, ...rest } = props; + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + } = useNumberInput(rest); + + const inputProps = getInputProps(); + + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( +
          + + + +
          + ); +}); + +CompactNumberInput.propTypes = { + className: PropTypes.string, +}; + +export default function UseNumberInputCompact() { + const [value, setValue] = React.useState(); + + return ( +
          + setValue(val)} + /> +
          Current value: {value ?? ' '}
          +
          + ); +} diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.tsx b/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.tsx new file mode 100644 index 00000000000000..db825abe928e6b --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import useNumberInput, { + UseNumberInputParameters, +} from '@mui/base/unstable_useNumberInput'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; +import clsx from 'clsx'; + +const CompactNumberInput = React.forwardRef(function CompactNumberInput( + props: Omit, 'onChange'> & + UseNumberInputParameters, + ref: React.ForwardedRef, +) { + const { className, ...rest } = props; + const { + getRootProps, + getInputProps, + getIncrementButtonProps, + getDecrementButtonProps, + } = useNumberInput(rest); + + const inputProps = getInputProps(); + + inputProps.ref = useForkRef(inputProps.ref, ref); + + return ( +
          + + + +
          + ); +}); + +export default function UseNumberInputCompact() { + const [value, setValue] = React.useState(); + + return ( +
          + setValue(val)} + /> + +
          Current value: {value ?? ' '}
          +
          + ); +} diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.tsx.preview b/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.tsx.preview new file mode 100644 index 00000000000000..4dcd66b1bc1aac --- /dev/null +++ b/docs/data/base/components/number-input/UseNumberInputCompact/tailwind/index.tsx.preview @@ -0,0 +1,9 @@ + setValue(val)} +/> + +
          Current value: {value ?? ' '}
          \ No newline at end of file diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index e341a80d09163c..114f416ea86d5f 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -154,4 +154,4 @@ Here's an example of a custom component built using the `useNumberInput` hook wi Here's an example of a "compact" number input component using the hook that only consists of the stepper buttons. In this demo, `onChange` is used to write the latest value of the component to a state variable. -{{"demo": "UseNumberInputCompact.js", "defaultCodeOpen": false}} +{{"demo": "UseNumberInputCompact", "defaultCodeOpen": false}} From 73177887719b88c9c21cb36213bb92e2168bc929 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 28 Jul 2023 14:49:41 +0800 Subject: [PATCH 52/70] Update hook demo --- docs/data/base/components/number-input/UseNumberInput.js | 2 -- docs/data/base/components/number-input/UseNumberInput.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index a568df4deada20..17239bac8f5a84 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -81,8 +81,6 @@ const grey = { const StyledInputRoot = styled('div')( ({ theme }) => ` - width: 320px; - font-family: IBM Plex Sans, sans-serif; font-size: 0.875rem; font-weight: 400; diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index ff06120b198157..5edc38da08aa39 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -86,8 +86,6 @@ const grey = { const StyledInputRoot: React.ElementType = styled('div')( ({ theme }) => ` - width: 320px; - font-family: IBM Plex Sans, sans-serif; font-size: 0.875rem; font-weight: 400; From 8ea23499e95f4a9f4da408db144440a58ab3b0aa Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 28 Jul 2023 16:08:30 +0800 Subject: [PATCH 53/70] Drop the component prop and single letter type names --- .../Unstable_NumberInput/NumberInput.test.tsx | 1 + .../src/Unstable_NumberInput/NumberInput.tsx | 7 +++--- .../Unstable_NumberInput/NumberInput.types.ts | 22 +++++++++---------- .../unstable_useNumberInput/useNumberInput.ts | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx index 28c0a20e564882..1f3338779f8bb4 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx @@ -36,6 +36,7 @@ describe('', () => { testWithElement: 'button', }, }, + skip: ['componentProp'], })); it('should be able to attach input ref passed through props', () => { diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 31f2985aebc19e..05bd599362db36 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -41,11 +41,11 @@ const useUtilityClasses = (ownerState: NumberInputOwnerState) => { * * Demos: * - * - [Number Input](https://mui.com/base/react-number-input/) + * - [Number Input](https://mui.com/base-ui/react-number-input/) * * API: * - * - [NumberInput API](https://mui.com/base/react-number-input/components-api/#number-input) + * - [NumberInput API](https://mui.com/base-ui/react-number-input/components-api/#number-input) */ const NumberInput = React.forwardRef(function NumberInput( props: NumberInputProps, @@ -53,7 +53,6 @@ const NumberInput = React.forwardRef(function NumberInput( ) { const { className, - component, defaultValue, disabled, error, @@ -120,7 +119,7 @@ const NumberInput = React.forwardRef(function NumberInput( placeholder, }; - const Root = component ?? slots.root ?? 'div'; + const Root = slots.root ?? 'div'; const rootProps: WithOptionalOwnerState = useSlotProps({ elementType: Root, getSlotProps: getRootProps, diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts b/packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts index b5a204b323616c..513ba18a0a3b1d 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.types.ts @@ -1,4 +1,4 @@ -import { OverrideProps, Simplify } from '@mui/types'; +import { Simplify } from '@mui/types'; import { FormControlState } from '../FormControl'; import { UseNumberInputParameters, @@ -6,7 +6,7 @@ import { UseNumberInputIncrementButtonSlotProps, UseNumberInputDecrementButtonSlotProps, } from '../unstable_useNumberInput/useNumberInput.types'; -import { SlotComponentProps } from '../utils'; +import { PolymorphicProps, SlotComponentProps } from '../utils'; export interface NumberInputRootSlotPropsOverrides {} export interface NumberInputInputSlotPropsOverrides {} @@ -52,20 +52,20 @@ export type NumberInputOwnProps = Omit & { }; }; -export interface NumberInputTypeMap

          { - props: P & NumberInputOwnProps; - defaultComponent: D; +export interface NumberInputTypeMap< + AdditionalProps = {}, + RootComponentType extends React.ElementType = 'div', +> { + props: AdditionalProps & NumberInputOwnProps; + defaultComponent: RootComponentType; } export type NumberInputProps< - D extends React.ElementType = NumberInputTypeMap['defaultComponent'], - P = {}, -> = OverrideProps, D> & { - component?: D; -}; + RootComponentType extends React.ElementType = NumberInputTypeMap['defaultComponent'], +> = PolymorphicProps, RootComponentType>; export type NumberInputOwnerState = Simplify< - Omit & { + NumberInputOwnProps & { formControlContext: FormControlState | undefined; focused: boolean; isIncrementDisabled: boolean; diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index 837fff2a3f2184..f01c1a36912fbe 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -28,11 +28,11 @@ function parseInput(v: string): string { * * Demos: * - * - [Number Input](https://mui.com/base/react-number-input/#hook) + * - [Number Input](https://mui.com/base-ui/react-number-input/#hook) * * API: * - * - [useNumberInput API](https://mui.com/base/react-number-input/hooks-api/#use-number-input) + * - [useNumberInput API](https://mui.com/base-ui/react-number-input/hooks-api/#use-number-input) */ export default function useNumberInput( parameters: UseNumberInputParameters, From 2ac68aa64a2adccf6fb607160a8d5eddc186eede Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 28 Jul 2023 16:41:43 +0800 Subject: [PATCH 54/70] Update api docs --- .../base-ui/api/number-input-unstyled.json | 41 --------- docs/pages/base-ui/api/number-input.json | 11 ++- docs/pages/base-ui/api/use-number-input.json | 2 +- .../number-input/number-input.json | 52 +++++++---- .../use-number-input/use-number-input.json | 86 +++++++++++++------ 5 files changed, 103 insertions(+), 89 deletions(-) delete mode 100644 docs/pages/base-ui/api/number-input-unstyled.json diff --git a/docs/pages/base-ui/api/number-input-unstyled.json b/docs/pages/base-ui/api/number-input-unstyled.json deleted file mode 100644 index 0b1bfd3603a676..00000000000000 --- a/docs/pages/base-ui/api/number-input-unstyled.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "props": { - "component": { "type": { "name": "elementType" } }, - "defaultValue": { "type": { "name": "any" } }, - "disabled": { "type": { "name": "bool" } }, - "error": { "type": { "name": "bool" } }, - "id": { "type": { "name": "string" } }, - "max": { "type": { "name": "number" } }, - "min": { "type": { "name": "number" } }, - "onChange": { "type": { "name": "func" } }, - "onInputChange": { "type": { "name": "func" } }, - "readOnly": { "type": { "name": "bool" } }, - "required": { "type": { "name": "bool" } }, - "shiftMultiplier": { "type": { "name": "number" } }, - "slotProps": { - "type": { - "name": "shape", - "description": "{ decrementButton?: func
          | object, incrementButton?: func
          | object, input?: func
          | object, root?: func
          | object }" - }, - "default": "{}" - }, - "slots": { - "type": { - "name": "shape", - "description": "{ decrementButton?: elementType, incrementButton?: elementType, input?: elementType, root?: elementType }" - }, - "default": "{}" - }, - "step": { "type": { "name": "number" } }, - "value": { "type": { "name": "any" } } - }, - "name": "NumberInput", - "styles": { "classes": [], "globalClasses": {}, "name": null }, - "spread": true, - "muiName": "MuiNumberInput", - "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx", - "inheritance": null, - "demos": "

          ", - "cssComponent": false -} diff --git a/docs/pages/base-ui/api/number-input.json b/docs/pages/base-ui/api/number-input.json index fd7548c3d840b8..598d02dc94ac3d 100644 --- a/docs/pages/base-ui/api/number-input.json +++ b/docs/pages/base-ui/api/number-input.json @@ -8,8 +8,10 @@ "max": { "type": { "name": "number" } }, "min": { "type": { "name": "number" } }, "onChange": { "type": { "name": "func" } }, - "onValueChange": { "type": { "name": "func" } }, + "onInputChange": { "type": { "name": "func" } }, + "readOnly": { "type": { "name": "bool" } }, "required": { "type": { "name": "bool" } }, + "shiftMultiplier": { "type": { "name": "number" } }, "slotProps": { "type": { "name": "shape", @@ -22,7 +24,8 @@ "name": "shape", "description": "{ decrementButton?: elementType, incrementButton?: elementType, input?: elementType, root?: elementType }" }, - "default": "{}" + "default": "{}", + "additionalPropsInfo": { "slotsApi": true } }, "step": { "type": { "name": "number" } }, "value": { "type": { "name": "any" } } @@ -32,8 +35,8 @@ "spread": true, "muiName": "MuiNumberInput", "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/NumberInput/NumberInput.tsx", + "filename": "/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 9d1deecb0b96ff..4c8bb9bf6177d0 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -116,5 +116,5 @@ }, "name": "useNumberInput", "filename": "/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts", - "demos": "" + "demos": "" } diff --git a/docs/translations/api-docs-base/number-input/number-input.json b/docs/translations/api-docs-base/number-input/number-input.json index ff207044f2fd6d..e89a1e96957e5f 100644 --- a/docs/translations/api-docs-base/number-input/number-input.json +++ b/docs/translations/api-docs-base/number-input/number-input.json @@ -1,22 +1,42 @@ { "componentDescription": "", "propDescriptions": { - "component": "The component used for the root node. Either a string to use a HTML element or a component.", - "defaultValue": "The default value. Use when the component is not controlled.", - "disabled": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component.", - "error": "If true, the input will indicate an error by setting the aria-invalid attribute on the input and the Mui-error class on the root element.", - "id": "The id of the input element.", - "max": "The maximum value.", - "min": "The minimum value.", - "onInputChange": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", - "onChange": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset.", - "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active, with the addition that they are now keyboard focusable.", - "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", - "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing or decrementing the value. Defaults to 10.", - "slotProps": "The props used for each slot inside the NumberInput.", - "slots": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component. See Slots API below for more details.", - "step": "The amount that the value changes on each increment or decrement.", - "value": "The current value. Use when the component is controlled." + "component": { + "description": "The component used for the root node. Either a string to use a HTML element or a component." + }, + "defaultValue": { + "description": "The default value. Use when the component is not controlled." + }, + "disabled": { + "description": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component." + }, + "error": { + "description": "If true, the input will indicate an error by setting the aria-invalid attribute on the input and the Mui-error class on the root element." + }, + "id": { "description": "The id of the input element." }, + "max": { "description": "The maximum value." }, + "min": { "description": "The minimum value." }, + "onInputChange": { + "description": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid"." + }, + "onChange": { + "description": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset." + }, + "readOnly": { + "description": "If true, the input element becomes read-only. The stepper buttons remain active, with the addition that they are now keyboard focusable." + }, + "required": { + "description": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component." + }, + "shiftMultiplier": { + "description": "Multiplier applied to step if the shift key is held while incrementing or decrementing the value. Defaults to 10." + }, + "slotProps": { "description": "The props used for each slot inside the NumberInput." }, + "slots": { + "description": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component." + }, + "step": { "description": "The amount that the value changes on each increment or decrement." }, + "value": { "description": "The current value. Use when the component is controlled." } }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index 5c28f2249a7e87..581e8922441982 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -1,34 +1,66 @@ { "hookDescription": "", "parametersDescriptions": { - "defaultValue": "The default value. Use when the component is not controlled.", - "disabled": "If true, the component is disabled.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", - "error": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", - "inputId": "The id attribute of the input element.", - "inputRef": "The ref of the input element.", - "max": "The maximum value.", - "min": "The minimum value.", - "onChange": "Callback fired after the value is clamped and changes - when the is blurred or when\nthe stepper buttons are triggered.\nCalled with undefined when the value is unset.", - "onInputChange": "Callback fired when the value changes after each keypress, before clamping is applied.\nNote that event.target.value may contain values that fall outside of min and max or\nare otherwise \"invalid\".", - "readOnly": "If true, the input element becomes read-only. The stepper buttons remain active,\nwith the addition that they are now keyboard focusable.", - "required": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component.", - "shiftMultiplier": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10.", - "step": "The amount that the value changes on each increment or decrement.", - "value": "The current value. Use when the component is controlled." + "defaultValue": { + "description": "The default value. Use when the component is not controlled." + }, + "disabled": { + "description": "If true, the component is disabled.\nThe prop defaults to the value (false) inherited from the parent FormControl component." + }, + "error": { + "description": "If true, the input will indicate an error by setting the aria-invalid attribute.\nThe prop defaults to the value (false) inherited from the parent FormControl component." + }, + "inputId": { "description": "The id attribute of the input element." }, + "inputRef": { "description": "The ref of the input element." }, + "max": { "description": "The maximum value." }, + "min": { "description": "The minimum value." }, + "onChange": { + "description": "Callback fired after the value is clamped and changes - when the is blurred or when\nthe stepper buttons are triggered.\nCalled with undefined when the value is unset." + }, + "onInputChange": { + "description": "Callback fired when the value changes after each keypress, before clamping is applied.\nNote that event.target.value may contain values that fall outside of min and max or\nare otherwise "invalid"." + }, + "readOnly": { + "description": "If true, the input element becomes read-only. The stepper buttons remain active,\nwith the addition that they are now keyboard focusable." + }, + "required": { + "description": "If true, the input element is required.\nThe prop defaults to the value (false) inherited from the parent FormControl component." + }, + "shiftMultiplier": { + "description": "Multiplier applied to step if the shift key is held while incrementing\nor decrementing the value. Defaults to 10." + }, + "step": { "description": "The amount that the value changes on each increment or decrement." }, + "value": { "description": "The current value. Use when the component is controlled." } }, "returnValueDescriptions": { - "disabled": "If true, the component will be disabled.", - "error": "If true, the input will indicate an error by setting the aria-invalid attribute.", - "focused": "If true, the input will be focused.", - "formControlContext": "Return value from the useFormControlContext hook.", - "getDecrementButtonProps": "Resolver for the decrement button slot's props.", - "getIncrementButtonProps": "Resolver for the increment button slot's props.", - "getInputProps": "Resolver for the input slot's props.", - "getRootProps": "Resolver for the root slot's props.", - "inputValue": "The dirty value of the input element when it is in focus.", - "isDecrementDisabled": "If true, the decrement button will be disabled.\ne.g. when the value is already at min", - "isIncrementDisabled": "If true, the increment button will be disabled.\ne.g. when the value is already at max", - "required": "If true, the input will indicate that it's required.", - "value": "The clamped value of the input element." + "disabled": { "description": "If true, the component will be disabled." }, + "error": { + "description": "If true, the input will indicate an error by setting the aria-invalid attribute." + }, + "focused": { "description": "If true, the input will be focused." }, + "formControlContext": { + "description": "Return value from the useFormControlContext hook." + }, + "getDecrementButtonProps": { + "description": "Resolver for the decrement button slot's props." + }, + "getIncrementButtonProps": { + "description": "Resolver for the increment button slot's props." + }, + "getInputProps": { "description": "Resolver for the input slot's props." }, + "getRootProps": { "description": "Resolver for the root slot's props." }, + "inputValue": { + "description": "The dirty value of the input element when it is in focus." + }, + "isDecrementDisabled": { + "description": "If true, the decrement button will be disabled.\ne.g. when the value is already at min" + }, + "isIncrementDisabled": { + "description": "If true, the increment button will be disabled.\ne.g. when the value is already at max" + }, + "required": { + "description": "If true, the input will indicate that it's required." + }, + "value": { "description": "The clamped value of the input element." } } } From 2563733984217fcc5ae4b77036fe7ed34ddfc0e5 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 28 Jul 2023 19:47:38 +0800 Subject: [PATCH 55/70] Generate proptypes --- .../src/Unstable_NumberInput/NumberInput.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 05bd599362db36..3b3aae41888398 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -184,11 +184,6 @@ NumberInput.propTypes /* remove-proptypes */ = { * @ignore */ className: PropTypes.string, - /** - * The component used for the root node. - * Either a string to use a HTML element or a component. - */ - component: PropTypes.elementType, /** * The default value. Use when the component is not controlled. */ @@ -219,20 +214,26 @@ NumberInput.propTypes /* remove-proptypes */ = { */ onBlur: PropTypes.func, /** - * Callback fired when the value changes, before clamping is applied. Note that - * `event.target.value` may contain values that fall outside of `min` and `max` or - * are otherwise "invalid". + * Callback fired after the value is clamped and changes - when the is blurred or when + * the stepper buttons are triggered. + * Called with `undefined` when the value is unset. + * + * @param {React.FocusEvent|React.PointerEvent|React.KeyboardEvent} event The event source of the callback + * @param {number|undefined} value The new value of the component */ - onInputChange: PropTypes.func, + onChange: PropTypes.func, /** * @ignore */ onFocus: PropTypes.func, /** - * Callback fired after the value is clamped and changes. - * Called with `undefined` when the value is unset. + * Callback fired when the value changes after each keypress, before clamping is applied. + * Note that `event.target.value` may contain values that fall outside of `min` and `max` or + * are otherwise "invalid". + * + * @param {React.ChangeEvent} event The event source of the callback. */ - onChange: PropTypes.func, + onInputChange: PropTypes.func, /** * @ignore */ From fd7ba0dfeacc8640109ab7e5c032726c8ba0e280 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Sat, 29 Jul 2023 00:43:23 +0800 Subject: [PATCH 56/70] Update api docs --- docs/pages/base-ui/api/number-input.json | 17 ++++++++++++++--- .../number-input/number-input.json | 16 +++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/pages/base-ui/api/number-input.json b/docs/pages/base-ui/api/number-input.json index 598d02dc94ac3d..184c5bf3d7187e 100644 --- a/docs/pages/base-ui/api/number-input.json +++ b/docs/pages/base-ui/api/number-input.json @@ -1,14 +1,25 @@ { "props": { - "component": { "type": { "name": "elementType" } }, "defaultValue": { "type": { "name": "any" } }, "disabled": { "type": { "name": "bool" } }, "error": { "type": { "name": "bool" } }, "id": { "type": { "name": "string" } }, "max": { "type": { "name": "number" } }, "min": { "type": { "name": "number" } }, - "onChange": { "type": { "name": "func" } }, - "onInputChange": { "type": { "name": "func" } }, + "onChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent, value: number | undefined) => void", + "describedArgs": ["event", "value"] + } + }, + "onInputChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(event: React.ChangeEvent) => void", + "describedArgs": ["event"] + } + }, "readOnly": { "type": { "name": "bool" } }, "required": { "type": { "name": "bool" } }, "shiftMultiplier": { "type": { "name": "number" } }, diff --git a/docs/translations/api-docs-base/number-input/number-input.json b/docs/translations/api-docs-base/number-input/number-input.json index e89a1e96957e5f..38c1223cffe99d 100644 --- a/docs/translations/api-docs-base/number-input/number-input.json +++ b/docs/translations/api-docs-base/number-input/number-input.json @@ -1,9 +1,6 @@ { "componentDescription": "", "propDescriptions": { - "component": { - "description": "The component used for the root node. Either a string to use a HTML element or a component." - }, "defaultValue": { "description": "The default value. Use when the component is not controlled." }, @@ -16,11 +13,16 @@ "id": { "description": "The id of the input element." }, "max": { "description": "The maximum value." }, "min": { "description": "The minimum value." }, - "onInputChange": { - "description": "Callback fired when the <input> value changes, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid"." - }, "onChange": { - "description": "Callback fired after the value is clamped and changes. Called with undefined when the value is unset." + "description": "Callback fired after the value is clamped and changes - when the <input> is blurred or when the stepper buttons are triggered. Called with undefined when the value is unset.", + "typeDescriptions": { + "event": "The event source of the callback", + "value": "The new value of the component" + } + }, + "onInputChange": { + "description": "Callback fired when the <input> value changes after each keypress, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", + "typeDescriptions": { "event": "The event source of the callback." } }, "readOnly": { "description": "If true, the input element becomes read-only. The stepper buttons remain active, with the addition that they are now keyboard focusable." From 6ea82672bd8c234ca9dcaf6bf7dff502fcea2a20 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 31 Jul 2023 22:26:43 +0800 Subject: [PATCH 57/70] Update readOnly behavior --- docs/pages/base-ui/api/number-input.json | 2 +- docs/pages/base-ui/api/use-number-input.json | 2 +- .../Unstable_NumberInput/NumberInput.test.tsx | 42 +------------------ .../src/Unstable_NumberInput/NumberInput.tsx | 18 ++++++-- .../numberInputClasses.ts | 3 ++ .../useNumberInput.test.tsx | 1 - .../unstable_useNumberInput/useNumberInput.ts | 3 +- .../useNumberInput.types.ts | 1 + 8 files changed, 23 insertions(+), 49 deletions(-) diff --git a/docs/pages/base-ui/api/number-input.json b/docs/pages/base-ui/api/number-input.json index 184c5bf3d7187e..ab15ad7756169b 100644 --- a/docs/pages/base-ui/api/number-input.json +++ b/docs/pages/base-ui/api/number-input.json @@ -20,7 +20,7 @@ "describedArgs": ["event"] } }, - "readOnly": { "type": { "name": "bool" } }, + "readOnly": { "type": { "name": "bool" }, "default": "false" }, "required": { "type": { "name": "bool" } }, "shiftMultiplier": { "type": { "name": "number" } }, "slotProps": { diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 4c8bb9bf6177d0..cc492c8bccc430 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -36,7 +36,7 @@ "description": "React.ChangeEventHandler<HTMLInputElement>" } }, - "readOnly": { "type": { "name": "boolean", "description": "boolean" } }, + "readOnly": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, "required": { "type": { "name": "boolean", "description": "boolean" } }, "shiftMultiplier": { "type": { "name": "number", "description": "number" } }, "step": { "type": { "name": "number", "description": "number" } }, diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx index 1f3338779f8bb4..908306a5f29983 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import userEvent from '@testing-library/user-event'; +import { act, createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; import NumberInput, { numberInputClasses, NumberInputOwnerState, } from '@mui/base/Unstable_NumberInput'; -import { act, createMount, createRenderer, describeConformanceUnstyled } from 'test/utils'; describe('', () => { const mount = createMount(); @@ -414,10 +414,8 @@ describe('', () => { expect(handleChange.args[0][1]).to.equal(9); expect(input.value).to.equal('9'); }); - }); - describe('prop: readOnly', () => { - it('stepper buttons should not be in the tab order when readOnly is false', async () => { + it('only includes the input element in the tab order', async () => { const user = userEvent.setup(); const { getByTestId } = render( @@ -433,41 +431,5 @@ describe('', () => { await user.keyboard('[Tab]'); expect(document.activeElement).to.equal(document.body); }); - - it('tab order should be increment button, decrement button when readOnly is true', async () => { - const user = userEvent.setup(); - - const { getByTestId } = render( - , - ); - - const incrementButton = getByTestId('increment-btn'); - const decrementButton = getByTestId('decrement-btn'); - expect(document.activeElement).to.equal(document.body); - - await user.keyboard('[Tab]'); - expect(document.activeElement).to.equal(decrementButton); - - await user.keyboard('[Tab]'); - expect(document.activeElement).to.equal(incrementButton); - - await user.keyboard('[Tab]'); - expect(document.activeElement).to.equal(document.body); - }); }); }); diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 3b3aae41888398..98b69aafaf92f1 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -18,8 +18,15 @@ import { EventHandlers, useSlotProps, WithOptionalOwnerState } from '../utils'; import { useClassNamesOverride } from '../utils/ClassNameConfigurator'; const useUtilityClasses = (ownerState: NumberInputOwnerState) => { - const { disabled, error, focused, formControlContext, isIncrementDisabled, isDecrementDisabled } = - ownerState; + const { + disabled, + error, + focused, + readOnly, + formControlContext, + isIncrementDisabled, + isDecrementDisabled, + } = ownerState; const slots = { root: [ @@ -27,9 +34,10 @@ const useUtilityClasses = (ownerState: NumberInputOwnerState) => { disabled && 'disabled', error && 'error', focused && 'focused', + readOnly && 'readOnly', Boolean(formControlContext) && 'formControl', ], - input: ['input', disabled && 'disabled'], + input: ['input', disabled && 'disabled', readOnly && 'readOnly'], incrementButton: ['incrementButton', isIncrementDisabled && 'disabled'], decrementButton: ['decrementButton', isDecrementDisabled && 'disabled'], }; @@ -65,7 +73,7 @@ const NumberInput = React.forwardRef(function NumberInput( onChange, placeholder, required, - readOnly, + readOnly = false, shiftMultiplier, step, value, @@ -108,6 +116,7 @@ const NumberInput = React.forwardRef(function NumberInput( disabled: disabledState, error: errorState, focused, + readOnly, formControlContext, isIncrementDisabled, isDecrementDisabled, @@ -241,6 +250,7 @@ NumberInput.propTypes /* remove-proptypes */ = { /** * If `true`, the `input` element becomes read-only. The stepper buttons remain active, * with the addition that they are now keyboard focusable. + * @default false */ readOnly: PropTypes.bool, /** diff --git a/packages/mui-base/src/Unstable_NumberInput/numberInputClasses.ts b/packages/mui-base/src/Unstable_NumberInput/numberInputClasses.ts index c4219524fa9da8..e7373f4cc8efa0 100644 --- a/packages/mui-base/src/Unstable_NumberInput/numberInputClasses.ts +++ b/packages/mui-base/src/Unstable_NumberInput/numberInputClasses.ts @@ -14,6 +14,8 @@ export interface NumberInputClasses { focused: string; /** Class name applied to the root element if `disabled={true}`. */ disabled: string; + /** State class applied to the root element if `readOnly={true}`. */ + readOnly: string; /** State class applied to the root element if `error={true}`. */ error: string; /** Class name applied to the input element. */ @@ -35,6 +37,7 @@ const numberInputClasses: NumberInputClasses = generateUtilityClasses('MuiNumber 'formControl', 'focused', 'disabled', + 'readOnly', 'error', 'input', 'incrementButton', diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx index 38d15510465e55..d8b5de3b45351e 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx @@ -41,7 +41,6 @@ describe('useNumberInput', () => { expect(inputProps['aria-valuemin']).to.equal(10); expect(inputProps['aria-valuemax']).to.equal(100); expect(inputProps['aria-disabled']).to.equal(true); - expect(inputProps.tabIndex).to.equal(0); expect(decrementButtonProps.tabIndex).to.equal(-1); expect(decrementButtonProps['aria-controls']).to.equal(INPUT_ID); diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index f01c1a36912fbe..eb4eea6ffeb0ed 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -317,7 +317,6 @@ export default function useNumberInput( spellCheck: 'false', required: requiredProp, readOnly: readOnlyProp, - tabIndex: readOnlyProp ? -1 : 0, 'aria-disabled': disabledProp, disabled: disabledProp, }; @@ -333,7 +332,7 @@ export default function useNumberInput( const stepperButtonCommonProps = { 'aria-controls': inputId, - tabIndex: readOnlyProp ? 0 : -1, + tabIndex: -1, }; const isIncrementDisabled = diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index 22168c790f66a9..cc1c4b2d489351 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -77,6 +77,7 @@ export interface UseNumberInputParameters { /** * If `true`, the `input` element becomes read-only. The stepper buttons remain active, * with the addition that they are now keyboard focusable. + * @default false */ readOnly?: boolean; /** From b887e89d38b64ffd281fbe0d58c488b529592fe5 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 16:10:56 +0800 Subject: [PATCH 58/70] Fix generating import statements for unstable items in Base API pages --- .../components/ComponentsApiContent.js | 26 ++++++++++++++++--- .../src/modules/components/HooksApiContent.js | 8 +++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/src/modules/components/ComponentsApiContent.js b/docs/src/modules/components/ComponentsApiContent.js index 061c8537860e18..3bb8628ed574a9 100644 --- a/docs/src/modules/components/ComponentsApiContent.js +++ b/docs/src/modules/components/ComponentsApiContent.js @@ -109,11 +109,29 @@ export default function ComponentsApiContent(props) { slotGuideLink = '/base-ui/guides/overriding-component-structure/'; } - const source = filename - .replace(/\/packages\/mui(-(.+?))?\/src/, (match, dash, pkg) => `@mui/${pkg}`) + // convert paths like `/packages/mui-base/src/Input...` to `@mui/base/Input...` + const packageAndFilename = filename.replace( + /\/packages\/mui(-(.+?))?\/src/, + (match, dash, pkg) => `@mui/${pkg}`, + ); + + const source = packageAndFilename // convert things like `/Table/Table.js` to `` .replace(/\/([^/]+)\/\1\.(js|tsx)$/, ''); + let importName = pageContent.name; + let namedImportPath = `${source}/${importName}`; + let defaultImportPath = source; + + if (/Unstable_/.test(source)) { + namedImportPath = source.replace(/\/[^/]*$/, ''); + defaultImportPath = packageAndFilename + .replace(/Unstable_/, '') + .replace(/\/([^/]+)\/\1\.(js|tsx)$/, ''); + + importName = `Unstable_${importName} as ${importName}`; + } + // The `ref` is forwarded to the root element. let refHint = t('api-docs.refRootElement'); if (forwardsRefTo == null) { @@ -146,9 +164,9 @@ export default function ComponentsApiContent(props) { diff --git a/docs/src/modules/components/HooksApiContent.js b/docs/src/modules/components/HooksApiContent.js index b8063707519649..3fef27c35dfaa2 100644 --- a/docs/src/modules/components/HooksApiContent.js +++ b/docs/src/modules/components/HooksApiContent.js @@ -63,6 +63,12 @@ export default function HooksApiContent(props) { const hookNameKebabCase = kebabCase(hookName); + let defaultImportName = hookName; + + if (/unstable_/.test(filename)) { + defaultImportName = `unstable_${hookName} as ${hookName}`; + } + return ( @@ -72,7 +78,7 @@ export default function HooksApiContent(props) { code={` import ${hookName} from '${source.split('/').slice(0, -1).join('/')}'; // ${t('or')} -import { ${hookName} } from '${source.split('/').slice(0, 2).join('/')}';`} +import { ${defaultImportName} } from '${source.split('/').slice(0, 2).join('/')}';`} language="jsx" /> From 3b9aac2c1f7bca076260e25c7935c6a82f807258 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 16:40:28 +0800 Subject: [PATCH 59/70] Fix arrows in NumberInputIntroduction --- .../number-input/NumberInputIntroduction.js | 15 +++++++++------ .../number-input/NumberInputIntroduction.tsx | 15 +++++++++------ .../api-docs-base/number-input/number-input.json | 4 ++-- .../use-number-input/use-number-input.json | 4 ++-- .../src/Unstable_NumberInput/NumberInput.tsx | 4 ++-- .../useNumberInput.types.ts | 4 ++-- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/data/base/components/number-input/NumberInputIntroduction.js b/docs/data/base/components/number-input/NumberInputIntroduction.js index 0d66e75d918816..a86b1eedc1b6cb 100644 --- a/docs/data/base/components/number-input/NumberInputIntroduction.js +++ b/docs/data/base/components/number-input/NumberInputIntroduction.js @@ -16,9 +16,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref children: '▴', }, decrementButton: { - // it's flipped with CSS, the downward pointing - // triangle looks weird - children: '▴', + children: '▾', }, }} {...props} @@ -105,12 +103,18 @@ const StyledInputElement = styled('input')( const StyledButton = styled('button')( ({ theme }) => ` + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + appearance: none; + padding: 0; width: 19px; height: 19px; - font-family: IBM Plex Sans, sans-serif; + font-family: system-ui, sans-serif; font-size: 0.875rem; box-sizing: border-box; - line-height: 1; + line-height: 1.2; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 0; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; @@ -133,7 +137,6 @@ const StyledButton = styled('button')( &.${numberInputClasses.decrementButton} { grid-column: 2/3; grid-row: 2/3; - transform: rotate(180deg); } `, ); diff --git a/docs/data/base/components/number-input/NumberInputIntroduction.tsx b/docs/data/base/components/number-input/NumberInputIntroduction.tsx index df0e5c86492666..6405133fe5624d 100644 --- a/docs/data/base/components/number-input/NumberInputIntroduction.tsx +++ b/docs/data/base/components/number-input/NumberInputIntroduction.tsx @@ -22,9 +22,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( children: '▴', }, decrementButton: { - // it's flipped with CSS, the downward pointing - // triangle looks weird - children: '▴', + children: '▾', }, }} {...props} @@ -111,12 +109,18 @@ const StyledInputElement = styled('input')( const StyledButton = styled('button')( ({ theme }) => ` + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + appearance: none; + padding: 0; width: 19px; height: 19px; - font-family: IBM Plex Sans, sans-serif; + font-family: system-ui, sans-serif; font-size: 0.875rem; box-sizing: border-box; - line-height: 1; + line-height: 1.2; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 0; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; @@ -139,7 +143,6 @@ const StyledButton = styled('button')( &.${numberInputClasses.decrementButton} { grid-column: 2/3; grid-row: 2/3; - transform: rotate(180deg); } `, ); diff --git a/docs/translations/api-docs-base/number-input/number-input.json b/docs/translations/api-docs-base/number-input/number-input.json index 38c1223cffe99d..b588e42ddadeb0 100644 --- a/docs/translations/api-docs-base/number-input/number-input.json +++ b/docs/translations/api-docs-base/number-input/number-input.json @@ -14,14 +14,14 @@ "max": { "description": "The maximum value." }, "min": { "description": "The minimum value." }, "onChange": { - "description": "Callback fired after the value is clamped and changes - when the <input> is blurred or when the stepper buttons are triggered. Called with undefined when the value is unset.", + "description": "Callback fired after the value is clamped and changes - when the input is blurred or when the stepper buttons are triggered. Called with undefined when the value is unset.", "typeDescriptions": { "event": "The event source of the callback", "value": "The new value of the component" } }, "onInputChange": { - "description": "Callback fired when the <input> value changes after each keypress, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", + "description": "Callback fired when the input value changes after each keypress, before clamping is applied. Note that event.target.value may contain values that fall outside of min and max or are otherwise "invalid".", "typeDescriptions": { "event": "The event source of the callback." } }, "readOnly": { diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json index 581e8922441982..575cb5d734d751 100644 --- a/docs/translations/api-docs/use-number-input/use-number-input.json +++ b/docs/translations/api-docs/use-number-input/use-number-input.json @@ -15,10 +15,10 @@ "max": { "description": "The maximum value." }, "min": { "description": "The minimum value." }, "onChange": { - "description": "Callback fired after the value is clamped and changes - when the is blurred or when\nthe stepper buttons are triggered.\nCalled with undefined when the value is unset." + "description": "Callback fired after the value is clamped and changes - when the input is blurred or when\nthe stepper buttons are triggered.\nCalled with undefined when the value is unset." }, "onInputChange": { - "description": "Callback fired when the value changes after each keypress, before clamping is applied.\nNote that event.target.value may contain values that fall outside of min and max or\nare otherwise "invalid"." + "description": "Callback fired when the input value changes after each keypress, before clamping is applied.\nNote that event.target.value may contain values that fall outside of min and max or\nare otherwise "invalid"." }, "readOnly": { "description": "If true, the input element becomes read-only. The stepper buttons remain active,\nwith the addition that they are now keyboard focusable." diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 98b69aafaf92f1..3d540dc6eb2c41 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -223,7 +223,7 @@ NumberInput.propTypes /* remove-proptypes */ = { */ onBlur: PropTypes.func, /** - * Callback fired after the value is clamped and changes - when the is blurred or when + * Callback fired after the value is clamped and changes - when the `input` is blurred or when * the stepper buttons are triggered. * Called with `undefined` when the value is unset. * @@ -236,7 +236,7 @@ NumberInput.propTypes /* remove-proptypes */ = { */ onFocus: PropTypes.func, /** - * Callback fired when the value changes after each keypress, before clamping is applied. + * Callback fired when the `input` value changes after each keypress, before clamping is applied. * Note that `event.target.value` may contain values that fall outside of `min` and `max` or * are otherwise "invalid". * diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index cc1c4b2d489351..a5e211dfcf8e57 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -41,7 +41,7 @@ export interface UseNumberInputParameters { onBlur?: React.FocusEventHandler; onClick?: React.MouseEventHandler; /** - * Callback fired when the value changes after each keypress, before clamping is applied. + * Callback fired when the `input` value changes after each keypress, before clamping is applied. * Note that `event.target.value` may contain values that fall outside of `min` and `max` or * are otherwise "invalid". * @@ -50,7 +50,7 @@ export interface UseNumberInputParameters { onInputChange?: React.ChangeEventHandler; onFocus?: React.FocusEventHandler; /** - * Callback fired after the value is clamped and changes - when the is blurred or when + * Callback fired after the value is clamped and changes - when the `input` is blurred or when * the stepper buttons are triggered. * Called with `undefined` when the value is unset. * From 19b5af253b37f66dd9d6f234cc3abac70fd8b9e3 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 18:28:08 +0800 Subject: [PATCH 60/70] Fix all html arrows in the demos --- .../number-input/NumberInputBasic/css/index.js | 11 ++++++++--- .../number-input/NumberInputBasic/css/index.tsx | 11 ++++++++--- .../NumberInputBasic/css/index.tsx.preview | 2 +- .../number-input/NumberInputBasic/system/index.js | 15 +++++++++------ .../NumberInputBasic/system/index.tsx | 15 +++++++++------ .../NumberInputBasic/tailwind/index.js | 8 +++----- .../NumberInputBasic/tailwind/index.tsx | 8 +++----- 7 files changed, 41 insertions(+), 29 deletions(-) diff --git a/docs/data/base/components/number-input/NumberInputBasic/css/index.js b/docs/data/base/components/number-input/NumberInputBasic/css/index.js index df68009ab91585..b1d91afecb99ad 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/css/index.js +++ b/docs/data/base/components/number-input/NumberInputBasic/css/index.js @@ -9,7 +9,7 @@ export default function NumberInputBasic() { slotProps={{ root: { className: 'CustomNumberInput' }, input: { className: 'input' }, - decrementButton: { className: 'btn decrement', children: '▴' }, + decrementButton: { className: 'btn decrement', children: '▾' }, incrementButton: { className: 'btn increment', children: '▴' }, }} aria-label="Demo number input" @@ -104,9 +104,15 @@ function Styles() { } .CustomNumberInput .btn { + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + appearance: none; + padding: 0; width: 19px; height: 19px; - font-family: IBM Plex Sans, sans-serif; + font-family: system-ui, sans-serif; font-size: 0.875rem; box-sizing: border-box; line-height: 1; @@ -132,7 +138,6 @@ function Styles() { &.decrement { grid-column: 2/3; grid-row: 2/3; - transform: rotate(180deg); } } `} diff --git a/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx b/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx index df68009ab91585..b1d91afecb99ad 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx +++ b/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx @@ -9,7 +9,7 @@ export default function NumberInputBasic() { slotProps={{ root: { className: 'CustomNumberInput' }, input: { className: 'input' }, - decrementButton: { className: 'btn decrement', children: '▴' }, + decrementButton: { className: 'btn decrement', children: '▾' }, incrementButton: { className: 'btn increment', children: '▴' }, }} aria-label="Demo number input" @@ -104,9 +104,15 @@ function Styles() { } .CustomNumberInput .btn { + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + appearance: none; + padding: 0; width: 19px; height: 19px; - font-family: IBM Plex Sans, sans-serif; + font-family: system-ui, sans-serif; font-size: 0.875rem; box-sizing: border-box; line-height: 1; @@ -132,7 +138,6 @@ function Styles() { &.decrement { grid-column: 2/3; grid-row: 2/3; - transform: rotate(180deg); } } `} diff --git a/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx.preview b/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx.preview index 35702570c75717..beb20ef0d8c30d 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx.preview +++ b/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx.preview @@ -3,7 +3,7 @@ slotProps={{ root: { className: 'CustomNumberInput' }, input: { className: 'input' }, - decrementButton: { className: 'btn decrement', children: '▴' }, + decrementButton: { className: 'btn decrement', children: '▾' }, incrementButton: { className: 'btn increment', children: '▴' }, }} aria-label="Demo number input" diff --git a/docs/data/base/components/number-input/NumberInputBasic/system/index.js b/docs/data/base/components/number-input/NumberInputBasic/system/index.js index dcc4ea9bd0b093..4f2a18e30d4e65 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/system/index.js +++ b/docs/data/base/components/number-input/NumberInputBasic/system/index.js @@ -16,9 +16,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref children: '▴', }, decrementButton: { - // it's flipped with CSS, the downward pointing - // triangle looks weird - children: '▴', + children: '▾', }, }} {...props} @@ -110,12 +108,18 @@ const StyledInputElement = styled('input')( const StyledButton = styled('button')( ({ theme }) => ` + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + appearance: none; + padding: 0; width: 19px; height: 19px; - font-family: IBM Plex Sans, sans-serif; + font-family: system-ui, sans-serif; font-size: 0.875rem; box-sizing: border-box; - line-height: 1; + line-height: 1.2; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 0; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; @@ -138,7 +142,6 @@ const StyledButton = styled('button')( &.${numberInputClasses.decrementButton} { grid-column: 2/3; grid-row: 2/3; - transform: rotate(180deg); } `, ); diff --git a/docs/data/base/components/number-input/NumberInputBasic/system/index.tsx b/docs/data/base/components/number-input/NumberInputBasic/system/index.tsx index 8a8294d6d7b6c2..132d4bf1db664e 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/system/index.tsx +++ b/docs/data/base/components/number-input/NumberInputBasic/system/index.tsx @@ -22,9 +22,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( children: '▴', }, decrementButton: { - // it's flipped with CSS, the downward pointing - // triangle looks weird - children: '▴', + children: '▾', }, }} {...props} @@ -116,12 +114,18 @@ const StyledInputElement = styled('input')( const StyledButton = styled('button')( ({ theme }) => ` + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + appearance: none; + padding: 0; width: 19px; height: 19px; - font-family: IBM Plex Sans, sans-serif; + font-family: system-ui, sans-serif; font-size: 0.875rem; box-sizing: border-box; - line-height: 1; + line-height: 1.2; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 0; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; @@ -144,7 +148,6 @@ const StyledButton = styled('button')( &.${numberInputClasses.decrementButton} { grid-column: 2/3; grid-row: 2/3; - transform: rotate(180deg); } `, ); diff --git a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js index bd0ffb0aafacfc..311d889fbd7f09 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js +++ b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js @@ -21,14 +21,12 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref incrementButton: { children: '▴', className: - 'w-[20px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2', + "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2", }, decrementButton: { - // it's flipped with CSS, the downward pointing - // triangle looks weird - children: '▴', + children: '▾', className: - 'w-[20px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3 rotate-180', + "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3", }, }} {...props} diff --git a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx index 2e1a81a952fda2..874379e0abe081 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx +++ b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx @@ -27,14 +27,12 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( incrementButton: { children: '▴', className: - 'w-[20px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2', + "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2", }, decrementButton: { - // it's flipped with CSS, the downward pointing - // triangle looks weird - children: '▴', + children: '▾', className: - 'w-[20px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3 rotate-180', + "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3", }, }} {...props} From 087e442383f40302486bdb44fe9131888718a2bb Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 18:34:03 +0800 Subject: [PATCH 61/70] Misc fixes --- .../mui-base/src/Unstable_NumberInput/NumberInput.test.tsx | 1 + .../mui-base/src/unstable_useNumberInput/useNumberInput.ts | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx index 908306a5f29983..fe8bb2f8b69c54 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.test.tsx @@ -161,6 +161,7 @@ describe('', () => { const incrementButton = getByTestId('increment-btn'); const decrementButton = getByTestId('decrement-btn'); + // press Shift key without releasing it await user.keyboard('{Shift>}'); await user.click(incrementButton); await user.click(incrementButton); diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index eb4eea6ffeb0ed..4b489325c80ab7 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -101,13 +101,6 @@ export default function useNumberInput( const handleFocus = (otherHandlers: Record | undefined>) => (event: React.FocusEvent) => { - // Fix a bug with IE11 where the focus/blur events are triggered - // while the component is disabled. - if (formControlContext && formControlContext?.disabled) { - event.stopPropagation(); - return; - } - otherHandlers.onFocus?.(event); if (event.defaultPrevented) { From 9e545a0b7d69abdad2cd1fdc7aef9700760575e6 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 20:18:44 +0800 Subject: [PATCH 62/70] Type onBlur more explicitly --- packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts | 1 - .../src/unstable_useNumberInput/useNumberInput.types.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index 4b489325c80ab7..754b9db595845c 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -93,7 +93,6 @@ export default function useNumberInput( if (!formControlContext && disabledProp && focused) { setFocused(false); - // @ts-ignore onBlur?.(); } }, [formControlContext, disabledProp, focused, onBlur]); diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index a5e211dfcf8e57..6415fc75732421 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -38,7 +38,7 @@ export interface UseNumberInputParameters { * The prop defaults to the value (`false`) inherited from the parent FormControl component. */ error?: boolean; - onBlur?: React.FocusEventHandler; + onBlur?: (event?: React.FocusEvent) => void; onClick?: React.MouseEventHandler; /** * Callback fired when the `input` value changes after each keypress, before clamping is applied. From 414442f723efad4d01b186433ed6e43157e8a76f Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 21:02:05 +0800 Subject: [PATCH 63/70] Update api docs --- docs/pages/base-ui/api/use-number-input.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index cc492c8bccc430..03508578d30d7e 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -14,8 +14,8 @@ "min": { "type": { "name": "number", "description": "number" } }, "onBlur": { "type": { - "name": "React.FocusEventHandler<HTMLInputElement>", - "description": "React.FocusEventHandler<HTMLInputElement>" + "name": "(event?: React.FocusEvent) => void", + "description": "(event?: React.FocusEvent) => void" } }, "onChange": { From 105c8c1d712a33722dba9f6e8659cf6d5e1ef035 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 22:26:19 +0800 Subject: [PATCH 64/70] Styling fixes for demos --- .../NumberInputBasic/css/index.js | 48 ++++++++--------- .../NumberInputBasic/css/index.tsx | 48 ++++++++--------- .../NumberInputBasic/tailwind/index.js | 6 +-- .../NumberInputBasic/tailwind/index.tsx | 6 +-- .../number-input/QuantityInput/css/index.js | 40 +++++++------- .../number-input/QuantityInput/css/index.tsx | 40 +++++++------- .../QuantityInput/tailwind/index.js | 2 +- .../QuantityInput/tailwind/index.tsx | 2 +- .../UseNumberInputCompact/css/index.js | 52 +++++++++---------- .../UseNumberInputCompact/css/index.tsx | 52 +++++++++---------- 10 files changed, 148 insertions(+), 148 deletions(-) diff --git a/docs/data/base/components/number-input/NumberInputBasic/css/index.js b/docs/data/base/components/number-input/NumberInputBasic/css/index.js index b1d91afecb99ad..04b6cab9946f82 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/css/index.js +++ b/docs/data/base/components/number-input/NumberInputBasic/css/index.js @@ -72,19 +72,15 @@ function Styles() { grid-template-columns: 1fr 19px; grid-template-rows: 1fr 1fr; overflow: hidden; + } - &:hover { - border-color: ${cyan[400]}; - } - - &.${numberInputClasses.focused} { - border-color: ${cyan[400]}; - box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; - } + .CustomNumberInput:hover { + border-color: ${cyan[400]}; + } - &:focus-visible { - outline: 0; - } + .CustomNumberInput.${numberInputClasses.focused} { + border-color: ${cyan[400]}; + box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; } .CustomNumberInput .input { @@ -103,6 +99,10 @@ function Styles() { outline: 0; } + .CustomNumberInput .input:focus-visible { + outline: 0; + } + .CustomNumberInput .btn { display: flex; flex-flow: row nowrap; @@ -123,22 +123,22 @@ function Styles() { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; + } - &:hover { - background: ${isDarkMode ? grey[800] : grey[50]}; - border-color: ${isDarkMode ? grey[600] : grey[300]}; - cursor: pointer; - } + .CustomNumberInput .btn:hover { + background: ${isDarkMode ? grey[800] : grey[50]}; + border-color: ${isDarkMode ? grey[600] : grey[300]}; + cursor: pointer; + } - &.increment { - grid-column: 2/3; - grid-row: 1/2; - } + .CustomNumberInput .btn.increment { + grid-column: 2/3; + grid-row: 1/2; + } - &.decrement { - grid-column: 2/3; - grid-row: 2/3; - } + .CustomNumberInput .btn.decrement { + grid-column: 2/3; + grid-row: 2/3; } `} diff --git a/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx b/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx index b1d91afecb99ad..04b6cab9946f82 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx +++ b/docs/data/base/components/number-input/NumberInputBasic/css/index.tsx @@ -72,19 +72,15 @@ function Styles() { grid-template-columns: 1fr 19px; grid-template-rows: 1fr 1fr; overflow: hidden; + } - &:hover { - border-color: ${cyan[400]}; - } - - &.${numberInputClasses.focused} { - border-color: ${cyan[400]}; - box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; - } + .CustomNumberInput:hover { + border-color: ${cyan[400]}; + } - &:focus-visible { - outline: 0; - } + .CustomNumberInput.${numberInputClasses.focused} { + border-color: ${cyan[400]}; + box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; } .CustomNumberInput .input { @@ -103,6 +99,10 @@ function Styles() { outline: 0; } + .CustomNumberInput .input:focus-visible { + outline: 0; + } + .CustomNumberInput .btn { display: flex; flex-flow: row nowrap; @@ -123,22 +123,22 @@ function Styles() { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; + } - &:hover { - background: ${isDarkMode ? grey[800] : grey[50]}; - border-color: ${isDarkMode ? grey[600] : grey[300]}; - cursor: pointer; - } + .CustomNumberInput .btn:hover { + background: ${isDarkMode ? grey[800] : grey[50]}; + border-color: ${isDarkMode ? grey[600] : grey[300]}; + cursor: pointer; + } - &.increment { - grid-column: 2/3; - grid-row: 1/2; - } + .CustomNumberInput .btn.increment { + grid-column: 2/3; + grid-row: 1/2; + } - &.decrement { - grid-column: 2/3; - grid-row: 2/3; - } + .CustomNumberInput .btn.decrement { + grid-column: 2/3; + grid-row: 2/3; } `} diff --git a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js index 311d889fbd7f09..6ec1ea01da471a 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js +++ b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.js @@ -16,17 +16,17 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref }), input: { className: - 'col-start-1 col-end-2 row-start-1 row-end-3 text-sm leading-normal text-slate-900 bg-inherit border-0 rounded-[inherit] dark:text-slate-300 px-3 py-2 outline-0', + 'col-start-1 col-end-2 row-start-1 row-end-3 text-sm leading-normal text-slate-900 bg-inherit border-0 rounded-[inherit] dark:text-slate-300 px-3 py-2 outline-0 focus-visible:outline-0 focus-visible:outline-none', }, incrementButton: { children: '▴', className: - "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2", + 'font-[system-ui] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-[1.2] border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2', }, decrementButton: { children: '▾', className: - "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3", + 'font-[system-ui] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-[1.2] border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3', }, }} {...props} diff --git a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx index 874379e0abe081..f2f2266e13df1b 100644 --- a/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx +++ b/docs/data/base/components/number-input/NumberInputBasic/tailwind/index.tsx @@ -22,17 +22,17 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( }), input: { className: - 'col-start-1 col-end-2 row-start-1 row-end-3 text-sm leading-normal text-slate-900 bg-inherit border-0 rounded-[inherit] dark:text-slate-300 px-3 py-2 outline-0', + 'col-start-1 col-end-2 row-start-1 row-end-3 text-sm leading-normal text-slate-900 bg-inherit border-0 rounded-[inherit] dark:text-slate-300 px-3 py-2 outline-0 focus-visible:outline-0 focus-visible:outline-none', }, incrementButton: { children: '▴', className: - "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2", + 'font-[system-ui] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-[1.2] border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-1 row-end-2', }, decrementButton: { children: '▾', className: - "font-['system-ui'] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-none border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3", + 'font-[system-ui] flex flex-row flex-nowrap justify-center items-center appearance-none p-0 w-[19px] h-[19px] text-sm box-border leading-[1.2] border-0 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-300 transition-all duration-[120ms] hover:cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600 col-start-2 col-end-3 row-start-2 row-end-3', }, }} {...props} diff --git a/docs/data/base/components/number-input/QuantityInput/css/index.js b/docs/data/base/components/number-input/QuantityInput/css/index.js index a7257ee7156b78..40b2207eaa063c 100644 --- a/docs/data/base/components/number-input/QuantityInput/css/index.js +++ b/docs/data/base/components/number-input/QuantityInput/css/index.js @@ -97,19 +97,19 @@ function Styles() { min-width: 0; width: 4rem; text-align: center; + } - &:hover { - border-color: ${cyan[400]}; - } + .QuantityInput .input:hover { + border-color: ${cyan[400]}; + } - &:focus { - border-color: ${cyan[400]}; - box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; - } + .QuantityInput .input:focus { + border-color: ${cyan[400]}; + box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; + } - &:focus-visible { - outline: 0; - } + .QuantityInput .input:focus-visible { + outline: 0; } .QuantityInput .btn { @@ -132,19 +132,19 @@ function Styles() { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; + } - &:hover { - background: ${isDarkMode ? cyan[800] : cyan[100]}; - cursor: pointer; - } + .QuantityInput .btn:hover { + background: ${isDarkMode ? cyan[800] : cyan[100]}; + cursor: pointer; + } - &:focus-visible { - outline: 0; - } + .QuantityInput .btn:focus-visible { + outline: 0; + } - &.increment { - order: 1; - } + .QuantityInput .btn.increment { + order: 1; } `} diff --git a/docs/data/base/components/number-input/QuantityInput/css/index.tsx b/docs/data/base/components/number-input/QuantityInput/css/index.tsx index 1adc762d9a0ae3..b5ea9808e6beb4 100644 --- a/docs/data/base/components/number-input/QuantityInput/css/index.tsx +++ b/docs/data/base/components/number-input/QuantityInput/css/index.tsx @@ -100,19 +100,19 @@ function Styles() { min-width: 0; width: 4rem; text-align: center; + } - &:hover { - border-color: ${cyan[400]}; - } + .QuantityInput .input:hover { + border-color: ${cyan[400]}; + } - &:focus { - border-color: ${cyan[400]}; - box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; - } + .QuantityInput .input:focus { + border-color: ${cyan[400]}; + box-shadow: 0 0 0 3px ${isDarkMode ? cyan[500] : cyan[200]}; + } - &:focus-visible { - outline: 0; - } + .QuantityInput .input:focus-visible { + outline: 0; } .QuantityInput .btn { @@ -135,19 +135,19 @@ function Styles() { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; + } - &:hover { - background: ${isDarkMode ? cyan[800] : cyan[100]}; - cursor: pointer; - } + .QuantityInput .btn:hover { + background: ${isDarkMode ? cyan[800] : cyan[100]}; + cursor: pointer; + } - &:focus-visible { - outline: 0; - } + .QuantityInput .btn:focus-visible { + outline: 0; + } - &.increment { - order: 1; - } + .QuantityInput .btn.increment { + order: 1; } `} diff --git a/docs/data/base/components/number-input/QuantityInput/tailwind/index.js b/docs/data/base/components/number-input/QuantityInput/tailwind/index.js index 34410d35031e0b..5ff3f216ffeef9 100644 --- a/docs/data/base/components/number-input/QuantityInput/tailwind/index.js +++ b/docs/data/base/components/number-input/QuantityInput/tailwind/index.js @@ -13,7 +13,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref }, input: { className: - 'leading-snug text-sm text-inherit bg-white dark:bg-slate-900 border border-solid border-slate-300 dark:border-slate-600 rounded-[4px] my-0 mx-1 py-2.5 px-3 outline-0 min-w-0 w-16 text-center focus-visible:outline-0 hover:border-purple-500 dark:hover:border-purple-500 focus:border-purple-500 dark:focus:border-purple-500', + 'leading-snug text-sm text-inherit bg-white dark:bg-slate-900 border border-solid border-slate-300 dark:border-slate-600 rounded-[4px] my-0 mx-1 py-2.5 px-3 outline-0 min-w-0 w-16 text-center focus-visible:outline-0 hover:border-purple-500 dark:hover:border-purple-500 focus:border-purple-500 dark:focus:border-purple-500 focus:shadow-outline-purple dark:focus:shadow-outline-purple focus-visible:outline-none', }, incrementButton: { children: , diff --git a/docs/data/base/components/number-input/QuantityInput/tailwind/index.tsx b/docs/data/base/components/number-input/QuantityInput/tailwind/index.tsx index d7073e9db65f1a..867f0ba4be7bb1 100644 --- a/docs/data/base/components/number-input/QuantityInput/tailwind/index.tsx +++ b/docs/data/base/components/number-input/QuantityInput/tailwind/index.tsx @@ -16,7 +16,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( }, input: { className: - 'leading-snug text-sm text-inherit bg-white dark:bg-slate-900 border border-solid border-slate-300 dark:border-slate-600 rounded-[4px] my-0 mx-1 py-2.5 px-3 outline-0 min-w-0 w-16 text-center focus-visible:outline-0 hover:border-purple-500 dark:hover:border-purple-500 focus:border-purple-500 dark:focus:border-purple-500', + 'leading-snug text-sm text-inherit bg-white dark:bg-slate-900 border border-solid border-slate-300 dark:border-slate-600 rounded-[4px] my-0 mx-1 py-2.5 px-3 outline-0 min-w-0 w-16 text-center focus-visible:outline-0 hover:border-purple-500 dark:hover:border-purple-500 focus:border-purple-500 dark:focus:border-purple-500 focus:shadow-outline-purple dark:focus:shadow-outline-purple focus-visible:outline-none', }, incrementButton: { children: , diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/css/index.js b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.js index 16eaab8665f162..606d202b86713a 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact/css/index.js +++ b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.js @@ -123,10 +123,10 @@ function Styles() { background: ${isDarkMode ? grey[800] : grey[200]}; border-color: ${isDarkMode ? grey[800] : grey[200]}; overflow: auto; + } - &:hover { - border-color: ${cyan[500]}; - } + .CompactNumberInput:hover { + border-color: ${cyan[500]}; } .CompactNumberInput .input { @@ -145,30 +145,30 @@ function Styles() { border: 0; color: inherit; background: ${isDarkMode ? grey[900] : grey[50]}; + } + + .CompactNumberInput .btn:hover { + cursor: pointer; + background: ${cyan[500]}; + color: ${grey[50]}; + } + + .CompactNumberInput .btn:focus-visible { + outline: 0; + background: ${cyan[500]}; + color: ${isDarkMode ? grey[300] : grey[50]}; + } + + .CompactNumberInput .btn.increment { + grid-area: increment; + border-top-left-radius: 0.35rem; + border-top-right-radius: 0.35rem; + } - &:hover { - cursor: pointer; - background: ${cyan[500]}; - color: ${grey[50]}; - } - - &:focus-visible { - outline: 0; - background: ${cyan[500]}; - color: ${isDarkMode ? grey[300] : grey[50]}; - } - - &.increment { - grid-area: increment; - border-top-left-radius: 0.35rem; - border-top-right-radius: 0.35rem; - } - - &.decrement { - grid-area: decrement; - border-bottom-left-radius: 0.35rem; - border-bottom-right-radius: 0.35rem; - } + .CompactNumberInput .btn.decrement { + grid-area: decrement; + border-bottom-left-radius: 0.35rem; + border-bottom-right-radius: 0.35rem; } .layout { diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx index 198c29d0cba6fd..417aa536ca9d81 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx +++ b/docs/data/base/components/number-input/UseNumberInputCompact/css/index.tsx @@ -130,10 +130,10 @@ function Styles() { background: ${isDarkMode ? grey[800] : grey[200]}; border-color: ${isDarkMode ? grey[800] : grey[200]}; overflow: auto; + } - &:hover { - border-color: ${cyan[500]}; - } + .CompactNumberInput:hover { + border-color: ${cyan[500]}; } .CompactNumberInput .input { @@ -152,30 +152,30 @@ function Styles() { border: 0; color: inherit; background: ${isDarkMode ? grey[900] : grey[50]}; + } + + .CompactNumberInput .btn:hover { + cursor: pointer; + background: ${cyan[500]}; + color: ${grey[50]}; + } + + .CompactNumberInput .btn:focus-visible { + outline: 0; + background: ${cyan[500]}; + color: ${isDarkMode ? grey[300] : grey[50]}; + } + + .CompactNumberInput .btn.increment { + grid-area: increment; + border-top-left-radius: 0.35rem; + border-top-right-radius: 0.35rem; + } - &:hover { - cursor: pointer; - background: ${cyan[500]}; - color: ${grey[50]}; - } - - &:focus-visible { - outline: 0; - background: ${cyan[500]}; - color: ${isDarkMode ? grey[300] : grey[50]}; - } - - &.increment { - grid-area: increment; - border-top-left-radius: 0.35rem; - border-top-right-radius: 0.35rem; - } - - &.decrement { - grid-area: decrement; - border-bottom-left-radius: 0.35rem; - border-bottom-right-radius: 0.35rem; - } + .CompactNumberInput .btn.decrement { + grid-area: decrement; + border-bottom-left-radius: 0.35rem; + border-bottom-right-radius: 0.35rem; } .layout { From ca3b51b0f2bea110fa8d36c0f2be2a07dccd7177 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 1 Aug 2023 23:03:31 +0800 Subject: [PATCH 65/70] More styling fixes --- .../components/number-input/UseNumberInput.js | 74 ++++++++++--------- .../number-input/UseNumberInput.tsx | 74 ++++++++++--------- 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index 17239bac8f5a84..bee3f7c0290a66 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -19,7 +19,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(props, ref return ( - + - + ` + width: 1.5rem; + height: 1rem; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + font-size: 0.875rem; + box-sizing: border-box; + border: 0; + + &.increment, + &.decrement { + background: ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; + + &:hover { + cursor: pointer; + background: ${blue[400]}; + color: ${grey[50]}; + } + } + + &.increment { + grid-column: 1/2; + grid-row: 1/2; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + + &.decrement { + grid-column: 1/2; + grid-row: 2/3; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + `, +); diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index 5edc38da08aa39..d582afc7005af0 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -24,7 +24,7 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput( return ( - + - + ` + width: 1.5rem; + height: 1rem; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + font-size: 0.875rem; + box-sizing: border-box; + border: 0; + + &.increment, + &.decrement { + background: ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; + + &:hover { + cursor: pointer; + background: ${blue[400]}; + color: ${grey[50]}; + } + } + + &.increment { + grid-column: 1/2; + grid-row: 1/2; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + + &.decrement { + grid-column: 1/2; + grid-row: 2/3; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + `, +); From 25d8a68d565251924f3c07e0c898f630c08aa3b7 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 2 Aug 2023 14:25:38 +0800 Subject: [PATCH 66/70] Another styling fix --- docs/data/base/components/number-input/UseNumberInput.js | 5 +++-- docs/data/base/components/number-input/UseNumberInput.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index bee3f7c0290a66..0d1be9b2103d76 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -138,13 +138,14 @@ const StyledStepperButton = styled('button')( &.increment, &.decrement { - background: ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; - &:hover { cursor: pointer; background: ${blue[400]}; color: ${grey[50]}; } + + background: ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; + color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]}; } &.increment { diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index d582afc7005af0..3640fd50997431 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -143,13 +143,14 @@ const StyledStepperButton = styled('button')( &.increment, &.decrement { - background: ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; - &:hover { cursor: pointer; background: ${blue[400]}; color: ${grey[50]}; } + + background: ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; + color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]}; } &.increment { From 652c650aca6fd729ccd7e7968fd6bf36a06684d8 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 2 Aug 2023 17:41:15 +0800 Subject: [PATCH 67/70] Fix component import name --- docs/src/modules/components/ComponentsApiContent.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/modules/components/ComponentsApiContent.js b/docs/src/modules/components/ComponentsApiContent.js index 3bb8628ed574a9..506eb2aa483508 100644 --- a/docs/src/modules/components/ComponentsApiContent.js +++ b/docs/src/modules/components/ComponentsApiContent.js @@ -119,9 +119,10 @@ export default function ComponentsApiContent(props) { // convert things like `/Table/Table.js` to `` .replace(/\/([^/]+)\/\1\.(js|tsx)$/, ''); - let importName = pageContent.name; - let namedImportPath = `${source}/${importName}`; + const namedImportName = pageContent.name; + let namedImportPath = `${source}/${namedImportName}`; let defaultImportPath = source; + let defaultImportName = namedImportName; if (/Unstable_/.test(source)) { namedImportPath = source.replace(/\/[^/]*$/, ''); @@ -129,7 +130,7 @@ export default function ComponentsApiContent(props) { .replace(/Unstable_/, '') .replace(/\/([^/]+)\/\1\.(js|tsx)$/, ''); - importName = `Unstable_${importName} as ${importName}`; + defaultImportName = `Unstable_${namedImportName} as ${namedImportName}`; } // The `ref` is forwarded to the root element. @@ -164,9 +165,9 @@ export default function ComponentsApiContent(props) { From 087e1f27814b7b5dc5dcaf074c72d90f112299da Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 2 Aug 2023 18:03:43 +0800 Subject: [PATCH 68/70] Fix svg sizing in buttons for mobile Safari --- docs/data/base/components/number-input/UseNumberInput.js | 5 +++++ docs/data/base/components/number-input/UseNumberInput.tsx | 5 +++++ .../number-input/UseNumberInputCompact/system/index.js | 1 + .../number-input/UseNumberInputCompact/system/index.tsx | 1 + 4 files changed, 12 insertions(+) diff --git a/docs/data/base/components/number-input/UseNumberInput.js b/docs/data/base/components/number-input/UseNumberInput.js index 0d1be9b2103d76..b4ae5cd8ca3a6b 100644 --- a/docs/data/base/components/number-input/UseNumberInput.js +++ b/docs/data/base/components/number-input/UseNumberInput.js @@ -135,6 +135,11 @@ const StyledStepperButton = styled('button')( font-size: 0.875rem; box-sizing: border-box; border: 0; + padding: 0; + + & > svg { + transform: scale(0.8); + } &.increment, &.decrement { diff --git a/docs/data/base/components/number-input/UseNumberInput.tsx b/docs/data/base/components/number-input/UseNumberInput.tsx index 3640fd50997431..aa3ccb008b187f 100644 --- a/docs/data/base/components/number-input/UseNumberInput.tsx +++ b/docs/data/base/components/number-input/UseNumberInput.tsx @@ -140,6 +140,11 @@ const StyledStepperButton = styled('button')( font-size: 0.875rem; box-sizing: border-box; border: 0; + padding: 0; + + & > svg { + transform: scale(0.8); + } &.increment, &.decrement { diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/system/index.js b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.js index a0e913e98ab6ba..322e5ffb804839 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact/system/index.js +++ b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.js @@ -126,6 +126,7 @@ const StyledStepperButton = styled('button')( font-size: 0.875rem; box-sizing: border-box; border: 0; + padding: 0; color: inherit; background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; diff --git a/docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx index a5710dd0211ef1..8ca35b81556ee9 100644 --- a/docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx +++ b/docs/data/base/components/number-input/UseNumberInputCompact/system/index.tsx @@ -133,6 +133,7 @@ const StyledStepperButton = styled('button')( font-size: 0.875rem; box-sizing: border-box; border: 0; + padding: 0; color: inherit; background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; From 173559759fbdfc984ae5bfb4c61e81d13ff62eac Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 2 Aug 2023 18:08:31 +0800 Subject: [PATCH 69/70] Fix variable names --- .../modules/components/ComponentsApiContent.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/modules/components/ComponentsApiContent.js b/docs/src/modules/components/ComponentsApiContent.js index 506eb2aa483508..6671b57e4033f2 100644 --- a/docs/src/modules/components/ComponentsApiContent.js +++ b/docs/src/modules/components/ComponentsApiContent.js @@ -119,18 +119,18 @@ export default function ComponentsApiContent(props) { // convert things like `/Table/Table.js` to `` .replace(/\/([^/]+)\/\1\.(js|tsx)$/, ''); - const namedImportName = pageContent.name; - let namedImportPath = `${source}/${namedImportName}`; - let defaultImportPath = source; - let defaultImportName = namedImportName; + const defaultImportName = pageContent.name; + let defaultImportPath = `${source}/${defaultImportName}`; + let namedImportPath = source; + let namedImportName = defaultImportName; if (/Unstable_/.test(source)) { - namedImportPath = source.replace(/\/[^/]*$/, ''); - defaultImportPath = packageAndFilename + defaultImportPath = source.replace(/\/[^/]*$/, ''); + namedImportPath = packageAndFilename .replace(/Unstable_/, '') .replace(/\/([^/]+)\/\1\.(js|tsx)$/, ''); - defaultImportName = `Unstable_${namedImportName} as ${namedImportName}`; + namedImportName = `Unstable_${defaultImportName} as ${defaultImportName}`; } // The `ref` is forwarded to the root element. @@ -165,9 +165,9 @@ export default function ComponentsApiContent(props) { From 420053dc0fd4a8ee922377cb42d1892aa0626f2c Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 2 Aug 2023 18:43:58 +0800 Subject: [PATCH 70/70] Change product to productId in md --- docs/data/base/components/number-input/number-input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/base/components/number-input/number-input.md b/docs/data/base/components/number-input/number-input.md index 114f416ea86d5f..3fd2115b77585b 100644 --- a/docs/data/base/components/number-input/number-input.md +++ b/docs/data/base/components/number-input/number-input.md @@ -1,5 +1,5 @@ --- -product: base +productId: base-ui title: React Number Input component and hook components: NumberInput hooks: useNumberInput