Skip to content

Commit

Permalink
Initial component commit
Browse files Browse the repository at this point in the history
  • Loading branch information
chromaticWaster committed Feb 5, 2024
1 parent 5587d5d commit 739303f
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"tab": true,
"toast": true,
"alert": true,
"expander": true
"expander": true,
"mobile-field": true
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"toast",
"tab",
"alert",
"expander"
"expander",
"mobile-field"
],
"exports": {
"./*": "./dist/*/index.js"
Expand Down
51 changes: 51 additions & 0 deletions src/mobile-field/MobileField.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
testClearableBehaviour,
testCustomClearableSlotBehaviour,
testDisabledBehaviour,
testErrorBehaviour,
testHintBehaviour,
testLabelBehaviour,
testPrefixBehaviour,
testSuffixBehaviour,
testValueBehaviour
} from '../core/OmniInputPlaywright.js';
import { expect, mockEventListener, test, withCoverage } from '../utils/JestPlaywright.js';
import type { MobileField } from './MobileField.js';

test(`Mobile Field - Visual and Behaviour`, async ({ page }) => {
await withCoverage(page, async () => {
await page.goto('/components/mobile-field/');
await page.evaluate(() => document.fonts.ready);

const mobileField = page.locator('[data-testid]').first();
mobileField.evaluate(async (t: MobileField) => {
t.value = '';
await t.updateComplete;
});

// Confirm that the component matches the provided screenshot.
await expect(mobileField).toHaveScreenshot('mobile-field.png');

const inputFn = await mockEventListener(mobileField, 'input');

const inputField = mobileField.locator('#inputField');

const value = '12345';
await inputField.type(value);

await expect(inputField).toHaveValue(value);

await expect(inputFn).toBeCalledTimes(value.length);
await expect(mobileField).toHaveScreenshot('mobile-field-value.png');
});
});

test('Mobile Field - Label Behaviour', testLabelBehaviour('omni-mobile-field'));
test('Mobile Field - Hint Behaviour', testHintBehaviour('omni-mobile-field'));
test('Mobile Field - Error Behaviour', testErrorBehaviour('omni-mobile-field'));
test('Mobile Field - Value Behaviour', testValueBehaviour('omni-mobile-field'));
test('Mobile Field - Clearable Behaviour', testClearableBehaviour('omni-mobile-field'));
test('Mobile Field - Custom Clear Slot Behaviour', testCustomClearableSlotBehaviour('omni-mobile-field'));
test('Mobile Field - Prefix Behaviour', testPrefixBehaviour('omni-mobile-field'));
test('Mobile Field - Suffix Behaviour', testSuffixBehaviour('omni-mobile-field'));
test('Mobile Field - Disabled Behaviour', testDisabledBehaviour('omni-mobile-field'));
71 changes: 71 additions & 0 deletions src/mobile-field/MobileField.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { html, nothing } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import {
BaseArgs,
ClearableStory,
CustomClearableSlot,
DisabledStory,
ErrorStory,
HintStory,
LabelStory,
PrefixStory,
SuffixStory,
ValueStory
} from '../core/OmniInputStories.js';
import { ifNotEmpty } from '../utils/Directives.js';
import { ComponentStoryFormat, assignToSlot, getSourceFromLit } from '../utils/StoryUtils.js';

import './MobileField.js';

interface Args extends BaseArgs {
countryCode: boolean;
}

