Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[base-ui] Create useNumberInput and NumberInput #36119

Merged
merged 70 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
c3c9b97
[base] Add useNumberInput
mj12albert Feb 1, 2023
a8418f0
Add aria attributes, more types.
mj12albert Feb 27, 2023
c5f9b59
Add NumberInputUnstyled
mj12albert Feb 23, 2023
3a9b8f9
Redo change handlers
mj12albert Feb 28, 2023
fb52a63
Add increment and decrement buttons
mj12albert Mar 1, 2023
3e19da1
Keyboard handlers and shift multiplier
mj12albert Mar 2, 2023
40bee99
Apply code review changes
mj12albert Mar 3, 2023
df5b8e8
Extract number check util
mj12albert Mar 3, 2023
de0cb54
Separate directories
mj12albert Mar 3, 2023
39c1e2c
Update docs
mj12albert Mar 3, 2023
7fc5122
NumberInput introduction demo
mj12albert Mar 3, 2023
c508ec3
Store quantity picker demo
mj12albert Mar 9, 2023
fdf42c9
Add a demo using useNumberInput
mj12albert Mar 22, 2023
0734764
Update NumberInputUnstyled to support useClassNamesOverride
mj12albert Mar 22, 2023
e95f643
Add second hook demo
mj12albert Mar 23, 2023
b0ea58c
Fix compatibility with ComponentPageTabs
mj12albert Apr 12, 2023
23d9d36
Write docs page
mj12albert Apr 12, 2023
ce96976
Add explanation of min, max, step props
mj12albert Apr 12, 2023
e187960
Add explanation of shiftMultiplier prop
mj12albert Apr 13, 2023
7b733c5
Simplify hook demo
mj12albert Apr 17, 2023
e9267ff
Design revisions
mj12albert Apr 18, 2023
e424858
Format demos
mj12albert Apr 18, 2023
1a67068
Expand introduction paragraph
mj12albert Apr 19, 2023
1f8162f
Update onValueChange signature
mj12albert Apr 19, 2023
11bff81
Align dirty value updater naming
mj12albert Apr 21, 2023
188e02e
Remove Unstyled suffix
mj12albert Apr 28, 2023
8450b0b
Tweak stepping behavior
mj12albert May 1, 2023
e0c81e2
Add readOnly prop
mj12albert May 2, 2023
1b9ed63
Align with latest API changes
mj12albert May 11, 2023
dc6f1ee
Add tests
mj12albert May 15, 2023
8c8e8a8
Add onValueChange test
mj12albert May 15, 2023
7a774be
Add keyboard interaction tests
mj12albert May 15, 2023
e4a73d9
Add stepper button tests
mj12albert May 16, 2023
18868e4
Test out @testing-library/user-event
mj12albert May 17, 2023
b3161e6
Update keyboard interaction tests
mj12albert May 17, 2023
bef43f2
Add more stepper button tests
mj12albert May 17, 2023
64d459f
Refactor tests using user-event
mj12albert May 17, 2023
10e8c33
Improve tab order tests
mj12albert May 17, 2023
0f2b804
Cleanup
mj12albert May 18, 2023
e6c57a5
Remove spinbutton role, add aria-controls
mj12albert May 19, 2023
68b2641
Fix NumberInput test missing slots
mj12albert May 19, 2023
eb93cbb
A few visual tweaks on the Demos
zanivan May 22, 2023
bcf5a67
yarn docs:typescript:formatted
zanivan May 22, 2023
f5e8422
Add unstable prefix
mj12albert May 24, 2023
db39c57
Rename change handlers
mj12albert May 29, 2023
ccd3199
Add use-client
mj12albert Jul 26, 2023
63c0d44
Docs fixes
mj12albert Jul 26, 2023
6ba86c5
Add Tailwind and plain CSS versions of basic demo
mj12albert Jul 26, 2023
1058f01
Update intro demo
mj12albert Jul 27, 2023
b281e29
Update QuantityInput demo
mj12albert Jul 27, 2023
98d9217
Update CompactNumberInput demo
mj12albert Jul 27, 2023
7317788
Update hook demo
mj12albert Jul 28, 2023
8ea2349
Drop the component prop and single letter type names
mj12albert Jul 28, 2023
2ac68aa
Update api docs
mj12albert Jul 28, 2023
2563733
Generate proptypes
mj12albert Jul 28, 2023
fd7ba0d
Update api docs
mj12albert Jul 28, 2023
6ea8267
Update readOnly behavior
mj12albert Jul 31, 2023
b887e89
Fix generating import statements for unstable items in Base API pages
mj12albert Aug 1, 2023
3b9aac2
Fix arrows in NumberInputIntroduction
mj12albert Aug 1, 2023
19b5af2
Fix all html arrows in the demos
mj12albert Aug 1, 2023
087e442
Misc fixes
mj12albert Aug 1, 2023
9e545a0
Type onBlur more explicitly
mj12albert Aug 1, 2023
414442f
Update api docs
mj12albert Aug 1, 2023
105c8c1
Styling fixes for demos
mj12albert Aug 1, 2023
ca3b51b
More styling fixes
mj12albert Aug 1, 2023
25d8a68
Another styling fix
mj12albert Aug 2, 2023
652c650
Fix component import name
mj12albert Aug 2, 2023
087e1f2
Fix svg sizing in buttons for mobile Safari
mj12albert Aug 2, 2023
1735597
Fix variable names
mj12albert Aug 2, 2023
420053d
Change product to productId in md
mj12albert Aug 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add increment and decrement buttons
  • Loading branch information
