From 3345b52da4d149101a99e06f664a8720d1e407b8 Mon Sep 17 00:00:00 2001 From: Donnie Date: Sun, 1 Nov 2020 09:52:05 -0800 Subject: [PATCH] Fix divider issue, restore arrow down/up behavior, enter key clicks item --- src/components/ha-divider-list-item.ts | 25 - src/components/ha-sidebar-combined.ts | 1201 +++++++++++++++++ src/components/ha-sidebar-panel-list.ts | 29 +- src/components/ha-sidebar.ts | 6 +- src/layouts/home-assistant-main.ts | 5 +- .../ha-sortable-style-ha-clickable.ts | 3 +- 6 files changed, 1216 insertions(+), 53 deletions(-) delete mode 100644 src/components/ha-divider-list-item.ts create mode 100644 src/components/ha-sidebar-combined.ts diff --git a/src/components/ha-divider-list-item.ts b/src/components/ha-divider-list-item.ts deleted file mode 100644 index 5f3353b104..0000000000 --- a/src/components/ha-divider-list-item.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ListItem } from "@material/mwc-list/mwc-list-item"; -import { css, CSSResult, customElement } from "lit-element"; - -@customElement("ha-divider-list-item") -export class HaDividerListItem extends ListItem { - noninteractive = true; - - static get styles(): CSSResult[] { - return [ - super.styles, - css` - :host { - height: var(--ha-divider-height, 48px); - border: 1px solid black; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-divider-list-item": HaDividerListItem; - } -} diff --git a/src/components/ha-sidebar-combined.ts b/src/components/ha-sidebar-combined.ts new file mode 100644 index 0000000000..c55ecdbd7e --- /dev/null +++ b/src/components/ha-sidebar-combined.ts @@ -0,0 +1,1201 @@ +import "./ha-sidebar-panel-list"; +import "@material/mwc-list/mwc-list-item"; +import "@material/mwc-button/mwc-button"; +import "@material/mwc-icon-button"; +import { + mdiBell, + mdiCellphoneCog, + mdiClose, + mdiMenu, + mdiMenuOpen, + mdiPlus, + mdiViewDashboard, +} from "@mdi/js"; +import { + css, + CSSResult, + customElement, + eventOptions, + html, + internalProperty, + LitElement, + property, + PropertyValues, + query, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { fireEvent } from "../common/dom/fire_event"; +import { computeDomain } from "../common/entity/compute_domain"; +import { computeRTL } from "../common/util/compute_rtl"; +import { ActionHandlerDetail } from "../data/lovelace"; +import { LocalStorage } from "../common/decorators/local-storage"; +import { + PersistentNotification, + subscribeNotifications, +} from "../data/persistent_notification"; +import { + ExternalConfig, + getExternalConfig, +} from "../external_app/external_config"; +import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; +import { haStyleScrollbar } from "../resources/styles"; +import type { HomeAssistant, PanelInfo } from "../types"; +import "./ha-icon"; +import "./ha-menu-button"; +import "./ha-svg-icon"; +import "./user/ha-user-badge"; +import memoizeOne from "memoize-one"; +import { compare } from "../common/string/compare"; +import { guard } from "lit-html/directives/guard"; +import { HaClickableListItem } from "./ha-clickable-list-item"; +import { List } from "@material/mwc-list"; + +const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"]; + +const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body; + +const SORT_VALUE_URL_PATHS = { + map: 1, + logbook: 2, + history: 3, + "developer-tools": 9, + hassio: 10, + config: 11, +}; + +let Sortable; + +const panelSorter = ( + reverseSort: string[], + defaultPanel: string, + a: PanelInfo, + b: PanelInfo +) => { + const indexA = reverseSort.indexOf(a.url_path); + const indexB = reverseSort.indexOf(b.url_path); + if (indexA !== indexB) { + if (indexA < indexB) { + return 1; + } + return -1; + } + return defaultPanelSorter(defaultPanel, a, b); +}; + +const defaultPanelSorter = ( + defaultPanel: string, + a: PanelInfo, + b: PanelInfo +) => { + // Put all the Lovelace at the top. + const aLovelace = a.component_name === "lovelace"; + const bLovelace = b.component_name === "lovelace"; + + if (a.url_path === defaultPanel) { + return -1; + } + if (b.url_path === defaultPanel) { + return 1; + } + + if (aLovelace && bLovelace) { + return compare(a.title!, b.title!); + } + if (aLovelace && !bLovelace) { + return -1; + } + if (bLovelace) { + return 1; + } + + const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS; + const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS; + + if (aBuiltIn && bBuiltIn) { + return SORT_VALUE_URL_PATHS[a.url_path] - SORT_VALUE_URL_PATHS[b.url_path]; + } + if (aBuiltIn) { + return -1; + } + if (bBuiltIn) { + return 1; + } + // both not built in, sort by title + return compare(a.title!, b.title!); +}; + +const computePanels = memoizeOne( + ( + panels: HomeAssistant["panels"], + defaultPanel: HomeAssistant["defaultPanel"], + panelsOrder: string[], + hiddenPanels: string[] + ): [PanelInfo[], PanelInfo[]] => { + if (!panels) { + return [[], []]; + } + + const beforeSpacer: PanelInfo[] = []; + const afterSpacer: PanelInfo[] = []; + + Object.values(panels).forEach((panel) => { + if ( + hiddenPanels.includes(panel.url_path) || + (!panel.title && panel.url_path !== defaultPanel) + ) { + return; + } + (SHOW_AFTER_SPACER.includes(panel.url_path) + ? afterSpacer + : beforeSpacer + ).push(panel); + }); + + const reverseSort = [...panelsOrder].reverse(); + + beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); + afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); + + return [beforeSpacer, afterSpacer]; + } +); + +const isListItem = (element: Element): element is HaClickableListItem => { + return element.hasAttribute("mwc-list-item"); +}; + +const isNodeElement = (node: Node): node is Element => { + return node.nodeType === Node.ELEMENT_NODE; +}; + +@customElement("ha-sidebar-combined") +class HaSidebarCombined extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public narrow!: boolean; + + @property({ type: Boolean }) public alwaysExpand = false; + + @property({ type: Boolean, reflect: true }) public expanded = false; + + @property({ type: Boolean }) public editMode = false; + + // property used only in css + // @ts-ignore + @property({ type: Boolean, reflect: true }) public rtl = false; + + @internalProperty() private _renderEmptySortable = false; + + @internalProperty() private _externalConfig?: ExternalConfig; + + @internalProperty() private _notifications?: PersistentNotification[]; + + @query("mwc-list.ha-scrollbar", false) + private _standardPanelList?: List; + + @query("mwc-list.utility-panels", false) + private _utilityPanelList?: List; + + private _mouseLeaveTimeout?: number; + + private _tooltipHideTimeout?: number; + + private _recentKeydownActiveUntil = 0; + + private _sortable?; + + // @ts-ignore + @LocalStorage("sidebarPanelOrder", true, { + attribute: false, + }) + private _panelOrder: string[] = []; + + // @ts-ignore + @LocalStorage("sidebarHiddenPanels", true, { + attribute: false, + }) + private _hiddenPanels: string[] = []; + + protected render() { + if (!this.hass) { + return html``; + } + const debug = false; + + // prettier-ignore + return debug + ? html` + ${this._renderNormalPanels()} + ${this._renderUtilityPanels()} ` + : this.render2(); + } + + protected render2() { + if (!this.hass) { + return html``; + } + + // prettier-ignore + return html` + ${this._renderHeader()} ${this._renderAllPanels()} + + ${this._renderNotifications()} + ${this._renderUserItem()} + +
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + if ( + changedProps.has("expanded") || + changedProps.has("narrow") || + changedProps.has("alwaysExpand") || + changedProps.has("_externalConfig") || + changedProps.has("_notifications") || + changedProps.has("editMode") || + changedProps.has("_renderEmptySortable") || + changedProps.has("_hiddenPanels") || + (changedProps.has("_panelOrder") && !this.editMode) + ) { + return true; + } + if (!this.hass || !changedProps.has("hass")) { + return false; + } + const oldHass = changedProps.get("hass") as HomeAssistant; + if (!oldHass) { + return true; + } + const hass = this.hass; + return ( + hass.panels !== oldHass.panels || + hass.panelUrl !== oldHass.panelUrl || + hass.user !== oldHass.user || + hass.localize !== oldHass.localize || + hass.language !== oldHass.language || + hass.states !== oldHass.states || + hass.defaultPanel !== oldHass.defaultPanel + ); + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + if (this.hass && this.hass.auth.external) { + getExternalConfig(this.hass.auth.external).then(() => {}); + } + subscribeNotifications(this.hass.connection, (notifications) => { + this._notifications = notifications; + }); + } + + protected updated(changedProps) { + super.updated(changedProps); + if (changedProps.has("alwaysExpand")) { + this.expanded = this.alwaysExpand; + } + if (changedProps.has("editMode")) { + if (this.editMode) { + this._activateEditMode(); + } else { + this._deactivateEditMode(); + } + } + if (!changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.language !== this.hass.language) { + this.rtl = computeRTL(this.hass); + } + + if (!SUPPORT_SCROLL_IF_NEEDED) { + return; + } + if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) { + const selectedEl = this.shadowRoot!.querySelector(".iron-selected"); + if (selectedEl) { + // @ts-ignore + selectedEl.scrollIntoViewIfNeeded(); + } + } + } + + private _renderNormalPanels() { + const [beforeSpacer] = computePanels( + this.hass.panels, + this.hass.defaultPanel, + this._panelOrder, + this._hiddenPanels + ); + + // prettier-ignore + return html` + + ${this.editMode + ? this._renderPanelsEdit(beforeSpacer) + : this._renderPanels(beforeSpacer)} + + `; + } + + private _renderPanels(panels: PanelInfo[]) { + return panels + .filter((panel) => !this._isUtilityPanel(panel)) + .map((panel) => + this._renderPanel( + panel.url_path, + panel.url_path === this.hass.defaultPanel + ? panel.title || this.hass.localize("panel.states") + : this.hass.localize(`panel.${panel.title}`) || panel.title, + panel.icon, + panel.url_path === this.hass.defaultPanel && !panel.icon + ? mdiViewDashboard + : undefined + ) + ); + } + + private _renderPanel( + urlPath: string, + title: string | null, + icon?: string | null, + iconPath?: string | null + ) { + return html` + + ${iconPath + ? html`` + : html``} + + ${title} + ${this.editMode + ? html` + + ` + : ""} + + `; + } + + private async _hidePanel(ev: Event) { + ev.preventDefault(); + const panel = (ev.currentTarget as any).panel; + if (this._hiddenPanels.includes(panel)) { + return; + } + // Make a copy for Memoize + this._hiddenPanels = [...this._hiddenPanels, panel]; + this._renderEmptySortable = true; + await this.updateComplete; + const container = this.shadowRoot!.getElementById("sortable")!; + while (container.lastElementChild) { + container.removeChild(container.lastElementChild); + } + this._renderEmptySortable = false; + } + + private async _unhidePanel(ev: Event) { + ev.preventDefault(); + const panel = (ev.currentTarget as any).panel; + this._hiddenPanels = this._hiddenPanels.filter( + (hidden) => hidden !== panel + ); + this._renderEmptySortable = true; + await this.updateComplete; + const container = this.shadowRoot!.getElementById("sortable")!; + while (container.lastElementChild) { + container.removeChild(container.lastElementChild); + } + this._renderEmptySortable = false; + } + + private _setFocusFirstListItem() { + // @ts-ignore + // list._getItemAtIndex(0)?.rippleHandlers.startFocus(); + this._utilityPanelList?.focusItemAtIndex(0); + } + + private _setFocusLastListItem() { + // @ts-ignore + // list._getItemAtIndex(0)?.rippleHandlers.startFocus(); + const list = this._standardPanelList; + list?.focusItemAtIndex(list?.items.length - 1); + } + + private _renderUtilityPanels() { + const [, afterSpacer] = computePanels( + this.hass.panels, + this.hass.defaultPanel, + this._panelOrder, + this._hiddenPanels + ); + + // prettier-ignore + return html` + + ${afterSpacer.map((panel) => + this._renderPanel( + panel.url_path, + panel.url_path === this.hass.defaultPanel + ? panel.title || this.hass.localize("panel.states") + : this.hass.localize(`panel.${panel.title}`) || panel.title, + panel.icon, + panel.url_path === this.hass.defaultPanel && !panel.icon + ? mdiViewDashboard + : undefined + ) + )} + ${this._renderExternalConfiguration()} + + + `; + } + + private _renderExternalConfiguration() { + return html`${this._externalConfig && this._externalConfig.hasSettingsScreen + ? html` + + + + ${this.hass.localize("ui.sidebar.external_app_configuration")} + + + ` + : ""}`; + } + + private _handleExternalAppConfiguration(ev: Event) { + ev.preventDefault(); + this.hass.auth.external!.fireMessage({ + type: "config_screen/show", + }); + } + + private _isUtilityPanel(panel: PanelInfo) { + return ( + panel.component_name === "developer-tools" || + panel.component_name === "config" || + panel.component_name === "external-config" + ); + } + + private _renderPanelsEdit(beforeSpacer: PanelInfo[]) { + // prettier-ignore + return html`
+ ${guard([this._hiddenPanels, this._renderEmptySortable], () => + this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer) + )} +
+ ${this._renderSpacer()} + ${this._renderHiddenPanels()} `; + } + + private _renderHiddenPanels() { + return html` ${this._hiddenPanels.length + ? html`${this._hiddenPanels.map((url) => { + const panel = this.hass.panels[url]; + if (!panel) { + return ""; + } + return html` + + ${panel.url_path === this.hass.defaultPanel + ? this.hass.localize("panel.states") + : this.hass.localize(`panel.${panel.title}`) || + panel.title} + + + + `; + })} + ${this._renderSpacer()}` + : ""}`; + } + + private _renderHeader() { + return html``; + } + + private _renderAllPanels() { + return html` ${this._renderNormalPanels()} ${this._renderUtilityPanels()} `; + } + + private _renderSpacer(enabled = true) { + return enabled + ? html`` + : html``; + } + + private _renderNotifications() { + let notificationCount = this._notifications + ? this._notifications.length + : 0; + for (const entityId in this.hass.states) { + if (computeDomain(entityId) === "configurator") { + notificationCount++; + } + } + + return html` + + + ${!this.expanded && notificationCount > 0 + ? html` + + ${notificationCount} + + ` + : ""} + + ${this.hass.localize("ui.notification_drawer.title")} + + ${this.expanded && notificationCount > 0 + ? html` + ${notificationCount} + ` + : ""} + + `; + } + + private _renderUserItem() { + return html` + + + + ${this.hass.user ? this.hass.user.name : ""} + + `; + } + + private get _tooltip() { + return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; + } + + private _handleAction(ev: CustomEvent) { + if (ev.detail.action !== "hold") { + return; + } + + fireEvent(this, "hass-edit-sidebar", { editMode: true }); + } + + private async _activateEditMode() { + if (!Sortable) { + const [sortableImport, sortStylesImport] = await Promise.all([ + import("sortablejs/modular/sortable.core.esm"), + import("../resources/ha-sortable-style-ha-clickable"), + ]); + + const style = document.createElement("style"); + style.innerHTML = sortStylesImport.sortableStyles.cssText; + this.shadowRoot!.appendChild(style); + + Sortable = sortableImport.Sortable; + Sortable.mount(sortableImport.OnSpill); + Sortable.mount(sortableImport.AutoScroll()); + } + + await this.updateComplete; + + this._createSortable(); + } + + private _createSortable() { + this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), { + animation: 150, + fallbackClass: "sortable-fallback", + dataIdAttr: "data-panel", + handle: "ha-clickable-list-item", + onSort: async () => { + this._panelOrder = this._sortable.toArray(); + }, + }); + } + + private _deactivateEditMode() { + this._sortable?.destroy(); + this._sortable = undefined; + } + + private _closeEditMode() { + fireEvent(this, "hass-edit-sidebar", { editMode: false }); + } + + private _itemMouseEnter(ev: MouseEvent) { + // On keypresses on the listbox, we're going to ignore mouse enter events + // for 100ms so that we ignore it when pressing down arrow scrolls the + // sidebar causing the mouse to hover a new icon + if ( + this.expanded || + new Date().getTime() < this._recentKeydownActiveUntil + ) { + return; + } + if (this._mouseLeaveTimeout) { + clearTimeout(this._mouseLeaveTimeout); + this._mouseLeaveTimeout = undefined; + } + this._showTooltip(ev.currentTarget); + } + + private _itemMouseLeave() { + if (this._mouseLeaveTimeout) { + clearTimeout(this._mouseLeaveTimeout); + } + this._mouseLeaveTimeout = window.setTimeout(() => { + this._hideTooltip(); + }, 500); + } + + private _listboxFocusIn(ev) { + // REMINDER to ask what this line is for + if (this.expanded || ev.target.nodeName !== "A") { + return; + } + this._showTooltip(ev.target.querySelector("ha-clickable-list-item")); + } + + private _listboxFocusOut() { + this._hideTooltip(); + } + + @eventOptions({ + passive: true, + }) + private _listboxScroll() { + // On keypresses on the listbox, we're going to ignore scroll events + // for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip + // will not be hidden. + if (new Date().getTime() < this._recentKeydownActiveUntil) { + return; + } + this._hideTooltip(); + } + + private _getIndexOfTarget(evt: Event): number { + const listbox = evt.currentTarget as List; + const elements = listbox.items; + const path = evt.composedPath(); + + for (const pathItem of path as Node[]) { + let index = -1; + if (isNodeElement(pathItem) && isListItem(pathItem)) { + index = elements.indexOf(pathItem); + } + + if (index !== -1) { + return index; + } + } + + return -1; + } + + private _listboxKeydown(ev: KeyboardEvent) { + const [beforeSpacer] = computePanels( + this.hass.panels, + this.hass.defaultPanel, + this._panelOrder, + this._hiddenPanels + ); + + const curIndex = this._getIndexOfTarget(ev); + const curList = ev.currentTarget as List; + + if (ev.code === "ArrowDown" && curList === this._standardPanelList) { + const isLastItem = curIndex === beforeSpacer.length - 1; + + if (isLastItem) { + this._setFocusFirstListItem(); + } + } else if (ev.code === "ArrowUp" && curList === this._utilityPanelList) { + const isFirstItem = curIndex === 0; + + if (isFirstItem) { + this._setFocusLastListItem(); + } + } else if (ev.code === "Enter") { + (ev.target as HaClickableListItem)?.shadowRoot + ?.querySelector("a") + ?.click(); + } + + this._recentKeydownActiveUntil = new Date().getTime() + 100; + } + + private _showTooltip(item) { + if (this._tooltipHideTimeout) { + clearTimeout(this._tooltipHideTimeout); + this._tooltipHideTimeout = undefined; + } + const tooltip = this._tooltip; + const listbox = this.shadowRoot!.querySelector("mwc-list")!; + let top = item.offsetTop + 11; + if (listbox.contains(item)) { + top -= listbox.scrollTop; + } + tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML; + tooltip.style.display = "block"; + tooltip.style.top = `${top}px`; + tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`; + } + + private _hideTooltip() { + // Delay it a little in case other events are pending processing. + if (!this._tooltipHideTimeout) { + this._tooltipHideTimeout = window.setTimeout(() => { + this._tooltipHideTimeout = undefined; + this._tooltip.style.display = "none"; + }, 10); + } + } + + private _handleShowNotificationDrawer() { + fireEvent(this, "hass-show-notifications"); + } + + private _toggleSidebar(ev: CustomEvent) { + if (ev.detail.action !== "tap") { + return; + } + fireEvent(this, "hass-toggle-menu"); + } + + static get styles(): CSSResult[] { + return [ + haStyleScrollbar, + css` + :host { + /* height: calc(100% - var(--header-height)); */ + height: 100%; + display: block; + overflow: hidden; + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + border-right: 1px solid var(--divider-color); + background-color: var(--sidebar-background-color); + width: 64px; + } + :host([expanded]) { + width: 256px; + width: calc(256px + env(safe-area-inset-left)); + } + :host([rtl]) { + border-right: 0; + border-left: 1px solid var(--divider-color); + } + :host([rtl]) .menu { + padding-left: 8.5px; + padding-right: calc(8.5px + env(safe-area-inset-right)); + } + :host([expanded]) .menu { + width: calc(256px + env(safe-area-inset-left)); + } + :host([rtl][expanded]) .menu { + width: calc(256px + env(safe-area-inset-right)); + } + .menu mwc-icon-button { + color: var(--sidebar-icon-color); + } + :host([expanded]) .menu mwc-icon-button { + margin-right: 23px; + } + :host([expanded][rtl]) .menu mwc-icon-button { + margin-right: 0px; + margin-left: 23px; + } + + .title { + width: 100%; + display: none; + } + :host([narrow]) .title { + padding: 0 0px; + } + :host([expanded]) .title { + display: initial; + } + .title mwc-button { + width: 100%; + } + #sortable, + .hidden-panel { + display: none; + } + + div.panels { + height: 100%; + position: relative; + } + + mwc-list.ha-scrollbar { + height: calc(100% - var(--header-height) - 232px); + --mdc-list-vertical-padding: 4px 0; + padding: 4px 0; + display: flex; + justify-content: space-between; + flex-direction: column; + box-sizing: border-box; + overflow-x: hidden; + background: none; + margin-left: env(safe-area-inset-left); + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + border-right: 1px solid var(--divider-color); + background-color: var(--sidebar-background-color); + width: 64px; + } + :host([rtl]) mwc-list { + border-right: 0; + border-left: 1px solid var(--divider-color); + } + + a { + text-decoration: none; + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + position: relative; + outline: 0; + } + + mwc-list-item { + box-sizing: border-box; + margin: 4px 8px; + border-radius: 4px; + width: 48px; + --mdc-list-item-graphic-margin: 16px; + --mdc-list-item-meta-size: 32px; + } + :host([expanded]) mwc-list { + width: 256px; + width: calc(256px + env(safe-area-inset-left)); + } + + :host([rtl]) mwc-list-item { + padding-left: auto; + padding-right: 12px; + } + + ha-icon[slot="graphic"], + ha-svg-icon[slot="graphic"] { + color: var(--sidebar-icon-color); + } + + [slot="graphic"] { + width: 100%; + } + + :host([rtl]) mwc-list { + margin-left: initial; + margin-right: env(safe-area-inset-right); + } + .menu { + height: var(--header-height); + display: flex; + padding: 0 8.5px; + border-bottom: 1px solid transparent; + white-space: nowrap; + font-weight: 400; + color: var(--primary-text-color); + border-bottom: 1px solid var(--divider-color); + background-color: var(--primary-background-color); + font-size: 20px; + align-items: center; + padding-left: calc(8.5px + env(safe-area-inset-left)); + } + + .iron-selected mwc-list-item::before, + a:not(.iron-selected):focus::before { + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + content: ""; + transition: opacity 15ms linear; + will-change: opacity; + } + .iron-selected mwc-list-item::before { + background-color: var(--sidebar-selected-icon-color); + opacity: 0.12; + } + a:not(.iron-selected):focus::before { + background-color: currentColor; + opacity: var(--dark-divider-opacity); + margin: 4px 8px; + } + .iron-selected mwc-list-item:focus::before, + .iron-selected:focus mwc-list-item::before { + opacity: 0.2; + } + + .iron-selected mwc-list-item[pressed]:before { + opacity: 0.37; + } + + mwc-list-item span { + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + width: 100%; + } + + a.iron-selected ha-icon, + a.iron-selected ha-svg-icon, + a.iron-selected mwc-list-item ha-icon, + a.iron-selected mwc-list-item ha-svg-icon { + color: var(--sidebar-selected-icon-color); + } + + a.iron-selected .item-text { + color: var(--sidebar-selected-text-color); + } + + mwc-list-item mwc-list-item .item-text, + mwc-list-item .item-text { + display: none; + max-width: calc(100% - 56px); + } + :host([expanded]) mwc-list-item .item-text { + display: block; + } + + .divider { + padding: 10px 0; + } + .divider::before { + display: block; + height: 100px; + background-color: var(--divider-color); + } + .notifications-container { + display: flex; + margin-left: env(safe-area-inset-left); + } + :host([rtl]) .notifications-container { + margin-left: initial; + margin-right: env(safe-area-inset-right); + } + .notifications { + cursor: pointer; + } + .notifications .item-text { + flex: 1; + } + .profile { + margin-left: env(safe-area-inset-left); + } + :host([rtl]) .profile { + margin-left: initial; + margin-right: env(safe-area-inset-right); + } + .profile mwc-list-item { + padding-left: 4px; + } + :host([rtl]) .profile mwc-list-item { + padding-left: auto; + padding-right: 4px; + } + .profile .item-text { + margin-left: 8px; + } + :host([rtl]) .profile .item-text { + margin-right: 8px; + } + + .notification-badge { + /* min-width: 20px; + box-sizing: border-box; + border-radius: 50%; + font-weight: 400; + background-color: var(--accent-color); + line-height: 30px; + text-align: center; + padding: 0px 4px; + color: var(--text-accent-color, var(--text-primary-color)); + font-size: 14px; */ + } + ha-svg-icon + .notification-badge { + position: absolute; + bottom: 14px; + left: 26px; + font-size: 0.65em; + } + + .spacer { + flex: 1; + pointer-events: none; + } + + .bottom-spacer { + flex: 1; + pointer-events: none; + height: 100%; + } + + .subheader { + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + padding: 16px; + white-space: nowrap; + } + + .dev-tools { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0 8px; + width: 256px; + box-sizing: border-box; + } + + .dev-tools a { + color: var(--sidebar-icon-color); + } + + .tooltip { + display: none; + position: absolute; + opacity: 0.9; + border-radius: 2px; + white-space: nowrap; + color: var(--sidebar-background-color); + background-color: var(--sidebar-text-color); + padding: 4px; + font-weight: 500; + } + + :host([rtl]) .menu mwc-icon-button { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-combined": HaSidebarCombined; + } +} diff --git a/src/components/ha-sidebar-panel-list.ts b/src/components/ha-sidebar-panel-list.ts index 878c912a66..3dba47a987 100644 --- a/src/components/ha-sidebar-panel-list.ts +++ b/src/components/ha-sidebar-panel-list.ts @@ -1,4 +1,3 @@ -import "./ha-divider-list-item"; import "./ha-clickable-list-item"; import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list"; @@ -168,6 +167,8 @@ class HaSidebarPanelList extends LitElement { private _recentKeydownActiveUntil = 0; + private _sortable?; + // @ts-ignore @LocalStorage("sidebarPanelOrder", true, { attribute: false, @@ -180,8 +181,6 @@ class HaSidebarPanelList extends LitElement { }) private _hiddenPanels: string[] = []; - private _sortable?; - protected render() { if (!this.hass) { return html``; @@ -293,7 +292,7 @@ class HaSidebarPanelList extends LitElement { ? this._renderPanelsEdit(beforeSpacer) : this._renderPanels(beforeSpacer)} - + @@ -632,9 +631,9 @@ class HaSidebarPanelList extends LitElement { haStyleScrollbar, css` :host { - height: calc(100% + var(--header-height)); - /* position: absolute; */ - position: relative; + height: calc(100% - var(--header-height) - 132px); + justify-content: space-between; + flex-direction: column; display: block; overflow: hidden; -ms-user-select: none; @@ -643,10 +642,6 @@ class HaSidebarPanelList extends LitElement { border-right: 1px solid var(--divider-color); background-color: var(--sidebar-background-color); width: 64px; - /* height: calc(100% - var(--header-height) - 132px); - height: calc( - 100% - var(--header-height) - 132px - env(safe-area-inset-bottom) - ); */ } :host([expanded]) { width: 256px; @@ -701,12 +696,8 @@ class HaSidebarPanelList extends LitElement { position: relative; } - ha-divider-list-item { - /* --ha-divider-height: calc(100vh - var(--header-height)); */ - } - mwc-list.ha-scrollbar { - height: 100%; + height: calc(100% - var(--header-height) - 42px); --mdc-list-vertical-padding: 4px 0; padding: 4px 0; display: flex; @@ -717,12 +708,6 @@ class HaSidebarPanelList extends LitElement { margin-left: env(safe-area-inset-left); } - mwc-list.utility-panels { - position: absolute; - bottom: 0px; - width: 100%; - } - :host([rtl]) mwc-list { margin-left: initial; margin-right: env(safe-area-inset-right); diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 98746d1506..4f6d24bec2 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -96,7 +96,8 @@ class HaSidebar extends LitElement { @scroll=${this._listboxScroll} @keydown=${this._listboxKeydown} > - ${this._renderNotifications()} ${this._renderUserItem()} + ${this._renderNotifications()} + ${this._renderUserItem()} ${this._renderSpacer()}
@@ -446,7 +447,8 @@ class HaSidebar extends LitElement { haStyleScrollbar, css` :host { - height: calc(100% - var(--header-height)); + /* height: calc(100% - var(--header-height)); */ + height: 100%; display: block; overflow: hidden; -ms-user-select: none; diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index f90e26b861..c85e6eb39d 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -1,3 +1,4 @@ +import "../components/ha-sidebar-combined"; import "@polymer/app-layout/app-drawer-layout/app-drawer-layout"; import type { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/app-drawer-layout"; import "@polymer/app-layout/app-drawer/app-drawer"; @@ -85,13 +86,13 @@ class HomeAssistantMain extends LitElement { .persistent=${!this.narrow && this.hass.dockedSidebar !== "always_hidden"} > - + >