diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index f4a5500d93..15482b68d0 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -109,7 +109,7 @@ const AREAS = [ const SCHEMAS: { name: string; - input: Record; + input: Record; }[] = [ { name: "One of each", @@ -166,7 +166,9 @@ const SCHEMAS: { object: { name: "Object", selector: { object: {} } }, select_radio: { name: "Select (Radio)", - selector: { select: { options: ["Option 1", "Option 2"] } }, + selector: { + select: { options: ["Option 1", "Option 2"], mode: "list" }, + }, }, select: { name: "Select", @@ -183,6 +185,22 @@ const SCHEMAS: { }, }, }, + select_custom: { + name: "Select (Custom)", + selector: { + select: { + custom_value: true, + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, + }, icon: { name: "Icon", selector: { icon: {} } }, media: { name: "Media", selector: { media: {} } }, location: { name: "Location", selector: { location: {} } }, @@ -203,6 +221,34 @@ const SCHEMAS: { entity: { name: "Entity", selector: { entity: { multiple: true } } }, device: { name: "Device", selector: { device: { multiple: true } } }, area: { name: "Area", selector: { area: { multiple: true } } }, + select: { + name: "Select Multiple", + selector: { + select: { + multiple: true, + custom_value: true, + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, + }, + select_checkbox: { + name: "Select Multiple (Checkbox)", + required: false, + selector: { + select: { + mode: "list", + multiple: true, + options: ["Option 1", "Option 2", "Option 3", "Option 4"], + }, + }, + }, }, }, ]; diff --git a/src/components/ha-chip-set.ts b/src/components/ha-chip-set.ts index 2e659c8353..699f1b0a73 100644 --- a/src/components/ha-chip-set.ts +++ b/src/components/ha-chip-set.ts @@ -25,13 +25,7 @@ export class HaChipSet extends LitElement { ${unsafeCSS(chipStyles)} slot::slotted(ha-chip) { - margin: 4px; - } - slot::slotted(ha-chip:first-of-type) { - margin-left: -4px; - } - slot::slotted(ha-chip:last-of-type) { - margin-right: -4px; + margin: 4px 4px 4px 0; } `; } diff --git a/src/components/ha-chip.ts b/src/components/ha-chip.ts index 2a41ffc760..f22e0e29ad 100644 --- a/src/components/ha-chip.ts +++ b/src/components/ha-chip.ts @@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators"; export class HaChip extends LitElement { @property({ type: Boolean }) public hasIcon = false; + @property({ type: Boolean }) public hasTrailingIcon = false; + @property({ type: Boolean }) public noText = false; protected render(): TemplateResult { @@ -30,6 +32,11 @@ export class HaChip extends LitElement { + ${this.hasTrailingIcon + ? html`
+ +
` + : null} `; } @@ -53,14 +60,20 @@ export class HaChip extends LitElement { color: var(--ha-chip-text-color, var(--primary-text-color)); } - .mdc-chip__icon--leading { - --mdc-icon-size: 20px; + .mdc-chip__icon--leading, + .mdc-chip__icon--trailing { + --mdc-icon-size: 18px; + line-height: 14px; color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); } .mdc-chip.no-text .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { margin-right: -4px; } + + span[role="gridcell"] { + line-height: 14px; + } `; } } diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 7c44541bcc..c1bc266ed9 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -110,14 +110,18 @@ export class HaComboBox extends LitElement { return this._comboBox.selectedItem; } + public setInputValue(value: string) { + this._comboBox.value = value; + } + protected render(): TemplateResult { return html` + typeof option === "object" ? option : { value: option, label: option } + ); + + if (!this.selector.select.custom_value && this._mode === "list") { + if (!this.selector.select.multiple || this.required) { + return html` +
+ ${this.label} + ${options.map( + (item: SelectOption) => html` + + + + ` + )} +
+ `; + } + return html`
- ${this.label} - ${this.selector.select.options.map((item: string | SelectOption) => { - const value = typeof item === "object" ? item.value : item; - const label = typeof item === "object" ? item.label : item; - - return html` - - html` + + - - `; - })} + @change=${this._checkboxChanged} + > + + ` + )}
`; } + if (this.selector.select.multiple) { + const value = + !this.value || this.value === "" ? [] : (this.value as string[]); + + return html` + + ${value?.map( + (item, idx) => + html` + + ${options.find((option) => option.value === item)?.label || + item} + + + ` + )} + + + !this.value?.includes(item.value))} + @filter-changed=${this._filterChanged} + @value-changed=${this._comboBoxValueChanged} + > + `; + } + + if (this.selector.select.custom_value) { + if ( + this.value !== undefined && + !options.find((option) => option.value === this.value) + ) { + options.unshift({ value: this.value, label: this.value }); + } + + return html` + + `; + } + return html` - ${this.selector.select.options.map((item: string | SelectOption) => { - const value = typeof item === "object" ? item.value : item; - const label = typeof item === "object" ? item.label : item; - - return html`${label}`; - })} + ${options.map( + (item: SelectOption) => html` + ${item.label} + ` + )} `; } + private get _mode(): "list" | "dropdown" { + return ( + this.selector.select.mode || + (this.selector.select.options.length < 6 ? "list" : "dropdown") + ); + } + private _valueChanged(ev) { ev.stopPropagation(); - if (this.disabled || !ev.target.value) { + const value = ev.detail?.value || ev.target.value; + if (this.disabled || !value) { return; } fireEvent(this, "value-changed", { - value: ev.target.value, + value: value, }); } - static get styles(): CSSResultGroup { - return css` - ha-select { - width: 100%; + private _checkboxChanged(ev) { + ev.stopPropagation(); + if (this.disabled) { + return; + } + + let newValue: string[]; + const value: string = ev.target.value; + const checked = ev.target.checked; + + if (checked) { + if (!this.value) { + newValue = [value]; + } else if (this.value.includes(value)) { + return; + } else { + newValue = [...this.value, value]; } - mwc-formfield { - display: block; + } else { + if (!this.value?.includes(value)) { + return; } - `; + newValue = (this.value as string[]).filter((v) => v !== value); + } + + fireEvent(this, "value-changed", { + value: newValue, + }); } + + private async _removeItem(ev) { + const value: string[] = [...(this.value! as string[])]; + value.splice(ev.target.idx, 1); + + fireEvent(this, "value-changed", { + value, + }); + await this.updateComplete; + this._filterChanged(); + } + + private _comboBoxValueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const newValue = ev.detail.value; + + if (this.disabled || newValue === "") { + return; + } + + if (!this.selector.select.multiple) { + fireEvent(this, "value-changed", { + value: newValue, + }); + return; + } + + if (newValue !== undefined && this.value?.includes(newValue)) { + return; + } + + setTimeout(() => { + this._filterChanged(); + this.comboBox.setInputValue(""); + }, 0); + + const currentValue = + !this.value || this.value === "" ? [] : (this.value as string[]); + + fireEvent(this, "value-changed", { + value: [...currentValue, newValue], + }); + } + + private _filterChanged(ev?: CustomEvent): void { + this._filter = ev?.detail.value || ""; + + const filteredItems = this.comboBox.items?.filter((item) => { + if (this.selector.select.multiple && this.value?.includes(item.value)) { + return false; + } + const label = item.label || item.value; + return label.toLowerCase().includes(this._filter?.toLowerCase()); + }); + + if (this._filter && this.selector.select.custom_value) { + filteredItems?.unshift({ label: this._filter, value: this._filter }); + } + + this.comboBox.filteredItems = filteredItems; + } + + static styles = css` + ha-select, + mwc-formfield, + ha-formfield { + display: block; + } + `; } declare global { diff --git a/src/data/selector.ts b/src/data/selector.ts index 076fa78aa3..10cda864b7 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -170,6 +170,9 @@ export interface SelectOption { export interface SelectSelector { select: { + multiple?: boolean; + custom_value?: boolean; + mode?: "list" | "dropdown"; options: string[] | SelectOption[]; }; } @@ -209,6 +212,7 @@ export interface TargetSelector { }; }; } + export interface ThemeSelector { // eslint-disable-next-line @typescript-eslint/ban-types theme: {};