mj12albert committed Aug 1, 2023
commit fb52a63039ed149d9babb002a73aa64282e9129e
7 changes: 5 additions & 2 deletions docs/pages/base-ui/api/number-input-unstyled.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
"slotProps": {
"type": {
"name": "shape",
"description": "{ input?: func<br>&#124;&nbsp;object, root?: func<br>&#124;&nbsp;object }"
"description": "{ decrementButton?: func<br>&#124;&nbsp;object, incrementButton?: func<br>&#124;&nbsp;object, input?: func<br>&#124;&nbsp;object, root?: func<br>&#124;&nbsp;object }"
},
"default": "{}"
},
"slots": {
"type": { "name": "shape", "description": "{ input?: elementType, root?: elementType }" },
"type": {
"name": "shape",
"description": "{ decrementButton?: elementType, incrementButton?: elementType, input?: elementType, root?: elementType }"
},
"default": "{}"
}
},
Expand Down
29 changes: 28 additions & 1 deletion docs/pages/base-ui/api/use-number-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
"type": { "name": "React.FocusEventHandler", "description": "React.FocusEventHandler" }
},
"onValueChange": {
"type": { "name": "(value: number) =&gt; void", "description": "(value: number) =&gt; void" }
"type": {
"name": "(value: number | undefined) =&gt; void",
"description": "(value: number | undefined) =&gt; void"
}
},
"required": { "type": { "name": "boolean", "description": "boolean" } },
"step": { "type": { "name": "number", "description": "number" } },
Expand Down Expand Up @@ -59,6 +62,20 @@
},
"required": true
},
"getDecrementButtonProps": {
"type": {
"name": "&lt;TOther extends Record&lt;string, any&gt; = {}&gt;(externalProps?: TOther) =&gt; UseNumberInputDecrementButtonSlotProps&lt;TOther&gt;",
"description": "&lt;TOther extends Record&lt;string, any&gt; = {}&gt;(externalProps?: TOther) =&gt; UseNumberInputDecrementButtonSlotProps&lt;TOther&gt;"
},
"required": true
},
"getIncrementButtonProps": {
"type": {
"name": "&lt;TOther extends Record&lt;string, any&gt; = {}&gt;(externalProps?: TOther) =&gt; UseNumberInputIncrementButtonSlotProps&lt;TOther&gt;",
"description": "&lt;TOther extends Record&lt;string, any&gt; = {}&gt;(externalProps?: TOther) =&gt; UseNumberInputIncrementButtonSlotProps&lt;TOther&gt;"
},
"required": true
},
"getInputProps": {
"type": {
"name": "&lt;TOther extends Record&lt;string, any&gt; = {}&gt;(externalProps?: TOther) =&gt; UseNumberInputInputSlotProps&lt;TOther&gt;",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
"error": "If <code>true</code>, the <code>input</code> will indicate an error by setting the <code>aria-invalid</code> attribute.",
"focused": "If <code>true</code>, the <code>input</code> will be focused.",
"formControlContext": "Return value from the <code>useFormControlContext</code> 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 <code>value</code> of the <code>input</code> element when it is in focus.",
"isDecrementDisabled": "If <code>true</code>, the decrement button will be disabled.\ne.g. when the <code>value</code> is already at <code>min</code>",
"isIncrementDisabled": "If <code>true</code>, the increment button will be disabled.\ne.g. when the <code>value</code> is already at <code>max</code>",
"required": "If <code>true</code>, the <code>input</code> will indicate that it's required.",
"value": "The clamped <code>value</code> of the <code>input</code> element."
}
Expand Down
46 changes: 44 additions & 2 deletions packages/mui-base/src/NumberInputUnstyled/NumberInputUnstyled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
NumberInputUnstyledProps,
NumberInputUnstyledRootSlotProps,
NumberInputUnstyledInputSlotProps,
NumberInputUnstyledIncrementButtonSlotProps,
NumberInputUnstyledDecrementButtonSlotProps,
NumberInputUnstyledTypeMap,
} from './NumberInputUnstyled.types';
import { EventHandlers, useSlotProps, WithOptionalOwnerState } from '../utils';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -107,20 +123,42 @@ const NumberInputUnstyled = React.forwardRef(function NumberInputUnstyled(
});

const Input = slots.input ?? 'input';

const inputProps: WithOptionalOwnerState<NumberInputUnstyledInputSlotProps> = 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<NumberInputUnstyledIncrementButtonSlotProps> =
useSlotProps({
elementType: IncrementButton,
getSlotProps: getIncrementButtonProps,
externalSlotProps: slotProps.incrementButton,
ownerState,
className: [classes.incrementButton, incrementButtonStateClasses],
});

