Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[NumberInput] Hold stepper buttons to continuously change value #161

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
'use client';
import * as React from 'react';
import MuiError from '@mui/internal-babel-macros/MuiError.macro';
import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils';
import {
unstable_useForkRef as useForkRef,
unstable_useId as useId,
unstable_useTimeout as useTimeout,
} from '@mui/utils';
import { extractEventHandlers } from '../utils/extractEventHandlers';
import { MuiCancellableEvent } from '../utils/MuiCancellableEvent';
import { useControllableReducer } from '../utils/useControllableReducer';
Expand Down Expand Up @@ -82,6 +86,8 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
}
}, []);

const holdTimer = useTimeout();

const inputRef = React.useRef<HTMLInputElement>(null);
const handleInputRef = useForkRef(inputRef, inputRefProp, handleInputRefWarning);

Expand Down Expand Up @@ -400,12 +406,40 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
};
};

const handleStepperButtonMouseDown = (event: React.PointerEvent) => {
event.preventDefault();
const recurse = (payload: {
actionType: typeof NumberInputActionTypes.increment | typeof NumberInputActionTypes.decrement;
event: React.PointerEvent;
}) => {
const { actionType, event } = payload;

if (inputRef.current) {
inputRef.current.focus();
}
holdTimer.start(100, () => {
dispatch({
type: actionType,
event,
applyMultiplier: !!event.shiftKey,
});
recurse({ actionType, event });
});
};

const handleStepperButtonMouseDown =
(direction: StepDirection) => (event: React.PointerEvent) => {
event.preventDefault();

if (inputRef.current) {
inputRef.current.focus();
}

const actionType = {
up: NumberInputActionTypes.increment,
down: NumberInputActionTypes.decrement,
}[direction];

recurse({ actionType, event });
};

const handleStepperButtonMouseUp = () => {
holdTimer.clear();
};

const stepperButtonCommonProps = {
Expand All @@ -424,7 +458,8 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
...stepperButtonCommonProps,
disabled: isIncrementDisabled,
'aria-disabled': isIncrementDisabled,
onMouseDown: handleStepperButtonMouseDown,
onMouseDown: handleStepperButtonMouseDown('up'),
onMouseUp: handleStepperButtonMouseUp,
onClick: handleStep('up'),
};
};
Expand All @@ -440,7 +475,8 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
...stepperButtonCommonProps,
disabled: isDecrementDisabled,
'aria-disabled': isDecrementDisabled,
onMouseDown: handleStepperButtonMouseDown,
onMouseDown: handleStepperButtonMouseDown('down'),
onMouseUp: handleStepperButtonMouseUp,
onClick: handleStep('down'),
};
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, screen, fireEvent } from '@mui/internal-test-utils';
import { unstable_useNumberInput as useNumberInput } from '@mui/base/unstable_useNumberInput';

describe('useNumberInput2', () => {
const { clock, render } = createRenderer();

describe('press and hold', () => {
clock.withFakeTimers();

it('should call onChange continuously', () => {
const handleChange = spy();
function NumberInput(props: { defaultValue: number }) {
const { getInputProps, getIncrementButtonProps } = useNumberInput({
...props,
onChange: handleChange,
});

return (
<div role="group">
<button {...getIncrementButtonProps()} data-testid="incrementBtn" />
<input data-testid="test-input" {...getInputProps()} />
</div>
);
}
render(<NumberInput defaultValue={0} />);

const incrementBtn = screen.getByTestId('incrementBtn');

fireEvent.mouseDown(incrementBtn); // onChange x1

clock.tick(100); // onChange x2
clock.tick(100); // onChange x3
clock.tick(100); // onChange x4
clock.runToLast();
Comment on lines +34 to +37
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clock.tick(300) doesn't work because if I don't run the first timer the subsequent ones will never be created

2nd try: I thought this was needed to work:

clock.tick(100)
clock.runToLast();
clock.tick(100)
clock.runToLast();
clock.tick(100)
clock.runToLast();

because I thought if I didn't run it after each 100ms, the timer would get cleared?


expect(handleChange.callCount).to.equal(4);
});
});
});
Loading