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

Feature/mask mixin #140

Merged
merged 15 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
29 changes: 16 additions & 13 deletions packages/library/components/inputter/src/inputter-component.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { html, MuonElement, classMap, ScopedElementsMixin } from '@muons/library';
import { html, MuonElement, ScopedElementsMixin, classMap, styleMap } from '@muons/library';
import {
INPUTTER_TYPE,
INPUTTER_DETAIL_TOGGLE_OPEN,
Expand All @@ -7,23 +7,23 @@ import {
INPUTTER_VALIDATION_WARNING_ICON
} from '@muons/library/build/tokens/es6/muon-tokens';
import { ValidationMixin } from '@muons/library/mixins/validation-mixin';
import { MaskMixin } from '@muons/library/mixins/mask-mixin';
import { DetailMixin } from '@muons/library/mixins/detail-mixin';
import { Icon } from '@muons/library/components/icon';
import styles from './styles.css';

/**
* Allow for inputs
* A component to allow for user inputs of type text, radio, checkbox, select,
* date, tel, number, textarea, search.
*
* @element inputter
*/

export class Inputter extends ScopedElementsMixin(ValidationMixin(MuonElement)) {
export class Inputter extends ScopedElementsMixin(MaskMixin(ValidationMixin(MuonElement))) {

static get properties() {
return {
helper: { type: String },
mask: { type: String },
separator: { type: String },
isHelperOpen: { type: Boolean }
};
}
Expand Down Expand Up @@ -52,12 +52,6 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MuonElement))
return html`<inputter-icon name="${INPUTTER_VALIDATION_WARNING_ICON}" class="validation-icon"></inputter-icon>`;
}

get validity() {
this.pristine = false;
this.validate();
return this._validity;
}

/**
* A method to check availability of tip details slot.
* @returns {Boolean} - availability of tip details slot.
Expand Down Expand Up @@ -94,15 +88,24 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MuonElement))
get standardTemplate() {
const classes = {
'slotted-content': true,
'select-arrow': this._inputType === this._isSelect
'select-arrow': this._isSelect,
'has-mask': this.mask
};

let styles = {};
if (this.mask) {
styles = {
'--maxlength': this.mask.length
};
}

return html `
<div class="${classMap(classes)}">
<div class="${classMap(classes)}" style="${styleMap(styles)}">
${this._isMultiple ? this._headingTemplate : this._labelTemplate}
${this._helperTemplate}
<div class="input-holder">
${super.standardTemplate}
${this._maskTemplate}
</div>
</div>
${this._validationMessageTemplate}`;
Expand Down
29 changes: 29 additions & 0 deletions packages/library/components/inputter/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@
:host {
display: block;

& .has-mask {
position: relative;

& .input-mask,
& ::slotted(input) {
padding: 0.5rem 1rem;
margin: 0.75rem 0;
letter-spacing: 0.5rem;
max-width: calc((var(--maxlength) + 1) * 1rem);
}

& .input-mask {
display: inline-block;
position: absolute;
left: 0;
color: lightslategray;
white-space: pre;
z-index: -1;
text-align: start;
font-size: 1.5rem;
}

& ::slotted(input) {
background-color: transparent;
font-size: 1.25rem;
padding-right: 1.25rem;
}
}

& .validation {
display: flex;
margin: 0.5rem 0;
Expand Down
18 changes: 17 additions & 1 deletion packages/library/components/inputter/story.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,33 @@ const textareaInputText = (args) => `
export const Textarea = (args) => details.template(args, textareaInputText);
Textarea.args = { label: 'A label', value: 'gas', validation: '[&quot;isRequired&quot;]' };

export const Mask = (args) => details.template(args, innerInputText);
Mask.args = { label: 'A label', value: '', mask: '000000' };

export const Separator = (args) => details.template(args, innerInputText);
Separator.args = { label: 'A label', value: '', separator: '-', mask: ' - - ' };

const innerInputDate = (args) => `
<label slot="label">${args.label}</label>
<input type="text" value="${args.value}" />
`;
export const Date = (args) => details.template(args, innerInputDate);
Date.args = { label: 'A label', value: '', validation: '[&quot;isRequired&quot;,&quot;minDate(\'11/11/2021\')&quot;]' };

export const DateMask = (args) => details.template(args, innerInputDate);
DateMask.args = { label: 'A label', value: '', mask: 'dd/mm/yyyy', separator: '/', validation: '[&quot;isRequired&quot;,&quot;minDate(\'11/11/2021\')&quot;]' };

const innerInputTel = (args) => `
<label slot="label">${args.label}</label>
<input type="tel" value="${args.value}" pattern="[0-9]{3}" title="match the pattern"/>
`;

export const Tel = (args) => details.template(args, innerInputTel);
Tel.args = { label: 'A label', value: '', validation: '[&quot;isRequired&quot;]' };
Tel.args = { label: 'A label', value: '', validation: '[&quot;isRequired&quot;]', mask: '000-000-0000', separator: '-' };

const innerInputNumber = (args) => `
<label slot="label">${args.label}</label>
<input type="number" value="${args.value}" />
`;
export const Number = (args) => details.template(args, innerInputNumber);
Number.args = { label: 'A label', value: '', validation: '[&quot;isRequired&quot;]' };
2 changes: 1 addition & 1 deletion packages/library/mixins/form-element-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const FormElementMixin = (superClass) =>
}

firstUpdated() {

super.firstUpdated();
this._slottedInputs.forEach((input) => {
input.addEventListener('change', this._onChange.bind(this));
input.addEventListener('blur', this._onBlur.bind(this));
Expand Down
151 changes: 151 additions & 0 deletions packages/library/mixins/mask-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { html, ifDefined } from '@muons/library';
import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { FormElementMixin } from './form-element-mixin';

/**
* A mixin to enable mask and separator features to a form element.
* `mask` property is supported for input of type text, date, tel.
* `separator` property is supported for input of type text, date, tel.
* @mixin
*/

