@3mo/field
Version:
A set of field web components.
280 lines (251 loc) • 7.18 kB
JavaScript
import { __decorate } from "tslib";
import { css, html, property, Component, component } from '@a11d/lit';
import { SlotController } from '@3mo/slot-controller';
import { DirectionsByLanguage } from '@3mo/localization';
import { ThemeController } from '@3mo/theme';
import '@3mo/theme';
/**
* @element mo-field
*
* @attr label
* @attr readonly
* @attr disabled
* @attr required
* @attr dense
* @attr populated
* @attr invalid
*
* @slot - The field's content
* @slot start - Content to be placed at the start of the field
* @slot end - Content to be placed at the end of the field
*
* @cssprop --mo-field-background - The field's background color
*/
let Field = class Field extends Component {
constructor() {
super(...arguments);
this.label = '';
this.readonly = false;
this.disabled = false;
this.required = false;
this.dense = false;
this.populated = false;
this.invalid = false;
this.active = false;
this.slotController = new SlotController(this);
this.themeController = new ThemeController(this);
}
static get styles() {
return css `
:host {
--mo-field-label-scale-on-focus: scale(0.75);
--mo-field-label-translate-on-focus: translateY(-50%);
--mo-field-label-transform-on-focus : var(--mo-field-label-translate-on-focus) var(--mo-field-label-scale-on-focus);
position: relative;
overflow: hidden;
display: flex;
gap: 4px;
min-width: 0;
border-start-start-radius: var(--mo-field-border-start-start-radius, var(--mo-border-radius));
border-start-end-radius: var(--mo-field-border-start-end-radius, var(--mo-border-radius));
box-sizing: border-box;
background: var(--mo-field-background, var(--mo-field-background-default));
gap: 6px;
/* TODO: Better handling of height */
height: 40px;
justify-content: center;
align-items: center;
}
:host([data-theme=light]) {
--mo-field-background-default: color-mix(in srgb, black, transparent 91%);
}
:host([data-theme=dark]) {
--mo-field-background-default: color-mix(in srgb, black, transparent 50%);
}
:host([active]) {
outline: none;
}
:host([dense]) {
height: 32px;
--mo-field-label-scale-on-focus: scale(1);
}
:host([disabled]) {
pointer-events: none;
opacity: 0.5;
}
div {
position: relative;
flex: 1 1 auto;
height: 100%;
}
div:first-child {
margin-inline-start: 10px;
}
slot[name=start]:first-child {
margin-inline-start: 8px;
}
div:last-child {
margin-inline-end: var(--mo-field-input-padding-inline-end, 10px);
}
slot[name=end]:last-child {
margin-inline-end: 8px;
}
slot:not([name])::slotted(*) {
border: 0px;
width: 100%;
font-family: inherit;
outline: none;
padding: 0.8rem 0 0 0;
height: calc(100% - 0.8rem);
color: var(--mo-color-foreground);
background-color: transparent;
text-align: inherit;
}
:host([dense]) slot:not([name])::slotted(*) {
padding: 0;
height: 100%;
}
${this.labelStyles}
${this.caretStyles}
${this.indicatorLineStyles}
slot {
color: var(--mo-color-gray);
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
`;
}
static get labelStyles() {
return css `
span {
position: absolute;
top: min(50%, 30px);
transform: var(--mo-field-label-translate-on-focus);
color: var(--mo-color-gray);
transition: .1s ease-out;
pointer-events: none;
white-space: nowrap;
overflow: hidden !important;
text-overflow: ellipsis;
max-width: 100%;
user-select: none;
}
div[data-direction=left] span {
left: 0px;
transform-origin: top left;
}
div[data-direction=right] span {
right: 0px;
transform-origin: top right;
}
:host([dense][populated]) span {
visibility: hidden;
}
:host([active]) span, :host([populated]) span {
top: 14px;
transform: var(--mo-field-label-transform-on-focus);
}
:host([active]) span {
color: var(--mo-color-accent);
}
:host([invalid]) span {
color: var(--mo-color-red);
}
`;
}
static get indicatorLineStyles() {
return css `
:host {
border-bottom: 1px solid var(--mo-color-gray-transparent);
}
:host:after {
--mo-field-initial-outline-width: 10px;
content: '';
position: absolute;
bottom: -1px;
height: 2px;
inset-inline-start: calc(calc(100% - var(--mo-field-initial-outline-width)) / 2);
width: var(--mo-field-initial-outline-width);
visibility: hidden;
background-color: var(--mo-color-accent);
transition: 0.2s ease all;
}
:host(:focus), :host([active]) {
border-bottom: 1px solid var(--mo-color-accent);
}
:host([active]):after {
visibility: visible;
width: 100%;
inset-inline-start: 0px;
}
:host([invalid]) {
border-bottom: 1px solid var(--mo-color-red);
}
:host([invalid]):after {
background-color: var(--mo-color-red);
}
`;
}
static get caretStyles() {
return css `
::slotted(*) {
caret-color: var(--mo-color-accent);
}
:host([readonly]) ::slotted(*) {
caret-color: transparent;
}
:host([invalid]) ::slotted(*) {
caret-color: var(--mo-color-red);
}
`;
}
get direction() {
const styles = getComputedStyle(this);
const isRtl = DirectionsByLanguage.get() === 'rtl'
|| this.slotController.getAssignedElements('').some(e => e.getAttribute('dir') === 'rtl');
return isRtl
? ['end', 'left'].includes(styles.textAlign) ? 'left' : 'right'
: ['end', 'right'].includes(styles.textAlign) ? 'right' : 'left';
}
get template() {
return html `
${!this.slotController.hasAssignedContent('start') ? html.nothing : html `<slot name='start'></slot>`}
<div part='container' data-direction=${this.direction}>
<span>${this.label} ${this.required ? '*' : ''}</span>
<slot></slot>
</div>
${!this.slotController.hasAssignedContent('end') ? html.nothing : html `<slot name='end'></slot>`}
`;
}
};
Field.shadowRootOptions = { ...Component.shadowRootOptions, delegatesFocus: true };
__decorate([
property({ reflect: true })
], Field.prototype, "label", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "readonly", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "disabled", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "required", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "dense", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "populated", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "invalid", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Field.prototype, "active", void 0);
Field = __decorate([
component('mo-field')
], Field);
export { Field };