229 lines
5.3 KiB
TypeScript
229 lines
5.3 KiB
TypeScript
|
|
import {
|
||
|
|
DeesElement,
|
||
|
|
css,
|
||
|
|
cssManager,
|
||
|
|
customElement,
|
||
|
|
html,
|
||
|
|
property,
|
||
|
|
type TemplateResult,
|
||
|
|
} from '@design.estate/dees-element';
|
||
|
|
|
||
|
|
import { mobileComponentStyles } from '../../00componentstyles.js';
|
||
|
|
import { demoFunc } from './dees-mobile-input.demo.js';
|
||
|
|
|
||
|
|
export type InputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface HTMLElementTagNameMap {
|
||
|
|
'dees-mobile-input': DeesMobileInput;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@customElement('dees-mobile-input')
|
||
|
|
export class DeesMobileInput extends DeesElement {
|
||
|
|
public static demo = demoFunc;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor type: InputType = 'text';
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor placeholder: string = '';
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor value: string = '';
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor disabled: boolean = false;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor id: string = '';
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor name: string = '';
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor label: string = '';
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor error: string = '';
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor required: boolean = false;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor autocomplete: string = '';
|
||
|
|
|
||
|
|
public static styles = [
|
||
|
|
cssManager.defaultStyles,
|
||
|
|
mobileComponentStyles,
|
||
|
|
css`
|
||
|
|
:host {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
.input-wrapper {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.375rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
label {
|
||
|
|
font-size: 0.875rem;
|
||
|
|
font-weight: 500;
|
||
|
|
color: var(--dees-foreground);
|
||
|
|
}
|
||
|
|
|
||
|
|
label .required {
|
||
|
|
color: var(--dees-danger);
|
||
|
|
margin-left: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
input {
|
||
|
|
width: 100%;
|
||
|
|
height: 2.5rem;
|
||
|
|
padding: 0 0.75rem;
|
||
|
|
/* 16px minimum to prevent iOS zoom */
|
||
|
|
font-size: 1rem;
|
||
|
|
line-height: 1.25rem;
|
||
|
|
color: var(--dees-foreground);
|
||
|
|
background: var(--dees-background);
|
||
|
|
border: 1px solid var(--dees-input);
|
||
|
|
border-radius: calc(var(--dees-radius) - 2px);
|
||
|
|
outline: none;
|
||
|
|
transition: all var(--dees-transition-fast);
|
||
|
|
box-sizing: border-box;
|
||
|
|
-webkit-appearance: none;
|
||
|
|
font-family: inherit;
|
||
|
|
}
|
||
|
|
|
||
|
|
input:focus {
|
||
|
|
outline: 2px solid transparent;
|
||
|
|
outline-offset: 2px;
|
||
|
|
border-color: var(--dees-ring);
|
||
|
|
box-shadow: 0 0 0 2px var(--dees-background), 0 0 0 4px var(--dees-ring);
|
||
|
|
}
|
||
|
|
|
||
|
|
input:disabled {
|
||
|
|
opacity: 0.5;
|
||
|
|
cursor: not-allowed;
|
||
|
|
background: var(--dees-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
input::placeholder {
|
||
|
|
color: var(--dees-muted-foreground);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Remove number input spinners */
|
||
|
|
input::-webkit-outer-spin-button,
|
||
|
|
input::-webkit-inner-spin-button {
|
||
|
|
-webkit-appearance: none;
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
input[type=number] {
|
||
|
|
-moz-appearance: textfield;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Error state */
|
||
|
|
:host([error]) input,
|
||
|
|
input.error {
|
||
|
|
border-color: var(--dees-danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
:host([error]) input:focus,
|
||
|
|
input.error:focus {
|
||
|
|
box-shadow: 0 0 0 2px var(--dees-background), 0 0 0 4px var(--dees-danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-message {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--dees-danger);
|
||
|
|
margin-top: 0.25rem;
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
];
|
||
|
|
|
||
|
|
private handleInput(e: Event) {
|
||
|
|
const input = e.target as HTMLInputElement;
|
||
|
|
this.value = input.value;
|
||
|
|
this.dispatchEvent(new CustomEvent('input', {
|
||
|
|
detail: { value: input.value },
|
||
|
|
bubbles: true,
|
||
|
|
composed: true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleChange(e: Event) {
|
||
|
|
const input = e.target as HTMLInputElement;
|
||
|
|
this.value = input.value;
|
||
|
|
this.dispatchEvent(new CustomEvent('change', {
|
||
|
|
detail: { value: input.value },
|
||
|
|
bubbles: true,
|
||
|
|
composed: true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleFocus() {
|
||
|
|
// Emit input-focus for keyboard detection
|
||
|
|
this.dispatchEvent(new CustomEvent('input-focus', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBlur() {
|
||
|
|
// Emit input-blur for keyboard detection
|
||
|
|
this.dispatchEvent(new CustomEvent('input-blur', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
public render(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="input-wrapper">
|
||
|
|
${this.label ? html`
|
||
|
|
<label for=${this.id || 'input'}>
|
||
|
|
${this.label}
|
||
|
|
${this.required ? html`<span class="required">*</span>` : ''}
|
||
|
|
</label>
|
||
|
|
` : ''}
|
||
|
|
<input
|
||
|
|
.type=${this.type}
|
||
|
|
.placeholder=${this.placeholder}
|
||
|
|
.value=${this.value}
|
||
|
|
?disabled=${this.disabled}
|
||
|
|
?required=${this.required}
|
||
|
|
.id=${this.id || 'input'}
|
||
|
|
.name=${this.name}
|
||
|
|
.autocomplete=${this.autocomplete}
|
||
|
|
class=${this.error ? 'error' : ''}
|
||
|
|
@input=${this.handleInput}
|
||
|
|
@change=${this.handleChange}
|
||
|
|
@focus=${this.handleFocus}
|
||
|
|
@blur=${this.handleBlur}
|
||
|
|
/>
|
||
|
|
${this.error ? html`
|
||
|
|
<div class="error-message">${this.error}</div>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Focus the input programmatically
|
||
|
|
*/
|
||
|
|
public focus() {
|
||
|
|
const input = this.shadowRoot?.querySelector('input');
|
||
|
|
input?.focus();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Blur the input programmatically
|
||
|
|
*/
|
||
|
|
public blur() {
|
||
|
|
const input = this.shadowRoot?.querySelector('input');
|
||
|
|
input?.blur();
|
||
|
|
}
|
||
|
|
}
|