diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts
index 239f9b1c28..a3af6c30e0 100644
--- a/demo/src/ha-demo.ts
+++ b/demo/src/ha-demo.ts
@@ -73,6 +73,7 @@ export class HaDemo extends HomeAssistantAppEl {
name: null,
icon: null,
labels: [],
+ categories: {},
platform: "co2signal",
hidden_by: null,
entity_category: null,
@@ -90,6 +91,7 @@ export class HaDemo extends HomeAssistantAppEl {
name: null,
icon: null,
labels: [],
+ categories: {},
platform: "co2signal",
hidden_by: null,
entity_category: null,
diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts
index 94d79dc434..ca1e83c0b6 100644
--- a/gallery/src/pages/misc/integration-card.ts
+++ b/gallery/src/pages/misc/integration-card.ts
@@ -200,6 +200,7 @@ const createEntityRegistryEntries = (
unique_id: "updater",
options: null,
labels: [],
+ categories: {},
},
];
diff --git a/src/components/chips/ha-assist-chip.ts b/src/components/chips/ha-assist-chip.ts
index ba18e6b248..6e9e6bc7c9 100644
--- a/src/components/chips/ha-assist-chip.ts
+++ b/src/components/chips/ha-assist-chip.ts
@@ -4,22 +4,32 @@ import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-assist-chip")
+// @ts-ignore
export class HaAssistChip extends MdAssistChip {
@property({ type: Boolean, reflect: true }) filled = false;
+ @property({ type: Boolean }) active = false;
+
static override styles = [
...super.styles,
css`
:host {
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-on-surface: var(--primary-text-color);
- --md-assist-chip-container-shape: 16px;
+ --md-assist-chip-container-shape: var(
+ --ha-assist-chip-container-shape,
+ 16px
+ );
--md-assist-chip-outline-color: var(--outline-color);
--md-assist-chip-label-text-weight: 400;
--ha-assist-chip-filled-container-color: rgba(
var(--rgb-primary-text-color),
0.15
);
+ --ha-assist-chip-active-container-color: rgba(
+ var(--rgb-primary-color),
+ 0.15
+ );
}
/** Material 3 doesn't have a filled chip, so we have to make our own **/
.filled {
@@ -31,10 +41,21 @@ export class HaAssistChip extends MdAssistChip {
background-color: var(--ha-assist-chip-filled-container-color);
}
/** Set the size of mdc icons **/
- ::slotted([slot="icon"]) {
+ ::slotted([slot="icon"]),
+ ::slotted([slot="trailingIcon"]) {
display: flex;
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
}
+
+ .trailing.icon ::slotted(*),
+ .trailing.icon svg {
+ margin-inline-end: unset;
+ margin-inline-start: var(--_icon-label-space);
+ }
+ :where(.active)::before {
+ background: var(--ha-assist-chip-active-container-color);
+ opacity: var(--ha-assist-chip-active-container-opacity);
+ }
`,
];
@@ -45,6 +66,30 @@ export class HaAssistChip extends MdAssistChip {
return super.renderOutline();
}
+
+ protected override getContainerClasses() {
+ return {
+ ...super.getContainerClasses(),
+ active: this.active,
+ };
+ }
+
+ protected override renderPrimaryContent() {
+ return html`
+
+ ${this.renderLeadingIcon()}
+
+ ${this.label}
+
+
+ ${this.renderTrailingIcon()}
+
+ `;
+ }
+
+ protected renderTrailingIcon() {
+ return html``;
+ }
}
declare global {
diff --git a/src/components/data-table/ha-data-table-labels.ts b/src/components/data-table/ha-data-table-labels.ts
new file mode 100644
index 0000000000..84ad9ca81a
--- /dev/null
+++ b/src/components/data-table/ha-data-table-labels.ts
@@ -0,0 +1,118 @@
+import { css, html, LitElement, nothing, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import "../chips/ha-assist-chip";
+import { repeat } from "lit/directives/repeat";
+import { LabelRegistryEntry } from "../../data/label_registry";
+import { computeCssColor } from "../../common/color/compute-color";
+import { fireEvent } from "../../common/dom/fire_event";
+
+@customElement("ha-data-table-labels")
+class HaDataTableLabels extends LitElement {
+ @property({ attribute: false }) public labels!: LabelRegistryEntry[];
+
+ protected render(): TemplateResult {
+ return html`
+
+ ${repeat(
+ this.labels.slice(0, 2),
+ (label) => label.label_id,
+ (label) => this._renderLabel(label, true)
+ )}
+ ${this.labels.length > 2
+ ? html`
+
+ ${repeat(
+ this.labels.slice(2),
+ (label) => label.label_id,
+ (label) =>
+ html`
+ ${this._renderLabel(label, false)}
+ `
+ )}
+ `
+ : nothing}
+
+ `;
+ }
+
+ private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
+ const color = label?.color ? computeCssColor(label.color) : undefined;
+ return html`
+ ${label?.icon
+ ? html``
+ : nothing}
+ `;
+ }
+
+ private _labelClicked(ev: Event) {
+ const label = (ev.currentTarget as any).item as LabelRegistryEntry;
+ fireEvent(this, "label-clicked", { label });
+ }
+
+ protected _handleIconOverflowMenuOpened(e) {
+ e.stopPropagation();
+ // If this component is used inside a data table, the z-index of the row
+ // needs to be increased. Otherwise the ha-button-menu would be displayed
+ // underneath the next row in the table.
+ const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
+ if (row) {
+ row.style.zIndex = "1";
+ }
+ }
+
+ protected _handleIconOverflowMenuClosed() {
+ const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
+ if (row) {
+ row.style.zIndex = "";
+ }
+ }
+
+ static get styles() {
+ return css`
+ :host {
+ display: block;
+ flex-grow: 1;
+ margin-top: 4px;
+ height: 22px;
+ }
+ ha-chip-set {
+ position: fixed;
+ flex-wrap: nowrap;
+ }
+ ha-assist-chip {
+ border: 1px solid var(--color);
+ --md-assist-chip-icon-size: 16px;
+ --md-assist-chip-container-height: 20px;
+ --md-assist-chip-leading-space: 12px;
+ --md-assist-chip-trailing-space: 12px;
+ --ha-assist-chip-active-container-color: var(--color);
+ --ha-assist-chip-active-container-opacity: 0.3;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-data-table-labels": HaDataTableLabels;
+ }
+ interface HASSDomEvents {
+ "label-clicked": { label: LabelRegistryEntry };
+ }
+}
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index 8c0cd60405..ea1e0c4924 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -32,6 +32,7 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
+import { groupBy } from "../../common/util/group-by";
declare global {
// for fire event
@@ -67,13 +68,20 @@ export interface DataTableSortColumnData {
filterKey?: string;
valueColumn?: string;
direction?: SortingDirection;
+ groupable?: boolean;
}
export interface DataTableColumnData extends DataTableSortColumnData {
main?: boolean;
title: TemplateResult | string;
label?: TemplateResult | string;
- type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
+ type?:
+ | "numeric"
+ | "icon"
+ | "icon-button"
+ | "overflow"
+ | "overflow-menu"
+ | "flex";
template?: (row: T) => TemplateResult | string | typeof nothing;
width?: string;
maxWidth?: string;
@@ -95,6 +103,8 @@ export interface SortableColumnContainer {
[key: string]: ClonedDataTableColumnData;
}
+const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
+
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -129,14 +139,16 @@ export class HaDataTable extends LitElement {
@property({ type: String }) public filter = "";
+ @property() public groupColumn?: string;
+
+ @property() public sortColumn?: string;
+
+ @property() public sortDirection: SortingDirection = null;
+
@state() private _filterable = false;
@state() private _filter = "";
- @state() private _sortColumn?: string;
-
- @state() private _sortDirection: SortingDirection = null;
-
@state() private _filteredData: DataTableRowData[] = [];
@state() private _headerHeight = 0;
@@ -195,8 +207,14 @@ export class HaDataTable extends LitElement {
for (const columnId in this.columns) {
if (this.columns[columnId].direction) {
- this._sortDirection = this.columns[columnId].direction!;
- this._sortColumn = columnId;
+ this.sortDirection = this.columns[columnId].direction!;
+ this.sortColumn = columnId;
+
+ fireEvent(this, "sorting-changed", {
+ column: columnId,
+ direction: this.sortDirection,
+ });
+
break;
}
}
@@ -226,11 +244,16 @@ export class HaDataTable extends LitElement {
properties.has("data") ||
properties.has("columns") ||
properties.has("_filter") ||
- properties.has("_sortColumn") ||
- properties.has("_sortDirection")
+ properties.has("sortColumn") ||
+ properties.has("sortDirection") ||
+ properties.has("groupColumn")
) {
this._sortFilterData();
}
+
+ if (properties.has("selectable")) {
+ this._items = [...this._items];
+ }
}
protected render() {
@@ -263,75 +286,79 @@ export class HaDataTable extends LitElement {
})}
>