export const Interactive: ComponentStoryFormat<Args> = {
render: (args) => html`
<omni-mobile-field
data-testid="test-mobile-field"
label="${ifNotEmpty(args.label)}"
value="${args.value}"
hint="${ifNotEmpty(args.hint)}"
error="${ifNotEmpty(args.error)}"
?country-code="${args.countryCode}"
?disabled="${args.disabled}"
?clearable="${args.clearable}"
>${args.prefix ? html`${'\r\n'}${unsafeHTML(assignToSlot('prefix', args.prefix))}` : nothing}
${args.clear ? html`${'\r\n'}${unsafeHTML(assignToSlot('clear', args.clear))}` : nothing}${
args.suffix ? html`${'\r\n'}${unsafeHTML(assignToSlot('suffix', args.suffix))}` : nothing
}${args.prefix || args.suffix || args.clear ? '\r\n' : nothing}</omni-mobile-field>
`,
frameworkSources: [
{
framework: 'Vue',
load: (args) =>
getSourceFromLit(Interactive!.render!(args), undefined, (s) =>
s.replace(' disabled', ' :disabled="true"').replace(' clearable', ' :clearable="true"')
)
}
],
name: 'Interactive',
args: {
label: 'Label',
value: '',
hint: '',
error: '',
disabled: false,
prefix: '',
suffix: '',
clear: '',
countryCode: true
}
};

