From 681bf7afe7ac3a73afbc7fa109b6232d33394138 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 29 Aug 2023 22:45:46 +0800 Subject: [PATCH 01/14] New numberInputReducer --- .../src/Unstable_NumberInput/NumberInput.tsx | 2 +- .../numberInputAction.types.ts | 46 ++++++++++++++++ .../numberInputReducer.test.ts | 35 ++++++++++++ .../numberInputReducer.ts | 55 +++++++++++++++++++ .../unstable_useNumberInput/useNumberInput.ts | 6 +- .../useNumberInput.types.ts | 41 ++++++++++++-- 6 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts create mode 100644 packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts create mode 100644 packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx index 9dc712214a31c4..29ba361e8c0528 100644 --- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx +++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx @@ -307,7 +307,7 @@ NumberInput.propTypes /* remove-proptypes */ = { /** * The current value. Use when the component is controlled. */ - value: PropTypes.any, + value: PropTypes.number, } as any; export { NumberInput }; diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts new file mode 100644 index 00000000000000..53d7fc5268c208 --- /dev/null +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -0,0 +1,46 @@ +export const NumberInputActionTypes = { + blur: 'numberInput:blur', + focus: 'numberInput:focus', + inputChange: 'numberInput:inputChange', + click: 'numberInput:click', + increment: 'numberInput:increment', + decrement: 'numberInput:decrement', +} as const; + +interface NumberInputBlurAction { + type: typeof NumberInputActionTypes.blur; + event: React.FocusEvent; +} + +interface NumberInputFocusAction { + type: typeof NumberInputActionTypes.focus; + event: React.FocusEvent; +} + +interface NumberInputInputChangeAction { + type: typeof NumberInputActionTypes.inputChange; + event: React.ChangeEvent; +} + +interface NumberInputClickAction { + type: typeof NumberInputActionTypes.click; + event: React.MouseEvent; +} + +interface NumberInputIncrementAction { + type: typeof NumberInputActionTypes.increment; + event: React.PointerEvent | React.KeyboardEvent; +} + +interface NumberInputDecrementAction { + type: typeof NumberInputActionTypes.decrement; + event: React.PointerEvent | React.KeyboardEvent; +} + +export type NumberInputAction = + | NumberInputBlurAction + | NumberInputFocusAction + | NumberInputInputChangeAction + | NumberInputClickAction + | NumberInputIncrementAction + | NumberInputDecrementAction; diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts new file mode 100644 index 00000000000000..654215b3082008 --- /dev/null +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; +import { NumberInputState, NumberInputReducerAction } from './useNumberInput.types'; +import { NumberInputActionTypes } from './numberInputAction.types'; +import { numberInputReducer } from './numberInputReducer'; +import { getInputValueAsString as defaultGetInputValueAsString } from './useNumberInput'; + +describe('numberInputReducer', () => { + describe('action: blur', () => { + it('snaps the inputValue based on min, max, and step', () => { + const state: NumberInputState = { + value: 0, + inputValue: '2', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.blur, + event: { + currentTarget: { + value: '2', + }, + } as unknown as React.FocusEvent, + context: { + step: 3, + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(3); + expect(result.inputValue).to.equal('3'); + }); + }); +}); diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts new file mode 100644 index 00000000000000..b6ffc329141bef --- /dev/null +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -0,0 +1,55 @@ +import { + NumberInputActionContext, + NumberInputReducerAction, + NumberInputState, +} from './useNumberInput.types'; +import { NumberInputActionTypes } from './numberInputAction.types'; +import { clamp } from './utils'; + +// extracted from handleValueChange +function getNewValue(oldValue: number | undefined, context: NumberInputActionContext) { + const { min, max, step } = context; + + const newValue = oldValue === undefined ? undefined : clamp(oldValue, min, max, step); + + const newInputValue = newValue === undefined ? '' : String(newValue); + + return { + value: newValue, + inputValue: newInputValue, + }; +} + +function handleBlur( + state: State, + context: NumberInputActionContext, + event: React.FocusEvent, +) { + const { getInputValueAsString } = context; + + const parsedValue = getInputValueAsString(event.currentTarget.value); + + const intermediateValue = + parsedValue === '' || parsedValue === '-' ? undefined : parseInt(parsedValue, 10); + + const newValues = getNewValue(intermediateValue, context); + + return { + ...state, + ...newValues, + }; +} + +export function numberInputReducer( + state: NumberInputState, + action: NumberInputReducerAction, +): NumberInputState { + const { type, context, event } = action; + + switch (type) { + case NumberInputActionTypes.blur: + return handleBlur(state, context, event); + default: + return state; + } +} diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index 8314a22113433f..e55b1b6515864c 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -24,7 +24,7 @@ const STEP_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown']; const SUPPORTED_KEYS = [...STEP_KEYS, 'Home', 'End']; -function parseInput(v: string): string { +export function getInputValueAsString(v: string): string { return v ? String(v.trim()) : String(v); } @@ -159,7 +159,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI otherHandlers.onInputChange?.(event); - const val = parseInput(event.currentTarget.value); + const val = getInputValueAsString(event.currentTarget.value); if (val === '' || val === '-') { setDirtyValue(val); @@ -175,7 +175,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI const handleBlur = (otherHandlers: Record | undefined>) => (event: React.FocusEvent) => { - const val = parseInput(event.currentTarget.value); + const val = getInputValueAsString(event.currentTarget.value); otherHandlers.onBlur?.(event); diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index 6415fc75732421..ab3c8feb3a6b51 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -1,10 +1,41 @@ import * as React from 'react'; import { FormControlState } from '../FormControl'; +import { NumberInputAction } from './numberInputAction.types'; +import { ActionWithContext } from '../utils/useControllableReducer.types'; -export type UseNumberInputChangeHandler = ( - e: React.KeyboardEvent, - value: number | null, -) => void; +/** + * The internal state of the NumberInput. + * Modify via the reducer only. + */ +export interface NumberInputState { + /** + * The clamped `value` of the `input` element. + */ + value: number | undefined; + /** + * The dirty `value` of the `input` element when it is in focus. + */ + inputValue: string | undefined; +} + +/** + * Additional props passed to the number input reducer actions. + */ +export type NumberInputActionContext = { + min?: number; + max?: number; + step?: number; + shiftMultiplier: number; + /** + * A function that parses the raw input value + */ + getInputValueAsString: (val: string) => string; +}; + +export type NumberInputReducerAction = ActionWithContext< + NumberInputAction, + NumberInputActionContext & CustomActionContext +>; export interface UseNumberInputParameters { /** @@ -83,7 +114,7 @@ export interface UseNumberInputParameters { /** * The current value. Use when the component is controlled. */ - value?: unknown; + value?: number; } export interface UseNumberInputRootSlotOwnProps { From 9d08a385e1f3ff2229d53d170cc5d370cf8dd013 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 31 Aug 2023 01:34:58 +0800 Subject: [PATCH 02/14] Add increment and decrement actions --- docs/pages/base-ui/api/number-input.json | 2 +- docs/pages/base-ui/api/use-number-input.json | 2 +- .../numberInputReducer.test.ts | 138 ++++++++++++++++++ .../numberInputReducer.ts | 62 +++++++- .../unstable_useNumberInput/useNumberInput.ts | 3 +- .../useNumberInput.types.ts | 2 + 6 files changed, 201 insertions(+), 8 deletions(-) diff --git a/docs/pages/base-ui/api/number-input.json b/docs/pages/base-ui/api/number-input.json index c3d5691a4e999f..77667da4fe300f 100644 --- a/docs/pages/base-ui/api/number-input.json +++ b/docs/pages/base-ui/api/number-input.json @@ -41,7 +41,7 @@ }, "startAdornment": { "type": { "name": "node" } }, "step": { "type": { "name": "number" } }, - "value": { "type": { "name": "any" } } + "value": { "type": { "name": "number" } } }, "name": "NumberInput", "imports": [ diff --git a/docs/pages/base-ui/api/use-number-input.json b/docs/pages/base-ui/api/use-number-input.json index 86c5283261df82..25f06d787f6a7d 100644 --- a/docs/pages/base-ui/api/use-number-input.json +++ b/docs/pages/base-ui/api/use-number-input.json @@ -40,7 +40,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" } } + "value": { "type": { "name": "number", "description": "number" } } }, "returnValue": { "disabled": { diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 654215b3082008..1027508db4b1c6 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -32,4 +32,142 @@ describe('numberInputReducer', () => { expect(result.inputValue).to.equal('3'); }); }); + + describe('action: increment', () => { + it('increments the value', () => { + const state: NumberInputState = { + value: 0, + inputValue: '0', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.increment, + event: {} as any, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(1); + expect(result.inputValue).to.equal('1'); + }); + + it('increments the value based on the step prop', () => { + const state: NumberInputState = { + value: 0, + inputValue: '0', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.increment, + event: {} as any, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 5, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(5); + expect(result.inputValue).to.equal('5'); + }); + + it('applys the shiftMultiplier when incrementing with shift+click', () => { + const state: NumberInputState = { + value: 0, + inputValue: '0', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.increment, + event: { + shiftKey: true, + } as React.PointerEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 1, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(10); + expect(result.inputValue).to.equal('10'); + }); + }); + + describe('action: decrement', () => { + it('decrements the value', () => { + const state: NumberInputState = { + value: 15, + inputValue: '15', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.decrement, + event: {} as any, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(14); + expect(result.inputValue).to.equal('14'); + }); + + it('decrements the value based on the step prop', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.decrement, + event: {} as any, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 2, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(8); + expect(result.inputValue).to.equal('8'); + }); + + it('applys the shiftMultiplier when decrementing with shift+click', () => { + const state: NumberInputState = { + value: 20, + inputValue: '20', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.decrement, + event: { + shiftKey: true, + } as React.PointerEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 1, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(10); + expect(result.inputValue).to.equal('10'); + }); + }); }); diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index b6ffc329141bef..c86b64fd44ca74 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -2,12 +2,13 @@ import { NumberInputActionContext, NumberInputReducerAction, NumberInputState, + StepDirection, } from './useNumberInput.types'; import { NumberInputActionTypes } from './numberInputAction.types'; -import { clamp } from './utils'; +import { clamp, isNumber } from './utils'; // extracted from handleValueChange -function getNewValue(oldValue: number | undefined, context: NumberInputActionContext) { +function getClampedValues(oldValue: number | undefined, context: NumberInputActionContext) { const { min, max, step } = context; const newValue = oldValue === undefined ? undefined : clamp(oldValue, min, max, step); @@ -20,6 +21,37 @@ function getNewValue(oldValue: number | undefined, context: NumberInputActionCon }; } +function getMultiplier(context: NumberInputActionContext, event: React.SyntheticEvent) { + const { shiftMultiplier } = context; + return (event as React.PointerEvent).shiftKey || + (event as React.KeyboardEvent).key === 'PageUp' || + (event as React.KeyboardEvent).key === 'PageDown' + ? shiftMultiplier + : 1; +} + +function stepValue( + state: NumberInputState, + context: NumberInputActionContext, + direction: StepDirection, + multiplier: number, +) { + const { value } = state; + const { step = 1, min, max } = context; + + if (isNumber(value)) { + return { + up: value + (step ?? 1) * multiplier, + down: value - (step ?? 1) * multiplier, + }[direction]; + } + + return { + up: min ?? 0, + down: max ?? 0, + }[direction]; +} + function handleBlur( state: State, context: NumberInputActionContext, @@ -32,11 +64,27 @@ function handleBlur( const intermediateValue = parsedValue === '' || parsedValue === '-' ? undefined : parseInt(parsedValue, 10); - const newValues = getNewValue(intermediateValue, context); + const clampedValues = getClampedValues(intermediateValue, context); return { ...state, - ...newValues, + ...clampedValues, + }; +} + +function handleStep( + state: State, + context: NumberInputActionContext, + direction: StepDirection, + multiplier: number, +) { + const newValue = stepValue(state, context, direction, multiplier); + + const clampedValues = getClampedValues(newValue, context); + + return { + ...state, + ...clampedValues, }; } @@ -46,9 +94,15 @@ export function numberInputReducer( ): NumberInputState { const { type, context, event } = action; + const multiplier = getMultiplier(context, event); + switch (type) { case NumberInputActionTypes.blur: return handleBlur(state, context, event); + case NumberInputActionTypes.increment: + return handleStep(state, context, 'up', multiplier); + case NumberInputActionTypes.decrement: + return handleStep(state, context, 'down', multiplier); default: return state; } diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index e55b1b6515864c..cbc359afb97b3d 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -14,12 +14,11 @@ import { UseNumberInputIncrementButtonSlotProps, UseNumberInputDecrementButtonSlotProps, UseNumberInputReturnValue, + StepDirection, } from './useNumberInput.types'; import { clamp, isNumber } from './utils'; import { extractEventHandlers } from '../utils/extractEventHandlers'; -type StepDirection = 'up' | 'down'; - const STEP_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown']; const SUPPORTED_KEYS = [...STEP_KEYS, 'Home', 'End']; diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index ab3c8feb3a6b51..5290108fcddb9a 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -3,6 +3,8 @@ import { FormControlState } from '../FormControl'; import { NumberInputAction } from './numberInputAction.types'; import { ActionWithContext } from '../utils/useControllableReducer.types'; +export type StepDirection = 'up' | 'down'; + /** * The internal state of the NumberInput. * Modify via the reducer only. From 2a71c2b352879cc389574ed18969736a4ca411a2 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 31 Aug 2023 18:29:23 +0800 Subject: [PATCH 03/14] Add keydown action tests --- .../numberInputAction.types.ts | 7 + .../numberInputReducer.test.ts | 299 ++++++++++++++++++ .../numberInputReducer.ts | 15 + .../useNumberInput.types.ts | 4 +- 4 files changed, 323 insertions(+), 2 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts index 53d7fc5268c208..d79337371e4552 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -2,6 +2,7 @@ export const NumberInputActionTypes = { blur: 'numberInput:blur', focus: 'numberInput:focus', inputChange: 'numberInput:inputChange', + keyDown: 'numberInput:keyDown', click: 'numberInput:click', increment: 'numberInput:increment', decrement: 'numberInput:decrement', @@ -22,6 +23,11 @@ interface NumberInputInputChangeAction { event: React.ChangeEvent; } +interface NumberInputKeyDownAction { + type: typeof NumberInputActionTypes.keyDown; + event: React.KeyboardEvent; +} + interface NumberInputClickAction { type: typeof NumberInputActionTypes.click; event: React.MouseEvent; @@ -41,6 +47,7 @@ export type NumberInputAction = | NumberInputBlurAction | NumberInputFocusAction | NumberInputInputChangeAction + | NumberInputKeyDownAction | NumberInputClickAction | NumberInputIncrementAction | NumberInputDecrementAction; diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 1027508db4b1c6..947bdf671b32aa 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -170,4 +170,303 @@ describe('numberInputReducer', () => { expect(result.inputValue).to.equal('10'); }); }); + + describe('action: keyDown', () => { + describe('key: ArrowUp', () => { + it('increments', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowUp', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(11); + expect(result.inputValue).to.equal('11'); + }); + + it('increments based on a custom step', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowUp', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 5, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(15); + expect(result.inputValue).to.equal('15'); + }); + + it('increments and applies shiftMultiplier when Shift is held', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowUp', + shiftKey: true, + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(20); + expect(result.inputValue).to.equal('20'); + }); + + it('sets value to min when there is no value', () => { + const state: NumberInputState = { + value: '', + inputValue: '', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowUp', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + min: -20, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(-20); + expect(result.inputValue).to.equal('-20'); + }); + }); + + describe('key: ArrowDown', () => { + it('decrements', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowDown', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(9); + expect(result.inputValue).to.equal('9'); + }); + + it('decrements based on a custom step', () => { + const state: NumberInputState = { + value: 12, + inputValue: '12', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowDown', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 4, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(8); + expect(result.inputValue).to.equal('8'); + }); + + it('decrements and applies shiftMultiplier when Shift is held', () => { + const state: NumberInputState = { + value: 35, + inputValue: '35', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowDown', + shiftKey: true, + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(25); + expect(result.inputValue).to.equal('25'); + }); + + it('sets value to max when there is no value', () => { + const state: NumberInputState = { + value: '', + inputValue: '', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'ArrowDown', + shiftKey: true, + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + max: 99, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(99); + expect(result.inputValue).to.equal('99'); + }); + }); + + describe('key: PageUp', () => { + it('increments and applies shiftMultiplier', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'PageUp', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(20); + expect(result.inputValue).to.equal('20'); + }); + }); + + describe('key: PageDown', () => { + it('decrements and applies shiftMultiplier', () => { + const state: NumberInputState = { + value: 44, + inputValue: '44', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'PageDown', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(34); + expect(result.inputValue).to.equal('34'); + }); + }); + + describe('key: Home', () => { + it('increments to max if max is set', () => { + const state: NumberInputState = { + value: 44, + inputValue: '44', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'Home', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + max: 99, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(99); + expect(result.inputValue).to.equal('99'); + }); + }); + + describe('key: End', () => { + it('decrements to min if min is set', () => { + const state: NumberInputState = { + value: 44, + inputValue: '44', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'End', + } as React.KeyboardEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + min: 1, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(1); + expect(result.inputValue).to.equal('1'); + }); + }); + }); }); diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index c86b64fd44ca74..28b3ab4c27317e 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -88,6 +88,19 @@ function handleStep( }; } +function handleKeyDown( + state: State, + context: NumberInputActionContext, + event: React.KeyboardEvent, +) { + switch (event.key) { + default: + break; + } + + return state; +} + export function numberInputReducer( state: NumberInputState, action: NumberInputReducerAction, @@ -103,6 +116,8 @@ export function numberInputReducer( return handleStep(state, context, 'up', multiplier); case NumberInputActionTypes.decrement: return handleStep(state, context, 'down', multiplier); + case NumberInputActionTypes.keyDown: + return handleKeyDown(state, context, event); default: return state; } diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts index 5290108fcddb9a..7e7c5ca64a4046 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts @@ -13,11 +13,11 @@ export interface NumberInputState { /** * The clamped `value` of the `input` element. */ - value: number | undefined; + value?: number | ''; /** * The dirty `value` of the `input` element when it is in focus. */ - inputValue: string | undefined; + inputValue?: string; } /** From 56dd1dde6f56817af60dd9699fa9a250b8776098 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 31 Aug 2023 22:43:33 +0800 Subject: [PATCH 04/14] Add keyDown handler --- .../numberInputAction.types.ts | 7 +- .../numberInputReducer.test.ts | 68 ++++++++++++++-- .../numberInputReducer.ts | 78 +++++++++++++++---- 3 files changed, 129 insertions(+), 24 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts index d79337371e4552..52e679999495f9 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -26,6 +26,7 @@ interface NumberInputInputChangeAction { interface NumberInputKeyDownAction { type: typeof NumberInputActionTypes.keyDown; event: React.KeyboardEvent; + key: string; } interface NumberInputClickAction { @@ -35,12 +36,14 @@ interface NumberInputClickAction { interface NumberInputIncrementAction { type: typeof NumberInputActionTypes.increment; - event: React.PointerEvent | React.KeyboardEvent; + // triggering a button with the keyboard fires a PointerEvent + event: React.PointerEvent; } interface NumberInputDecrementAction { type: typeof NumberInputActionTypes.decrement; - event: React.PointerEvent | React.KeyboardEvent; + // triggering a button with the keyboard fires a PointerEvent + event: React.PointerEvent; } export type NumberInputAction = diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 947bdf671b32aa..0a5d2d7c9b4f5f 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -181,9 +181,8 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'ArrowUp', - } as React.KeyboardEvent, + event: {} as any, + key: 'ArrowUp', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -204,9 +203,8 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'ArrowUp', - } as React.KeyboardEvent, + event: {} as any, + key: 'ArrowUp', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -232,6 +230,7 @@ describe('numberInputReducer', () => { key: 'ArrowUp', shiftKey: true, } as React.KeyboardEvent, + key: 'ArrowUp', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -255,6 +254,7 @@ describe('numberInputReducer', () => { event: { key: 'ArrowUp', } as React.KeyboardEvent, + key: 'ArrowUp', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -281,6 +281,7 @@ describe('numberInputReducer', () => { event: { key: 'ArrowDown', } as React.KeyboardEvent, + key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -304,6 +305,7 @@ describe('numberInputReducer', () => { event: { key: 'ArrowDown', } as React.KeyboardEvent, + key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -329,6 +331,7 @@ describe('numberInputReducer', () => { key: 'ArrowDown', shiftKey: true, } as React.KeyboardEvent, + key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -353,6 +356,7 @@ describe('numberInputReducer', () => { key: 'ArrowDown', shiftKey: true, } as React.KeyboardEvent, + key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -379,6 +383,7 @@ describe('numberInputReducer', () => { event: { key: 'PageUp', } as React.KeyboardEvent, + key: 'PageUp', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -404,6 +409,7 @@ describe('numberInputReducer', () => { event: { key: 'PageDown', } as React.KeyboardEvent, + key: 'PageDown', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -429,6 +435,7 @@ describe('numberInputReducer', () => { event: { key: 'Home', } as React.KeyboardEvent, + key: 'Home', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -441,6 +448,30 @@ describe('numberInputReducer', () => { expect(result.value).to.equal(99); expect(result.inputValue).to.equal('99'); }); + + it('does not change the state if max is not set', () => { + const state: NumberInputState = { + value: 44, + inputValue: '44', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'Home', + } as React.KeyboardEvent, + key: 'Home', + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(44); + expect(result.inputValue).to.equal('44'); + }); }); describe('key: End', () => { @@ -455,6 +486,7 @@ describe('numberInputReducer', () => { event: { key: 'End', } as React.KeyboardEvent, + key: 'End', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -467,6 +499,30 @@ describe('numberInputReducer', () => { expect(result.value).to.equal(1); expect(result.inputValue).to.equal('1'); }); + + it('does not change the state if min is not set', () => { + const state: NumberInputState = { + value: 44, + inputValue: '44', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.keyDown, + event: { + key: 'End', + } as React.KeyboardEvent, + key: 'End', + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(44); + expect(result.inputValue).to.equal('44'); + }); }); }); }); diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index 28b3ab4c27317e..3837bb18820540 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -21,15 +21,6 @@ function getClampedValues(oldValue: number | undefined, context: NumberInputActi }; } -function getMultiplier(context: NumberInputActionContext, event: React.SyntheticEvent) { - const { shiftMultiplier } = context; - return (event as React.PointerEvent).shiftKey || - (event as React.KeyboardEvent).key === 'PageUp' || - (event as React.KeyboardEvent).key === 'PageDown' - ? shiftMultiplier - : 1; -} - function stepValue( state: NumberInputState, context: NumberInputActionContext, @@ -52,6 +43,17 @@ function stepValue( }[direction]; } +function getDirection(key: string) { + return { + ArrowUp: 'up', + ArrowDown: 'down', + PageUp: 'up', + PageDown: 'down', + Home: 'up', + End: 'down', + }[key] as StepDirection; +} + function handleBlur( state: State, context: NumberInputActionContext, @@ -75,9 +77,11 @@ function handleBlur( function handleStep( state: State, context: NumberInputActionContext, + event: React.PointerEvent, direction: StepDirection, - multiplier: number, ) { + const multiplier = event.shiftKey ? context.shiftMultiplier : 1; + const newValue = stepValue(state, context, direction, multiplier); const clampedValues = getClampedValues(newValue, context); @@ -92,8 +96,52 @@ function handleKeyDown( state: State, context: NumberInputActionContext, event: React.KeyboardEvent, + key: string, ) { - switch (event.key) { + const { min, max, shiftMultiplier } = context; + + const multiplier = event.shiftKey || key === 'PageUp' || key === 'PageDown' ? shiftMultiplier : 1; + + switch (key) { + case 'ArrowUp': + case 'ArrowDown': + case 'PageUp': + case 'PageDown': { + const direction = getDirection(key); + + const newValue = stepValue(state, context, direction, multiplier); + + const clampedValues = getClampedValues(newValue, context); + + return { + ...state, + ...clampedValues, + }; + } + + case 'Home': { + if (!isNumber(max)) { + break; + } + + return { + ...state, + value: max, + inputValue: String(max), + }; + } + + case 'End': { + if (!isNumber(min)) { + break; + } + return { + ...state, + value: min, + inputValue: String(min), + }; + } + default: break; } @@ -107,17 +155,15 @@ export function numberInputReducer( ): NumberInputState { const { type, context, event } = action; - const multiplier = getMultiplier(context, event); - switch (type) { case NumberInputActionTypes.blur: return handleBlur(state, context, event); case NumberInputActionTypes.increment: - return handleStep(state, context, 'up', multiplier); + return handleStep(state, context, event, 'up'); case NumberInputActionTypes.decrement: - return handleStep(state, context, 'down', multiplier); + return handleStep(state, context, event, 'down'); case NumberInputActionTypes.keyDown: - return handleKeyDown(state, context, event); + return handleKeyDown(state, context, event, action.key); default: return state; } From 4e2bc6a396924d4fbd3468f93a525acc599f9dd5 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 1 Sep 2023 11:05:43 +0800 Subject: [PATCH 05/14] Implement inputChange --- .../numberInputAction.types.ts | 2 +- .../numberInputReducer.test.ts | 102 ++++++++++++++++++ .../numberInputReducer.ts | 34 +++++- 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts index 52e679999495f9..70df0fb6861ee7 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -10,7 +10,7 @@ export const NumberInputActionTypes = { interface NumberInputBlurAction { type: typeof NumberInputActionTypes.blur; - event: React.FocusEvent; + event: React.FocusEvent; } interface NumberInputFocusAction { diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 0a5d2d7c9b4f5f..034047ed224f67 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -33,6 +33,108 @@ describe('numberInputReducer', () => { }); }); + describe('action: inputChange', () => { + it('value contains integers only', () => { + const state: NumberInputState = { + value: 0, + inputValue: '0', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.inputChange, + event: { + currentTarget: { + value: '1', + }, + } as React.ChangeEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(1); + expect(result.inputValue).to.equal('1'); + }); + + it('value contains invalid characters', () => { + const state: NumberInputState = { + value: 1, + inputValue: '1', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.inputChange, + event: { + currentTarget: { + value: '1a', + }, + } as React.ChangeEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(1); + expect(result.inputValue).to.equal('1'); + }); + + it('value is minus sign', () => { + const state: NumberInputState = { + value: -1, + inputValue: '-1', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.inputChange, + event: { + currentTarget: { + value: '-', + }, + } as React.ChangeEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(''); + expect(result.inputValue).to.equal('-'); + }); + + it('empty value', () => { + const state: NumberInputState = { + value: 1, + inputValue: '1', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.inputChange, + event: { + currentTarget: { + value: '', + }, + } as React.ChangeEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(''); + expect(result.inputValue).to.equal(''); + }); + }); + describe('action: increment', () => { it('increments the value', () => { const state: NumberInputState = { diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index 3837bb18820540..4ef29114668bb0 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -57,11 +57,11 @@ function getDirection(key: string) { function handleBlur( state: State, context: NumberInputActionContext, - event: React.FocusEvent, + event: React.FocusEvent, ) { const { getInputValueAsString } = context; - const parsedValue = getInputValueAsString(event.currentTarget.value); + const parsedValue = getInputValueAsString((event.currentTarget as HTMLInputElement).value); const intermediateValue = parsedValue === '' || parsedValue === '-' ? undefined : parseInt(parsedValue, 10); @@ -74,6 +74,34 @@ function handleBlur( }; } +function handleInputChange( + state: State, + context: NumberInputActionContext, + event: React.ChangeEvent, +) { + const { getInputValueAsString } = context; + + const parsedValue = getInputValueAsString((event.currentTarget as HTMLInputElement).value); + + if (parsedValue === '' || parsedValue === '-') { + return { + ...state, + inputValue: parsedValue, + value: '', + }; + } + + if (parsedValue.match(/^-?\d+?$/)) { + return { + ...state, + inputValue: parsedValue, + value: parseInt(parsedValue, 10), + }; + } + + return state; +} + function handleStep( state: State, context: NumberInputActionContext, @@ -158,6 +186,8 @@ export function numberInputReducer( switch (type) { case NumberInputActionTypes.blur: return handleBlur(state, context, event); + case NumberInputActionTypes.inputChange: + return handleInputChange(state, context, event); case NumberInputActionTypes.increment: return handleStep(state, context, event, 'up'); case NumberInputActionTypes.decrement: From fa876a8207fb2ad42eae97abeea942a5d2c4794f Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 1 Sep 2023 14:08:03 +0800 Subject: [PATCH 06/14] Remove unnecessary action types --- .../numberInputAction.types.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts index 70df0fb6861ee7..f09adbfeb01a4c 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -1,9 +1,7 @@ export const NumberInputActionTypes = { blur: 'numberInput:blur', - focus: 'numberInput:focus', inputChange: 'numberInput:inputChange', keyDown: 'numberInput:keyDown', - click: 'numberInput:click', increment: 'numberInput:increment', decrement: 'numberInput:decrement', } as const; @@ -13,11 +11,6 @@ interface NumberInputBlurAction { event: React.FocusEvent; } -interface NumberInputFocusAction { - type: typeof NumberInputActionTypes.focus; - event: React.FocusEvent; -} - interface NumberInputInputChangeAction { type: typeof NumberInputActionTypes.inputChange; event: React.ChangeEvent; @@ -29,11 +22,6 @@ interface NumberInputKeyDownAction { key: string; } -interface NumberInputClickAction { - type: typeof NumberInputActionTypes.click; - event: React.MouseEvent; -} - interface NumberInputIncrementAction { type: typeof NumberInputActionTypes.increment; // triggering a button with the keyboard fires a PointerEvent @@ -48,9 +36,7 @@ interface NumberInputDecrementAction { export type NumberInputAction = | NumberInputBlurAction - | NumberInputFocusAction | NumberInputInputChangeAction | NumberInputKeyDownAction - | NumberInputClickAction | NumberInputIncrementAction | NumberInputDecrementAction; From aa85dbd1171d6ecc4d2360f52d67b1be4c11d6cf Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 1 Sep 2023 15:30:05 +0800 Subject: [PATCH 07/14] Update blur action tests --- .../numberInputReducer.test.ts | 89 +++++++++++++++++-- .../numberInputReducer.ts | 8 +- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 034047ed224f67..a999ca3d12a615 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -6,30 +6,107 @@ import { getInputValueAsString as defaultGetInputValueAsString } from './useNumb describe('numberInputReducer', () => { describe('action: blur', () => { - it('snaps the inputValue based on min, max, and step', () => { + it('clamps the inputValue', () => { + const state: NumberInputState = { + value: 1, + inputValue: '1', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.blur, + event: { + currentTarget: { + value: '1', + }, + } as unknown as React.FocusEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(1); + expect(result.inputValue).to.equal('1'); + }); + + it('clamps the inputValue with a custom step', () => { const state: NumberInputState = { value: 0, - inputValue: '2', + inputValue: '0', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.blur, + event: { + currentTarget: { + value: '3', + }, + } as unknown as React.FocusEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + step: 4, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(4); + expect(result.inputValue).to.equal('4'); + }); + + it('clamps the inputValue within min if min is set', () => { + const state: NumberInputState = { + value: 0, + inputValue: '0', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.blur, + event: { + currentTarget: { + value: '0', + }, + } as unknown as React.FocusEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + min: 5, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(5); + expect(result.inputValue).to.equal('5'); + }); + + it('clamps the inputValue within max if max is set', () => { + const state: NumberInputState = { + value: 10, + inputValue: '10', }; const action: NumberInputReducerAction = { type: NumberInputActionTypes.blur, event: { currentTarget: { - value: '2', + value: '10', }, } as unknown as React.FocusEvent, context: { - step: 3, getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, + max: 9, }, }; const result = numberInputReducer(state, action); - expect(result.value).to.equal(3); - expect(result.inputValue).to.equal('3'); + expect(result.value).to.equal(9); + expect(result.inputValue).to.equal('9'); }); }); diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index 4ef29114668bb0..7667811881fa0c 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -8,15 +8,15 @@ import { NumberInputActionTypes } from './numberInputAction.types'; import { clamp, isNumber } from './utils'; // extracted from handleValueChange -function getClampedValues(oldValue: number | undefined, context: NumberInputActionContext) { +function getClampedValues(rawValue: number | undefined, context: NumberInputActionContext) { const { min, max, step } = context; - const newValue = oldValue === undefined ? undefined : clamp(oldValue, min, max, step); + const clampedValue = rawValue === undefined ? undefined : clamp(rawValue, min, max, step); - const newInputValue = newValue === undefined ? '' : String(newValue); + const newInputValue = clampedValue === undefined ? '' : String(clampedValue); return { - value: newValue, + value: clampedValue, inputValue: newInputValue, }; } From 8ae322ecde01a0a024a32e1e6493677141a5c25b Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 1 Sep 2023 15:32:28 +0800 Subject: [PATCH 08/14] Update tests --- .../numberInputReducer.test.ts | 43 +++++-------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index a999ca3d12a615..e26beb81d5faac 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -406,7 +406,6 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, event: { - key: 'ArrowUp', shiftKey: true, } as React.KeyboardEvent, key: 'ArrowUp', @@ -430,9 +429,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'ArrowUp', - } as React.KeyboardEvent, + event: {} as any, key: 'ArrowUp', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -457,9 +454,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'ArrowDown', - } as React.KeyboardEvent, + event: {} as any, key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -481,9 +476,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'ArrowDown', - } as React.KeyboardEvent, + event: {} as any, key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -507,7 +500,6 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, event: { - key: 'ArrowDown', shiftKey: true, } as React.KeyboardEvent, key: 'ArrowDown', @@ -531,10 +523,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'ArrowDown', - shiftKey: true, - } as React.KeyboardEvent, + event: {} as any, key: 'ArrowDown', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -559,9 +548,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'PageUp', - } as React.KeyboardEvent, + event: {} as any, key: 'PageUp', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -585,9 +572,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'PageDown', - } as React.KeyboardEvent, + event: {} as any, key: 'PageDown', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -611,9 +596,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'Home', - } as React.KeyboardEvent, + event: {} as any, key: 'Home', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -636,9 +619,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'Home', - } as React.KeyboardEvent, + event: {} as any, key: 'Home', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -662,9 +643,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'End', - } as React.KeyboardEvent, + event: {} as any, key: 'End', context: { getInputValueAsString: defaultGetInputValueAsString, @@ -687,9 +666,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.keyDown, - event: { - key: 'End', - } as React.KeyboardEvent, + event: {} as any, key: 'End', context: { getInputValueAsString: defaultGetInputValueAsString, From 9c1d96f1c43f6587d25e3552cb560fcda62d58ad Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 1 Sep 2023 18:30:20 +0800 Subject: [PATCH 09/14] Blur empty value --- .../numberInputReducer.test.ts | 25 +++++++++++++++++++ .../numberInputReducer.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index e26beb81d5faac..736e38f79e85fe 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -108,6 +108,31 @@ describe('numberInputReducer', () => { expect(result.value).to.equal(9); expect(result.inputValue).to.equal('9'); }); + + it('empty value', () => { + const state: NumberInputState = { + value: '', + inputValue: '', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.blur, + event: { + currentTarget: { + value: '', + }, + } as unknown as React.FocusEvent, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result.value).to.equal(''); + expect(result.inputValue).to.equal(''); + }); }); describe('action: inputChange', () => { diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index 7667811881fa0c..b028ef074c9443 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -11,7 +11,7 @@ import { clamp, isNumber } from './utils'; function getClampedValues(rawValue: number | undefined, context: NumberInputActionContext) { const { min, max, step } = context; - const clampedValue = rawValue === undefined ? undefined : clamp(rawValue, min, max, step); + const clampedValue = rawValue === undefined ? '' : clamp(rawValue, min, max, step); const newInputValue = clampedValue === undefined ? '' : String(clampedValue); From 25062b2bd1d125fc6dc8595fc6b66b261eae3ed5 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 13 Oct 2023 15:05:21 +0800 Subject: [PATCH 10/14] Refactor away events --- .../numberInputAction.types.ts | 40 +- .../numberInputReducer.test.ts | 476 ++++-------------- .../numberInputReducer.ts | 104 ++-- .../unstable_useNumberInput/useNumberInput.ts | 2 + 4 files changed, 146 insertions(+), 476 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts index f09adbfeb01a4c..955a6dcb669a29 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -1,42 +1,44 @@ export const NumberInputActionTypes = { - blur: 'numberInput:blur', + clamp: 'numberInput:clamp', inputChange: 'numberInput:inputChange', - keyDown: 'numberInput:keyDown', increment: 'numberInput:increment', decrement: 'numberInput:decrement', + decrementToMin: 'numberInput:decrementToMin', + incrementToMax: 'numberInput:incrementToMax', } as const; -interface NumberInputBlurAction { - type: typeof NumberInputActionTypes.blur; - event: React.FocusEvent; +interface NumberInputClampAction { + type: typeof NumberInputActionTypes.clamp; + inputValue: string; } interface NumberInputInputChangeAction { type: typeof NumberInputActionTypes.inputChange; - event: React.ChangeEvent; -} - -interface NumberInputKeyDownAction { - type: typeof NumberInputActionTypes.keyDown; - event: React.KeyboardEvent; - key: string; + inputValue: string; } interface NumberInputIncrementAction { type: typeof NumberInputActionTypes.increment; - // triggering a button with the keyboard fires a PointerEvent - event: React.PointerEvent; + shiftKey: boolean; } interface NumberInputDecrementAction { type: typeof NumberInputActionTypes.decrement; - // triggering a button with the keyboard fires a PointerEvent - event: React.PointerEvent; + shiftKey: boolean; +} + +interface NumberInputIncrementToMaxAction { + type: typeof NumberInputActionTypes.incrementToMax; +} + +interface NumberInputDecrementToMinAction { + type: typeof NumberInputActionTypes.decrementToMin; } export type NumberInputAction = - | NumberInputBlurAction + | NumberInputClampAction | NumberInputInputChangeAction - | NumberInputKeyDownAction | NumberInputIncrementAction - | NumberInputDecrementAction; + | NumberInputDecrementAction + | NumberInputIncrementToMaxAction + | NumberInputDecrementToMinAction; diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 736e38f79e85fe..61fbf9b10a6d60 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -5,7 +5,7 @@ import { numberInputReducer } from './numberInputReducer'; import { getInputValueAsString as defaultGetInputValueAsString } from './useNumberInput'; describe('numberInputReducer', () => { - describe('action: blur', () => { + describe('action: clamp', () => { it('clamps the inputValue', () => { const state: NumberInputState = { value: 1, @@ -13,12 +13,8 @@ describe('numberInputReducer', () => { }; const action: NumberInputReducerAction = { - type: NumberInputActionTypes.blur, - event: { - currentTarget: { - value: '1', - }, - } as unknown as React.FocusEvent, + type: NumberInputActionTypes.clamp, + inputValue: '1', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -38,12 +34,8 @@ describe('numberInputReducer', () => { }; const action: NumberInputReducerAction = { - type: NumberInputActionTypes.blur, - event: { - currentTarget: { - value: '3', - }, - } as unknown as React.FocusEvent, + type: NumberInputActionTypes.clamp, + inputValue: '3', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -64,12 +56,8 @@ describe('numberInputReducer', () => { }; const action: NumberInputReducerAction = { - type: NumberInputActionTypes.blur, - event: { - currentTarget: { - value: '0', - }, - } as unknown as React.FocusEvent, + type: NumberInputActionTypes.clamp, + inputValue: '0', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -90,12 +78,8 @@ describe('numberInputReducer', () => { }; const action: NumberInputReducerAction = { - type: NumberInputActionTypes.blur, - event: { - currentTarget: { - value: '10', - }, - } as unknown as React.FocusEvent, + type: NumberInputActionTypes.clamp, + inputValue: '10', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -116,12 +100,8 @@ describe('numberInputReducer', () => { }; const action: NumberInputReducerAction = { - type: NumberInputActionTypes.blur, - event: { - currentTarget: { - value: '', - }, - } as unknown as React.FocusEvent, + type: NumberInputActionTypes.clamp, + inputValue: '', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -144,11 +124,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.inputChange, - event: { - currentTarget: { - value: '1', - }, - } as React.ChangeEvent, + inputValue: '1', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -169,11 +145,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.inputChange, - event: { - currentTarget: { - value: '1a', - }, - } as React.ChangeEvent, + inputValue: '1a', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -194,11 +166,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.inputChange, - event: { - currentTarget: { - value: '-', - }, - } as React.ChangeEvent, + inputValue: '-', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -219,11 +187,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.inputChange, - event: { - currentTarget: { - value: '', - }, - } as React.ChangeEvent, + inputValue: '', context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -246,7 +210,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.increment, - event: {} as any, + shiftKey: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -267,7 +231,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.increment, - event: {} as any, + shiftKey: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -289,9 +253,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.increment, - event: { - shiftKey: true, - } as React.PointerEvent, + shiftKey: true, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -315,7 +277,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.decrement, - event: {} as any, + shiftKey: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -336,7 +298,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.decrement, - event: {} as any, + shiftKey: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -358,9 +320,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.decrement, - event: { - shiftKey: true, - } as React.PointerEvent, + shiftKey: true, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -375,335 +335,87 @@ describe('numberInputReducer', () => { }); }); - describe('action: keyDown', () => { - describe('key: ArrowUp', () => { - it('increments', () => { - const state: NumberInputState = { - value: 10, - inputValue: '10', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'ArrowUp', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(11); - expect(result.inputValue).to.equal('11'); - }); - - it('increments based on a custom step', () => { - const state: NumberInputState = { - value: 10, - inputValue: '10', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'ArrowUp', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - step: 5, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(15); - expect(result.inputValue).to.equal('15'); - }); - - it('increments and applies shiftMultiplier when Shift is held', () => { - const state: NumberInputState = { - value: 10, - inputValue: '10', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: { - shiftKey: true, - } as React.KeyboardEvent, - key: 'ArrowUp', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(20); - expect(result.inputValue).to.equal('20'); - }); - - it('sets value to min when there is no value', () => { - const state: NumberInputState = { - value: '', - inputValue: '', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'ArrowUp', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - min: -20, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(-20); - expect(result.inputValue).to.equal('-20'); - }); - }); + describe('action: incrementToMax', () => { + it('sets the value to max if max is set', () => { + const state: NumberInputState = { + value: 20, + inputValue: '20', + }; - describe('key: ArrowDown', () => { - it('decrements', () => { - const state: NumberInputState = { - value: 10, - inputValue: '10', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'ArrowDown', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(9); - expect(result.inputValue).to.equal('9'); - }); - - it('decrements based on a custom step', () => { - const state: NumberInputState = { - value: 12, - inputValue: '12', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'ArrowDown', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - step: 4, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(8); - expect(result.inputValue).to.equal('8'); - }); - - it('decrements and applies shiftMultiplier when Shift is held', () => { - const state: NumberInputState = { - value: 35, - inputValue: '35', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: { - shiftKey: true, - } as React.KeyboardEvent, - key: 'ArrowDown', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(25); - expect(result.inputValue).to.equal('25'); - }); - - it('sets value to max when there is no value', () => { - const state: NumberInputState = { - value: '', - inputValue: '', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'ArrowDown', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - max: 99, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(99); - expect(result.inputValue).to.equal('99'); - }); - }); + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.incrementToMax, + context: { + getInputValueAsString: defaultGetInputValueAsString, + max: 99, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); - describe('key: PageUp', () => { - it('increments and applies shiftMultiplier', () => { - const state: NumberInputState = { - value: 10, - inputValue: '10', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'PageUp', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(20); - expect(result.inputValue).to.equal('20'); - }); + expect(result.value).to.equal(99); + expect(result.inputValue).to.equal('99'); }); - describe('key: PageDown', () => { - it('decrements and applies shiftMultiplier', () => { - const state: NumberInputState = { - value: 44, - inputValue: '44', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'PageDown', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(34); - expect(result.inputValue).to.equal('34'); - }); + it('does not change the state if max is not set', () => { + const state: NumberInputState = { + value: 20, + inputValue: '20', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.incrementToMax, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result).to.equal(state); }); + }); + + describe('action: decrementToMin', () => { + it('sets the value to min if min is set', () => { + const state: NumberInputState = { + value: 20, + inputValue: '20', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.decrementToMin, + context: { + getInputValueAsString: defaultGetInputValueAsString, + min: 1, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); - describe('key: Home', () => { - it('increments to max if max is set', () => { - const state: NumberInputState = { - value: 44, - inputValue: '44', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'Home', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - max: 99, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(99); - expect(result.inputValue).to.equal('99'); - }); - - it('does not change the state if max is not set', () => { - const state: NumberInputState = { - value: 44, - inputValue: '44', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'Home', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(44); - expect(result.inputValue).to.equal('44'); - }); + expect(result.value).to.equal(1); + expect(result.inputValue).to.equal('1'); }); - describe('key: End', () => { - it('decrements to min if min is set', () => { - const state: NumberInputState = { - value: 44, - inputValue: '44', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'End', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - min: 1, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(1); - expect(result.inputValue).to.equal('1'); - }); - - it('does not change the state if min is not set', () => { - const state: NumberInputState = { - value: 44, - inputValue: '44', - }; - - const action: NumberInputReducerAction = { - type: NumberInputActionTypes.keyDown, - event: {} as any, - key: 'End', - context: { - getInputValueAsString: defaultGetInputValueAsString, - shiftMultiplier: 10, - }, - }; - - const result = numberInputReducer(state, action); - - expect(result.value).to.equal(44); - expect(result.inputValue).to.equal('44'); - }); + it('does not change the state if min is not set', () => { + const state: NumberInputState = { + value: 20, + inputValue: '20', + }; + + const action: NumberInputReducerAction = { + type: NumberInputActionTypes.decrementToMin, + context: { + getInputValueAsString: defaultGetInputValueAsString, + shiftMultiplier: 10, + }, + }; + + const result = numberInputReducer(state, action); + + expect(result).to.equal(state); }); }); }); diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index b028ef074c9443..65fcf33a2de384 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -43,25 +43,14 @@ function stepValue( }[direction]; } -function getDirection(key: string) { - return { - ArrowUp: 'up', - ArrowDown: 'down', - PageUp: 'up', - PageDown: 'down', - Home: 'up', - End: 'down', - }[key] as StepDirection; -} - -function handleBlur( +function handleClamp( state: State, context: NumberInputActionContext, - event: React.FocusEvent, + inputValue: string, ) { const { getInputValueAsString } = context; - const parsedValue = getInputValueAsString((event.currentTarget as HTMLInputElement).value); + const parsedValue = getInputValueAsString(inputValue); const intermediateValue = parsedValue === '' || parsedValue === '-' ? undefined : parseInt(parsedValue, 10); @@ -77,11 +66,11 @@ function handleBlur( function handleInputChange( state: State, context: NumberInputActionContext, - event: React.ChangeEvent, + inputValue: string, ) { const { getInputValueAsString } = context; - const parsedValue = getInputValueAsString((event.currentTarget as HTMLInputElement).value); + const parsedValue = getInputValueAsString(inputValue); if (parsedValue === '' || parsedValue === '-') { return { @@ -102,13 +91,15 @@ function handleInputChange( return state; } +// use this for ArrowUp, ArrowDown +// use this with shiftKey: true for PageUp, PageDown function handleStep( state: State, context: NumberInputActionContext, - event: React.PointerEvent, + shiftKey: boolean, direction: StepDirection, ) { - const multiplier = event.shiftKey ? context.shiftMultiplier : 1; + const multiplier = shiftKey ? context.shiftMultiplier : 1; const newValue = stepValue(state, context, direction, multiplier); @@ -120,80 +111,43 @@ function handleStep( }; } -function handleKeyDown( +function handleToMinOrMax( state: State, context: NumberInputActionContext, - event: React.KeyboardEvent, - key: string, + to: 'min' | 'max', ) { - const { min, max, shiftMultiplier } = context; - - const multiplier = event.shiftKey || key === 'PageUp' || key === 'PageDown' ? shiftMultiplier : 1; - - switch (key) { - case 'ArrowUp': - case 'ArrowDown': - case 'PageUp': - case 'PageDown': { - const direction = getDirection(key); - - const newValue = stepValue(state, context, direction, multiplier); - - const clampedValues = getClampedValues(newValue, context); - - return { - ...state, - ...clampedValues, - }; - } - - case 'Home': { - if (!isNumber(max)) { - break; - } - - return { - ...state, - value: max, - inputValue: String(max), - }; - } - - case 'End': { - if (!isNumber(min)) { - break; - } - return { - ...state, - value: min, - inputValue: String(min), - }; - } + const newValue = context[to]; - default: - break; + if (!isNumber(newValue)) { + return state; } - return state; + return { + ...state, + value: newValue, + inputValue: String(newValue), + }; } export function numberInputReducer( state: NumberInputState, action: NumberInputReducerAction, ): NumberInputState { - const { type, context, event } = action; + const { type, context } = action; switch (type) { - case NumberInputActionTypes.blur: - return handleBlur(state, context, event); + case NumberInputActionTypes.clamp: + return handleClamp(state, context, action.inputValue); case NumberInputActionTypes.inputChange: - return handleInputChange(state, context, event); + return handleInputChange(state, context, action.inputValue); case NumberInputActionTypes.increment: - return handleStep(state, context, event, 'up'); + return handleStep(state, context, action.shiftKey, 'up'); case NumberInputActionTypes.decrement: - return handleStep(state, context, event, 'down'); - case NumberInputActionTypes.keyDown: - return handleKeyDown(state, context, event, action.key); + return handleStep(state, context, action.shiftKey, 'down'); + case NumberInputActionTypes.incrementToMax: + return handleToMinOrMax(state, context, 'max'); + case NumberInputActionTypes.decrementToMin: + return handleToMinOrMax(state, context, 'min'); default: return state; } diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index cbc359afb97b3d..19f0e66d3c3368 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -158,6 +158,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI otherHandlers.onInputChange?.(event); + // TODO: event.currentTarget.value will be passed straight into the InputChange action const val = getInputValueAsString(event.currentTarget.value); if (val === '' || val === '-') { @@ -174,6 +175,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI const handleBlur = (otherHandlers: Record | undefined>) => (event: React.FocusEvent) => { + // TODO: event.currentTarget.value will be passed straight into the Blur action, or just pass inputValue from state const val = getInputValueAsString(event.currentTarget.value); otherHandlers.onBlur?.(event); From 7a6ce95dc49c73cdc950cc2a7e8dae69b6d47c44 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 13 Oct 2023 15:07:54 +0800 Subject: [PATCH 11/14] Rename shiftKey to applyMultiplier --- .../numberInputAction.types.ts | 4 ++-- .../numberInputReducer.test.ts | 12 ++++++------ .../unstable_useNumberInput/numberInputReducer.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts index 955a6dcb669a29..1373e6d9924c0c 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts @@ -19,12 +19,12 @@ interface NumberInputInputChangeAction { interface NumberInputIncrementAction { type: typeof NumberInputActionTypes.increment; - shiftKey: boolean; + applyMultiplier: boolean; } interface NumberInputDecrementAction { type: typeof NumberInputActionTypes.decrement; - shiftKey: boolean; + applyMultiplier: boolean; } interface NumberInputIncrementToMaxAction { diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts index 61fbf9b10a6d60..461b40901c64f7 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts @@ -210,7 +210,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.increment, - shiftKey: false, + applyMultiplier: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -231,7 +231,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.increment, - shiftKey: false, + applyMultiplier: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -253,7 +253,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.increment, - shiftKey: true, + applyMultiplier: true, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -277,7 +277,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.decrement, - shiftKey: false, + applyMultiplier: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -298,7 +298,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.decrement, - shiftKey: false, + applyMultiplier: false, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, @@ -320,7 +320,7 @@ describe('numberInputReducer', () => { const action: NumberInputReducerAction = { type: NumberInputActionTypes.decrement, - shiftKey: true, + applyMultiplier: true, context: { getInputValueAsString: defaultGetInputValueAsString, shiftMultiplier: 10, diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index 65fcf33a2de384..d7aeed79776912 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -91,15 +91,15 @@ function handleInputChange( return state; } -// use this for ArrowUp, ArrowDown -// use this with shiftKey: true for PageUp, PageDown +// use this for ArrowUp, ArrowDown, button clicks +// use this with applyMultiplier: true for PageUp, PageDown, button shift-clicks function handleStep( state: State, context: NumberInputActionContext, - shiftKey: boolean, + applyMultiplier: boolean, direction: StepDirection, ) { - const multiplier = shiftKey ? context.shiftMultiplier : 1; + const multiplier = applyMultiplier ? context.shiftMultiplier : 1; const newValue = stepValue(state, context, direction, multiplier); @@ -141,9 +141,9 @@ export function numberInputReducer( case NumberInputActionTypes.inputChange: return handleInputChange(state, context, action.inputValue); case NumberInputActionTypes.increment: - return handleStep(state, context, action.shiftKey, 'up'); + return handleStep(state, context, action.applyMultiplier, 'up'); case NumberInputActionTypes.decrement: - return handleStep(state, context, action.shiftKey, 'down'); + return handleStep(state, context, action.applyMultiplier, 'down'); case NumberInputActionTypes.incrementToMax: return handleToMinOrMax(state, context, 'max'); case NumberInputActionTypes.decrementToMin: From 89310d837e1f56fee3e76542649dc1afe61d43ed Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 17 Oct 2023 16:17:27 +0800 Subject: [PATCH 12/14] Update some naming --- .../numberInputReducer.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index d7aeed79776912..f3fbc18ba9fbfd 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -50,10 +50,12 @@ function handleClamp( ) { const { getInputValueAsString } = context; - const parsedValue = getInputValueAsString(inputValue); + const numberValueAsString = getInputValueAsString(inputValue); const intermediateValue = - parsedValue === '' || parsedValue === '-' ? undefined : parseInt(parsedValue, 10); + numberValueAsString === '' || numberValueAsString === '-' + ? undefined + : parseInt(numberValueAsString, 10); const clampedValues = getClampedValues(intermediateValue, context); @@ -70,21 +72,21 @@ function handleInputChange( ) { const { getInputValueAsString } = context; - const parsedValue = getInputValueAsString(inputValue); + const numberValueAsString = getInputValueAsString(inputValue); - if (parsedValue === '' || parsedValue === '-') { + if (numberValueAsString === '' || numberValueAsString === '-') { return { ...state, - inputValue: parsedValue, + inputValue: numberValueAsString, value: '', }; } - if (parsedValue.match(/^-?\d+?$/)) { + if (numberValueAsString.match(/^-?\d+?$/)) { return { ...state, - inputValue: parsedValue, - value: parseInt(parsedValue, 10), + inputValue: numberValueAsString, + value: parseInt(numberValueAsString, 10), }; } From 12b05881749eef66336e73cdf59d9557e4af5083 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 17 Oct 2023 16:22:51 +0800 Subject: [PATCH 13/14] Update Direction type --- .../numberInputReducer.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index f3fbc18ba9fbfd..a80d37b3da521b 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -2,11 +2,12 @@ import { NumberInputActionContext, NumberInputReducerAction, NumberInputState, - StepDirection, } from './useNumberInput.types'; import { NumberInputActionTypes } from './numberInputAction.types'; import { clamp, isNumber } from './utils'; +type Direction = '+' | '-'; + // extracted from handleValueChange function getClampedValues(rawValue: number | undefined, context: NumberInputActionContext) { const { min, max, step } = context; @@ -24,7 +25,7 @@ function getClampedValues(rawValue: number | undefined, context: NumberInputActi function stepValue( state: NumberInputState, context: NumberInputActionContext, - direction: StepDirection, + direction: Direction, multiplier: number, ) { const { value } = state; @@ -32,14 +33,14 @@ function stepValue( if (isNumber(value)) { return { - up: value + (step ?? 1) * multiplier, - down: value - (step ?? 1) * multiplier, + '+': value + (step ?? 1) * multiplier, + '-': value - (step ?? 1) * multiplier, }[direction]; } return { - up: min ?? 0, - down: max ?? 0, + '+': min ?? 0, + '-': max ?? 0, }[direction]; } @@ -98,8 +99,8 @@ function handleInputChange( function handleStep( state: State, context: NumberInputActionContext, + direction: Direction, applyMultiplier: boolean, - direction: StepDirection, ) { const multiplier = applyMultiplier ? context.shiftMultiplier : 1; @@ -143,9 +144,9 @@ export function numberInputReducer( case NumberInputActionTypes.inputChange: return handleInputChange(state, context, action.inputValue); case NumberInputActionTypes.increment: - return handleStep(state, context, action.applyMultiplier, 'up'); + return handleStep(state, context, '+', action.applyMultiplier); case NumberInputActionTypes.decrement: - return handleStep(state, context, action.applyMultiplier, 'down'); + return handleStep(state, context, '-', action.applyMultiplier); case NumberInputActionTypes.incrementToMax: return handleToMinOrMax(state, context, 'max'); case NumberInputActionTypes.decrementToMin: From fe53e22a208cde56f6155b01eb0bd54608b8d39b Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 3 Nov 2023 15:25:01 +0800 Subject: [PATCH 14/14] Revert "Update Direction type" 12b05881749eef66336e73cdf59d9557e4af5083 --- .../numberInputReducer.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts index a80d37b3da521b..f3fbc18ba9fbfd 100644 --- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts +++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts @@ -2,12 +2,11 @@ import { NumberInputActionContext, NumberInputReducerAction, NumberInputState, + StepDirection, } from './useNumberInput.types'; import { NumberInputActionTypes } from './numberInputAction.types'; import { clamp, isNumber } from './utils'; -type Direction = '+' | '-'; - // extracted from handleValueChange function getClampedValues(rawValue: number | undefined, context: NumberInputActionContext) { const { min, max, step } = context; @@ -25,7 +24,7 @@ function getClampedValues(rawValue: number | undefined, context: NumberInputActi function stepValue( state: NumberInputState, context: NumberInputActionContext, - direction: Direction, + direction: StepDirection, multiplier: number, ) { const { value } = state; @@ -33,14 +32,14 @@ function stepValue( if (isNumber(value)) { return { - '+': value + (step ?? 1) * multiplier, - '-': value - (step ?? 1) * multiplier, + up: value + (step ?? 1) * multiplier, + down: value - (step ?? 1) * multiplier, }[direction]; } return { - '+': min ?? 0, - '-': max ?? 0, + up: min ?? 0, + down: max ?? 0, }[direction]; } @@ -99,8 +98,8 @@ function handleInputChange( function handleStep( state: State, context: NumberInputActionContext, - direction: Direction, applyMultiplier: boolean, + direction: StepDirection, ) { const multiplier = applyMultiplier ? context.shiftMultiplier : 1; @@ -144,9 +143,9 @@ export function numberInputReducer( case NumberInputActionTypes.inputChange: return handleInputChange(state, context, action.inputValue); case NumberInputActionTypes.increment: - return handleStep(state, context, '+', action.applyMultiplier); + return handleStep(state, context, action.applyMultiplier, 'up'); case NumberInputActionTypes.decrement: - return handleStep(state, context, '-', action.applyMultiplier); + return handleStep(state, context, action.applyMultiplier, 'down'); case NumberInputActionTypes.incrementToMax: return handleToMinOrMax(state, context, 'max'); case NumberInputActionTypes.decrementToMin: