Skip to content

Commit

Permalink
feat: add textfield custom element
Browse files Browse the repository at this point in the history
  • Loading branch information
digitalsadhu committed Aug 14, 2022
1 parent b642ea1 commit b2dd061
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 0 deletions.
131 changes: 131 additions & 0 deletions packages/textfield/index.js
@@ -0,0 +1,131 @@
import { css, html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { fclasses, FabricElement } from '../utils';

class FabricTextField extends FabricElement {
static properties = {
disabled: { type: Boolean },
invalid: { type: Boolean },
id: { type: String },
label: { type: String },
helpText: { type: String, attribute: 'help-text' },
max: { type: Number },
min: { type: Number },
minLength: { type: Number, attribute: 'min-length' },
maxLength: { type: Number, attribute: 'max-length' },
name: { type: String },
pattern: { type: String },
placeholder: { type: String },
readOnly: { type: Boolean, attribute: 'read-only' },
required: { type: Boolean },
type: { type: String },
value: { type: String },
};

// Slotted elements remain in lightDOM which allows for control of their style outside of shadowDOM.
// ::slotted([Simple Selector]) confirms to Specificity rules, but (being simple) does not add weight to lightDOM skin selectors,
// so never gets higher Specificity. Thus in order to overwrite style linked within shadowDOM, we need to use !important.
// https://stackoverflow.com/a/61631668
static styles = css`
:host {
display: block;
}
::slotted(:last-child) {
margin-bottom: 0px !important;
}
`;

constructor() {
super();
this.type = 'text';
}

get _outerWrapperStyles() {
return fclasses({
// 'has-suffix': hasSuffix,
// 'has-prefix': hasPrefix,
});
}

get _innerWrapperStyles() {
return fclasses({
'input mb-0': true,
'input--is-invalid': this.invalid,
'input--is-disabled': this.disabled,
'input--is-read-only': this.readOnly,
});
}

get _label() {
if (this.label) {
return html`<label for="${this._id}">${this.label}</label>`;
}
}

get _helpId() {
if (this.helpText) return `${this._id}__hint`;
}

get _id() {
return 'textfield';
}

get _error() {
if (this.invalid && this._helpId) return this._helpId;
}

blurHandler(e) {
this.dispatchEvent(new CustomEvent('blur'));
}

changeHandler(e) {
this.dispatchEvent(new CustomEvent('change'));
}

focusHandler(e) {
this.dispatchEvent(new CustomEvent('focus'));
}

render() {
return html`
${this._fabricStylesheet}
<div class="${this._outerWrapperStyles}">
<div class="${this._innerWrapperStyles}">
${this._label}
<div class="relative">
<input
type="${this.type}"
min="${ifDefined(this.min)}"
max="${ifDefined(this.max)}"
minlength="${ifDefined(this.minLength)}"
maxlength="${ifDefined(this.maxLength)}"
name="${ifDefined(this.name)}"
pattern="${ifDefined(this.pattern)}"
placeholder="${ifDefined(this.placeholder)}"
value="${ifDefined(this.value)}"
aria-describedby="${ifDefined(this._helpId)}"
aria-errormessage="${ifDefined(this._error)}"
aria-invalid="${ifDefined(this.invalid)}"
id="${this._id}"
?disabled="${this.disabled}"
?readonly="${this.readOnly}"
?required="${this.required}"
@onBlur=${this.blurHandler}
@onChange=${this.changeHandler}
@onFocus=${this.focusHandler}
/>
<slot></slot>
</div>
${this.helpText &&
html`<div class="input__sub-text" id="${this._helpId}">${this.helpText}</div>`}
</div>
</div>
`;
}
}

if (!customElements.get('f-textfield')) {
customElements.define('f-textfield', FabricTextField);
}

export { FabricTextField };
37 changes: 37 additions & 0 deletions packages/textfield/test.js
@@ -0,0 +1,37 @@
/* eslint-disable no-undef */
import tap, { test, beforeEach, teardown } from 'tap';
import { chromium } from 'playwright';
import { addContentToPage } from '../../tests/utils/index.js';

tap.before(async () => {
const browser = await chromium.launch({ headless: true });
tap.context.browser = browser;
});

beforeEach(async (t) => {
const { browser } = t.context;
const context = await browser.newContext();
t.context.page = await context.newPage();
});

teardown(async () => {
const { browser } = tap.context;
browser.close();
});

test('Text field component with a value attribute is rendered on the page', async (t) => {
// GIVEN: A box component
const component = `
<f-textfield value="this is a textfield"></f-textfield>
`;

// WHEN: the component is added to the page
const page = await addContentToPage({
page: t.context.page,
content: component,
});

// THEN: the component is visible in the DOM
const locator = await page.locator('f-textfield');
t.equal(await locator.getAttribute('value'), 'this is a textfield', 'value should be defined');
});

0 comments on commit b2dd061

Please sign in to comment.