Skip to content
Merged
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
1 change: 1 addition & 0 deletions UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f

- `ResourceList.Item` opens url in new tab if command or control keys are pressed during click ([#690](https://github.com/Shopify/polaris-react/pull/690))
- Added `primaryAction` prop to `SkeletonPage` ([#488](https://github.com/Shopify/polaris-react/pull/488))
- Added support for press-and-hold to increment and decrement value in a type="number" `TextField` ([#573](https://github.com/Shopify/polaris-react/pull/573)) (thanks to [@andrewpye](https://github.com/andrewpye) for the [original issue](https://github.com/Shopify/polaris-react/issues/420))

### Bug fixes

Expand Down
26 changes: 25 additions & 1 deletion src/components/TextField/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default class TextField extends React.PureComponent<Props, State> {
}

private input: HTMLElement;
private buttonPressTimer: number;

constructor(props: Props) {
super(props);
Expand Down Expand Up @@ -205,7 +206,11 @@ export default class TextField extends React.PureComponent<Props, State> {

const spinnerMarkup =
type === 'number' && !disabled ? (
<Spinner onChange={this.handleNumberChange} />
<Spinner
onChange={this.handleNumberChange}
onMouseDown={this.handleButtonPress}
onMouseUp={this.handleButtonRelease}
/>
) : null;

const style = multiline && height ? {height} : null;
Expand Down Expand Up @@ -380,6 +385,25 @@ export default class TextField extends React.PureComponent<Props, State> {
private handleClick() {
this.input.focus();
}

@autobind
private handleButtonPress(onChange: Function) {
const minInterval = 50;
const decrementBy = 10;
let interval = 200;

const onChangeInterval = () => {
if (interval > minInterval) interval -= decrementBy;
onChange();
this.buttonPressTimer = window.setTimeout(onChangeInterval, interval);
};
this.buttonPressTimer = window.setTimeout(onChangeInterval, interval);
}

@autobind
private handleButtonRelease() {
clearTimeout(this.buttonPressTimer);
}
}

function normalizeAutoComplete(autoComplete?: boolean) {
Expand Down
17 changes: 16 additions & 1 deletion src/components/TextField/components/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ import * as styles from '../../TextField.scss';
export interface Props {
onChange(delta: number): void;
onClick?(): void;
onMouseDown(onChange: Function): void;
onMouseUp(): void;
}

export default function Spinner({onChange, onClick}: Props) {
export default function Spinner({
onChange,
onClick,
onMouseDown,
onMouseUp,
}: Props) {
function handleStep(step: number) {
return () => onChange(step);
}

function handleMouseDown(onChange: Function) {
return () => onMouseDown(onChange);
}

return (
<div className={styles.Spinner} onClick={onClick} aria-hidden>
<div
role="button"
className={styles.Segment}
tabIndex={-1}
onClick={handleStep(1)}
onMouseDown={handleMouseDown(handleStep(1))}
onMouseUp={onMouseUp}
>
<div className={styles.SpinnerIcon}>
<Icon source="caretUp" />
Expand All @@ -31,6 +44,8 @@ export default function Spinner({onChange, onClick}: Props) {
className={styles.Segment}
tabIndex={-1}
onClick={handleStep(-1)}
onMouseDown={handleMouseDown(handleStep(-1))}
onMouseUp={onMouseUp}
>
<div className={styles.SpinnerIcon}>
<Icon source="caretDown" />
Expand Down
73 changes: 73 additions & 0 deletions src/components/TextField/components/Spinner/tests/Spinner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';
import {shallow} from 'enzyme';
import {noop} from '@shopify/javascript-utilities/other';
import Spinner from '../Spinner';

describe('<Spinner />', () => {
describe('onChange', () => {
it('adds a change callback', () => {
const spy = jest.fn();
const spinner = shallow(
<Spinner onChange={spy} onMouseDown={noop} onMouseUp={noop} />,
);
spinner
.find('[role="button"]')
.first()
.simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(1);
});
});

describe('onClick', () => {
it('adds a click callback', () => {
const spy = jest.fn();
const spinner = shallow(
<Spinner
onClick={spy}
onChange={noop}
onMouseDown={noop}
onMouseUp={noop}
/>,
);
spinner.simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
});

describe('onMouseDown', () => {
it('adds a mouse down callback which calls the change callback', () => {
const mouseDownSpy = jest.fn((callback) => {
callback();
});
const changeSpy = jest.fn();
const spinner = shallow(
<Spinner
onChange={changeSpy}
onMouseDown={mouseDownSpy}
onMouseUp={noop}
/>,
);
spinner
.find('[role="button"]')
.first()
.simulate('mousedown');
expect(mouseDownSpy).toHaveBeenCalledTimes(1);
expect(changeSpy).toHaveBeenCalledTimes(1);
});
});

describe('onMouseUp', () => {
it('adds a mouse up callback', () => {
const spy = jest.fn();
const spinner = shallow(
<Spinner onChange={noop} onMouseDown={noop} onMouseUp={spy} />,
);
spinner
.find('[role="button"]')
.first()
.simulate('mouseup');
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
46 changes: 46 additions & 0 deletions src/components/TextField/tests/TextField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,52 @@ describe('<TextField />', () => {
.simulate('click');
expect(spy).toHaveBeenCalledWith('1.976', 'MyTextField');
});

it('decrements on mouse down', () => {
jest.useFakeTimers();
const spy = jest.fn();
const element = mountWithAppProvider(
<TextField
id="MyTextField"
label="TextField"
type="number"
value="3"
onChange={spy}
/>,
);
element
.find('[role="button"]')
.last()
.simulate('mousedown');

jest.runOnlyPendingTimers();
expect(spy).toHaveBeenCalledWith('2', 'MyTextField');
});

it('stops decrementing on mouse up', () => {
jest.useFakeTimers();
const spy = jest.fn();
const element = mountWithAppProvider(
<TextField
id="MyTextField"
label="TextField"
type="number"
value="3"
onChange={spy}
/>,
);
element
.find('[role="button"]')
.last()
.simulate('mousedown');
element
.find('[role="button"]')
.last()
.simulate('mouseup');

jest.runOnlyPendingTimers();
expect(element.prop('value')).toBe('3');
});
});
});

Expand Down