export const Label = LabelStory<BaseArgs>('omni-mobile-field');
export const Hint = HintStory<BaseArgs>('omni-mobile-field');
export const Error_Label = ErrorStory<BaseArgs>('omni-mobile-field');
export const Value = ValueStory<BaseArgs>('omni-mobile-field', 123);
export const Clearable = ClearableStory<BaseArgs>('omni-mobile-field', 123);
export const Custom_Clear_Slot = CustomClearableSlot<BaseArgs>('omni-mobile-field', 123);
export const Prefix = PrefixStory<BaseArgs>('omni-mobile-field');
export const Suffix = SuffixStory<BaseArgs>('omni-mobile-field');
export const Disabled = DisabledStory<BaseArgs>('omni-mobile-field', 123);
253 changes: 253 additions & 0 deletions src/mobile-field/MobileField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { css, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { live } from 'lit/directives/live.js';
import { OmniFormElement, ifDefined } from '../core/OmniFormElement.js';

/**
* Input control to enter a mobile number.
*
* @import
* ```js
* import '@capitec/omni-components/mobile-field';
* ```
* @example
* ```html
* <omni-mobile-field
* label="Enter a mobile number"
* value=5555555
* hint="Required"
* error="Field level error message"
* disabled>
* </omni-mobile-field>
* ```
*
* @element omni-mobile-field
*
* @cssprop --omni-mobile-field-text-align - Mobile field text align.
* @cssprop --omni-mobile-field-font-color - Mobile field font color.
* @cssprop --omni-mobile-field-font-family - Mobile field font family.
* @cssprop --omni-mobile-field-font-size - Mobile field font size.
* @cssprop --omni-mobile-field-font-weight - Mobile field font weight.
* @cssprop --omni-mobile-field-padding - Mobile field padding.
* @cssprop --omni-mobile-field-height - Mobile field height.
* @cssprop --omni-mobile-field-width - Mobile field width.
*
* @cssprop --omni-mobile-field-disabled-font-color - Mobile field disabled font color.
* @cssprop --omni-mobile-field-error-font-color - Mobile field error font color.
*/
@customElement('omni-mobile-field')
export class MobileField extends OmniFormElement {
@query('#inputField')
private _inputElement?: HTMLInputElement;

/**
* Indicator if the component should allow the entry of a country code ie: +21 .
* @attr [country-code]
*/
@property({ type: Boolean, reflect: true, attribute: 'country-code' }) countryCode?: boolean;

/**
* Disables native on screen keyboards for the component.
* @attr [no-native-keyboard]
*/
@property({ type: Boolean, reflect: true, attribute: 'no-native-keyboard' }) noNativeKeyboard?: boolean;

override connectedCallback() {
super.connectedCallback();
this.addEventListener('input', this._keyInput.bind(this), {
capture: true
});
this.addEventListener('keydown', this._keyDown.bind(this), {
capture: true
});
}

// Added for browsers that allow text values entered into a input when type is set to number.
override async attributeChangedCallback(name: string, _old: string | null, value: string | null): Promise<void> {
super.attributeChangedCallback(name, _old, value);
if (name === 'value') {
if (new RegExp('^[0-9]+$').test(value as string) === false) {
return;
}
}
}

override focus(options?: FocusOptions | undefined): void {
if (this._inputElement) {
this._inputElement.focus(options);
} else {
super.focus(options);
}
}

_keyDown(e: KeyboardEvent) {
const input = this._inputElement as HTMLInputElement;
// Stop alpha keys
if (e.key >= 'a' && e.key <= 'z') {
e.preventDefault();
return;
}

console.log('event', e);
console.log('event key', e.key);
console.log('is number', this._isNumber(e.key as string));

if (input && e.key) {
if (this._isValid(e.key)) {
} else {
e.preventDefault();
return;
}

// console.log('selectionStart', input.selectionStart);
// console.log('selectionEnd',input.selectionEnd);
// if(this.countryCode){
// if(input.selectionStart === 0 && input.selectionEnd === 0){
// if(e.shiftKey === true && e.key !== '+'){
// e.preventDefault();
// return;
// }
// } else if (!this._isNumber(e.key as string) && e.key !== 'Backspace') {
// e.preventDefault();
// return;
// }
// } else {
// if (!this._isNumber(e.key as string) && e.key !== 'Backspace') {
// e.preventDefault();
// return;
// }
// }

// if (input.selectionStart === 0 && input.selectionEnd === 0) {

// if(this.countryCode && ( e.key !== '+' || !this._isNumber(e.key as string))){
// e.preventDefault();
// return;
// }
// }

// if(!this._isNumber(e.key as string)){
// e.preventDefault();
// return;
// }
}
}

_keyInput() {
const input = this._inputElement as HTMLInputElement;
this.value = input?.value;
}

// Check if the value provided is valid, if there is invalid alpha characters they are removed.
_sanitiseMobileValue() {
this.value = this.value?.toString()?.replace(/^([+]\d{2})?\d{10}$/gi, '');

if (this._inputElement) {
this._inputElement.value = this.value as string;
}
}

// Used to check if the value provided in a valid mobile number value.
_isMobileNumber(number: string) {
return /^([+]\d{2})?\d{10}$/.test(number);
}

_isNumber(number: string) {
return /\d/.test(number);
}

_isValid(keyValue: string) {
const input = this._inputElement as HTMLInputElement;

if (keyValue === 'Backspace') {
return true;
}

if (/\d/.test(keyValue)) {
return true;
}

if (input.selectionStart === 0 && input.selectionEnd === 0) {
if (keyValue === '+') {
return true;
}
}

return false;
}

static override get styles() {
return [
super.styles,
css`
.field {
flex: 1 1 auto;
border: none;
background: none;
box-shadow: none;
outline: 0;
padding: 0;
margin: 0;
text-align: var(--omni-mobile-field-text-align, left);
color: var(--omni-mobile-field-font-color, var(--omni-font-color));
font-family: var(--omni-mobile-field-font-family, var(--omni-font-family));
font-size: var(--omni-mobile-field-font-size, var(--omni-font-size));
font-weight: var(--omni-mobile-field-font-weight, var(--omni-font-weight));
padding: var(--omni-mobile-field-padding, 10px);
height: var(--omni-mobile-field-height, 100%);
width: var(--omni-mobile-field-width, 100%);
}
.field.disabled {
color: var(--omni-mobile-field-disabled-font-color, #7C7C7C);
}
.field.error {
color: var(--omni-mobile-field-error-font-color, var(--omni-font-color));
}
/* Used to not display default stepper */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
`
];
}

protected override renderContent() {
const field: ClassInfo = {
field: true,
disabled: this.disabled,
error: this.error as string
};
return html`
<input
class=${classMap(field)}
id="inputField"
data-omni-keyboard-mode="tel"
type="tel"
inputmode="${ifDefined(this.noNativeKeyboard ? 'none' : undefined)}"
.value=${live(this.value as string)}
?readOnly=${this.disabled}
tabindex="${this.disabled ? -1 : 0}" />
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'omni-mobile-field': MobileField;
}
}
1 change: 1 addition & 0 deletions src/mobile-field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './MobileField.js';

0 comments on commit 739303f

Please sign in to comment.