export const MaskMixin = dedupeMixin((superclass) =>
class MaskMixinClass extends FormElementMixin(superclass) {
static get properties() {
return {
mask: {
type: String
},

separator: {
type: String
}
};
}

constructor() {
super();

this.mask = '';
this.separator = '';
}

firstUpdated() {
super.firstUpdated();

if (this.mask) {
this._slottedInputs.map((input) => {
input.addEventListener('input', this._onInput.bind(this));
input.setAttribute('maxlength', this.mask.length);
});
}
}

/**
* A method to handle `input` event when `mask` is provided.
* @param {Event} inputEvent - event while 'input.
* @returns {undefined}
* @protected
* @override
*/
_onInput(inputEvent) {
inputEvent.stopPropagation();
inputEvent.preventDefault();
const inputElement = this._slottedInputs[0];
if (ifDefined(this.separator)) {
this.updateValue(inputElement);
} else {
this.value = inputElement.value;
}
}

_processValue(value) {
value = super._processValue(value);
if (ifDefined(this.separator)) {
value = this.formatWithMaskAndSeparator(value);
this._slottedInputs[0].value = value;
}
return value;
}

/**
* A method to update the form element value with separator in adjusted indices and cursor position.
*
* @param {HTMLInputElement} input - HTMLInputElement value to be updated with seperators
* @returns {undefined}
*/
updateValue(input) {
let value = input.value;
let cursor = input.selectionStart;
const diff = this.value.length - value.length;

if (diff > 0 && this.mask.charAt(cursor) === this.separator) {
value = value.slice(0, cursor - 1) + (cursor < value.length ? value.slice(cursor) : '');
cursor -= 1;
}
const formattedValue = this.formatWithMaskAndSeparator(value);
input.value = formattedValue;
this.value = formattedValue;

if (this.mask.charAt(cursor) === this.separator) {
cursor += 1;
}
this.updateComplete.then(() => {
input.setSelectionRange(cursor, cursor);
});
}

/**
* A method to format the form element value with separator adjusted to correct indices
* after editing the form element value.
*
* @param {String} value - value of the form element.
* @return {String} - value with adjusted separator in correct indices.
*/
formatWithMaskAndSeparator(value) {
const formattedValue = this.__formatInputWithoutSeparator(value);
const parts = this.mask.split(this.separator);
let processedValue = '';
let length = 0;
let currentLength = 0;

for (let i = 0; i < parts.length && length < formattedValue.length; i++) {
const remainingLength = formattedValue.length - length;
const splitPoint = remainingLength > parts[i].length ? parts[i].length : remainingLength;

processedValue += formattedValue.substr(length, splitPoint);
currentLength += parts[i].length;

if (i < (parts.length - 1) && processedValue.length === currentLength) {
processedValue += this.separator;
currentLength += 1;
}

length += parts[i].length;
}

return processedValue;
}

/**
* A method to remove separator from the value of the form element.
*
* @param {String} value - form element value.
* @return {String} - value with separator removed.
*/
__formatInputWithoutSeparator(value) {
return value.split(this.separator).join('');
}

get _maskTemplate() {
if (this.mask) {
const length = this.value ? this.value.length : 0;
let updatedMask = new Array(length + 1).join(' ');
updatedMask += this.mask.slice(length);
return html`<div aria-hidden="true" class="input-mask">${updatedMask}</div>`;
} else {
return undefined;
}
}
}
);
79 changes: 79 additions & 0 deletions packages/library/tests/components/inputter/inputter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint-disable no-undef */
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
import { Inputter } from '@muons/library/components/inputter';
import { defaultChecks } from '../../helpers';

