import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js"; import deepClone from "deep-clone-simple"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, eventOptions, property, query, state, } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { restoreScroll } from "../../common/decorators/restore-scroll"; import { fireEvent } from "../../common/dom/fire_event"; import { stringCompare } from "../../common/string/compare"; import { debounce } from "../../common/util/debounce"; import { groupBy } from "../../common/util/group-by"; import { haStyleScrollbar } from "../../resources/styles"; import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; import "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox"; import "../ha-svg-icon"; import "../search-input"; import { filterData, sortData } from "./sort-filter"; import type { LocalizeFunc } from "../../common/translations/localize"; import { nextRender } from "../../common/util/render-status"; export interface RowClickedEvent { id: string; } export interface SelectionChangedEvent { value: string[]; } export interface CollapsedChangedEvent { value: string[]; } export interface SortingChangedEvent { column: string; direction: SortingDirection; } export type SortingDirection = "desc" | "asc" | null; export type DataTableColumnContainer = Record< string, DataTableColumnData >; export interface DataTableSortColumnData { sortable?: boolean; filterable?: boolean; filterKey?: string; valueColumn?: string; direction?: SortingDirection; groupable?: boolean; moveable?: boolean; hideable?: boolean; defaultHidden?: boolean; showNarrow?: boolean; } export interface DataTableColumnData extends DataTableSortColumnData { main?: boolean; title: TemplateResult | string; label?: TemplateResult | string; type?: | "numeric" | "ip" | "icon" | "icon-button" | "overflow" | "overflow-menu" | "flex"; template?: (row: T) => TemplateResult | string | typeof nothing; extraTemplate?: (row: T) => TemplateResult | string | typeof nothing; minWidth?: string; maxWidth?: string; flex?: number; forceLTR?: boolean; hidden?: boolean; } export type ClonedDataTableColumnData = Omit & { title?: TemplateResult | string; }; export interface DataTableRowData { [key: string]: any; selectable?: boolean; } export type SortableColumnContainer = Record; const UNDEFINED_GROUP_KEY = "zzzzz_undefined"; @customElement("ha-data-table") export class HaDataTable extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public localizeFunc?: LocalizeFunc; @property({ type: Boolean }) public narrow = false; @property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Boolean }) public selectable = false; @property({ type: Boolean }) public clickable = false; @property({ attribute: "has-fab", type: Boolean }) public hasFab = false; /** * Add an extra row at the bottom of the data table * @type {TemplateResult} */ @property({ attribute: false }) public appendRow?; @property({ type: Boolean, attribute: "auto-height" }) public autoHeight = false; // eslint-disable-next-line lit/no-native-attributes @property({ type: String }) public id = "id"; @property({ attribute: false, type: String }) public noDataText?: string; @property({ attribute: false, type: String }) public searchLabel?: string; @property({ type: Boolean, attribute: "no-label-float" }) public noLabelFloat? = false; @property({ type: String }) public filter = ""; @property({ attribute: false }) public groupColumn?: string; @property({ attribute: false }) public groupOrder?: string[]; @property({ attribute: false }) public sortColumn?: string; @property({ attribute: false }) public sortDirection: SortingDirection = null; @property({ attribute: false }) public initialCollapsedGroups?: string[]; @property({ attribute: false }) public hiddenColumns?: string[]; @property({ attribute: false }) public columnOrder?: string[]; @state() private _filterable = false; @state() private _filter = ""; @state() private _filteredData: DataTableRowData[] = []; @state() private _headerHeight = 0; @query("slot[name='header']") private _header!: HTMLSlotElement; @state() private _collapsedGroups: string[] = []; @state() private _lastSelectedRowId: string | null = null; private _checkableRowsCount?: number; private _checkedRows: string[] = []; private _sortColumns: SortableColumnContainer = {}; private _curRequest = 0; private _lastUpdate = 0; // @ts-ignore @restoreScroll(".scroller") private _savedScrollPos?: number; private _debounceSearch = debounce( (value: string) => { this._filter = value; }, 100, false ); public clearSelection(): void { this._checkedRows = []; this._lastSelectedRowId = null; this._checkedRowsChanged(); } public selectAll(): void { this._checkedRows = this._filteredData .filter((data) => data.selectable !== false) .map((data) => data[this.id]); this._lastSelectedRowId = null; this._checkedRowsChanged(); } public select(ids: string[], clear?: boolean): void { if (clear) { this._checkedRows = []; } ids.forEach((id) => { const row = this._filteredData.find((data) => data[this.id] === id); if (row?.selectable !== false && !this._checkedRows.includes(id)) { this._checkedRows.push(id); } }); this._lastSelectedRowId = null; this._checkedRowsChanged(); } public unselect(ids: string[]): void { ids.forEach((id) => { const index = this._checkedRows.indexOf(id); if (index > -1) { this._checkedRows.splice(index, 1); } }); this._lastSelectedRowId = null; this._checkedRowsChanged(); } public connectedCallback() { super.connectedCallback(); if (this._filteredData.length) { // Force update of location of rows this._filteredData = [...this._filteredData]; } } protected firstUpdated() { this.updateComplete.then(() => this._calcTableHeight()); } protected updated() { const header = this.renderRoot.querySelector(".mdc-data-table__header-row"); if (!header) { return; } if (header.scrollWidth > header.clientWidth) { this.style.setProperty("--table-row-width", `${header.scrollWidth}px`); } else { this.style.removeProperty("--table-row-width"); } } public willUpdate(properties: PropertyValues) { super.willUpdate(properties); if (!this.hasUpdated) { loadVirtualizer(); } if (properties.has("columns")) { this._filterable = Object.values(this.columns).some( (column) => column.filterable ); if (!this.sortColumn) { for (const columnId in this.columns) { if (this.columns[columnId].direction) { this.sortDirection = this.columns[columnId].direction!; this.sortColumn = columnId; this._lastSelectedRowId = null; fireEvent(this, "sorting-changed", { column: columnId, direction: this.sortDirection, }); break; } } } const clonedColumns: DataTableColumnContainer = deepClone(this.columns); Object.values(clonedColumns).forEach( (column: ClonedDataTableColumnData) => { delete column.title; delete column.template; delete column.extraTemplate; } ); this._sortColumns = clonedColumns; } if (properties.has("filter")) { this._debounceSearch(this.filter); this._lastSelectedRowId = null; } if (properties.has("data")) { this._checkableRowsCount = this.data.filter( (row) => row.selectable !== false ).length; } if (!this.hasUpdated && this.initialCollapsedGroups) { this._collapsedGroups = this.initialCollapsedGroups; this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } else if (properties.has("groupColumn")) { this._collapsedGroups = []; this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } if ( properties.has("data") || properties.has("columns") || properties.has("_filter") || properties.has("sortColumn") || properties.has("sortDirection") ) { this._sortFilterData(); } if ( properties.has("_filter") || properties.has("sortColumn") || properties.has("sortDirection") ) { this._lastSelectedRowId = null; } if (properties.has("selectable") || properties.has("hiddenColumns")) { this._filteredData = [...this._filteredData]; } } private _sortedColumns = memoizeOne( (columns: DataTableColumnContainer, columnOrder?: string[]) => { if (!columnOrder || !columnOrder.length) { return columns; } return Object.keys(columns) .sort((a, b) => { const orderA = columnOrder!.indexOf(a); const orderB = columnOrder!.indexOf(b); if (orderA !== orderB) { if (orderA === -1) { return 1; } if (orderB === -1) { return -1; } } return orderA - orderB; }) .reduce((obj, key) => { obj[key] = columns[key]; return obj; }, {}) as DataTableColumnContainer; } ); protected render() { const localize = this.localizeFunc || this.hass.localize; const columns = this._sortedColumns(this.columns, this.columnOrder); const renderRow = (row: DataTableRowData, index: number) => this._renderRow(columns, this.narrow, row, index); return html`
${this._filterable ? html`
` : ""}
${this.selectable ? html`
` : ""} ${Object.entries(columns).map(([key, column]) => { if ( column.hidden || (this.columnOrder && this.columnOrder.includes(key) ? (this.hiddenColumns?.includes(key) ?? column.defaultHidden) : column.defaultHidden) ) { return nothing; } const sorted = key === this.sortColumn; const classes = { "mdc-data-table__header-cell--numeric": column.type === "numeric", "mdc-data-table__header-cell--icon": column.type === "icon", "mdc-data-table__header-cell--icon-button": column.type === "icon-button", "mdc-data-table__header-cell--overflow-menu": column.type === "overflow-menu", "mdc-data-table__header-cell--overflow": column.type === "overflow", sortable: Boolean(column.sortable), "not-sorted": Boolean(column.sortable && !sorted), }; return html`
${column.sortable ? html` ` : ""} ${column.title}
`; })}
${!this._filteredData.length ? html`
${this.noDataText || localize("ui.components.data-table.no-data")}
` : html` `}
`; } private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row; private _renderRow = ( columns: DataTableColumnContainer, narrow: boolean, row: DataTableRowData, index: number ) => { // not sure how this happens... if (!row) { return nothing; } if (row.append) { return html`
${row.content}
`; } if (row.empty) { return html`
`; } return html`
${this.selectable ? html`
` : ""} ${Object.entries(columns).map(([key, column]) => { if ( (narrow && !column.main && !column.showNarrow) || column.hidden || (this.columnOrder && this.columnOrder.includes(key) ? (this.hiddenColumns?.includes(key) ?? column.defaultHidden) : column.defaultHidden) ) { return nothing; } return html`
${column.template ? column.template(row) : narrow && column.main ? html`
${row[key]}
${Object.entries(columns) .filter( ([key2, column2]) => !column2.hidden && !column2.main && !column2.showNarrow && !(this.columnOrder && this.columnOrder.includes(key2) ? (this.hiddenColumns?.includes(key2) ?? column2.defaultHidden) : column2.defaultHidden) ) .map( ([key2, column2], i) => html`${i !== 0 ? " · " : nothing}${column2.template ? column2.template(row) : row[key2]}` )}
${column.extraTemplate ? column.extraTemplate(row) : nothing}` : html`${row[key]}${column.extraTemplate ? column.extraTemplate(row) : nothing}`}
`; })}
`; }; private async _sortFilterData() { const startTime = new Date().getTime(); const timeBetweenUpdate = startTime - this._lastUpdate; const timeBetweenRequest = startTime - this._curRequest; this._curRequest = startTime; const forceUpdate = !this._lastUpdate || (timeBetweenUpdate > 500 && timeBetweenRequest < 500); let filteredData = this.data; if (this._filter) { filteredData = await this._memFilterData( this.data, this._sortColumns, this._filter.trim() ); } if (!forceUpdate && this._curRequest !== startTime) { return; } const prom = this.sortColumn && this._sortColumns[this.sortColumn] ? sortData( filteredData, this._sortColumns[this.sortColumn], this.sortDirection, this.sortColumn, this.hass.locale.language ) : filteredData; const [data] = await Promise.all([prom, nextRender]); const curTime = new Date().getTime(); const elapsed = curTime - startTime; if (elapsed < 100) { await new Promise((resolve) => { setTimeout(resolve, 100 - elapsed); }); } if (!forceUpdate && this._curRequest !== startTime) { return; } this._lastUpdate = startTime; this._filteredData = data; } private _groupData = memoizeOne( ( data: DataTableRowData[], localize: LocalizeFunc, appendRow, hasFab: boolean, groupColumn: string | undefined, groupOrder: string[] | undefined, collapsedGroups: string[], sortColumn: string | undefined, sortDirection: SortingDirection ) => { if (appendRow || hasFab || groupColumn) { let items = [...data]; if (groupColumn) { const isGroupSortColumn = sortColumn === groupColumn; const grouped = groupBy(items, (item) => item[groupColumn]); if (grouped.undefined) { // make sure ungrouped items are at the bottom grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; delete grouped.undefined; } const sortedEntries: [string, DataTableRowData[]][] = Object.keys( grouped ) .sort((a, b) => { if (!groupOrder && isGroupSortColumn) { const comparison = stringCompare( a, b, this.hass.locale.language ); if (sortDirection === "asc") { return comparison; } return comparison * -1; } const orderA = groupOrder?.indexOf(a) ?? -1; const orderB = groupOrder?.indexOf(b) ?? -1; if (orderA !== orderB) { if (orderA === -1) { return 1; } if (orderB === -1) { return -1; } return orderA - orderB; } return stringCompare( ["", "-", "—"].includes(a) ? "zzz" : a, ["", "-", "—"].includes(b) ? "zzz" : b, this.hass.locale.language ); }) .reduce( (entries, key) => { const entry: [string, DataTableRowData[]] = [key, grouped[key]]; entries.push(entry); return entries; }, [] as [string, DataTableRowData[]][] ); const groupedItems: DataTableRowData[] = []; sortedEntries.forEach(([groupName, rows]) => { const collapsed = collapsedGroups.includes(groupName); groupedItems.push({ append: true, selectable: false, content: html`
${groupName === UNDEFINED_GROUP_KEY ? localize("ui.components.data-table.ungrouped") : groupName || ""}
`, }); if (!collapsedGroups.includes(groupName)) { groupedItems.push(...rows); } }); items = groupedItems; } if (appendRow) { items.push({ append: true, selectable: false, content: appendRow }); } if (hasFab) { items.push({ empty: true }); } return items; } return data; } ); private _memFilterData = memoizeOne( ( data: DataTableRowData[], columns: SortableColumnContainer, filter: string ): Promise => filterData(data, columns, filter) ); private _handleHeaderClick(ev: Event) { const columnId = (ev.currentTarget as any).columnId; if (!this.columns[columnId].sortable) { return; } if (!this.sortDirection || this.sortColumn !== columnId) { this.sortDirection = "asc"; } else if (this.sortDirection === "asc") { this.sortDirection = "desc"; } else { this.sortDirection = null; } this.sortColumn = this.sortDirection === null ? undefined : columnId; fireEvent(this, "sorting-changed", { column: columnId, direction: this.sortDirection, }); } private _handleHeaderRowCheckboxClick(ev: Event) { const checkbox = ev.target as HaCheckbox; if (checkbox.checked) { this.selectAll(); } else { this._checkedRows = []; this._checkedRowsChanged(); } this._lastSelectedRowId = null; } private _handleRowCheckboxClicked = (ev: Event) => { const checkbox = ev.currentTarget as HaCheckbox; const rowId = (checkbox as any).rowId; const groupedData = this._groupData( this._filteredData, this.localizeFunc || this.hass.localize, this.appendRow, this.hasFab, this.groupColumn, this.groupOrder, this._collapsedGroups, this.sortColumn, this.sortDirection ); if ( groupedData.find((data) => data[this.id] === rowId)?.selectable === false ) { return; } const rowIndex = groupedData.findIndex((data) => data[this.id] === rowId); if ( ev instanceof MouseEvent && ev.shiftKey && this._lastSelectedRowId !== null ) { const lastSelectedRowIndex = groupedData.findIndex( (data) => data[this.id] === this._lastSelectedRowId ); if (lastSelectedRowIndex > -1 && rowIndex > -1) { this._checkedRows = [ ...this._checkedRows, ...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex), ]; } } else if (!checkbox.checked) { if (!this._checkedRows.includes(rowId)) { this._checkedRows = [...this._checkedRows, rowId]; } } else { this._checkedRows = this._checkedRows.filter((row) => row !== rowId); } if (rowIndex > -1) { this._lastSelectedRowId = rowId; } this._checkedRowsChanged(); }; private _selectRange( groupedData: DataTableRowData[], startIndex: number, endIndex: number ) { const start = Math.min(startIndex, endIndex); const end = Math.max(startIndex, endIndex); const checkedRows: string[] = []; for (let i = start; i <= end; i++) { const row = groupedData[i]; if ( row && row.selectable !== false && !this._checkedRows.includes(row[this.id]) ) { checkedRows.push(row[this.id]); } } return checkedRows; } private _handleRowClick = (ev: Event) => { if ( ev .composedPath() .find((el) => [ "ha-checkbox", "ha-button", "ha-button", "ha-icon-button", "ha-assist-chip", ].includes((el as HTMLElement).localName) ) ) { return; } const rowId = (ev.currentTarget as any).rowId; fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); }; private _setTitle(ev: Event) { const target = ev.currentTarget as HTMLElement; if (target.scrollWidth > target.offsetWidth) { target.setAttribute("title", target.innerText); } } private _checkedRowsChanged() { // force scroller to update, change it's items if (this._filteredData.length) { this._filteredData = [...this._filteredData]; } fireEvent(this, "selection-changed", { value: this._checkedRows, }); } private _handleSearchChange(ev: CustomEvent): void { if (this.filter) { return; } this._lastSelectedRowId = null; this._debounceSearch(ev.detail.value); } private async _calcTableHeight() { if (this.autoHeight) { return; } await this.updateComplete; this._headerHeight = this._header.clientHeight; } @eventOptions({ passive: true }) private _saveScrollPos(e: Event) { this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; this.renderRoot.querySelector(".mdc-data-table__header-row")!.scrollLeft = ( e.target as HTMLDivElement ).scrollLeft; } @eventOptions({ passive: true }) private _scrollContent(e: Event) { this.renderRoot.querySelector("lit-virtualizer")!.scrollLeft = ( e.target as HTMLDivElement ).scrollLeft; } private _collapseGroup = (ev: Event) => { const groupName = (ev.currentTarget as any).group; if (this._collapsedGroups.includes(groupName)) { this._collapsedGroups = this._collapsedGroups.filter( (grp) => grp !== groupName ); } else { this._collapsedGroups = [...this._collapsedGroups, groupName]; } this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); }; public expandAllGroups() { this._collapsedGroups = []; this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } public collapseAllGroups() { if ( !this.groupColumn || !this.data.some((item) => item[this.groupColumn!]) ) { return; } const grouped = groupBy(this.data, (item) => item[this.groupColumn!]); if (grouped.undefined) { // undefined is a reserved group name grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; delete grouped.undefined; } this._collapsedGroups = Object.keys(grouped); this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } static get styles(): CSSResultGroup { return [ haStyleScrollbar, css` /* default mdc styles, colors changed, without checkbox styles */ :host { height: 100%; } .mdc-data-table__content { font-family: var(--ha-font-family-body); -moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing); -webkit-font-smoothing: var(--ha-font-smoothing); font-size: 0.875rem; line-height: var(--ha-line-height-condensed); font-weight: var(--ha-font-weight-normal); letter-spacing: 0.0178571429em; text-decoration: inherit; text-transform: inherit; } .mdc-data-table { background-color: var(--data-table-background-color); border-radius: var(--ha-border-radius-sm); border-width: 1px; border-style: solid; border-color: var(--divider-color); display: inline-flex; flex-direction: column; box-sizing: border-box; overflow: hidden; } .mdc-data-table__row--selected { background-color: rgba(var(--rgb-primary-color), 0.04); } .mdc-data-table__row { display: flex; height: var(--data-table-row-height, 52px); width: var(--table-row-width, 100%); } .mdc-data-table__row.empty-row { height: var( --data-table-empty-row-height, var(--data-table-row-height, 52px) ); } .mdc-data-table__row ~ .mdc-data-table__row { border-top: 1px solid var(--divider-color); } .mdc-data-table__row.clickable:not( .mdc-data-table__row--selected ):hover { background-color: rgba(var(--rgb-primary-text-color), 0.04); } .mdc-data-table__header-cell { color: var(--primary-text-color); } .mdc-data-table__cell { color: var(--primary-text-color); } .mdc-data-table__header-row { height: 56px; display: flex; border-bottom: 1px solid var(--divider-color); overflow: auto; } /* Hide scrollbar for Chrome, Safari and Opera */ .mdc-data-table__header-row::-webkit-scrollbar { display: none; } /* Hide scrollbar for IE, Edge and Firefox */ .mdc-data-table__header-row { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } .mdc-data-table__cell, .mdc-data-table__header-cell { padding-right: 16px; padding-left: 16px; min-width: 150px; align-self: center; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; box-sizing: border-box; } .mdc-data-table__cell.mdc-data-table__cell--flex { display: flex; overflow: initial; } .mdc-data-table__cell.mdc-data-table__cell--icon { overflow: initial; } .mdc-data-table__header-cell--checkbox, .mdc-data-table__cell--checkbox { /* @noflip */ padding-left: 16px; /* @noflip */ padding-right: 0; /* @noflip */ padding-inline-start: 16px; /* @noflip */ padding-inline-end: initial; width: 60px; min-width: 60px; } .mdc-data-table__table { height: 100%; width: 100%; border: 0; white-space: nowrap; position: relative; } .mdc-data-table__cell { font-family: var(--ha-font-family-body); -moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing); -webkit-font-smoothing: var(--ha-font-smoothing); font-size: 0.875rem; line-height: var(--ha-line-height-condensed); font-weight: var(--ha-font-weight-normal); letter-spacing: 0.0178571429em; text-decoration: inherit; text-transform: inherit; flex-grow: 0; flex-shrink: 0; } .mdc-data-table__cell a { color: inherit; text-decoration: none; } .mdc-data-table__cell--numeric { text-align: var(--float-end); } .mdc-data-table__cell--icon { color: var(--secondary-text-color); text-align: center; } .mdc-data-table__header-cell--icon, .mdc-data-table__cell--icon { min-width: 64px; flex: 0 0 64px !important; } .mdc-data-table__cell--icon img { width: 24px; height: 24px; } .mdc-data-table__header-cell.mdc-data-table__header-cell--icon { text-align: center; } .mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover, .mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not( .not-sorted ) { text-align: var(--float-start); } .mdc-data-table__cell--icon:first-child img, .mdc-data-table__cell--icon:first-child ha-icon, .mdc-data-table__cell--icon:first-child ha-svg-icon, .mdc-data-table__cell--icon:first-child ha-state-icon, .mdc-data-table__cell--icon:first-child ha-domain-icon, .mdc-data-table__cell--icon:first-child ha-service-icon { margin-left: 8px; margin-inline-start: 8px; margin-inline-end: initial; } .mdc-data-table__cell--icon:first-child state-badge { margin-right: -8px; margin-inline-end: -8px; margin-inline-start: initial; } .mdc-data-table__cell--overflow-menu, .mdc-data-table__header-cell--overflow-menu, .mdc-data-table__header-cell--icon-button, .mdc-data-table__cell--icon-button { min-width: 64px; flex: 0 0 64px !important; padding: 8px; } .mdc-data-table__header-cell--icon-button, .mdc-data-table__cell--icon-button { min-width: 56px; width: 56px; } .mdc-data-table__cell--overflow-menu, .mdc-data-table__cell--icon-button { color: var(--secondary-text-color); text-overflow: clip; } .mdc-data-table__header-cell--icon-button:first-child, .mdc-data-table__cell--icon-button:first-child, .mdc-data-table__header-cell--icon-button:last-child, .mdc-data-table__cell--icon-button:last-child { width: 64px; } .mdc-data-table__cell--overflow-menu:first-child, .mdc-data-table__header-cell--overflow-menu:first-child, .mdc-data-table__header-cell--icon-button:first-child, .mdc-data-table__cell--icon-button:first-child { padding-left: 16px; padding-inline-start: 16px; padding-inline-end: initial; } .mdc-data-table__cell--overflow-menu:last-child, .mdc-data-table__header-cell--overflow-menu:last-child, .mdc-data-table__header-cell--icon-button:last-child, .mdc-data-table__cell--icon-button:last-child { padding-right: 16px; padding-inline-end: 16px; padding-inline-start: initial; } .mdc-data-table__cell--overflow-menu, .mdc-data-table__cell--overflow, .mdc-data-table__header-cell--overflow-menu, .mdc-data-table__header-cell--overflow { overflow: initial; } .mdc-data-table__cell--icon-button a { color: var(--secondary-text-color); } .mdc-data-table__header-cell { font-family: var(--ha-font-family-body); -moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing); -webkit-font-smoothing: var(--ha-font-smoothing); font-size: var(--ha-font-size-s); line-height: var(--ha-line-height-normal); font-weight: var(--ha-font-weight-medium); letter-spacing: 0.0071428571em; text-decoration: inherit; text-transform: inherit; text-align: var(--float-start); } .mdc-data-table__header-cell--numeric { text-align: var(--float-end); } .mdc-data-table__header-cell--numeric.sortable:hover, .mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) { text-align: var(--float-start); } /* custom from here */ .group-header { padding-top: 12px; height: var(--data-table-row-height, 52px); padding-left: 12px; padding-inline-start: 12px; padding-inline-end: initial; width: 100%; font-weight: var(--ha-font-weight-medium); display: flex; align-items: center; cursor: pointer; background-color: var(--primary-background-color); } .group-header ha-icon-button { transition: transform 0.2s ease; } .group-header ha-icon-button.collapsed { transform: rotate(180deg); } :host { display: block; } .mdc-data-table { display: block; border-width: var(--data-table-border-width, 1px); height: 100%; } .mdc-data-table__header-cell { overflow: hidden; position: relative; } .mdc-data-table__header-cell span { position: relative; left: 0px; inset-inline-start: 0px; inset-inline-end: initial; } .mdc-data-table__header-cell.sortable { cursor: pointer; } .mdc-data-table__header-cell > * { transition: var(--float-start) 0.2s ease; } .mdc-data-table__header-cell ha-svg-icon { top: -3px; position: absolute; } .mdc-data-table__header-cell.not-sorted ha-svg-icon { left: -20px; inset-inline-start: -20px; inset-inline-end: initial; } .mdc-data-table__header-cell.sortable:not(.not-sorted) span, .mdc-data-table__header-cell.sortable.not-sorted:hover span { left: 24px; inset-inline-start: 24px; inset-inline-end: initial; } .mdc-data-table__header-cell.sortable:not(.not-sorted) ha-svg-icon, .mdc-data-table__header-cell.sortable:hover.not-sorted ha-svg-icon { left: 12px; inset-inline-start: 12px; inset-inline-end: initial; } .table-header { border-bottom: 1px solid var(--divider-color); } search-input { display: block; flex: 1; --mdc-text-field-fill-color: var(--sidebar-background-color); --mdc-text-field-idle-line-color: transparent; } slot[name="header"] { display: block; } .center { text-align: center; } .secondary { color: var(--secondary-text-color); } .scroller { height: calc(100% - 57px); overflow: overlay !important; } .mdc-data-table__table.auto-height .scroller { overflow-y: hidden !important; } .grows { flex-grow: 1; flex-shrink: 1; } .forceLTR { direction: ltr; } .clickable { cursor: pointer; } lit-virtualizer { contain: size layout !important; overscroll-behavior: contain; } `, ]; } } declare global { interface HTMLElementTagNameMap { "ha-data-table": HaDataTable; } // for fire event interface HASSDomEvents { "selection-changed": SelectionChangedEvent; "row-click": RowClickedEvent; "sorting-changed": SortingChangedEvent; "collapsed-changed": CollapsedChangedEvent; } }