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

fix(dial-pad): prevent focus loss after removal of delete button (VIV-2126) #1912

Merged
merged 29 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0d49cde
fix(dial-pad): prevent multiple events from firing
Sep 18, 2024
20c0006
Refactor
Sep 18, 2024
21c127a
Lint
Sep 18, 2024
22193ff
value sync
Sep 18, 2024
5b66bc9
Emit change on delete press
Sep 18, 2024
d26360a
Prevent multiple events
Sep 19, 2024
e9f8619
Lint
Sep 19, 2024
7d2f9cd
Refactor tests
Sep 19, 2024
2d42392
Refactor template
Sep 19, 2024
8847d18
Merge branch 'main' into VIV-2065-dial-pad-events-leak
YonatanKra Sep 19, 2024
921eb46
Update docs
Sep 19, 2024
2e2c428
Merge branch 'main' into VIV-2065-dial-pad-events-leak
YonatanKra Sep 19, 2024
c743b05
Lint
Sep 19, 2024
feeecf4
Merge branch 'VIV-2065-dial-pad-events-leak' of https://github.com/Vo…
Sep 19, 2024
ff72bc8
Remove obsolete code
Sep 19, 2024
ebfc47e
fix(dial-pad): prevent focus loss after removal of delete button
Sep 19, 2024
6a84650
Linting
Sep 19, 2024
b7f711e
Merge remote-tracking branch 'origin/main' into VIV-2126-fix-delete-b…
Sep 24, 2024
35271aa
Merge remote-tracking branch 'origin/main' into VIV-2126-fix-delete-b…
Sep 24, 2024
41e7e22
Refactor last character blur handler
Sep 24, 2024
6f32548
Merge branch 'main' into VIV-2126-fix-delete-button-blur-effect
YonatanKra Oct 21, 2024
9404e96
Refactor
Oct 21, 2024
2716a69
Merge branch 'main' into VIV-2126-fix-delete-button-blur-effect
YonatanKra Oct 21, 2024
0a08a1c
Merge branch 'main' into VIV-2126-fix-delete-button-blur-effect
YonatanKra Oct 21, 2024
f250bd2
Refactor blur prevention
Oct 22, 2024
62d4ed6
Refactor
Oct 22, 2024
77c90f5
Merge branch 'main' into VIV-2126-fix-delete-button-blur-effect
YonatanKra Oct 22, 2024
99c37cf
Merge branch 'main' into VIV-2126-fix-delete-button-blur-effect
TaylorJ76 Oct 22, 2024
662a66f
Merge branch 'main' into VIV-2126-fix-delete-button-blur-effect
TaylorJ76 Oct 22, 2024
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
18 changes: 9 additions & 9 deletions libs/components/src/lib/dial-pad/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,14 @@ You can change the error text with the `error-text` attribute.

<div class="table-wrapper">

| Name | Type | Bubbles | Composed | Description |
| -------------- | --------------------------- | ------- | -------- | ------------------------------------------- |
| `input` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the text field value changes |
| `change` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the text field value changes |
| `blur` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the text field loses focus |
| `focus` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the text field receives focus |
| `keypad-click` | `CustomEvent<HTMLElement> ` | Yes | Yes | Emitted when a digit button is clicked |
| `dial` | `CustomEvent<undefined> ` | Yes | Yes | Emitted when the call button is clicked |
| `end-call` | `CustomEvent<undefined> ` | Yes | Yes | Emitted when the end call button is clicked |
| Name | Type | Bubbles | Composed | Description |
| -------------- | --------------------------- | ------- | -------- | ----------------------------------------------- |
| `input` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the text field value changes |
| `change` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the text field value changes |
| `blur` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the dialpad loses focus |
| `focus` | `CustomEvent<undefined>` | Yes | Yes | Emitted when the dialpad children receive focus |
| `keypad-click` | `CustomEvent<HTMLElement> ` | Yes | Yes | Emitted when a digit button is clicked |
| `dial` | `CustomEvent<undefined> ` | Yes | Yes | Emitted when the call button is clicked |
| `end-call` | `CustomEvent<undefined> ` | Yes | Yes | Emitted when the end call button is clicked |

</div>
219 changes: 172 additions & 47 deletions libs/components/src/lib/dial-pad/dial-pad.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('vwc-dial-pad', () => {
expect(getTextField().placeholder).toEqual(placeholder);
});