const DecrementButton = slots.decrementButton ?? 'button';
const decrementButtonProps: WithOptionalOwnerState<NumberInputUnstyledDecrementButtonSlotProps> =
useSlotProps({
elementType: DecrementButton,
getSlotProps: getDecrementButtonProps,
externalSlotProps: slotProps.decrementButton,
// additionalProps: {},
ownerState,
className: [classes.decrementButton, decrementButtonStateClasses],
});

return (
<Root {...rootProps}>
<DecrementButton {...decrementButtonProps}>-</DecrementButton>
<Input {...inputProps} />
<IncrementButton {...incrementButtonProps}>+</IncrementButton>
</Root>
);
}) as OverridableComponent<NumberInputUnstyledTypeMap>;
Expand Down Expand Up @@ -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]),
}),
Expand All @@ -207,6 +247,8 @@ NumberInputUnstyled.propTypes /* remove-proptypes */ = {
* @default {}
*/
slots: PropTypes.shape({
decrementButton: PropTypes.elementType,
incrementButton: PropTypes.elementType,
input: PropTypes.elementType,
root: PropTypes.elementType,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UseNumberInputParameters, 'error'> & {
/**
Expand All @@ -30,6 +37,16 @@ export type NumberInputUnstyledOwnProps = Omit<UseNumberInputParameters, 'error'
NumberInputUnstyledInputSlotPropsOverrides,
NumberInputUnstyledOwnerState
>;
incrementButton?: SlotComponentProps<
'button',
NumberInputUnstyledIncrementButtonSlotPropsOverrides,
NumberInputUnstyledOwnerState
>;
decrementButton?: SlotComponentProps<
'button',
NumberInputUnstyledDecrementButtonSlotPropsOverrides,
NumberInputUnstyledOwnerState
>;
};
/**
* The components used for each slot inside the InputBase.
Expand All @@ -39,6 +56,8 @@ export type NumberInputUnstyledOwnProps = Omit<UseNumberInputParameters, 'error'
slots?: {
root?: React.ElementType;
input?: React.ElementType;
incrementButton?: React.ElementType;
decrementButton?: React.ElementType;
};
};

Expand Down Expand Up @@ -78,3 +97,15 @@ export type NumberInputUnstyledInputSlotProps = Simplify<
ref: React.Ref<HTMLInputElement>;
}
>;

export type NumberInputUnstyledIncrementButtonSlotProps = Simplify<
UseNumberInputIncrementButtonSlotProps & {
ownerState: NumberInputUnstyledOwnerState;
}
>;

export type NumberInputUnstyledDecrementButtonSlotProps = Simplify<
UseNumberInputDecrementButtonSlotProps & {
ownerState: NumberInputUnstyledOwnerState;
}
>;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +39,8 @@ const numberInputUnstyledClasses: NumberInputUnstyledClasses = generateUtilityCl
'disabled',
'error',
'input',
'incrementButton',
'decrementButton',
// 'adornedStart',
// 'adornedEnd',
],
Expand Down
71 changes: 65 additions & 6 deletions packages/mui-base/src/NumberInputUnstyled/useNumberInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
UseNumberInputParameters,
UseNumberInputRootSlotProps,
UseNumberInputInputSlotProps,
UseNumberInputIncrementButtonSlotProps,
UseNumberInputDecrementButtonSlotProps,
UseNumberInputReturnValue,
} from './useNumberInput.types';
import clamp from './clamp';
Expand All @@ -30,11 +32,9 @@ export default function useNumberInput(
parameters: UseNumberInputParameters,
): UseNumberInputReturnValue {
const {
// number
min,
max,
step,
//
defaultValue: defaultValueProp,
disabled: disabledProp = false,
error: errorProp = false,
Expand Down Expand Up @@ -108,7 +108,8 @@ export default function useNumberInput(
};

const handleValueChange =
() => (event: React.FocusEvent<HTMLInputElement>, val: number | undefined) => {
() =>
(event: React.FocusEvent<HTMLInputElement> | React.PointerEvent, val: number | undefined) => {
// 1. clamp the number
// 2. setInputValue(clamped_value)
// 3. call onValueChange(newValue)
Expand All @@ -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);
}
};

Expand Down Expand Up @@ -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 = <TOther extends Record<string, any> = {}>(
externalProps: TOther = {} as TOther,
): UseNumberInputRootSlotProps<TOther> => {
Expand Down Expand Up @@ -257,17 +282,51 @@ export default function useNumberInput(
};
};

const isIncrementDisabled =
typeof value === 'number' ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false;

const getIncrementButtonProps = <TOther extends Record<string, any> = {}>(
externalProps: TOther = {} as TOther,
): UseNumberInputIncrementButtonSlotProps<TOther> => {
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 = <TOther extends Record<string, any> = {}>(
externalProps: TOther = {} as TOther,
): UseNumberInputDecrementButtonSlotProps<TOther> => {
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,
};
Expand Down
Loading