const tagName = defineCE(Inputter);
const tag = unsafeStatic(tagName);

describe('Inputter', () => {
describe('standard default', async () => {
let inputter;
before(async () => {
inputter = await fixture(html`
<${tag}>
</${tag}>`);
});

it('default checks', async () => {
await defaultChecks(inputter);
});

it('default properties', async () => {
expect(inputter.type).to.equal('standard', 'default type is set');
expect(inputter.id).to.not.be.null; // eslint-disable-line no-unused-expressions
});
});
describe('text input', async () => {
describe('mask text', async () => {
let inputter;
let shadowRoot;
before(async () => {
inputter = await fixture(html`
<${tag} mask="0000">
<label slot="label">input label</label>
<input type="text" value=""/>
</${tag}>`);
shadowRoot = inputter.shadowRoot;
});

it('default checks', async () => {
await defaultChecks(inputter);
});

it('default properties', async () => {
expect(inputter.type).to.equal('standard', 'default type is set');
expect(inputter.id).to.not.be.null; // eslint-disable-line no-unused-expressions
expect(shadowRoot.querySelector('.has-mask')).to.not.be.null; // eslint-disable-line no-unused-expressions
});
});
});

describe('radio input', async () => {
describe('standard radio', async () => {
let inputter;
let shadowRoot;
before(async () => {
inputter = await fixture(html`
<${tag} heading="What is your heating source?">
<input type="radio" id="question-gas" name="question" value="gas"></input>
<label for="question-gas">Gas</label>
<input type="radio" id="question-electricity" name="question" value="electricity"></input>
<label for="question-electricity">Electricity</label>
</${tag}>`);
shadowRoot = inputter.shadowRoot;
});

it('default checks', async () => {
await defaultChecks(inputter);
});

it('default properties', async () => {
expect(inputter.type).to.equal('standard', 'default type is set');
expect(inputter.id).to.not.be.null; // eslint-disable-line no-unused-expressions
expect(shadowRoot.querySelector('.input-heading')).to.not.be.null; // eslint-disable-line no-unused-expressions
expect(shadowRoot.querySelector('.input-mask')).to.be.null; // eslint-disable-line no-unused-expressions
});
});
});
});
Loading