diff --git a/apps/docs/_data/components.json b/apps/docs/_data/components.json
index 3fef465a24..1be632767c 100644
--- a/apps/docs/_data/components.json
+++ b/apps/docs/_data/components.json
@@ -269,6 +269,11 @@
"title": "Audio Player",
"markdown": "./libs/components/src/lib/audio-player/README.md"
},
+ {
+ "title": "Dial Pad",
+ "status": "alpha",
+ "markdown": "./libs/components/src/lib/dial-pad/README.md"
+ },
{
"title": "Video Player",
"status": "alpha",
diff --git a/apps/vue-docs/docs/examples/components/dial-pad/BasicExample.vue b/apps/vue-docs/docs/examples/components/dial-pad/BasicExample.vue
new file mode 100644
index 0000000000..16e5e5cf64
--- /dev/null
+++ b/apps/vue-docs/docs/examples/components/dial-pad/BasicExample.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/vue-docs/docs/examples/dial-pad.md b/apps/vue-docs/docs/examples/dial-pad.md
new file mode 100644
index 0000000000..da0424464f
--- /dev/null
+++ b/apps/vue-docs/docs/examples/dial-pad.md
@@ -0,0 +1,22 @@
+# Dial Pad Examples
+
+## Basic
+
+
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
diff --git a/libs/components/src/lib/components.ts b/libs/components/src/lib/components.ts
index 25dd05b18f..a2489a17e0 100644
--- a/libs/components/src/lib/components.ts
+++ b/libs/components/src/lib/components.ts
@@ -17,6 +17,7 @@ export * from './combobox/definition';
export * from './data-grid/definition';
export * from './date-picker/definition';
export * from './date-range-picker/definition';
+export * from './dial-pad/definition';
export * from './dialog/definition';
export * from './divider/definition';
export * from './empty-state/definition';
diff --git a/libs/components/src/lib/dial-pad/README.md b/libs/components/src/lib/dial-pad/README.md
new file mode 100644
index 0000000000..3cbe319527
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/README.md
@@ -0,0 +1,112 @@
+# Dial Pad
+
+This is a composed component that allows users to enter / dial telephone numbers.
+
+```js
+
+```
+
+```html preview
+
+```
+
+## Members
+
+### Value
+
+To set the value of the input, use the `value` attribute to set the text displayed in the input.
+
+- Type: `string`
+- Default: `undefined`
+
+```html preview
+
+```
+
+### Helper Text
+
+To give extra context to the number that is being displayed, use the `helper-text` attribute to set the text displayed under the input.
+
+- Type: `string`
+- Default: `undefined`
+
+```html preview
+
+```
+
+### Placeholder
+
+To give a hint to the user of what to enter in the input, use the `placeholder` attribute to set the text displayed in the input.
+
+- Type: `string`
+- Default: `undefined`
+
+```html preview
+
+```
+
+### Disabled
+
+Use the `disabled` attribute to disable the keypad, input and Call/End call buttons.
+
+- Type: `boolean`
+- Default: `false`
+
+```html preview
+
+```
+
+### Call Active
+
+Use the `call-active` attribute (or `callActive` property) to enable the `end call button` and disable the `dial button`.
+
+- Type: `boolean`
+- Default: `false`
+
+```html preview
+
+```
+
+### No Call
+
+Use the `no-call` attribute (or `noCall` property) to disable call/end call functionality and hide the call/end call button.
+
+- Type: `boolean`
+- Default: `false`
+
+```html preview
+
+```
+
+### Pattern
+
+Use the `pattern` attribute to set the regex string of allowed characters in the input.
+Read more about [vwc-text-field validation](/components/text-field/#validation).
+You can change the error text with the `error-text` attribute.
+
+- Type: `string`
+- Default: `^[0-9#*]*$` (key pad buttons)
+
+```html preview
+
+```
+
+## Events
+
+
+
+| Name | Description |
+| -------------- | ------------------------------------------------------------------------------------------------------- |
+| `dial` | Emitted (with the value of the input) when the dial pad is submitted and there is a value in the input. |
+| `end-call` | Emitted when the end call button is clicked. |
+| `keypad-click` | Emitted when a keypad button is clicked. The `detail` object holds the clicked button. |
+| `input` | Emitted from the input element. |
+| `change` | Emitted from the input element. |
+| `blur` | Emitted from the input element. |
+| `focus` | Emitted from the input element. |
+
+
diff --git a/libs/components/src/lib/dial-pad/definition.ts b/libs/components/src/lib/dial-pad/definition.ts
new file mode 100644
index 0000000000..5ce16da5e1
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/definition.ts
@@ -0,0 +1,30 @@
+import type { FoundationElementDefinition } from '@microsoft/fast-foundation';
+import { registerFactory } from '../../shared/design-system';
+import { buttonRegistries } from '../button/definition';
+import { textFieldRegistries } from '../text-field/definition';
+import styles from './dial-pad.scss?inline';
+
+import { DialPad } from './dial-pad';
+import { DialPadTemplate as template } from './dial-pad.template';
+
+export const dialPadDefinition = DialPad.compose({
+ baseName: 'dial-pad',
+ template: template as any,
+ styles,
+});
+
+/**
+ * @internal
+ */
+export const dialPadRegistries = [
+ dialPadDefinition(),
+ ...buttonRegistries,
+ ...textFieldRegistries,
+];
+
+/**
+ * Registers the dial-pad element with the design system.
+ *
+ * @param prefix - the prefix to use for the component name
+ */
+export const registerDialPad = registerFactory(dialPadRegistries);
diff --git a/libs/components/src/lib/dial-pad/dial-pad.scss b/libs/components/src/lib/dial-pad/dial-pad.scss
new file mode 100644
index 0000000000..ebbec3ebf5
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/dial-pad.scss
@@ -0,0 +1,68 @@
+@use '../../../../../dist/libs/tokens/scss/tokens.constants' as constants;
+@use '../../../../shared/src/lib/sass/mixins/border-radius' as
+ border-radius-variables;
+@use '../../../../shared/src/lib/sass/mixins/connotation/config' with (
+ $connotations: accent,
+ $shades: contrast soft pale fierce firm-all faint dim,
+ $default: accent
+);
+@use '../../../../shared/src/lib/sass/mixins/connotation' as connotation;
+@use '../../../../shared/src/lib/sass/mixins/appearance/config' as
+ appearance-config with (
+ $appearances: duotone,
+ $states: idle hover disabled,
+ $default: duotone
+);
+@use '../../../../shared/src/lib/sass/mixins/appearance' as appearance;
+
+$gap: 16px;
+
+:host {
+ display: inline-block;
+ margin: 16px;
+ inline-size: 230px;
+}
+
+.base {
+ display: grid;
+ box-sizing: border-box;
+ grid-template-rows: 80px 1fr auto;
+}
+
+.digits {
+ display: grid;
+ gap: $gap;
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: repeat(4, 1fr);
+ inline-size: 100%;
+}
+
+.phone-field {
+ align-self: flex-start;
+ grid-column: 1 / -1;
+}
+
+.digit-btn {
+ @include appearance.appearance;
+ @include connotation.connotation(dial-pad);
+
+ --vvd-button-accent-primary: var(#{appearance.get-appearance-token(text)});
+
+ display: flex;
+ overflow: hidden;
+ flex-direction: column;
+ border-radius: #{border-radius-variables.$border-radius-expanded};
+ box-shadow: 0 0 0 1px var(#{appearance.get-appearance-token(outline)});
+ inline-size: 100%;
+
+ &-num {
+ .digit-btn:not(.disabled) & {
+ color: var(#{constants.$vvd-color-canvas-text});
+ }
+ }
+}
+
+.call-btn {
+ margin-top: 32px;
+ grid-column: 1/-1;
+}
diff --git a/libs/components/src/lib/dial-pad/dial-pad.spec.ts b/libs/components/src/lib/dial-pad/dial-pad.spec.ts
new file mode 100644
index 0000000000..a59b91dc6a
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/dial-pad.spec.ts
@@ -0,0 +1,275 @@
+import { axe, elementUpdated, fixture, getBaseElement } from '@vivid-nx/shared';
+import { FoundationElementRegistry } from '@microsoft/fast-foundation';
+import { TextField } from '../text-field/text-field';
+import { Button } from '../button/button';
+import { DialPad } from './dial-pad';
+import { dialPadDefinition } from './definition';
+import '.';
+
+const COMPONENT_TAG = 'vwc-dial-pad';
+
+describe('vwc-dial-pad', () => {
+ let element: DialPad;
+
+ function getTextField() {
+ return getBaseElement(element).querySelector('.phone-field') as TextField;
+ }
+
+ function getCallButton() {
+ return getBaseElement(element).querySelector('.call-btn') as Button;
+ }
+
+ function getDigitButtons() {
+ const digits: HTMLDivElement | null =
+ getBaseElement(element).querySelector('.digits');
+ return digits?.querySelectorAll('vwc-button') as NodeListOf;
+ }
+
+ function getDeleteButton() {
+ return getTextField().querySelector('vwc-button') as Button;
+ }
+
+ beforeEach(async () => {
+ element = (await fixture(
+ `<${COMPONENT_TAG}>${COMPONENT_TAG}>`
+ )) as DialPad;
+ });
+
+ describe('basic', () => {
+ it('should be initialized as a vwc-dial-pad', async () => {
+ expect(dialPadDefinition()).toBeInstanceOf(FoundationElementRegistry);
+ expect(element).toBeInstanceOf(DialPad);
+ expect(element.pattern).toEqual('^[0-9#*]*$');
+ expect(element.value).toEqual('');
+ expect(element.disabled).toBeFalsy();
+ expect(element.callActive).toBeFalsy();
+ expect(element.noCall).toBeFalsy();
+ });
+ });
+
+ describe('text-field', function () {
+ it('should set text field in dial pad', async function () {
+ expect(getTextField()).not.toBeNull();
+ });
+
+ it('should set value in text field when has value attribute', async function () {
+ const value = '123';
+ element.value = value;
+ await elementUpdated(element);
+ expect(getTextField().value).toEqual(value);
+ });
+
+ it('should set helperText in text field when has helper-text attribute', async function () {
+ const helperText = '123';
+ element.helperText = helperText;
+ await elementUpdated(element);
+ expect(getTextField().helperText).toEqual(helperText);
+ });
+
+ it('should set placeholder in text field when has placeholder attribute', async function () {
+ const placeholder = '123';
+ element.placeholder = placeholder;
+ await elementUpdated(element);
+ expect(getTextField().placeholder).toEqual(placeholder);
+ });
+
+ it('should activate number buttons when input event is fired a number', async function () {
+ expect(getDigitButtons()[3].active).toBeFalsy();
+ getTextField().dispatchEvent(new KeyboardEvent('keydown', { key: '4' }));
+ elementUpdated(element);
+ expect(getDigitButtons()[3].active).toBeTruthy();
+ });
+
+ it('should activate * button when input event is fired with *', async function () {
+ expect(getDigitButtons()[9].active).toBeFalsy();
+ getTextField().dispatchEvent(new KeyboardEvent('keydown', { key: '*' }));
+ elementUpdated(element);
+ expect(getDigitButtons()[9].active).toBeTruthy();
+ });
+
+ it('should activate # button when input event is fired with #', async function () {
+ expect(getDigitButtons()[11].active).toBeFalsy();
+ getTextField().dispatchEvent(new KeyboardEvent('keydown', { key: '#' }));
+ elementUpdated(element);
+ expect(getDigitButtons()[11].active).toBeTruthy();
+ });
+
+ it('should not activate any button when input event is fired with an undefined key', async function () {
+ for (let i = 0; i < 12; i++) {
+ expect(getDigitButtons()[i].active).toBeFalsy();
+ }
+ getTextField().dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
+ elementUpdated(element);
+ for (let i = 0; i < 12; i++) {
+ expect(getDigitButtons()[i].active).toBeFalsy();
+ }
+ });
+ });
+
+ describe('delete', function () {
+ it('should show delete button when text field has value', async function () {
+ element.value = '123';
+ await elementUpdated(element);
+ expect(getDeleteButton()).not.toBeNull();
+ });
+
+ it('should remove last character from text field when clicked on delete button', async function () {
+ element.value = '123';
+ await elementUpdated(element);
+ getDeleteButton().click();
+ await elementUpdated(element);
+ expect(getTextField().value).toEqual('12');
+ });
+ });
+
+ describe('keypad-click', function () {
+ it('should fire keypad-click event when clicked on keypad', 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 not set value in text field when clicked on digits div', async function () {
+ const digits: HTMLDivElement | null =
+ getBaseElement(element).querySelector('.digits');
+ digits?.click();
+ await elementUpdated(element);
+ expect(getTextField().value).toEqual('');
+ });
+ });
+
+ describe('dial', function () {
+ it('should fire dial event when clicked on call button', async function () {
+ const spy = jest.fn();
+ element.addEventListener('dial', spy);
+ await elementUpdated(element);
+ getCallButton().click();
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should fire dial event when enter is pressed on text field', async function () {
+ const spy = jest.fn();
+ element.addEventListener('dial', spy);
+ await elementUpdated(element);
+ getTextField().dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Enter' })
+ );
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should fire end-call event when clicked on call button when active', async function () {
+ const spy = jest.fn();
+ element.addEventListener('end-call', spy);
+ element.callActive = true;
+ await elementUpdated(element);
+ getCallButton().click();
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe.each(['input', 'change', 'blur', 'focus'])(
+ '%s event',
+ (eventName) => {
+ it('should be fired when user enters a valid text into the text field', async () => {
+ const spy = jest.fn();
+ element.addEventListener(eventName, spy);
+
+ element.value = '123';
+ getTextField().dispatchEvent(new InputEvent(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();
+ });
+
+ await elementUpdated(element);
+ expect(spy).toHaveBeenCalledTimes(12);
+ });
+ });
+
+ describe('disabled', function () {
+ it('should set text field disabled when has disabled attribute', async function () {
+ element.disabled = true;
+ await elementUpdated(element);
+ expect(getTextField().disabled).toEqual(true);
+ });
+
+ it('should set call button disabled when has disabled attribute', async function () {
+ element.disabled = true;
+ await elementUpdated(element);
+ expect(getCallButton().disabled).toEqual(true);
+ });
+
+ it('should set digit buttons disabled when has disabled attribute', async function () {
+ element.disabled = true;
+ await elementUpdated(element);
+ getDigitButtons().forEach((button) => {
+ expect(button.disabled).toEqual(true);
+ });
+ });
+ });
+
+ describe('active', function () {
+ it('should change call button connotation to "alert" when active', async function () {
+ element.callActive = true;
+ expect(getCallButton().connotation).toEqual('cta');
+ await elementUpdated(element);
+ expect(getCallButton().connotation).toEqual('alert');
+ });
+
+ it('should change call buttons label when active', async function () {
+ element.callActive = true;
+ expect(getCallButton().label).toEqual('Call');
+ await elementUpdated(element);
+ expect(getCallButton().label).toEqual('End call');
+ });
+ });
+
+ describe('no call', function () {
+ it('should not show call button when has no-call attribute', async function () {
+ element.noCall = true;
+ await elementUpdated(element);
+ expect(getCallButton()).toBeNull();
+ });
+ });
+
+ describe('a11y', () => {
+ it('should pass html a11y test', async () => {
+ expect(await axe(element)).toHaveNoViolations();
+ });
+ });
+});
diff --git a/libs/components/src/lib/dial-pad/dial-pad.template.ts b/libs/components/src/lib/dial-pad/dial-pad.template.ts
new file mode 100644
index 0000000000..c63a2733da
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/dial-pad.template.ts
@@ -0,0 +1,183 @@
+/* eslint-disable max-len */
+import { html, ref, when } from '@microsoft/fast-element';
+import { ViewTemplate } from '@microsoft/fast-element';
+import { classNames } from '@microsoft/fast-web-utilities';
+import type {
+ ElementDefinitionContext,
+ FoundationElementDefinition,
+} from '@microsoft/fast-foundation';
+import { keyEnter } from '@microsoft/fast-web-utilities';
+import { Button } from '../button/button';
+import { TextField } from '../text-field/text-field';
+import { Icon } from '../icon/icon';
+import type { DialPad } from './dial-pad';
+
+const getClasses = (_: DialPad) => classNames('base');
+
+function handleKeyDown(x: DialPad, e: KeyboardEvent) {
+ if (e.key === keyEnter) {
+ x._onDial();
+ } else {
+ const key = e.key === '*' ? 'Asterisk' : e.key === '#' ? 'Hashtag' : e.key;
+ const digit: Button | null = x.shadowRoot!.querySelector(`#btn${key}`);
+ if (digit) {
+ digit.active = true;
+ setTimeout(() => {
+ digit.active = false;
+ }, 200);
+ }
+ }
+ return true;
+}
+
+function renderTextField(textFieldTag: string, buttonTag: string) {
+ return html`<${textFieldTag} ${ref(
+ '_textFieldEl'
+ )} class="phone-field" internal-part
+ value="${(x) => x.value}" placeholder="${(x) => x.placeholder}"
+ ?disabled="${(x) => x.disabled}" helper-text="${(x) =>
+ 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()}">
+ ${when(
+ (x) => x.value && x.value.length && x.value.length > 0,
+ html`<${buttonTag}
+ slot="action-items" size='super-condensed' icon="backspace-line" aria-label="${(
+ x
+ ) => x.deleteAriaLabel || x.locale.dialPad.deleteLabel}"
+ appearance='ghost' ?disabled="${(x) => x.disabled}" @click="${(
+ x
+ ) => x._deleteLastCharacter()}">
+ ${buttonTag}>`
+ )}
+ ${textFieldTag}>`;
+}
+
+function renderDigits(buttonTag: string, iconTag: string) {
+ return html`
+ <${buttonTag} id='btn1' value='1' stacked label=" " size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitOneLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='one-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn2' value='2' stacked label='ABC' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitTwoLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='two-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn3' value='3' stacked label='DEF' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitThreeLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='three-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn4' value='4' stacked label='GHI' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitFourLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='four-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn5' value='5' stacked label='JKL' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitFiveLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='five-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn6' value='6' stacked label='MNO' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitSixLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='six-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn7' value='7' stacked label='PQRS' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitSevenLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='seven-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn8' value='8' stacked label='TUV' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitEightLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='eight-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn9' value='9' stacked label='WXYZ' size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitNineLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='nine-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btnAsterisk' value='*' stacked size='condensed' class="digit-btn" aria-label="${(
+ x
+ ) => x.locale.dialPad.digitAsteriskLabel}" ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='asterisk-2-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btn0' value='0' stacked label='+' size='condensed' class="digit-btn" aria-label=${(
+ x
+ ) => x.locale.dialPad.digitZeroLabel} ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='zero-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ <${buttonTag} id='btnHashtag' value='#' stacked size='condensed' class="digit-btn" aria-label=${(
+ x
+ ) => x.locale.dialPad.digitHashtagLabel} ?disabled="${(x) =>
+ x.disabled}" @click="${(x, c) =>
+ x._onDigit(
+ c.event
+ )}"><${iconTag} slot='icon' name='hashtag-solid' class='digit-btn-num'>${iconTag}>${buttonTag}>
+ `;
+}
+
+function renderDialButton(buttonTag: string) {
+ return html`<${buttonTag} class='call-btn'
+ size='expanded'
+ appearance="filled"
+ icon="${(x) => (x.callActive ? 'disable-call-line' : 'call-line')}"
+ connotation="${(x) => (x.callActive ? 'alert' : 'cta')}"
+ ?disabled="${(x) => x.disabled}"
+ @click="${(x) => x._onDial()}"
+ label="${(x) =>
+ x.callActive
+ ? x.locale.dialPad.endCallButtonLabel
+ : x.locale.dialPad.callButtonLabel}">
+ ${buttonTag}>`;
+}
+
+/**
+ * The template for the DialPad component.
+ *
+ * @param context - element definition context
+ * @public
+ */
+export const DialPadTemplate: (
+ context: ElementDefinitionContext,
+ definition: FoundationElementDefinition
+) => ViewTemplate = (context: ElementDefinitionContext) => {
+ const buttonTag = context.tagFor(Button);
+ const iconTag = context.tagFor(Icon);
+ const textFieldTag = context.tagFor(TextField);
+
+ return html`
+ ${renderTextField(textFieldTag, buttonTag)}
+
${renderDigits(buttonTag, iconTag)}
+ ${when((x) => !x.noCall, renderDialButton(buttonTag))}
+
`;
+};
diff --git a/libs/components/src/lib/dial-pad/dial-pad.ts b/libs/components/src/lib/dial-pad/dial-pad.ts
new file mode 100644
index 0000000000..62c5f0f3ff
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/dial-pad.ts
@@ -0,0 +1,167 @@
+import { applyMixins, FoundationElement } from '@microsoft/fast-foundation';
+import { attr } from '@microsoft/fast-element';
+import { Localized } from '../../shared/patterns';
+import { TextField } from '../text-field/text-field';
+import { Button } from '../button/button';
+
+/**
+ * Base class for dial-pad
+ *
+ * @public
+ * @component dial-pad
+ * @event change - Emitted when the text field value changes
+ * @event input - Emitted when the text field value changes
+ * @event blur - Emitted when the text field loses focus
+ * @event focus - Emitted when the text field receives focus
+ * @event keypad-click - Emitted when a digit button is clicked
+ * @event dial - Emitted when the call button is clicked
+ * @event end-call - Emitted when the end call button is clicked
+ * @vueModel modelValue value input `(event.target as any).value`
+ *
+ */
+
+export class DialPad extends FoundationElement {
+ /**
+ * @internal
+ */
+ _textFieldEl!: TextField;
+
+ /**
+ * Indicates the helper-text's text.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: helper-text
+ */
+ @attr({ attribute: 'helper-text' }) helperText: string | null = null;
+
+ /**
+ * Indicates the placeholder's text.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: placeholder
+ */
+ @attr placeholder: string | null = null;
+
+ /**
+ * Indicates the value's text.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: value
+ */
+ @attr({ mode: 'fromView' }) value: string = '';
+ valueChanged(_oldValue: string, newValue: string) {
+ if (
+ newValue !== undefined &&
+ newValue !== null &&
+ this._textFieldEl &&
+ newValue !== this._textFieldEl.value
+ ) {
+ this._textFieldEl.value = newValue;
+ this._textFieldEl.reportValidity();
+ }
+ }
+
+ /**
+ * Indicates the dial pad's pattern.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: pattern
+ */
+ @attr({ mode: 'fromView' }) pattern: string = '^[0-9#*]*$';
+
+ /**
+ * Indicates the disabled state of the dial-pad.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: disabled
+ */
+ @attr({ mode: 'boolean' }) disabled = false;
+
+ /**
+ * Indicates the active state of the dial-pad.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: call-active
+ */
+ @attr({ attribute: 'call-active', mode: 'boolean' }) callActive = false;
+
+ /**
+ * Indicates the no-call state of the dial-pad.
+ *
+ * @public
+ * @remarks
+ * HTML Attribute: no-call
+ */
+ @attr({ mode: 'boolean', attribute: 'no-call' }) noCall = false;
+
+ /**
+ *
+ * @internal
+ */
+ _onDigit = (e: Event) => {
+ this.value += (e.currentTarget as Button).value;
+
+ this.$emit('keypad-click', e.currentTarget);
+ this.$emit('input');
+ this.$emit('change');
+ };
+
+ /**
+ *
+ * @internal
+ */
+ _onDial = () => {
+ this.callActive ? this.$emit('end-call') : this.$emit('dial');
+ };
+
+ /**
+ *
+ * @internal
+ */
+ _deleteLastCharacter = () => {
+ this.value = this.value.slice(0, -1);
+ };
+
+ /**
+ *
+ * @internal
+ */
+ _handleInput = () => {
+ this.value = this._textFieldEl.value;
+ this.$emit('input');
+ };
+
+ /**
+ *
+ * @internal
+ */
+ _handleChange = () => {
+ this.value = this._textFieldEl.value;
+ this.$emit('change');
+ };
+
+ /**
+ *
+ * @internal
+ */
+ _handleFocus = () => {
+ this.$emit('focus');
+ };
+
+ /**
+ *
+ * @internal
+ */
+ _handleBlur = () => {
+ this.$emit('blur');
+ };
+}
+
+export interface DialPad extends Localized {}
+applyMixins(DialPad, Localized);
diff --git a/libs/components/src/lib/dial-pad/index.ts b/libs/components/src/lib/dial-pad/index.ts
new file mode 100644
index 0000000000..5138d2b226
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/index.ts
@@ -0,0 +1,3 @@
+import { registerDialPad } from './definition';
+
+registerDialPad();
diff --git a/libs/components/src/lib/dial-pad/locale.ts b/libs/components/src/lib/dial-pad/locale.ts
new file mode 100644
index 0000000000..8fe285f647
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/locale.ts
@@ -0,0 +1,18 @@
+export interface DialPadLocale {
+ inputLabel: string;
+ deleteButtonLabel: string;
+ callButtonLabel: string;
+ endCallButtonLabel: string;
+ digitOneLabel: string;
+ digitTwoLabel: string;
+ digitThreeLabel: string;
+ digitFourLabel: string;
+ digitFiveLabel: string;
+ digitSixLabel: string;
+ digitSevenLabel: string;
+ digitEightLabel: string;
+ digitNineLabel: string;
+ digitAsteriskLabel: string;
+ digitZeroLabel: string;
+ digitHashtagLabel: string;
+}
diff --git a/libs/components/src/lib/dial-pad/ui.test.ts b/libs/components/src/lib/dial-pad/ui.test.ts
new file mode 100644
index 0000000000..adac2ba131
--- /dev/null
+++ b/libs/components/src/lib/dial-pad/ui.test.ts
@@ -0,0 +1,37 @@
+import * as path from 'path';
+import { expect, test } from '@playwright/test';
+import type { Page } from '@playwright/test';
+import {
+ extractHTMLBlocksFromReadme,
+ loadComponents,
+ loadTemplate,
+} from '../../visual-tests/visual-tests-utils.js';
+
+const components = ['dial-pad'];
+
+test('should show the component', async ({ page }: { page: Page }) => {
+ const template = extractHTMLBlocksFromReadme(
+ path.join(new URL('.', import.meta.url).pathname, 'README.md')
+ ).reduce(
+ (htmlString: string, block: string) =>
+ `${htmlString} ${block}
`,
+ ''
+ );
+
+ await loadComponents({
+ page,
+ components,
+ });
+ await loadTemplate({
+ page,
+ template,
+ });
+
+ const testWrapper = await page.$('#wrapper');
+
+ await page.waitForLoadState('networkidle');
+
+ expect(await testWrapper?.screenshot()).toMatchSnapshot(
+ './snapshots/dial-pad.png'
+ );
+});
diff --git a/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Chromium-linux.png b/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Chromium-linux.png
new file mode 100644
index 0000000000..f314d6c32f
Binary files /dev/null and b/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Chromium-linux.png differ
diff --git a/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Firefox-linux.png b/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Firefox-linux.png
new file mode 100644
index 0000000000..6b544187dc
Binary files /dev/null and b/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Firefox-linux.png differ
diff --git a/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Safari-linux.png b/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Safari-linux.png
new file mode 100644
index 0000000000..e6e93709db
Binary files /dev/null and b/libs/components/src/lib/dial-pad/ui.test.ts-snapshots/snapshots-dial-pad-Desktop-Safari-linux.png differ
diff --git a/libs/components/src/locales/en-GB.ts b/libs/components/src/locales/en-GB.ts
index fb01ebe174..44b898b4a4 100644
--- a/libs/components/src/locales/en-GB.ts
+++ b/libs/components/src/locales/en-GB.ts
@@ -114,6 +114,24 @@ const enGB: Locale = {
startThumbLabel: 'min',
endThumbLabel: 'max',
},
+ dialPad: {
+ inputLabel: 'Phone number',
+ deleteButtonLabel: 'Delete',
+ callButtonLabel: 'Call',
+ endCallButtonLabel: 'End call',
+ digitOneLabel: '1',
+ digitTwoLabel: '2 ABC',
+ digitThreeLabel: '3 DEF',
+ digitFourLabel: '4 GHI',
+ digitFiveLabel: '5 JKL',
+ digitSixLabel: '6 MNO',
+ digitSevenLabel: '7 PQRS',
+ digitEightLabel: '8 TUV',
+ digitNineLabel: '9 WXYZ',
+ digitAsteriskLabel: '*',
+ digitZeroLabel: '0',
+ digitHashtagLabel: '#',
+ },
};
export default enGB;
diff --git a/libs/components/src/locales/en-US.ts b/libs/components/src/locales/en-US.ts
index 06a4e0099e..22a2c474bb 100644
--- a/libs/components/src/locales/en-US.ts
+++ b/libs/components/src/locales/en-US.ts
@@ -114,6 +114,24 @@ const enUS: Locale = {
startThumbLabel: 'min',
endThumbLabel: 'max',
},
+ dialPad: {
+ inputLabel: 'Phone number',
+ deleteButtonLabel: 'Delete',
+ callButtonLabel: 'Call',
+ endCallButtonLabel: 'End call',
+ digitOneLabel: '1',
+ digitTwoLabel: '2 ABC',
+ digitThreeLabel: '3 DEF',
+ digitFourLabel: '4 GHI',
+ digitFiveLabel: '5 JKL',
+ digitSixLabel: '6 MNO',
+ digitSevenLabel: '7 PQRS',
+ digitEightLabel: '8 TUV',
+ digitNineLabel: '9 WXYZ',
+ digitAsteriskLabel: '*',
+ digitZeroLabel: '0',
+ digitHashtagLabel: '#',
+ },
};
export default enUS;
diff --git a/libs/components/src/locales/ja-JP.ts b/libs/components/src/locales/ja-JP.ts
index b472796274..a878820104 100644
--- a/libs/components/src/locales/ja-JP.ts
+++ b/libs/components/src/locales/ja-JP.ts
@@ -114,6 +114,24 @@ const jaJP: Locale = {
startThumbLabel: '最小',
endThumbLabel: '最大',
},
+ dialPad: {
+ inputLabel: '電話番号',
+ deleteButtonLabel: '消去',
+ callButtonLabel: '電話',
+ endCallButtonLabel: '通話終了',
+ digitOneLabel: '1',
+ digitTwoLabel: '2 ABC',
+ digitThreeLabel: '3 DEF',
+ digitFourLabel: '4 GHI',
+ digitFiveLabel: '5 JKL',
+ digitSixLabel: '6 MNO',
+ digitSevenLabel: '7 PQRS',
+ digitEightLabel: '8 TUV',
+ digitNineLabel: '9 WXYZ',
+ digitAsteriskLabel: '*',
+ digitZeroLabel: '0',
+ digitHashtagLabel: '#',
+ },
};
export default jaJP;
diff --git a/libs/components/src/locales/zh-CN.ts b/libs/components/src/locales/zh-CN.ts
index cde7d3dfc3..3ec1154f53 100644
--- a/libs/components/src/locales/zh-CN.ts
+++ b/libs/components/src/locales/zh-CN.ts
@@ -114,6 +114,24 @@ const zhCN: Locale = {
startThumbLabel: '最小',
endThumbLabel: '最大',
},
+ dialPad: {
+ inputLabel: '电话号码',
+ deleteButtonLabel: '删除',
+ callButtonLabel: '称呼',
+ endCallButtonLabel: '结束通话',
+ digitOneLabel: '1',
+ digitTwoLabel: '2 ABC',
+ digitThreeLabel: '3 DEF',
+ digitFourLabel: '4 GHI',
+ digitFiveLabel: '5 JKL',
+ digitSixLabel: '6 MNO',
+ digitSevenLabel: '7 PQRS',
+ digitEightLabel: '8 TUV',
+ digitNineLabel: '9 WXYZ',
+ digitAsteriskLabel: '*',
+ digitZeroLabel: '0',
+ digitHashtagLabel: '#',
+ },
};
export default zhCN;
diff --git a/libs/components/src/shared/localization/Locale.ts b/libs/components/src/shared/localization/Locale.ts
index 1a4aee7709..033d469ff9 100644
--- a/libs/components/src/shared/localization/Locale.ts
+++ b/libs/components/src/shared/localization/Locale.ts
@@ -9,6 +9,7 @@ import type { SplitButtonLocale } from '../../lib/split-button/locale';
import type { VideoPlayerLocale } from '../../lib/video-player/locale';
import type { TimePickerLocale } from '../../lib/time-picker/locale';
import type { RangeSliderLocale } from '../../lib/range-slider/locale';
+import type { DialPadLocale } from '../../lib/dial-pad/locale';
export interface Locale {
datePicker: DatePickerLocale;
@@ -22,4 +23,5 @@ export interface Locale {
splitButton: SplitButtonLocale;
videoPlayer: VideoPlayerLocale;
rangeSlider: RangeSliderLocale;
+ dialPad: DialPadLocale;
}