mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-12 12:30:47 +00:00
Compare commits
5 Commits
copilot/fi
...
ha-icon-pi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c2d47ee3 | ||
|
|
01ba7571f0 | ||
|
|
70bb29e242 | ||
|
|
34443a009d | ||
|
|
e1dce358c3 |
@@ -103,6 +103,10 @@ export class HaGenericPicker extends LitElement {
|
||||
// helper to set new value after closing picker, to avoid flicker
|
||||
private _newValue?: string;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected render() {
|
||||
@@ -137,6 +141,8 @@ export class HaGenericPicker extends LitElement {
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
>
|
||||
@@ -205,11 +211,16 @@ export class HaGenericPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
const showError = this.invalid && this.errorMessage;
|
||||
const showHelper = !showError && this.helper;
|
||||
|
||||
if (!showError && !showHelper) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${showError ? this.errorMessage : this.helper}
|
||||
</ha-input-helper-text>`;
|
||||
}
|
||||
|
||||
private _dialogOpened = () => {
|
||||
@@ -308,6 +319,9 @@ export class HaGenericPicker extends LitElement {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
:host([invalid]) ha-input-helper-text {
|
||||
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type {
|
||||
ComboBoxDataProviderCallback,
|
||||
ComboBoxDataProviderParams,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -10,9 +6,10 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-icon";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-icon";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
interface IconItem {
|
||||
icon: string;
|
||||
@@ -21,7 +18,7 @@ interface IconItem {
|
||||
}
|
||||
|
||||
interface RankedIcon {
|
||||
icon: string;
|
||||
item: PickerComboBoxItem;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
@@ -67,13 +64,18 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||
${item.icon}
|
||||
<ha-icon .icon=${item.id} slot="start"></ha-icon>
|
||||
${item.id}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
const valueRenderer = (value: string) => html`
|
||||
<ha-icon .icon=${value} slot="start"></ha-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
export class HaIconPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -96,13 +98,11 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
item-value-path="icon"
|
||||
item-label-path="icon"
|
||||
.value=${this._value}
|
||||
allow-custom-value
|
||||
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
|
||||
.getItems=${this._getItems}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -110,69 +110,85 @@ export class HaIconPicker extends LitElement {
|
||||
.placeholder=${this.placeholder}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
.renderer=${rowRenderer}
|
||||
icon
|
||||
@opened-changed=${this._openedChanged}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchFn=${this._filterIcons}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.icon-picker.no_match"
|
||||
)}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
${this._value || this.placeholder
|
||||
? html`
|
||||
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
||||
</ha-icon>
|
||||
`
|
||||
: html`<slot slot="icon" name="fallback"></slot>`}
|
||||
</ha-combo-box>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||
private _filterIcons = memoizeOne(
|
||||
(filter: string, iconItems: IconItem[] = ICONS) => {
|
||||
(filter: string, items: PickerComboBoxItem[]): PickerComboBoxItem[] => {
|
||||
if (!filter) {
|
||||
return iconItems;
|
||||
return items;
|
||||
}
|
||||
|
||||
const filteredItems: RankedIcon[] = [];
|
||||
const addIcon = (icon: string, rank: number) =>
|
||||
filteredItems.push({ icon, rank });
|
||||
const addIcon = (item: PickerComboBoxItem, rank: number) =>
|
||||
filteredItems.push({ item, rank });
|
||||
|
||||
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
||||
for (const item of iconItems) {
|
||||
if (item.parts.has(filter)) {
|
||||
addIcon(item.icon, 1);
|
||||
} else if (item.keywords.includes(filter)) {
|
||||
addIcon(item.icon, 2);
|
||||
} else if (item.icon.includes(filter)) {
|
||||
addIcon(item.icon, 3);
|
||||
} else if (item.keywords.some((word) => word.includes(filter))) {
|
||||
addIcon(item.icon, 4);
|
||||
for (const item of items) {
|
||||
const iconName = item.id.split(":")[1] || item.id;
|
||||
const parts = iconName.split("-");
|
||||
const keywords = item.search_labels?.slice(1) || [];
|
||||
|
||||
if (parts.includes(filter)) {
|
||||
addIcon(item, 1);
|
||||
} else if (keywords.includes(filter)) {
|
||||
addIcon(item, 2);
|
||||
} else if (item.id.includes(filter)) {
|
||||
addIcon(item, 3);
|
||||
} else if (keywords.some((word) => word.includes(filter))) {
|
||||
addIcon(item, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow preview for custom icon not in list
|
||||
if (filteredItems.length === 0) {
|
||||
addIcon(filter, 0);
|
||||
addIcon(
|
||||
{
|
||||
id: filter,
|
||||
primary: filter,
|
||||
icon: filter,
|
||||
search_labels: [filter],
|
||||
sorting_label: filter,
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
|
||||
return filteredItems
|
||||
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
||||
.map((item) => item.item);
|
||||
}
|
||||
);
|
||||
|
||||
private _iconProvider = (
|
||||
params: ComboBoxDataProviderParams,
|
||||
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
|
||||
) => {
|
||||
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
|
||||
const iStart = params.page * params.pageSize;
|
||||
const iEnd = iStart + params.pageSize;
|
||||
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
|
||||
};
|
||||
private _getItems = (): PickerComboBoxItem[] =>
|
||||
ICONS.map((icon: IconItem) => ({
|
||||
id: icon.icon,
|
||||
primary: icon.icon,
|
||||
icon: icon.icon,
|
||||
search_labels: [
|
||||
icon.icon.split(":")[1] || icon.icon,
|
||||
...Array.from(icon.parts),
|
||||
...icon.keywords,
|
||||
],
|
||||
sorting_label: icon.icon,
|
||||
}));
|
||||
|
||||
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
const opened = ev.detail.value;
|
||||
if (opened && !ICONS_LOADED) {
|
||||
await loadIcons();
|
||||
protected firstUpdated() {
|
||||
if (!ICONS_LOADED) {
|
||||
loadIcons().then(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,15 +215,9 @@ export class HaIconPicker extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
*[slot="icon"] {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
}
|
||||
*[slot="prefix"] {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ export class HaPickerField extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public valueRenderer?: PickerValueRenderer;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||
|
||||
public async focus() {
|
||||
@@ -142,6 +146,11 @@ export class HaPickerField extends LitElement {
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
:host([invalid]) ha-combo-box-item:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
}
|
||||
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
|
||||
@@ -761,6 +761,9 @@
|
||||
"no_match": "No matching languages found",
|
||||
"no_languages": "No languages available"
|
||||
},
|
||||
"icon-picker": {
|
||||
"no_match": "No matching icons found"
|
||||
},
|
||||
"tts-picker": {
|
||||
"tts": "Text-to-speech",
|
||||
"none": "None"
|
||||
|
||||
Reference in New Issue
Block a user