Skip to content

Commit

Permalink
fix(number-field): add an "indeterminate" state
Browse files Browse the repository at this point in the history
  • Loading branch information
Westbrook committed Oct 7, 2021
1 parent 5ebc8e1 commit 8bde8a1
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 7 deletions.
51 changes: 45 additions & 6 deletions packages/number-field/src/NumberField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function isAndroid(): boolean {
}

export const FRAMES_PER_CHANGE = 5;
export const indeterminatePlaceholder = '-';

/**
* @element sp-number-field
Expand Down Expand Up @@ -85,6 +86,9 @@ export class NumberField extends TextfieldBase {
@property({ type: Boolean, reflect: true, attribute: 'hide-stepper' })
public hideStepper = false;

@property({ type: Boolean, reflect: true })
public indeterminate = false;

@property({ type: Boolean, reflect: true, attribute: 'keyboard-focused' })
public keyboardFocused = false;

Expand Down Expand Up @@ -126,6 +130,12 @@ export class NumberField extends TextfieldBase {
return this._value;
}

private get inputValue(): string {
return this.indeterminate
? this.formattedValue
: this.inputElement.value;
}

public _value = NaN;
private _trackingValue = '';

Expand Down Expand Up @@ -248,6 +258,7 @@ export class NumberField extends TextfieldBase {
this.dispatchEvent(
new Event('input', { bubbles: true, composed: true })
);
this.indeterminate = false;
this.focus();
}

Expand Down Expand Up @@ -290,7 +301,7 @@ export class NumberField extends TextfieldBase {

protected onFocus(): void {
super.onFocus();
this._trackingValue = this.inputElement.value;
this._trackingValue = this.inputValue;
this.keyboardFocused = true;
this.addEventListener('wheel', this.onScroll);
}
Expand All @@ -311,17 +322,42 @@ export class NumberField extends TextfieldBase {
this.keyboardFocused = false;
}

private wasIndeterminate = false;
private indeterminateValue?: number;

protected onChange(): void {
const value = this.convertValueToNumber(this.inputElement.value);
const value = this.convertValueToNumber(this.inputValue);
if (this.wasIndeterminate) {
this.wasIndeterminate = false;
this.indeterminateValue = undefined;
if (isNaN(value)) {
this.indeterminate = true;
return;
}
}
this.value = value;
super.onChange();
}

protected onInput(): void {
if (this.indeterminate) {
this.wasIndeterminate = true;
this.indeterminateValue = this.value;
this.inputElement.value = this.inputElement.value.replace(
indeterminatePlaceholder,
''
);
}
const { value, selectionStart } = this.inputElement;
if (this.numberParser.isValidPartialNumber(value)) {
const valueAsNumber = this.convertValueToNumber(value);
this._value = this.validateInput(valueAsNumber);
if (!value && this.indeterminateValue) {
this.indeterminate = true;
this._value = this.indeterminateValue;
} else {
this.indeterminate = false;
this._value = this.validateInput(valueAsNumber);
}
this._trackingValue = value;
return;
}
Expand All @@ -330,7 +366,9 @@ export class NumberField extends TextfieldBase {
const nextSelectStart =
(selectionStart || currentLength) -
(currentLength - previousLength);
this.inputElement.value = this._trackingValue;
this.inputElement.value = this.indeterminate
? indeterminatePlaceholder
: this._trackingValue;
this.inputElement.setSelectionRange(nextSelectStart, nextSelectStart);
}

Expand Down Expand Up @@ -363,7 +401,8 @@ export class NumberField extends TextfieldBase {
}

protected get displayValue(): string {
return this.formattedValue;
const indeterminateValue = this.focused ? '' : indeterminatePlaceholder;
return this.indeterminate ? indeterminateValue : this.formattedValue;
}

protected clearNumberFormatterCache(): void {
Expand Down Expand Up @@ -533,7 +572,7 @@ export class NumberField extends TextfieldBase {
changes.has('min')
) {
const value = this.numberParser.parse(
this.inputElement.value.replace(this._forcedUnit, '')
this.inputValue.replace(this._forcedUnit, '')
);
this.value = this.validateInput(value);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/number-field/stories/number-field.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export default {
type: 'boolean',
},
},
indeterminate: {
name: 'indeterminate',
type: { name: 'boolean', required: false },
description:
'Whether the value of the Number Field can be determined for display.',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
readonly: {
name: 'readonly',
type: { name: 'boolean', required: false },
Expand Down Expand Up @@ -159,6 +172,7 @@ export default {

interface StoryArgs {
disabled?: boolean;
indeterminate?: boolean;
invalid?: boolean;
value?: number;
placeholder?: string;
Expand All @@ -184,6 +198,22 @@ Default.args = {
value: 100,
};

export const indeterminate = (args: StoryArgs = {}): TemplateResult => {
return html`
<sp-field-label for="default">Enter a number</sp-field-label>
<sp-number-field
id="default"
...=${spreadProps(args)}
style="width: 150px"
></sp-number-field>
`;
};

indeterminate.args = {
value: 100,
indeterminate: true,
};

export const decimals = (args: StoryArgs): TemplateResult => {
return html`
<sp-field-label for="decimals">
Expand Down
148 changes: 147 additions & 1 deletion packages/number-field/test/number-field.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import {
currency,
decimals,
Default,
indeterminate,
percents,
pixels,
units,
} from '../stories/number-field.stories.js';
import '../sp-number-field.js';
import { FRAMES_PER_CHANGE, NumberField } from '..';
import { FRAMES_PER_CHANGE, indeterminatePlaceholder, NumberField } from '..';
import {
executeServerCommand,
sendKeys,
Expand Down Expand Up @@ -930,6 +931,151 @@ describe('NumberField', () => {
expect(el.value).to.equal(7);
});
});
describe('indeterminate', () => {
let el: NumberField;
beforeEach(async () => {
el = await getElFrom(indeterminate(indeterminate.args));
expect(el.formattedValue).to.equal('100');
expect(el.valueAsString).to.equal('100');
expect(el.value).to.equal(100);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal(indeterminatePlaceholder);
});
it('remove "-" on focus', async () => {
el.focus();
await elementUpdated(el);
expect(el.formattedValue).to.equal('100');
expect(el.valueAsString).to.equal('100');
expect(el.value).to.equal(100);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('');
el.blur();
await elementUpdated(el);
expect(el.formattedValue).to.equal('100');
expect(el.valueAsString).to.equal('100');
expect(el.value).to.equal(100);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal(indeterminatePlaceholder);
});
it('return to "-" after suplied value is removed', async () => {
el.focus();
await elementUpdated(el);
expect(el.formattedValue).to.equal('100');
expect(el.valueAsString).to.equal('100');
expect(el.value).to.equal(100);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('');
await sendKeys({ type: '50' });
await elementUpdated(el);
expect(el.formattedValue).to.equal('50');
expect(el.valueAsString).to.equal('50');
expect(el.value).to.equal(50);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('50');
await sendKeys({ press: 'Backspace' });
await sendKeys({ press: 'Backspace' });
await elementUpdated(el);
expect(el.formattedValue).to.equal('100');
expect(el.valueAsString).to.equal('100');
expect(el.value).to.equal(100);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('');
el.blur();
await elementUpdated(el);
expect(el.formattedValue).to.equal('100');
expect(el.valueAsString).to.equal('100');
expect(el.value).to.equal(100);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal(indeterminatePlaceholder);
});
it('starts from `value` on "ArrowUp" keypresses', async () => {
el.focus();
await elementUpdated(el);
await sendKeys({ press: 'ArrowUp' });
await elementUpdated(el);
expect(el.formattedValue).to.equal('101');
expect(el.valueAsString).to.equal('101');
expect(el.value).to.equal(101);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('101');
el.blur();
await elementUpdated(el);
expect(el.formattedValue).to.equal('101');
expect(el.valueAsString).to.equal('101');
expect(el.value).to.equal(101);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('101');
});
it('starts from `value` on click `.stepUp`', async () => {
el.focus();
await elementUpdated(el);
await clickBySelector(el, '.stepUp');
await elementUpdated(el);
expect(el.formattedValue).to.equal('101');
expect(el.valueAsString).to.equal('101');
expect(el.value).to.equal(101);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('101');
el.blur();
await elementUpdated(el);
expect(el.formattedValue).to.equal('101');
expect(el.valueAsString).to.equal('101');
expect(el.value).to.equal(101);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('101');
});
it('starts from `value` on "ArrowDown" keypresses', async () => {
el.focus();
await elementUpdated(el);
await sendKeys({ press: 'ArrowDown' });
await elementUpdated(el);
expect(el.formattedValue).to.equal('99');
expect(el.valueAsString).to.equal('99');
expect(el.value).to.equal(99);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('99');
el.blur();
await elementUpdated(el);
expect(el.formattedValue).to.equal('99');
expect(el.valueAsString).to.equal('99');
expect(el.value).to.equal(99);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('99');
});
it('starts from `value` on click `.stepDown`', async () => {
el.focus();
await elementUpdated(el);
await clickBySelector(el, '.stepDown');
await elementUpdated(el);
expect(el.formattedValue).to.equal('99');
expect(el.valueAsString).to.equal('99');
expect(el.value).to.equal(99);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('99');
el.blur();
await elementUpdated(el);
expect(el.formattedValue).to.equal('99');
expect(el.valueAsString).to.equal('99');
expect(el.value).to.equal(99);
expect(
(el as unknown as { displayValue: string }).displayValue
).to.equal('99');
});
});
it('removes the stepper UI with [hide-stepper]', async () => {
const el = await getElFrom(Default({ hideStepper: true }));
const stepUp = el.shadowRoot.querySelector('.stepUp');
Expand Down

0 comments on commit 8bde8a1

Please # to comment.