-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: Add phone field input * chore: Sort package.json components * feat: Automatically adjust cursor position after formatting * feat: Handle deletion of special characters in display * chore: PR Suggestions Co-authored-by: Tyler Senter <tyler.senter.427@gmail.com> * feat: Extract phone formats * fix: TS errors * chore: Add keyboard tests for phone input --------- Co-authored-by: Tyler Senter <tyler.senter.427@gmail.com>
- Loading branch information
1 parent
98e2075
commit 03091e0
Showing
8 changed files
with
458 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { fn } from '@ember/helper'; | ||
import { action, set } from '@ember/object'; | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import PhoneField from '@nrg-ui/ember/components/form/phone-field'; | ||
import bind from '@nrg-ui/ember/helpers/bind'; | ||
import FreestyleUsage from 'ember-freestyle/components/freestyle/usage'; | ||
import FreestyleSection from 'ember-freestyle/components/freestyle-section'; | ||
|
||
import CodeBlock from '../code-block'; | ||
|
||
// TypeScript doesn't recognize that this function is used in the template | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
function log(...msg: string[]) { | ||
console.log(msg.join(' ')); | ||
} | ||
|
||
class Model { | ||
@tracked | ||
property = ''; | ||
} | ||
|
||
export default class extends Component { | ||
@tracked | ||
class = ''; | ||
|
||
model = new Model(); | ||
|
||
@tracked | ||
basic = false; | ||
|
||
@tracked | ||
disabled = false; | ||
|
||
@tracked | ||
readonly = false; | ||
|
||
@tracked | ||
value = ''; | ||
|
||
@action | ||
update(key: string, value: unknown) { | ||
set(this, key, value); | ||
} | ||
|
||
<template> | ||
<FreestyleSection @name="Phone Field" as |Section|> | ||
<Section.subsection @name="Basic"> | ||
<FreestyleUsage> | ||
<:example> | ||
<PhoneField | ||
class={{this.class}} | ||
@basic={{this.basic}} | ||
@binding={{bind this.model "property"}} | ||
@disabled={{this.disabled}} | ||
@readonly={{this.readonly}} | ||
@onChange={{fn log "The value changed to"}} | ||
/> | ||
</:example> | ||
<:api as |Args|> | ||
<Args.String | ||
@name="class" | ||
@description="The class to apply to the button. Note that this is not an argument but rather a class applied directly to the button" | ||
@value={{this.class}} | ||
@onInput={{fn this.update "class"}} | ||
@options={{this.classOptions}} | ||
/> | ||
<Args.Bool | ||
@name="basic" | ||
@defaultValue={{false}} | ||
@description="When true, the border will be removed" | ||
@value={{this.basic}} | ||
@onInput={{fn this.update "basic"}} | ||
/> | ||
<Args.String | ||
@name="binding" | ||
@description="Create a two-way binding with the value" | ||
@value={{this.model.property}} | ||
@onInput={{fn this.update "model.property"}} | ||
/> | ||
<Args.Bool | ||
@name="disabled" | ||
@defaultValue={{false}} | ||
@description="When true, the input will be disabled" | ||
@value={{this.disabled}} | ||
@onInput={{fn this.update "disabled"}} | ||
/> | ||
<Args.Bool | ||
@name="readonly" | ||
@defaultValue={{false}} | ||
@description="When true, the input will be readonly" | ||
@value={{this.readonly}} | ||
@onInput={{fn this.update "readonly"}} | ||
/> | ||
<Args.Action | ||
@name="onChange" | ||
@description="The action to call when the value changes" | ||
> | ||
<CodeBlock | ||
@lang="typescript" | ||
@code="(newValue: string) => unknown" | ||
/> | ||
</Args.Action> | ||
</:api> | ||
</FreestyleUsage> | ||
</Section.subsection> | ||
</FreestyleSection> | ||
</template> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{{page-title "Phone Field"}} | ||
|
||
<div class="container mx-auto"> | ||
<F::PhoneField /> | ||
</div> |
211 changes: 211 additions & 0 deletions
211
apps/ember-test-app/tests/integration/components/form/phone-field-test.gts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import { fillIn, render, settled, click } from '@ember/test-helpers'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import PhoneField from '@nrg-ui/ember/components/form/phone-field'; | ||
import bind from '@nrg-ui/ember/helpers/bind'; | ||
import { setupRenderingTest } from 'ember-qunit'; | ||
import { module, test } from 'qunit'; | ||
|
||
class Model { | ||
@tracked | ||
value: string = ''; | ||
} | ||
|
||
async function clickAt(element: HTMLInputElement, position: number) { | ||
element.setSelectionRange(position, position); | ||
await click(element); | ||
} | ||
|
||
async function simulateBackspace(element: HTMLInputElement) { | ||
const cursorPosition = element.selectionStart ?? -1; | ||
const value = element.value; | ||
element.value = | ||
value.slice(0, cursorPosition - 1) + value.slice(cursorPosition); | ||
element.setSelectionRange(cursorPosition - 1, cursorPosition - 1); | ||
element.dispatchEvent( | ||
new InputEvent('input', { | ||
key: 'Backspace', | ||
inputType: 'deleteContentBackward', | ||
}), | ||
); | ||
await settled(); | ||
} | ||
|
||
async function simulateDelete(element: HTMLInputElement) { | ||
const cursorPosition = element.selectionStart ?? -1; | ||
const value = element.value; | ||
element.value = | ||
value.slice(0, cursorPosition) + value.slice(cursorPosition + 1); | ||
element.setSelectionRange(cursorPosition, cursorPosition); | ||
element.dispatchEvent( | ||
new InputEvent('input', { | ||
key: 'Delete', | ||
inputType: 'deleteContentForward', | ||
}), | ||
); | ||
await settled(); | ||
} | ||
|
||
module('Integration | Component | form/phone-field', function (hooks) { | ||
setupRenderingTest(hooks); | ||
|
||
test('it renders', async function (assert) { | ||
const model = new Model(); | ||
|
||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
|
||
assert.dom('input').hasAttribute('type', 'tel').hasClass('form-control'); | ||
}); | ||
|
||
test('it displays formatted phone numbers', async function (assert) { | ||
const model = new Model(); | ||
|
||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
|
||
await fillIn('input', '11234567890'); | ||
assert.dom('input').hasValue('+1 (123) 456-7890'); | ||
|
||
await fillIn('input', '1234567890'); | ||
assert.dom('input').hasValue('(123) 456-7890'); | ||
|
||
await fillIn('input', '4567890'); | ||
assert.dom('input').hasValue('456-7890'); | ||
|
||
await fillIn('input', '7890'); | ||
assert.dom('input').hasValue('7890'); | ||
}); | ||
|
||
test('it binds unformatted numbers', async function (assert) { | ||
const model = new Model(); | ||
|
||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
|
||
await fillIn('input', '+1 (123) 456-7890'); | ||
assert.strictEqual(model.value, '11234567890'); | ||
|
||
await fillIn('input', '(123) 456-7890'); | ||
assert.strictEqual(model.value, '1234567890'); | ||
|
||
await fillIn('input', '456-7890'); | ||
assert.strictEqual(model.value, '4567890'); | ||
}); | ||
|
||
test('it unformats pasted inputs', async function (assert) { | ||
const model = new Model(); | ||
|
||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
|
||
await fillIn('input', '1-12a345(6)78_90 '); | ||
assert.dom('input').hasValue('+1 (123) 456-7890'); | ||
assert.strictEqual(model.value, '11234567890'); | ||
}); | ||
|
||
test('it reacts to changes in model', async function (assert) { | ||
const model = new Model(); | ||
|
||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
assert.dom('input').hasValue(''); | ||
model.value = '11234567890'; | ||
await settled(); | ||
assert.dom('input').hasValue('+1 (123) 456-7890'); | ||
}); | ||
|
||
test('it allows for backspacing from end of string', async function (assert) { | ||
const model = new Model(); | ||
model.value = '11234567890'; | ||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
const element = this.element.querySelector('input') as HTMLInputElement; | ||
await clickAt(element, element.value.length); | ||
for (let i = 0; i < 4; i++) { | ||
await simulateBackspace(element); | ||
} | ||
assert.strictEqual(model.value, '1123456'); | ||
for (let i = 0; i < 3; i++) { | ||
await simulateBackspace(element); | ||
} | ||
assert.strictEqual(model.value, '1123'); | ||
for (let i = 0; i < 3; i++) { | ||
await simulateBackspace(element); | ||
} | ||
assert.strictEqual(model.value, '1'); | ||
}); | ||
|
||
test('it allows for deleting from beginning of string', async function (assert) { | ||
const model = new Model(); | ||
model.value = '1234567890123'; | ||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
const element = this.element.querySelector('input') as HTMLInputElement; | ||
await clickAt(element, 0); | ||
for (let i = 0; i < 4; i++) { | ||
await simulateDelete(element); | ||
} | ||
assert.strictEqual(model.value, '567890123'); | ||
for (let i = 0; i < 3; i++) { | ||
await simulateDelete(element); | ||
} | ||
assert.strictEqual(model.value, '890123'); | ||
for (let i = 0; i < 3; i++) { | ||
await simulateDelete(element); | ||
} | ||
assert.strictEqual(model.value, '123'); | ||
for (let i = 0; i < 3; i++) { | ||
await simulateDelete(element); | ||
} | ||
assert.strictEqual(model.value, ''); | ||
}); | ||
|
||
test('it allows for backspacing from after special characters', async function (assert) { | ||
const model = new Model(); | ||
model.value = '1234567890123'; | ||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
const element = this.element.querySelector('input') as HTMLInputElement; | ||
await clickAt(element, 11); | ||
await simulateBackspace(element); | ||
assert.strictEqual(model.value, '123457890123'); | ||
assert.strictEqual(element.selectionStart, 10); | ||
|
||
await simulateBackspace(element); | ||
await simulateBackspace(element); | ||
assert.strictEqual(model.value, '1237890123'); | ||
assert.strictEqual(element.selectionStart, 6); | ||
|
||
await simulateBackspace(element); | ||
await simulateBackspace(element); | ||
await simulateBackspace(element); | ||
assert.strictEqual(model.value, '7890123'); | ||
assert.strictEqual(element.selectionStart, 0); | ||
}); | ||
|
||
test('it allows for deleting from before special characters', async function (assert) { | ||
const model = new Model(); | ||
model.value = '1234567890123'; | ||
await render(<template> | ||
<PhoneField @binding={{bind model "value"}} /> | ||
</template>); | ||
const element = this.element.querySelector('input') as HTMLInputElement; | ||
await clickAt(element, 4); | ||
await simulateDelete(element); | ||
assert.strictEqual(model.value, '123567890123'); | ||
assert.strictEqual(element.selectionStart, 6); | ||
|
||
await clickAt(element, 0); | ||
await simulateDelete(element); | ||
assert.strictEqual(model.value, '23567890123'); | ||
assert.strictEqual(element.selectionStart, 1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.