it('should activate number buttons when input event is fired a number', async function () {
it('should activate number buttons when input event is fired with a number', async function () {
expect(getDigitButtons()[3].active).toBeFalsy();
getTextField().dispatchEvent(new KeyboardEvent('keydown', { key: '4' }));
elementUpdated(element);
Expand Down Expand Up @@ -123,46 +123,65 @@ describe('vwc-dial-pad', () => {
await elementUpdated(element);
expect(getTextField().value).toEqual('12');
});
});

describe('keypad-click', function () {
it('should fire keypad-click event when clicked on keypad', async function () {
it('should emit a change event', async () => {
const spy = jest.fn();
element.addEventListener('keypad-click', spy);
element.addEventListener('change', spy);
element.value = '123';
await elementUpdated(element);
getDigitButtons().forEach((button) => {
button.click();
});
expect(spy).toHaveBeenCalledTimes(12);
getDeleteButton().click();
await elementUpdated(element);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should fire keypad-click event with the button which was clicked', async function () {
it('should emit an input event', async () => {
const spy = jest.fn();
element.addEventListener('keypad-click', spy);
element.addEventListener('input', spy);
element.value = '123';
await elementUpdated(element);
getDigitButtons().forEach((button) => {
button.click();
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({ detail: button })
);
});
getDeleteButton().click();
await elementUpdated(element);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should set value in text field when clicked on keypad', async function () {
it('should stop prevent blur event after deleting the last value', async () => {
const spy = jest.fn();
element.addEventListener('blur', spy);
element.value = '1';
await elementUpdated(element);
getDigitButtons().forEach((button) => {
button.click();
});
getDeleteButton().click();
await elementUpdated(element);
expect(getTextField().value).toEqual('123456789*0#');
element.dispatchEvent(
new InputEvent('blur', { bubbles: true, composed: true })
);
expect(spy).toHaveBeenCalledTimes(0);
});

it('should not set value in text field when clicked on digits div', async function () {
const digits: HTMLDivElement | null =
getBaseElement(element).querySelector('.digits');
digits?.click();
it('should allow blur event when not after deleting the last value', async () => {
const spy = jest.fn();
element.addEventListener('blur', spy);
element.value = '1';
await elementUpdated(element);
expect(getTextField().value).toEqual('');
getDeleteButton().click();
await elementUpdated(element);
element.dispatchEvent(
new InputEvent('blur', { bubbles: true, composed: true })
);
element.dispatchEvent(
new InputEvent('blur', { bubbles: true, composed: true })
);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should focus on the dialpad after the blur was prevented', async () => {
element.value = '1';
await elementUpdated(element);
getDeleteButton().click();
await elementUpdated(element);
element.dispatchEvent(
new InputEvent('blur', { bubbles: true, composed: true })
);
expect(document.activeElement === element).toBe(true);
});
});

Expand Down Expand Up @@ -256,7 +275,7 @@ describe('vwc-dial-pad', () => {
expect(spy).toHaveBeenCalledTimes(1);
});

it('should not fire dial event when enter is pressed on delete button', async function () {
it('should prevent dial event when enter is pressed on delete button', async function () {
const spy = jest.fn();
element.value = '123';
element.addEventListener('dial', spy);
Expand All @@ -277,32 +296,138 @@ describe('vwc-dial-pad', () => {
});
});

describe.each(['input', 'change', 'blur', 'focus'])(
'%s event',
(eventName) => {
it('should be fired when user enters a valid text into the text field', async () => {
describe('events', () => {
function dispatchEvent(eventType: string) {
getTextField().dispatchEvent(
new InputEvent(eventType, { bubbles: true, composed: true })
);
}

function shouldFireEventOnceFromTextField(eventName: string) {
it('should fire only once on the dial pad element', async () => {
const spy = jest.fn();
element.addEventListener(eventName, spy);

element.value = '123';
getTextField().dispatchEvent(new InputEvent(eventName));
dispatchEvent(eventName);
await elementUpdated(element);

expect(spy).toHaveBeenCalledTimes(1);
});
}
);

describe.each(['input', 'change'])('%s event', (eventName) => {
it('should be fired when user clicks the keyboard buttons', async () => {
const spy = jest.fn();
element.addEventListener(eventName, spy);
getDigitButtons().forEach((button) => {
button.click();
function shouldFireOnDialPadButtonClick(eventName: string) {
it('should fire when user clicks the dial pad buttons', async () => {
const spy = jest.fn();
element.addEventListener(eventName, spy);
getDigitButtons().forEach((button) => {
button.click();
});

await elementUpdated(element);
expect(spy).toHaveBeenCalledTimes(12);
});
}

await elementUpdated(element);
expect(spy).toHaveBeenCalledTimes(12);
function shouldSetElementValueAfterEvent(eventName: string) {
it('should set element value after event', async () => {
const spy = jest.fn();
element.addEventListener(eventName, spy);

element.value = '123';
getTextField().value = '55';
dispatchEvent(eventName);
await elementUpdated(element);

expect(element.value).toBe('55');
});
}

describe('keypad-click', function () {
it('should fire keypad-click event when a keypad button is clicked', async function () {
const spy = jest.fn();
element.addEventListener('keypad-click', spy);
await elementUpdated(element);
getDigitButtons().forEach((button) => {
button.click();
});
expect(spy).toHaveBeenCalledTimes(12);
});

it('should fire keypad-click event with the button which was clicked', async function () {
const spy = jest.fn();
element.addEventListener('keypad-click', spy);
await elementUpdated(element);
getDigitButtons().forEach((button) => {
button.click();
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({ detail: button })
);
});
});

it('should set value in text field when clicked on keypad', async function () {
await elementUpdated(element);
getDigitButtons().forEach((button) => {
button.click();
});
await elementUpdated(element);
expect(getTextField().value).toEqual('123456789*0#');
});

it('should prevent focus and blur events on subsequent keypad buttons', async () => {
const spy = jest.fn();
element.addEventListener('focus', spy);
element.addEventListener('blur', spy);
getDigitButtons().forEach((button) => {
button.focus();
button.blur();
});
await elementUpdated(element);
expect(spy).toHaveBeenCalledTimes(1);
});
});

describe('focus event', () => {
const eventName = 'focus';
it('should prevent propagation of focus event from textfield', async () => {
const spy = jest.fn();
element.addEventListener(eventName, spy);

element.value = '123';
dispatchEvent(eventName);
await elementUpdated(element);

expect(spy).toHaveBeenCalledTimes(0);
});
});

describe('blur event', () => {
const eventName = 'blur';
it('should prevent propagation of blur event from textfield', async () => {
const spy = jest.fn();
element.addEventListener(eventName, spy);

element.value = '123';
dispatchEvent(eventName);
await elementUpdated(element);

expect(spy).toHaveBeenCalledTimes(0);
});
});

describe('input event', () => {
const eventName = 'input';
shouldFireOnDialPadButtonClick(eventName);
shouldFireEventOnceFromTextField(eventName);
shouldSetElementValueAfterEvent(eventName);
});

describe('change event', () => {
const eventName = 'change';
shouldFireOnDialPadButtonClick(eventName);
shouldFireEventOnceFromTextField(eventName);
shouldSetElementValueAfterEvent(eventName);
});
});

Expand Down Expand Up @@ -358,16 +483,16 @@ describe('vwc-dial-pad', () => {
});
});

describe('no call', function () {
it('should not show call button when has no-call attribute', async function () {
describe('noCall', function () {
it('should remove call button when has no-call attribute', async function () {
element.noCall = true;
await elementUpdated(element);
expect(getCallButton()).toBeNull();
});
});

describe('no input', function () {
it('should not show text field when has no-input attribute', async function () {
describe('noInput', function () {
it('should remove text field when has no-input attribute', async function () {
element.noInput = true;
await elementUpdated(element);
expect(getTextField()).toBeNull();
Expand All @@ -382,7 +507,7 @@ describe('vwc-dial-pad', () => {
});
});

describe('call button label', function () {
describe('callButtonLabel', function () {
it('should set call button label when has call-button-label attribute', async function () {
const label = '123';
element.callButtonLabel = label;
Expand Down
23 changes: 16 additions & 7 deletions libs/components/src/lib/dial-pad/dial-pad.template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-len */
import { html, ref, when } from '@microsoft/fast-element';
import { ExecutionContext, html, ref, when } from '@microsoft/fast-element';
import { ViewTemplate } from '@microsoft/fast-element';
import { classNames } from '@microsoft/fast-web-utilities';
import type {
Expand Down Expand Up @@ -39,6 +39,14 @@ function handleKeyDown(x: DialPad, e: KeyboardEvent) {
return true;
}

function syncFieldAndPadValues(x: DialPad) {
x.value = x._textFieldEl.value;
}

function stopPropagation(_: DialPad, { event: e }: ExecutionContext) {
e.stopImmediatePropagation();
}

function renderTextField(textFieldTag: string, buttonTag: string) {
return html<DialPad>`<${textFieldTag} ${ref(
'_textFieldEl'
Expand All @@ -48,10 +56,11 @@ function renderTextField(textFieldTag: string, buttonTag: string) {
x.helperText}" pattern="${(x) => x.pattern}"
aria-label="${(x) => x.locale.dialPad.inputLabel}"
@keydown="${(x, c) => handleKeyDown(x, c.event as KeyboardEvent)}"
@input="${(x) => x._handleInput()}" @change="${(x) =>
x._handleChange()}"
@blur="${(x) => x._handleBlur()}" @focus="${(x) =>
x._handleFocus()}">
@input="${syncFieldAndPadValues}"
@change="${syncFieldAndPadValues}"
@focus="${stopPropagation}"
@blur="${stopPropagation}"
>
${when(
(x) => x.value && x.value.length && x.value.length > 0,
html`<${buttonTag}
Expand Down Expand Up @@ -156,8 +165,8 @@ function renderDigits(buttonTag: string, iconTag: string) {
}

function renderDialButton(buttonTag: string) {
return html<DialPad>`<${buttonTag} class='call-btn'
size='expanded'
return html<DialPad>`<${buttonTag} class="call-btn"
size="expanded"
appearance="filled"
icon="${(x) => (x.callActive ? 'disable-call-line' : 'call-line')}"
connotation="${(x) => (x.callActive ? 'alert' : 'cta')}"
Expand Down
Loading
Loading