Skip to content

Commit 60d1c3e

Browse files
committed
VIV-2876 add support for long '0' press to render '+' (dial-pad)
1 parent c97b0f8 commit 60d1c3e

4 files changed

Lines changed: 191 additions & 0 deletions

File tree

libs/components/src/lib/dial-pad/VARIATIONS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
<vwc-dial-pad aria-label="Dial a telephone number"></vwc-dial-pad>
55
```
66

7+
## International Numbers
8+
9+
To support international numbers, you can use the pattern like `^\+?[0-9#*]*$` which allows an optional `+` at the beginning. Users can **long press** the `0` button to insert a `+` symbol (commonly used for international numbers).
10+
11+
```html preview
12+
<vwc-dial-pad pattern="^\+?[0-9#*]*$"></vwc-dial-pad>
13+
```
14+
715
## Labelling
816

917
### Helper Text

libs/components/src/lib/dial-pad/dial-pad.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,121 @@ describe('vwc-dial-pad', () => {
474474
});
475475
});
476476

477+
describe('long press on 0', () => {
478+
function getZeroButton(component: HTMLElement) {
479+
return getDigitButtons(component)[10] as Button;
480+
}
481+
482+
function createPointerEvent(
483+
type: string,
484+
options: EventInit = {}
485+
): PointerEvent {
486+
// Create a mock PointerEvent for test environment
487+
const event = new Event(type, options) as PointerEvent;
488+
Object.defineProperty(event, 'pointerType', {
489+
value: 'mouse',
490+
writable: true,
491+
});
492+
Object.defineProperty(event, 'pointerId', {
493+
value: 1,
494+
writable: true,
495+
});
496+
return event;
497+
}
498+
499+
it('should add "0" when tapping 0', async () => {
500+
getZeroButton(element).click();
501+
await Updates.next();
502+
expect(getTextField(element).value).toEqual('0');
503+
});
504+
505+
it('should add "+" when long pressing 0 and suppress subsequent click', async () => {
506+
element.pattern = '^\\+?[0-9#*]*$';
507+
await Updates.next();
508+
const btn = getZeroButton(element);
509+
vi.useFakeTimers();
510+
try {
511+
const pointerDown = createPointerEvent('pointerdown', {
512+
bubbles: true,
513+
});
514+
Object.defineProperty(pointerDown, 'currentTarget', {
515+
value: btn,
516+
writable: true,
517+
});
518+
btn.dispatchEvent(pointerDown);
519+
vi.advanceTimersByTime(600);
520+
btn.dispatchEvent(
521+
createPointerEvent('pointerup', {
522+
bubbles: true,
523+
})
524+
);
525+
vi.runAllTimers();
526+
} finally {
527+
vi.useRealTimers();
528+
}
529+
await Updates.next();
530+
531+
// Simulate potential click following pointerup; should be suppressed
532+
btn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
533+
await Updates.next();
534+
expect(getTextField(element).value).toEqual('+');
535+
});
536+
537+
it('should not trigger long press on 0 when disabled', async () => {
538+
element.pattern = '^\\+?[0-9#*]*$';
539+
element.disabled = true;
540+
await Updates.next();
541+
const btn = getZeroButton(element);
542+
vi.useFakeTimers();
543+
try {
544+
const pointerDown = createPointerEvent('pointerdown', {
545+
bubbles: true,
546+
});
547+
Object.defineProperty(pointerDown, 'currentTarget', {
548+
value: btn,
549+
writable: true,
550+
});
551+
btn.dispatchEvent(pointerDown);
552+
vi.advanceTimersByTime(600);
553+
vi.runAllTimers();
554+
} finally {
555+
vi.useRealTimers();
556+
}
557+
await Updates.next();
558+
expect(getTextField(element).value).toEqual('');
559+
});
560+
561+
it('should not add "+" when pointer leaves 0 button before long press completes', async () => {
562+
element.pattern = '^\\+?[0-9#*]*$';
563+
await Updates.next();
564+
const btn = getZeroButton(element);
565+
vi.useFakeTimers();
566+
try {
567+
const pointerDown = createPointerEvent('pointerdown', {
568+
bubbles: true,
569+
});
570+
Object.defineProperty(pointerDown, 'currentTarget', {
571+
value: btn,
572+
writable: true,
573+
});
574+
btn.dispatchEvent(pointerDown);
575+
vi.advanceTimersByTime(300);
576+
// Leave before long press completes
577+
btn.dispatchEvent(
578+
createPointerEvent('pointerleave', {
579+
bubbles: true,
580+
})
581+
);
582+
vi.advanceTimersByTime(300);
583+
vi.runAllTimers();
584+
} finally {
585+
vi.useRealTimers();
586+
}
587+
await Updates.next();
588+
expect(getTextField(element).value).toEqual('');
589+
});
590+
});
591+
477592
describe('Methods', () => {
478593
describe('focus', function () {
479594
it('should set the focus on the text field', async function () {

libs/components/src/lib/dial-pad/dial-pad.template.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,21 @@ function onDigitClick(
147147
digit: DialPadButton,
148148
{ parent: dialPad, event }: { parent: DialPad; event: MouseEvent }
149149
) {
150+
if (dialPad._suppressNextClick) {
151+
// Skip the click insertion if a long-press already handled it
152+
dialPad._suppressNextClick = false;
153+
return;
154+
}
150155
dialPad.value += digit.value;
151156

152157
dialPad.$emit('keypad-click', event.currentTarget);
153158
dialPad.$emit('input');
154159
dialPad.$emit('change');
160+
// Reset suppress flag after a delay to handle cases where click doesn't fire.
161+
// This ensures the next click won't be incorrectly suppressed
162+
window.setTimeout(() => {
163+
dialPad._suppressNextClick = false;
164+
}, 0);
155165
}
156166

157167
function renderDigits(
@@ -175,6 +185,11 @@ function renderDigits(
175185
c.parent.autofocus && c.parent.noInput && c.index === 0}"
176186
aria-label="${(x, c) => c.parent.locale.dialPad[x.ariaLabel]}"
177187
?disabled="${(_, c) => c.parent.disabled}"
188+
@pointerdown="${(x, c) =>
189+
c.parent._startLongPress(x.value, c.event as PointerEvent)}"
190+
@pointerup="${(_, c) => c.parent._endLongPress()}"
191+
@pointercancel="${(_, c) => c.parent._cancelLongPress()}"
192+
@pointerleave="${(_, c) => c.parent._cancelLongPress()}"
178193
@click="${onDigitClick}">
179194
<${iconTag} slot="icon"
180195
name="${(x) => x.icon}"

libs/components/src/lib/dial-pad/dial-pad.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,59 @@ export class DialPad extends Localized(VividElement) {
147147
@attr({ attribute: 'delete-aria-label' }) deleteAriaLabel: string | null =
148148
null;
149149

150+
/**
151+
* Long-press handling for digit '0' to insert '+'
152+
* @internal
153+
*/
154+
private _longPressTimeoutId: number | null = null;
155+
/**
156+
* Suppresses the next click handler after a long press has already handled input
157+
* @internal
158+
*/
159+
_suppressNextClick = false;
160+
161+
/**
162+
* @internal
163+
*/
164+
_startLongPress(digit: string, event: PointerEvent) {
165+
if (this.disabled || this.callActive || digit !== '0') return;
166+
167+
this._clearLongPressTimer();
168+
const target = event.currentTarget as HTMLElement | null;
169+
170+
this._longPressTimeoutId = window.setTimeout(() => {
171+
this._suppressNextClick = true;
172+
this.value += '+';
173+
this.$emit('keypad-click', target);
174+
this.$emit('input');
175+
this.$emit('change');
176+
}, 600);
177+
}
178+
179+
/**
180+
* @internal
181+
*/
182+
_endLongPress() {
183+
this._clearLongPressTimer();
184+
// Don't reset suppress flag here - let the click handler reset it
185+
// This ensures the click event can check the flag and suppress if needed
186+
// If click doesn't fire, the flag will be reset on the next click
187+
}
188+
189+
/**
190+
* @internal
191+
*/
192+
_cancelLongPress() {
193+
this._clearLongPressTimer();
194+
}
195+
196+
private _clearLongPressTimer() {
197+
if (this._longPressTimeoutId !== null) {
198+
clearTimeout(this._longPressTimeoutId);
199+
this._longPressTimeoutId = null;
200+
}
201+
}
202+
150203
/**
151204
*
152205
* @internal

0 commit comments

Comments
 (0)