Skip to content

Commit

Permalink
[WIP] Increment and decrement buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Mar 2, 2023
1 parent c959da6 commit 81afe0c
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 13 deletions.
7 changes: 5 additions & 2 deletions docs/pages/base/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/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>useFormControlUnstyledContext</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} />
<Input {...inputProps} />
<IncrementButton {...incrementButtonProps} />
</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 { FormControlUnstyledState } from '../FormControlUnstyled';
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 @@ -128,8 +129,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 @@ -197,6 +200,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 @@ -258,17 +283,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

0 comments on commit 81afe0c

Please sign in to comment.