From bfed61379868f28bad31dca71ec60793c3567426 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 1 Apr 2023 22:35:47 +0000 Subject: [PATCH 01/18] initial sidebar refactoring work, todo: sorting --- src/components/ha-sidebar-config.ts | 90 ++ src/components/ha-sidebar-notifications.ts | 74 ++ src/components/ha-sidebar-panel.ts | 58 + src/components/ha-sidebar-panels.ts | 144 +++ src/components/ha-sidebar-title.ts | 100 ++ src/components/ha-sidebar-tooltip.ts | 35 + src/components/ha-sidebar-user.ts | 54 + src/components/ha-sidebar.ts | 1178 ++------------------ src/layouts/home-assistant-main.ts | 2 +- src/resources/styles.ts | 80 +- 10 files changed, 721 insertions(+), 1094 deletions(-) create mode 100644 src/components/ha-sidebar-config.ts create mode 100644 src/components/ha-sidebar-notifications.ts create mode 100644 src/components/ha-sidebar-panel.ts create mode 100644 src/components/ha-sidebar-panels.ts create mode 100644 src/components/ha-sidebar-title.ts create mode 100644 src/components/ha-sidebar-tooltip.ts create mode 100644 src/components/ha-sidebar-user.ts diff --git a/src/components/ha-sidebar-config.ts b/src/components/ha-sidebar-config.ts new file mode 100644 index 000000000000..802154f33fd2 --- /dev/null +++ b/src/components/ha-sidebar-config.ts @@ -0,0 +1,90 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCog } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { throttle } from "../common/util/throttle"; +import { subscribeRepairsIssueRegistry } from "../data/repairs"; +import { updateCanInstall, UpdateEntity } from "../data/update"; +import { haStyleSidebarItem } from "../resources/styles"; +import { HomeAssistant } from "../types"; +import "./ha-svg-icon"; + +const styles = css` + :host { + width: 100%; + } + .item.expanded { + width: 100%; + } + .count { + margin-left: auto; + } +`; +@customElement("ha-sidebar-config") +class HaSidebarConfig extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public name = ""; + + @property({ type: Boolean }) public expanded = false; + + @property({ type: Boolean }) public selected = false; + + @state() private _updatesCount = 0; + + @state() private _issuesCount = 0; + + static styles = [haStyleSidebarItem, styles]; + + protected render() { + const notices = this._updatesCount + this._issuesCount; + return html` + + + ${!this.expanded && notices > 0 + ? html`${notices}` + : ""} + + ${this.name} + ${this.expanded && notices > 0 + ? html`${notices}` + : ""} + `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + this._checkUpdates(); + } + + private _checkUpdates = throttle(() => { + this._updatesCount = Object.keys(this.hass.states).filter( + (e) => + e.startsWith("update.") && + updateCanInstall(this.hass.states[e] as UpdateEntity) + ).length; + }, 5000); + + public hassSubscribe(): UnsubscribeFunc[] { + return this.hass.user?.is_admin + ? [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + this._issuesCount = repairs.issues.filter( + (issue) => !issue.ignored + ).length; + }), + ] + : []; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-config": HaSidebarConfig; + } +} diff --git a/src/components/ha-sidebar-notifications.ts b/src/components/ha-sidebar-notifications.ts new file mode 100644 index 000000000000..192fc779a7fb --- /dev/null +++ b/src/components/ha-sidebar-notifications.ts @@ -0,0 +1,74 @@ +import "@material/mwc-button/mwc-button"; +import { mdiBell } from "@mdi/js"; +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { + PersistentNotification, + subscribeNotifications, +} from "../data/persistent_notification"; +import { haStyleSidebarItem } from "../resources/styles"; +import { HomeAssistant } from "../types"; +import "./ha-svg-icon"; + +const styles = css` + :host { + width: 100%; + } + .item.expanded { + width: 100%; + } + .count { + margin-left: auto; + } +`; +@customElement("ha-sidebar-notifications") +class HaSidebarNotifications extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public expanded = false; + + @state() private _notifications?: PersistentNotification[]; + + static styles = [haStyleSidebarItem, styles]; + + protected render() { + const notificationCount = this._notifications + ? this._notifications.length + : 0; + return html``; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + subscribeNotifications(this.hass.connection, (notifications) => { + this._notifications = notifications; + }); + } + + private _showNotifications() { + fireEvent(this, "hass-show-notifications"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-notifications": HaSidebarNotifications; + } +} diff --git a/src/components/ha-sidebar-panel.ts b/src/components/ha-sidebar-panel.ts new file mode 100644 index 000000000000..3bf918b59c8e --- /dev/null +++ b/src/components/ha-sidebar-panel.ts @@ -0,0 +1,58 @@ +import "@material/mwc-button/mwc-button"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { haStyleSidebarItem } from "../resources/styles"; +import "./ha-svg-icon"; +import "./ha-icon"; + +const styles = css` + :host { + width: 100%; + } + .item.expanded { + width: 100%; + } + .count { + margin-left: auto; + } +`; +@customElement("ha-sidebar-panel") +class HaSidebarPanel extends LitElement { + @property() public path = ""; + + @property() public name = ""; + + @property() public icon = ""; + + @property() public iconPath = ""; + + @property({ type: Boolean }) public expanded = false; + + @property({ type: Boolean }) public selected = false; + + static styles = [haStyleSidebarItem, styles]; + + protected render() { + return html` + + ${this.iconPath + ? html`` + : html``} + + ${this.name} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-panel": HaSidebarPanel; + } +} diff --git a/src/components/ha-sidebar-panels.ts b/src/components/ha-sidebar-panels.ts new file mode 100644 index 000000000000..f0177baaf1a1 --- /dev/null +++ b/src/components/ha-sidebar-panels.ts @@ -0,0 +1,144 @@ +import "@material/mwc-button/mwc-button"; +import { + mdiCalendar, + mdiHammer, + mdiLightningBolt, + mdiChartBox, + mdiFormatListBulletedType, + mdiViewDashboard, + mdiTooltipAccount, + mdiPlayBoxMultiple, + mdiCart, +} from "@mdi/js"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../common/dom/fire_event"; +import { HomeAssistant, PanelInfo } from "../types"; +import "./ha-sidebar-config"; +import "./ha-sidebar-panel"; + +const styles = css` + :host { + display: contents; + } + .spacer { + flex: 1; + } +`; + +const SHOW_AFTER_SPACER = ["config", "developer-tools"]; +const PANEL_ICONS = { + calendar: mdiCalendar, + "developer-tools": mdiHammer, + energy: mdiLightningBolt, + history: mdiChartBox, + logbook: mdiFormatListBulletedType, + lovelace: mdiViewDashboard, + map: mdiTooltipAccount, + "media-browser": mdiPlayBoxMultiple, + "shopping-list": mdiCart, +}; +const _computePanels = ( + panels: HomeAssistant["panels"], + defaultPanel: HomeAssistant["defaultPanel"], + panelsOrder: string[], + hiddenPanels: string[], + locale: HomeAssistant["locale"] +): [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(); + return [beforeSpacer, afterSpacer]; +}; +const computePanels = memoizeOne(_computePanels); + +@customElement("ha-sidebar-panels") +class HaSidebarPanels extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public expanded = false; + + @property({ type: String }) public currentPanel = ""; + + static styles = styles; + + protected render() { + const [beforeSpacer, afterSpacer] = computePanels( + this.hass.panels, + this.hass.defaultPanel, + [], + [], + this.hass.locale + ); + const renderPanel = (panel: PanelInfo) => + panel.url_path === "config" + ? html`` + : html``; + return [ + ...beforeSpacer.map(renderPanel), + html`
`, + ...afterSpacer.map(renderPanel), + ]; + } + + private _panelOver(ev: CustomEvent) { + fireEvent(this, "panel-hover", ev.target as HTMLElement); + } + + private _panelLeave() { + fireEvent(this, "panel-leave"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-panels": HaSidebarPanels; + } + interface HASSDomEvents { + "panel-hover": HTMLElement; + "panel-leave": undefined; + } +} diff --git a/src/components/ha-sidebar-title.ts b/src/components/ha-sidebar-title.ts new file mode 100644 index 000000000000..8f5a495ee67f --- /dev/null +++ b/src/components/ha-sidebar-title.ts @@ -0,0 +1,100 @@ +import "@material/mwc-button/mwc-button"; +import { mdiMenu, mdiMenuOpen } from "@mdi/js"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../common/dom/fire_event"; +import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; +import { HomeAssistant } from "../types"; +import "./ha-icon-button"; + +const styles = css` + .menu { + display: flex; + flex-shrink: 0; + height: var(--header-height); + box-sizing: border-box; + align-items: center; + white-space: nowrap; + } + .menu .title { + color: var(--secondary-text-color); + font-weight: 500; + font-size: 14px; + line-height: 20px; + margin-left: 16px; + } + .menu ha-icon-button { + margin: 0 auto; + color: var(--sidebar-icon-color); + } + .menu mwc-button { + width: 100%; + } + .menu.expanded { + padding: calc((var(--header-height) - 20px) / 5) 12px 0 12px; + } + .menu.expanded ha-icon-button { + margin-right: 0; + } +`; +@customElement("ha-sidebar-title") +class HaSidebarTitle extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property({ type: Boolean }) public expanded = false; + + @property({ type: Boolean }) public editMode = false; + + static styles = styles; + + protected render() { + const classes = classMap({ menu: true, expanded: this.expanded }); + const saveEdits = html` + ${this.hass.localize("ui.sidebar.done")} + `; + const sidebarToggle = html` + + `; + return html`
+ ${this.editMode ? saveEdits : ""} + ${!this.expanded || this.editMode + ? "" + : html`Home Assistant`} + ${this.narrow || this.editMode ? "" : sidebarToggle} +
`; + } + + private _toggleSidebar(ev: CustomEvent) { + if (ev.detail.action !== "tap") return; + fireEvent(this, "hass-toggle-menu"); + } + + private _editModeOn(ev: CustomEvent) { + if (ev.detail.action !== "hold") return; + fireEvent(this, "hass-edit-sidebar", { editMode: true }); + } + + private _editModeOff() { + fireEvent(this, "hass-edit-sidebar", { editMode: false }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-title": HaSidebarTitle; + } +} diff --git a/src/components/ha-sidebar-tooltip.ts b/src/components/ha-sidebar-tooltip.ts new file mode 100644 index 000000000000..1d59ddc13173 --- /dev/null +++ b/src/components/ha-sidebar-tooltip.ts @@ -0,0 +1,35 @@ +import "@material/mwc-button/mwc-button"; +import { css, html, LitElement } from "lit"; +import { customElement } from "lit/decorators"; + +const styles = css` + div { + position: fixed; + background-color: var(--sidebar-text-color); + color: var(--sidebar-background-color); + padding: 4px; + border-radius: 2px; + transform: translateY(-50%); + white-space: nowrap; + display: none; + } +`; +@customElement("ha-sidebar-tooltip") +class HaSidebarTooltip extends LitElement { + static styles = styles; + + protected render() { + return html`
`; + } +} + +export interface TooltipPosition { + shown: boolean; + x: number; + y: number; +} +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-tooltip": HaSidebarTooltip; + } +} diff --git a/src/components/ha-sidebar-user.ts b/src/components/ha-sidebar-user.ts new file mode 100644 index 000000000000..cab17a026698 --- /dev/null +++ b/src/components/ha-sidebar-user.ts @@ -0,0 +1,54 @@ +import "@material/mwc-button/mwc-button"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { haStyleSidebarItem } from "../resources/styles"; +import { HomeAssistant } from "../types"; +import "./user/ha-user-badge"; + +const styles = css` + .icon { + height: 24px; + } + .user-icon { + display: inline-flex; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(0.8); + } +`; +@customElement("ha-sidebar-user") +class HaSidebarUser extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public expanded = false; + + @property({ type: Boolean }) public selected = false; + + static styles = [haStyleSidebarItem, styles]; + + protected render() { + return html` + + + + + + ${this.hass.user ? this.hass.user.name : ""} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-user": HaSidebarUser; + } +} diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 156b4e777cfb..8030dfe326bf 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -1,191 +1,34 @@ -import "@material/mwc-button/mwc-button"; -import { - mdiBell, - mdiCalendar, - mdiCart, - mdiCellphoneCog, - mdiChartBox, - mdiClose, - mdiCog, - mdiFormatListBulletedType, - mdiHammer, - mdiLightningBolt, - mdiMenu, - mdiMenuOpen, - mdiPlayBoxMultiple, - mdiPlus, - mdiTooltipAccount, - mdiViewDashboard, -} from "@mdi/js"; -import "@polymer/paper-item/paper-icon-item"; -import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { - css, - CSSResult, - CSSResultGroup, - html, - LitElement, - PropertyValues, - nothing, -} from "lit"; -import { customElement, eventOptions, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { guard } from "lit/directives/guard"; -import memoizeOne from "memoize-one"; -import { LocalStorage } from "../common/decorators/local-storage"; -import { fireEvent } from "../common/dom/fire_event"; -import { toggleAttribute } from "../common/dom/toggle_attribute"; -import { stringCompare } from "../common/string/compare"; -import { computeRTL } from "../common/util/compute_rtl"; -import { throttle } from "../common/util/throttle"; -import { ActionHandlerDetail } from "../data/lovelace"; -import { - PersistentNotification, - subscribeNotifications, -} from "../data/persistent_notification"; -import { subscribeRepairsIssueRegistry } from "../data/repairs"; -import { updateCanInstall, UpdateEntity } from "../data/update"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; -import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; -import { loadSortable, SortableInstance } from "../resources/sortable.ondemand"; -import { haStyleScrollbar } from "../resources/styles"; -import type { HomeAssistant, PanelInfo, Route } from "../types"; -import "./ha-icon"; -import "./ha-icon-button"; -import "./ha-menu-button"; -import "./ha-svg-icon"; -import "./user/ha-user-badge"; - -const SHOW_AFTER_SPACER = ["config", "developer-tools"]; - -const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body; - -const SORT_VALUE_URL_PATHS = { - energy: 1, - map: 2, - logbook: 3, - history: 4, - "developer-tools": 9, - config: 11, -}; - -const PANEL_ICONS = { - calendar: mdiCalendar, - "developer-tools": mdiHammer, - energy: mdiLightningBolt, - history: mdiChartBox, - logbook: mdiFormatListBulletedType, - lovelace: mdiViewDashboard, - map: mdiTooltipAccount, - "media-browser": mdiPlayBoxMultiple, - "shopping-list": mdiCart, -}; - -const panelSorter = ( - reverseSort: string[], - defaultPanel: string, - a: PanelInfo, - b: PanelInfo, - language: string -) => { - 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, language); -}; - -const defaultPanelSorter = ( - defaultPanel: string, - a: PanelInfo, - b: PanelInfo, - language: string -) => { - // 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 stringCompare(a.title!, b.title!, language); - } - 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 stringCompare(a.title!, b.title!, language); -}; - -const computePanels = memoizeOne( - ( - panels: HomeAssistant["panels"], - defaultPanel: HomeAssistant["defaultPanel"], - panelsOrder: string[], - hiddenPanels: string[], - locale: HomeAssistant["locale"] - ): [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, locale.language) - ); - afterSpacer.sort((a, b) => - panelSorter(reverseSort, defaultPanel, a, b, locale.language) - ); - - return [beforeSpacer, afterSpacer]; - } -); - +import type { HomeAssistant, Route } from "../types"; +import "./ha-sidebar-title"; +import "./ha-sidebar-panels"; +import "./ha-sidebar-notifications"; +import "./ha-sidebar-user"; +import "./ha-sidebar-tooltip"; + +const styles = css` + :host { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + .items { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 0 12px 12px; + overflow: hidden; + } + hr { + width: 100%; + height: 1px; + border: none; + background-color: var(--divider-color); + } +`; @customElement("ha-sidebar") class HaSidebar extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -198,934 +41,87 @@ class HaSidebar extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public editMode = false; - @state() private _notifications?: PersistentNotification[]; - - @state() private _updatesCount = 0; - - @state() private _issuesCount = 0; - - @state() private _renderEmptySortable = false; - - private _mouseLeaveTimeout?: number; + static styles = styles; - private _tooltipHideTimeout?: number; - - private _recentKeydownActiveUntil = 0; - - private sortableStyleLoaded = false; - - // @ts-ignore - @LocalStorage("sidebarPanelOrder", true, { - attribute: false, - }) - private _panelOrder: string[] = []; - - // @ts-ignore - @LocalStorage("sidebarHiddenPanels", true, { - attribute: false, - }) - private _hiddenPanels: string[] = []; - - private _sortable?: SortableInstance; - - public hassSubscribe(): UnsubscribeFunc[] { - return this.hass.user?.is_admin - ? [ - subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { - this._issuesCount = repairs.issues.filter( - (issue) => !issue.ignored - ).length; - }), - ] - : []; - } + private currentPanel = ""; protected render() { if (!this.hass) { return nothing; } - // prettier-ignore return html` - ${this._renderHeader()} - ${this._renderAllPanels()} - ${this._renderDivider()} - ${this._renderNotifications()} - ${this._renderUserItem()} -
-
+ +
+ +
+ + +
+ `; } - protected shouldUpdate(changedProps: PropertyValues): boolean { - if ( - changedProps.has("expanded") || - changedProps.has("narrow") || - changedProps.has("alwaysExpand") || - changedProps.has("_externalConfig") || - changedProps.has("_updatesCount") || - changedProps.has("_issuesCount") || - 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.locale !== oldHass.locale || - hass.states !== oldHass.states || - hass.defaultPanel !== oldHass.defaultPanel - ); - } - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - subscribeNotifications(this.hass.connection, (notifications) => { - this._notifications = notifications; - }); - } - - protected updated(changedProps) { - super.updated(changedProps); - if (changedProps.has("alwaysExpand")) { - toggleAttribute(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.locale !== this.hass.locale) { - toggleAttribute(this, "rtl", computeRTL(this.hass)); - } - - this._calculateCounts(); - - 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 _calculateCounts = throttle(() => { - let updateCount = 0; - - for (const entityId of Object.keys(this.hass.states)) { - if ( - entityId.startsWith("update.") && - updateCanInstall(this.hass.states[entityId] as UpdateEntity) - ) { - updateCount++; - } - } - - this._updatesCount = updateCount; - }, 5000); - - private _renderHeader() { - return html``; - } - - private _renderAllPanels() { - const [beforeSpacer, afterSpacer] = computePanels( - this.hass.panels, - this.hass.defaultPanel, - this._panelOrder, - this._hiddenPanels, - this.hass.locale - ); - - // Show the supervisor as beeing part of configuration - const selectedPanel = this.route.path?.startsWith("/hassio/") + protected updated() { + this.currentPanel = this.route.path?.startsWith("/hassio/") ? "config" : this.hass.panelUrl; - - // prettier-ignore - return html` - - ${this.editMode - ? this._renderPanelsEdit(beforeSpacer) - : this._renderPanels(beforeSpacer)} - ${this._renderSpacer()} - ${this._renderPanels(afterSpacer)} - ${this._renderExternalConfiguration()} - - `; - } - - private _renderPanels(panels: PanelInfo[]) { - return panels.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 - ? PANEL_ICONS.lovelace - : panel.url_path in PANEL_ICONS - ? PANEL_ICONS[panel.url_path] - : undefined - ) - ); - } - - private _renderPanel( - urlPath: string, - title: string | null, - icon?: string | null, - iconPath?: string | null - ) { - return urlPath === "config" - ? this._renderConfiguration(title) - : html` - - - ${iconPath - ? html`` - : html``} - ${title} - - ${this.editMode - ? html`` - : ""} - - `; - } - - 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 && !panel.icon - ? html`` - : panel.url_path in PANEL_ICONS - ? html`` - : html``} - ${panel.url_path === this.hass.defaultPanel - ? this.hass.localize("panel.states") - : this.hass.localize(`panel.${panel.title}`) || - panel.title} - - `; - })} - ${this._renderSpacer()}` - : ""}`; - } - - private _renderDivider() { - return html`
`; - } - - private _renderSpacer() { - return html`
`; - } - - private _renderConfiguration(title: string | null) { - return html` - - - ${!this.alwaysExpand && - (this._updatesCount > 0 || this._issuesCount > 0) - ? html` - - ${this._updatesCount + this._issuesCount} - - ` - : ""} - ${title} - ${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0) - ? html` - ${this._updatesCount + this._issuesCount} - ` - : ""} - - `; - } - - private _renderNotifications() { - const notificationCount = this._notifications - ? this._notifications.length - : 0; - - return html`
- - - ${!this.alwaysExpand && notificationCount > 0 - ? html` - - ${notificationCount} - - ` - : ""} - - ${this.hass.localize("ui.notification_drawer.title")} - - ${this.alwaysExpand && notificationCount > 0 - ? html` ${notificationCount} ` - : ""} - -
`; - } - - private _renderUserItem() { - return html` - - - - - ${this.hass.user ? this.hass.user.name : ""} - - - `; - } - - private _renderExternalConfiguration() { - return html`${!this.hass.user?.is_admin && - this.hass.auth.external?.config.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 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() { - await Promise.all([this._loadSortableStyle(), this._createSortable()]); - } - - private async _loadSortableStyle() { - if (this.sortableStyleLoaded) return; - - const sortStylesImport = await import("../resources/ha-sortable-style"); - - const style = document.createElement("style"); - style.innerHTML = (sortStylesImport.sortableStyles as CSSResult).cssText; - this.shadowRoot!.appendChild(style); - - this.sortableStyleLoaded = true; - await this.updateComplete; - } - - private async _createSortable() { - const Sortable = await loadSortable(); - this._sortable = new Sortable( - this.shadowRoot!.getElementById("sortable")!, - { - animation: 150, - fallbackClass: "sortable-fallback", - dataIdAttr: "data-panel", - handle: "paper-icon-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 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 _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.alwaysExpand || - new Date().getTime() < this._recentKeydownActiveUntil - ) { - return; - } - if (this._mouseLeaveTimeout) { - clearTimeout(this._mouseLeaveTimeout); - this._mouseLeaveTimeout = undefined; - } - this._showTooltip(ev.currentTarget as PaperIconItemElement); } - private _itemMouseLeave() { - if (this._mouseLeaveTimeout) { - clearTimeout(this._mouseLeaveTimeout); - } - this._mouseLeaveTimeout = window.setTimeout(() => { - this._hideTooltip(); - }, 500); + private _panelHover(ev: CustomEvent) { + this._mouseOverItem({ target: ev.detail }); } - private _listboxFocusIn(ev) { - if (this.alwaysExpand || ev.target.nodeName !== "A") { + private _mouseOverItem(ev: { target: HTMLElement }) { + const tooltipRoot = + this.shadowRoot?.querySelector("ha-sidebar-tooltip")?.shadowRoot; + const tooltip = tooltipRoot?.firstElementChild as HTMLElement; + if (!tooltip) return; + if (this.alwaysExpand) { + tooltip.style.display = "none"; return; } - this._showTooltip(ev.target.querySelector("paper-icon-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) { + const target = ev.target; + const targetPos = target.getBoundingClientRect(); + const label = target.shadowRoot?.querySelector(".name")?.innerHTML; + if (!label) { + tooltip.style.display = "none"; return; } - this._hideTooltip(); - } - - private _listboxKeydown() { - this._recentKeydownActiveUntil = new Date().getTime() + 100; - } - - private _showTooltip(item: PaperIconItemElement) { - if (this._tooltipHideTimeout) { - clearTimeout(this._tooltipHideTimeout); - this._tooltipHideTimeout = undefined; - } - const tooltip = this._tooltip; - const listbox = this.shadowRoot!.querySelector("paper-listbox")!; - let top = item.offsetTop + 11; - if (listbox.contains(item)) { - top -= listbox.scrollTop; - } - tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML; + tooltip.innerHTML = label; tooltip.style.display = "block"; - tooltip.style.top = `${top}px`; - tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`; + tooltip.style.left = this.offsetLeft + this.clientWidth + 4 + "px"; + tooltip.style.top = targetPos.top + targetPos.height / 2 + "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(): CSSResultGroup { - return [ - haStyleScrollbar, - css` - :host { - height: 100%; - display: block; - overflow: hidden; - -ms-user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - background-color: var(--sidebar-background-color); - width: 100%; - box-sizing: border-box; - } - .menu { - height: var(--header-height); - box-sizing: border-box; - display: flex; - padding: 0 4px; - border-bottom: 1px solid transparent; - white-space: nowrap; - font-weight: 400; - color: var(--sidebar-menu-button-text-color, --primary-text-color); - border-bottom: 1px solid var(--divider-color); - background-color: var( - --sidebar-menu-button-background-color, - --primary-background-color - ); - font-size: 20px; - align-items: center; - padding-left: calc(4px + env(safe-area-inset-left)); - } - :host([rtl]) .menu { - padding-left: 4px; - padding-right: calc(4px + 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 ha-icon-button { - color: var(--sidebar-icon-color); - } - .title { - margin-left: 19px; - width: 100%; - display: none; - } - :host([rtl]) .title { - margin-left: 0; - margin-right: 19px; - } - :host([narrow]) .title { - margin: 0; - padding: 0 16px; - } - :host([expanded]) .title { - display: initial; - } - :host([expanded]) .menu mwc-button { - margin: 0 8px; - } - .menu mwc-button { - width: 100%; - } - #sortable, - .hidden-panel { - display: none; - } - - paper-listbox { - padding: 4px 0; - display: flex; - flex-direction: column; - box-sizing: border-box; - height: calc(100% - var(--header-height) - 132px); - height: calc( - 100% - var(--header-height) - 132px - env(safe-area-inset-bottom) - ); - overflow-x: hidden; - background: none; - margin-left: env(safe-area-inset-left); - } - - :host([rtl]) paper-listbox { - margin-left: initial; - margin-right: env(safe-area-inset-right); - } - - a { - text-decoration: none; - color: var(--sidebar-text-color); - font-weight: 500; - font-size: 14px; - position: relative; - display: block; - outline: 0; - } - - paper-icon-item { - box-sizing: border-box; - margin: 4px; - padding-left: 12px; - border-radius: 4px; - --paper-item-min-height: 40px; - width: 48px; - } - :host([expanded]) paper-icon-item { - width: 248px; - } - :host([rtl]) paper-icon-item { - padding-left: auto; - padding-right: 12px; - } - - ha-icon[slot="item-icon"], - ha-svg-icon[slot="item-icon"] { - color: var(--sidebar-icon-color); - } - - .iron-selected paper-icon-item::before, - a:not(.iron-selected):focus::before { - border-radius: 4px; - position: absolute; - top: 0; - right: 2px; - bottom: 0; - left: 2px; - pointer-events: none; - content: ""; - transition: opacity 15ms linear; - will-change: opacity; - } - .iron-selected paper-icon-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 paper-icon-item:focus::before, - .iron-selected:focus paper-icon-item::before { - opacity: 0.2; - } - - .iron-selected paper-icon-item[pressed]:before { - opacity: 0.37; - } - - paper-icon-item span { - color: var(--sidebar-text-color); - font-weight: 500; - font-size: 14px; - } - - a.iron-selected paper-icon-item ha-icon, - a.iron-selected paper-icon-item ha-svg-icon { - color: var(--sidebar-selected-icon-color); - } - - a.iron-selected .item-text { - color: var(--sidebar-selected-text-color); - } - - paper-icon-item .item-text { - display: none; - max-width: calc(100% - 56px); - } - :host([expanded]) paper-icon-item .item-text { - display: block; - } - - .divider { - bottom: 112px; - padding: 10px 0; - } - .divider::before { - content: " "; - display: block; - height: 1px; - background-color: var(--divider-color); - } - .notifications-container, - .configuration-container { - display: flex; - margin-left: env(safe-area-inset-left); - } - :host([rtl]) .notifications-container, - :host([rtl]) .configuration-container { - margin-left: initial; - margin-right: env(safe-area-inset-right); - } - .notifications { - cursor: pointer; - } - .notifications .item-text, - .configuration .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 paper-icon-item { - padding-left: 4px; - } - :host([rtl]) .profile paper-icon-item { - padding-left: auto; - padding-right: 4px; - } - .profile .item-text { - margin-left: 8px; - } - :host([rtl]) .profile .item-text { - margin-right: 8px; - } - - .notification-badge, - .configuration-badge { - position: absolute; - left: calc(var(--app-drawer-width, 248px) - 42px); - min-width: 20px; - box-sizing: border-box; - border-radius: 50%; - font-weight: 400; - background-color: var(--accent-color); - line-height: 20px; - text-align: center; - padding: 0px 6px; - color: var(--text-accent-color, var(--text-primary-color)); - } - ha-svg-icon + .notification-badge, - ha-svg-icon + .configuration-badge { - position: absolute; - bottom: 14px; - left: 26px; - font-size: 0.65em; - } - - .spacer { - flex: 1; - pointer-events: none; - } - - .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 ha-icon-button { - -webkit-transform: scaleX(-1); - transform: scaleX(-1); - } - `, - ]; + private _mouseLeave() { + const tooltipRoot = + this.shadowRoot?.querySelector("ha-sidebar-tooltip")?.shadowRoot; + const tooltip = tooltipRoot?.firstElementChild as HTMLElement; + if (!tooltip) return; + tooltip.style.display = "none"; } } diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index c0a116163a79..4ec8afa6cb53 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -173,7 +173,7 @@ export class HomeAssistantMain extends LitElement { color: var(--primary-text-color); /* remove the grey tap highlights in iOS on the fullscreen touch targets */ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - --mdc-drawer-width: 56px; + --mdc-drawer-width: 80px; --mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width)); } :host([expanded]) { diff --git a/src/resources/styles.ts b/src/resources/styles.ts index e94aedc60cc2..378aa282d57d 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -56,9 +56,12 @@ export const derivedStyles = { "state-unavailable-color": "var(--state-icon-unavailable-color, var(--disabled-text-color))", "sidebar-text-color": "var(--primary-text-color)", + "rgb-sidebar-text-color": "var(--rgb-primary-text-color)", "sidebar-background-color": "var(--card-background-color)", - "sidebar-selected-text-color": "var(--primary-color)", - "sidebar-selected-icon-color": "var(--primary-color)", + "sidebar-selected-color": + "var(--sidebar-selected-text-color, var(--primary-color))", + "rgb-sidebar-selected-color": + "var(--rgb-sidebar-selected-text-color, var(--rgb-primary-color))", "sidebar-icon-color": "rgba(var(--rgb-primary-text-color), 0.6)", "switch-checked-color": "var(--primary-color)", "switch-checked-button-color": @@ -333,6 +336,79 @@ export const haStyleDialog = css` } `; +export const haStyleSidebarItem = css` + .item { + --rgb-text: var(--rgb-sidebar-text-color); + background-color: transparent; + color: rgb(var(--rgb-text)); + font-family: inherit; + text-decoration: none; + border: none; + cursor: pointer; + font-weight: 500; + font-size: 14px; + line-height: 20px; + + position: relative; + box-sizing: border-box; + display: flex; + align-items: center; + padding: 0 16px; + border-radius: var(--sidebar-item-radius, 56px); + height: 56px; + width: 56px; + margin: auto; + } + .item .icon { + position: relative; + text-align: left; + width: 24px; + } + .item .name { + display: none; + } + .item:hover { + color: rgb(var(--rgb-text)); + background-color: rgba(var(--rgb-text), 0.08); + } + .item:focus-visible, + .item:active { + color: rgb(var(--rgb-text)); + background-color: rgba(var(--rgb-text), 0.12); + } + .item[aria-selected="true"] { + --rgb-text: var(--rgb-sidebar-selected-color); + background-color: rgba(var(--rgb-text), 0.12); + } + .item.expanded { + margin: 0; + padding-right: 24px; + width: auto; + } + .item.expanded .icon { + margin-right: 12px; + } + .item.expanded .name { + display: initial; + } + .icon .badge { + position: absolute; + top: 0; + right: 0; + width: 16px; + height: 16px; + transform: translateX(50%); + border-radius: 16px; + background-color: var(--accent-color); + color: var(--text-accent-color, var(--text-primary-color)); + font-weight: 500; + font-size: 11px; + display: flex; + align-items: center; + justify-content: center; + } +`; + export const haStyleScrollbar = css` .ha-scrollbar::-webkit-scrollbar { width: 0.4rem; From e6f0319f9f55311aa8e49a4335465db8b0407d09 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 2 Apr 2023 03:10:00 +0000 Subject: [PATCH 02/18] add editing and fix stuff --- src/components/ha-sidebar-edit-panels.ts | 170 +++++++++++++++++++++++ src/components/ha-sidebar-panels.ts | 152 +++++++++++++++++--- src/components/ha-sidebar.ts | 1 + src/resources/styles.ts | 7 +- 4 files changed, 307 insertions(+), 23 deletions(-) create mode 100644 src/components/ha-sidebar-edit-panels.ts diff --git a/src/components/ha-sidebar-edit-panels.ts b/src/components/ha-sidebar-edit-panels.ts new file mode 100644 index 000000000000..7bdd9239cf2b --- /dev/null +++ b/src/components/ha-sidebar-edit-panels.ts @@ -0,0 +1,170 @@ +import { mdiClose, mdiPlus } from "@mdi/js"; +import { css, CSSResult, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { loadSortable, SortableInstance } from "../resources/sortable.ondemand"; +import { HomeAssistant } from "../types"; +import "./ha-icon"; +import "./ha-icon-button"; +import "./ha-svg-icon"; + +const styles = css` + .panel { + --rgb-text: var(--rgb-sidebar-text-color); + background-color: transparent; + color: rgb(var(--rgb-text)); + font-family: inherit; + border: none; + cursor: pointer; + width: 100%; + font-weight: 500; + font-size: 14px; + line-height: 20px; + + box-sizing: border-box; + display: flex; + align-items: center; + padding: 0 16px; + border-radius: var(--sidebar-item-radius, 56px); + height: 56px; + } + .panel .icon { + width: 36px; + text-align: left; + } + .panel:hover { + color: rgb(var(--rgb-text)); + background-color: rgba(var(--rgb-text), 0.08); + } + .panel:focus-visible, + .panel:active { + color: rgb(var(--rgb-text)); + background-color: rgba(var(--rgb-text), 0.12); + } + #sortable { + overflow: visible; + } + #sortable .panel { + cursor: grab; + } + .panel ha-icon-button { + margin-left: auto; + margin-right: -11px; + } + .panel ha-svg-icon { + margin-left: auto; + } +`; +@customElement("ha-sidebar-edit-panels") +class HaSidebarEditPanels extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public panels: any[] = []; + + @property() public hiddenPanels: any[] = []; + + static styles = styles; + + private _sortable?: SortableInstance; + + private sortableStyleLoaded = false; + + protected render() { + const renderPanel = (panel) => + html`
+ + ${panel.icon + ? html`` + : html``} + + ${panel.name} + +
`; + const renderHiddenPanel = (panel) => + html``; + return html`
${this.panels.map(renderPanel)}
+ ${this.hiddenPanels.map(renderHiddenPanel)}`; + } + + protected async firstUpdated() { + await Promise.all([this._loadSortableStyle(), this._createSortable()]); + } + + private async _loadSortableStyle() { + if (this.sortableStyleLoaded) return; + + const sortStylesImport = await import("../resources/ha-sortable-style"); + + const style = document.createElement("style"); + style.innerHTML = (sortStylesImport.sortableStyles as CSSResult).cssText; + this.shadowRoot!.appendChild(style); + + this.sortableStyleLoaded = true; + await this.updateComplete; + } + + private async _createSortable() { + const Sortable = await loadSortable(); + this._sortable = new Sortable( + this.shadowRoot!.getElementById("sortable")!, + { + animation: 150, + dataIdAttr: "data-panel", + handle: "div", + onSort: () => { + fireEvent(this, "panel-reorder", this._sortable!.toArray()); + }, + } + ); + } + + private _hidePanel(ev: CustomEvent) { + const panel = (ev.target as HTMLElement) + .closest("[data-panel]") + ?.getAttribute("data-panel"); + if (!panel) return; + fireEvent(this, "panel-hide", panel); + } + + private _showPanel(ev: CustomEvent) { + const panel = (ev.target as HTMLElement) + .closest("[data-panel]") + ?.getAttribute("data-panel"); + if (!panel) return; + fireEvent(this, "panel-show", panel); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-edit-panels": HaSidebarEditPanels; + } + interface HASSDomEvents { + "panel-reorder": string[]; + "panel-hide": string; + "panel-show": string; + } +} diff --git a/src/components/ha-sidebar-panels.ts b/src/components/ha-sidebar-panels.ts index f0177baaf1a1..f9151b4364ad 100644 --- a/src/components/ha-sidebar-panels.ts +++ b/src/components/ha-sidebar-panels.ts @@ -1,26 +1,32 @@ import "@material/mwc-button/mwc-button"; import { mdiCalendar, - mdiHammer, - mdiLightningBolt, + mdiCart, mdiChartBox, mdiFormatListBulletedType, - mdiViewDashboard, - mdiTooltipAccount, + mdiHammer, + mdiLightningBolt, mdiPlayBoxMultiple, - mdiCart, + mdiTooltipAccount, + mdiViewDashboard, } from "@mdi/js"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { LocalStorage } from "../common/decorators/local-storage"; import { fireEvent } from "../common/dom/fire_event"; +import { stringCompare } from "../common/string/compare"; import { HomeAssistant, PanelInfo } from "../types"; import "./ha-sidebar-config"; import "./ha-sidebar-panel"; +import "./ha-sidebar-edit-panels"; const styles = css` :host { - display: contents; + display: flex; + flex-direction: column; + overflow-y: auto; + flex-grow: 1; } .spacer { flex: 1; @@ -28,6 +34,14 @@ const styles = css` `; const SHOW_AFTER_SPACER = ["config", "developer-tools"]; +const SORT_VALUE_URL_PATHS = { + energy: 1, + map: 2, + logbook: 3, + history: 4, + "developer-tools": 9, + config: 11, +}; const PANEL_ICONS = { calendar: mdiCalendar, "developer-tools": mdiHammer, @@ -39,6 +53,47 @@ const PANEL_ICONS = { "media-browser": mdiPlayBoxMultiple, "shopping-list": mdiCart, }; +const panelSorter = ( + a: PanelInfo, + b: PanelInfo, + panelOrder: string[], + defaultPanel: string, + language: string +) => { + const indexA = panelOrder.indexOf(a.url_path); + const indexB = panelOrder.indexOf(b.url_path); + const order = indexA - indexB; + if (order) return order; + return defaultPanelSorter(defaultPanel, a, b, language); +}; +const defaultPanelSorter = ( + defaultPanel: string, + a: PanelInfo, + b: PanelInfo, + language: string +) => { + if (a.url_path === defaultPanel) return -1; + if (b.url_path === defaultPanel) return 1; + + const aLovelace = a.component_name === "lovelace"; + const bLovelace = b.component_name === "lovelace"; + + if (aLovelace && bLovelace) + return stringCompare(a.title!, b.title!, language); + if (aLovelace) 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; + + return stringCompare(a.title!, b.title!, language); +}; const _computePanels = ( panels: HomeAssistant["panels"], defaultPanel: HomeAssistant["defaultPanel"], @@ -66,7 +121,12 @@ const _computePanels = ( ).push(panel); }); - // const reverseSort = [...panelsOrder].reverse(); + beforeSpacer.sort((a, b) => + panelSorter(a, b, panelsOrder, defaultPanel, locale.language) + ); + afterSpacer.sort((a, b) => + panelSorter(a, b, panelsOrder, defaultPanel, locale.language) + ); return [beforeSpacer, afterSpacer]; }; const computePanels = memoizeOne(_computePanels); @@ -75,20 +135,38 @@ const computePanels = memoizeOne(_computePanels); class HaSidebarPanels extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public editMode = false; + @property({ type: Boolean }) public expanded = false; @property({ type: String }) public currentPanel = ""; + @LocalStorage("sidebarPanelOrder", true) + private _panelOrder: string[] = []; + + @LocalStorage("sidebarHiddenPanels", true) + private _hiddenPanels: string[] = []; + static styles = styles; protected render() { const [beforeSpacer, afterSpacer] = computePanels( this.hass.panels, this.hass.defaultPanel, - [], - [], + this._panelOrder, + this._hiddenPanels, this.hass.locale ); + const getName = (panel: PanelInfo) => + panel.url_path === this.hass.defaultPanel + ? panel.title || this.hass.localize("panel.states") + : this.hass.localize(`panel.${panel.title}`) || panel.title || ""; + const getIconPath = (panel: PanelInfo) => + panel.url_path === this.hass.defaultPanel && !panel.icon + ? PANEL_ICONS.lovelace + : panel.url_path in PANEL_ICONS + ? PANEL_ICONS[panel.url_path] + : undefined; const renderPanel = (panel: PanelInfo) => panel.url_path === "config" ? html``; return [ - ...beforeSpacer.map(renderPanel), + this.editMode + ? html` ({ + url_path: p.url_path, + name: getName(p), + icon: p.icon, + iconPath: getIconPath(p), + }))} + .hiddenPanels=${this._hiddenPanels.map((p) => { + const panel = this.hass.panels[p]; + if (!panel) return {}; + return { + url_path: p, + name: getName(panel), + icon: panel.icon, + iconPath: getIconPath(panel), + }; + })} + @panel-reorder=${this._panelReorder} + @panel-hide=${this._panelHide} + @panel-show=${this._panelShow} + >` + : html`${beforeSpacer.map(renderPanel)}`, html`
`, ...afterSpacer.map(renderPanel), ]; } + protected shouldUpdate(changedProps: PropertyValues) { + const nonOrderProps = [...changedProps.keys()].filter( + (p) => p !== "_panelOrder" + ); + return nonOrderProps.length > 0; + } + private _panelOver(ev: CustomEvent) { fireEvent(this, "panel-hover", ev.target as HTMLElement); } @@ -131,6 +233,18 @@ class HaSidebarPanels extends LitElement { private _panelLeave() { fireEvent(this, "panel-leave"); } + + private _panelReorder(ev: CustomEvent) { + this._panelOrder = ev.detail; + } + + private _panelHide(ev: CustomEvent) { + this._hiddenPanels = [...this._hiddenPanels, ev.detail]; + } + + private _panelShow(ev: CustomEvent) { + this._hiddenPanels = this._hiddenPanels.filter((p) => p !== ev.detail); + } } declare global { diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 8030dfe326bf..3fa9b1fbd402 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -62,6 +62,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { .hass=${this.hass} .expanded=${this.alwaysExpand} .currentPanel=${this.currentPanel} + .editMode=${this.editMode} @panel-hover=${this._panelHover} @panel-leave=${this._mouseLeave} > diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 378aa282d57d..3ba594b53313 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -353,10 +353,9 @@ export const haStyleSidebarItem = css` box-sizing: border-box; display: flex; align-items: center; - padding: 0 16px; + justify-content: center; border-radius: var(--sidebar-item-radius, 56px); height: 56px; - width: 56px; margin: auto; } .item .icon { @@ -382,8 +381,8 @@ export const haStyleSidebarItem = css` } .item.expanded { margin: 0; - padding-right: 24px; - width: auto; + padding: 0 24px 0 16px; + justify-content: initial; } .item.expanded .icon { margin-right: 12px; From 36779f9ca78219c70cf77822d70fd93def7d79a0 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 2 Apr 2023 14:57:39 +0000 Subject: [PATCH 03/18] rename some stuff and add external config --- ...r-config.ts => ha-sidebar-panel-config.ts} | 6 +-- src/components/ha-sidebar-panel-ext-config.ts | 51 +++++++++++++++++++ ...s.ts => ha-sidebar-panel-notifications.ts} | 8 +-- ...debar-user.ts => ha-sidebar-panel-user.ts} | 6 +-- src/components/ha-sidebar-panels.ts | 21 ++++++-- src/components/ha-sidebar.ts | 12 ++--- 6 files changed, 85 insertions(+), 19 deletions(-) rename src/components/{ha-sidebar-config.ts => ha-sidebar-panel-config.ts} (94%) create mode 100644 src/components/ha-sidebar-panel-ext-config.ts rename src/components/{ha-sidebar-notifications.ts => ha-sidebar-panel-notifications.ts} (91%) rename src/components/{ha-sidebar-user.ts => ha-sidebar-panel-user.ts} (90%) diff --git a/src/components/ha-sidebar-config.ts b/src/components/ha-sidebar-panel-config.ts similarity index 94% rename from src/components/ha-sidebar-config.ts rename to src/components/ha-sidebar-panel-config.ts index 802154f33fd2..0fe49d8d0306 100644 --- a/src/components/ha-sidebar-config.ts +++ b/src/components/ha-sidebar-panel-config.ts @@ -21,8 +21,8 @@ const styles = css` margin-left: auto; } `; -@customElement("ha-sidebar-config") -class HaSidebarConfig extends LitElement { +@customElement("ha-sidebar-panel-config") +class HaSidebarPanelConfig extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public name = ""; @@ -85,6 +85,6 @@ class HaSidebarConfig extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-sidebar-config": HaSidebarConfig; + "ha-sidebar-panel-config": HaSidebarPanelConfig; } } diff --git a/src/components/ha-sidebar-panel-ext-config.ts b/src/components/ha-sidebar-panel-ext-config.ts new file mode 100644 index 000000000000..52f2c090eeb1 --- /dev/null +++ b/src/components/ha-sidebar-panel-ext-config.ts @@ -0,0 +1,51 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCellphoneCog } from "@mdi/js"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { haStyleSidebarItem } from "../resources/styles"; +import "./ha-icon"; +import "./ha-svg-icon"; +import { HomeAssistant } from "../types"; + +const styles = css` + :host { + width: 100%; + } + .item { + width: 100%; + } +`; +@customElement("ha-sidebar-panel-ext-config") +class HaSidebarPanelExtConfig extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public name = ""; + + @property({ type: Boolean }) public expanded = false; + + static styles = [haStyleSidebarItem, styles]; + + protected render() { + return html``; + } + + private _showConfig() { + this.hass.auth.external!.fireMessage({ + type: "config_screen/show", + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sidebar-panel-ext-config": HaSidebarPanelExtConfig; + } +} diff --git a/src/components/ha-sidebar-notifications.ts b/src/components/ha-sidebar-panel-notifications.ts similarity index 91% rename from src/components/ha-sidebar-notifications.ts rename to src/components/ha-sidebar-panel-notifications.ts index 192fc779a7fb..f88ef8825dc0 100644 --- a/src/components/ha-sidebar-notifications.ts +++ b/src/components/ha-sidebar-panel-notifications.ts @@ -15,15 +15,15 @@ const styles = css` :host { width: 100%; } - .item.expanded { + .item { width: 100%; } .count { margin-left: auto; } `; -@customElement("ha-sidebar-notifications") -class HaSidebarNotifications extends LitElement { +@customElement("ha-sidebar-panel-notifications") +class HaSidebarPanelNotifications extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean }) public expanded = false; @@ -69,6 +69,6 @@ class HaSidebarNotifications extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-sidebar-notifications": HaSidebarNotifications; + "ha-sidebar-panel-notifications": HaSidebarPanelNotifications; } } diff --git a/src/components/ha-sidebar-user.ts b/src/components/ha-sidebar-panel-user.ts similarity index 90% rename from src/components/ha-sidebar-user.ts rename to src/components/ha-sidebar-panel-user.ts index cab17a026698..a931b8124eaf 100644 --- a/src/components/ha-sidebar-user.ts +++ b/src/components/ha-sidebar-panel-user.ts @@ -17,8 +17,8 @@ const styles = css` transform: translate(-50%, -50%) scale(0.8); } `; -@customElement("ha-sidebar-user") -class HaSidebarUser extends LitElement { +@customElement("ha-sidebar-panel-user") +class HaSidebarPanelUser extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean }) public expanded = false; @@ -49,6 +49,6 @@ class HaSidebarUser extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-sidebar-user": HaSidebarUser; + "ha-sidebar-panel-user": HaSidebarPanelUser; } } diff --git a/src/components/ha-sidebar-panels.ts b/src/components/ha-sidebar-panels.ts index f9151b4364ad..794ed05a311e 100644 --- a/src/components/ha-sidebar-panels.ts +++ b/src/components/ha-sidebar-panels.ts @@ -17,7 +17,8 @@ import { LocalStorage } from "../common/decorators/local-storage"; import { fireEvent } from "../common/dom/fire_event"; import { stringCompare } from "../common/string/compare"; import { HomeAssistant, PanelInfo } from "../types"; -import "./ha-sidebar-config"; +import "./ha-sidebar-panel-config"; +import "./ha-sidebar-panel-ext-config"; import "./ha-sidebar-panel"; import "./ha-sidebar-edit-panels"; @@ -169,7 +170,7 @@ class HaSidebarPanels extends LitElement { : undefined; const renderPanel = (panel: PanelInfo) => panel.url_path === "config" - ? html`` + >` : html``, ...afterSpacer.map(renderPanel), + ...(!this.hass.user?.is_admin && + this.hass.auth.external?.config.hasSettingsScreen + ? [ + html``, + ] + : []), ]; } diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 3fa9b1fbd402..fbf2762645c3 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -4,8 +4,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import type { HomeAssistant, Route } from "../types"; import "./ha-sidebar-title"; import "./ha-sidebar-panels"; -import "./ha-sidebar-notifications"; -import "./ha-sidebar-user"; +import "./ha-sidebar-panel-notifications"; +import "./ha-sidebar-panel-user"; import "./ha-sidebar-tooltip"; const styles = css` @@ -67,19 +67,19 @@ class HaSidebar extends SubscribeMixin(LitElement) { @panel-leave=${this._mouseLeave} >
- - + + > `; From d484b5f99cdb1e7701591167ef943de3d3d08857 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 2 Apr 2023 19:50:52 +0000 Subject: [PATCH 04/18] fix focusing the list of items --- src/components/ha-sidebar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index fbf2762645c3..5b57c861414f 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -57,7 +57,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { .expanded=${this.alwaysExpand} .editMode=${this.editMode} > -
+
Date: Mon, 3 Apr 2023 13:44:08 +0000 Subject: [PATCH 05/18] fix rtl (the old sidebar had a rtl bug, fun fact) --- src/components/ha-sidebar-panel-config.ts | 2 +- src/components/ha-sidebar-panel-notifications.ts | 2 +- src/components/ha-sidebar-title.ts | 14 +++++++++++--- src/resources/styles.ts | 5 +++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/ha-sidebar-panel-config.ts b/src/components/ha-sidebar-panel-config.ts index 0fe49d8d0306..2f8b1630b930 100644 --- a/src/components/ha-sidebar-panel-config.ts +++ b/src/components/ha-sidebar-panel-config.ts @@ -18,7 +18,7 @@ const styles = css` width: 100%; } .count { - margin-left: auto; + margin-inline-start: auto; } `; @customElement("ha-sidebar-panel-config") diff --git a/src/components/ha-sidebar-panel-notifications.ts b/src/components/ha-sidebar-panel-notifications.ts index f88ef8825dc0..40305660b343 100644 --- a/src/components/ha-sidebar-panel-notifications.ts +++ b/src/components/ha-sidebar-panel-notifications.ts @@ -19,7 +19,7 @@ const styles = css` width: 100%; } .count { - margin-left: auto; + margin-inline-start: auto; } `; @customElement("ha-sidebar-panel-notifications") diff --git a/src/components/ha-sidebar-title.ts b/src/components/ha-sidebar-title.ts index 8f5a495ee67f..893493a6350c 100644 --- a/src/components/ha-sidebar-title.ts +++ b/src/components/ha-sidebar-title.ts @@ -1,9 +1,10 @@ import "@material/mwc-button/mwc-button"; import { mdiMenu, mdiMenuOpen } from "@mdi/js"; -import { css, html, LitElement } from "lit"; +import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../common/dom/fire_event"; +import { computeRTL } from "../common/util/compute_rtl"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { HomeAssistant } from "../types"; import "./ha-icon-button"; @@ -28,6 +29,9 @@ const styles = css` margin: 0 auto; color: var(--sidebar-icon-color); } + .menu.rtl ha-icon-button { + transform: scaleX(-1); + } .menu mwc-button { width: 100%; } @@ -35,7 +39,7 @@ const styles = css` padding: calc((var(--header-height) - 20px) / 5) 12px 0 12px; } .menu.expanded ha-icon-button { - margin-right: 0; + margin-inline-end: 0; } `; @customElement("ha-sidebar-title") @@ -51,7 +55,11 @@ class HaSidebarTitle extends LitElement { static styles = styles; protected render() { - const classes = classMap({ menu: true, expanded: this.expanded }); + const classes = classMap({ + menu: true, + expanded: this.expanded, + rtl: computeRTL(this.hass), + }); const saveEdits = html` ${this.hass.localize("ui.sidebar.done")} `; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 3ba594b53313..75ee4adc079f 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -381,11 +381,12 @@ export const haStyleSidebarItem = css` } .item.expanded { margin: 0; - padding: 0 24px 0 16px; + padding-inline-start: 16px; + padding-inline-end: 24px; justify-content: initial; } .item.expanded .icon { - margin-right: 12px; + margin-inline-end: 12px; } .item.expanded .name { display: initial; From 45a8797c024d2f83b86aeccedaeead459da8819e Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 3 Jun 2023 01:47:12 +0000 Subject: [PATCH 06/18] Fix accessibility and other bugs - fix currentpanel logic - disable native outline, indicate focus on active page - prevent focus on container - space works to activate an item - typing and up/down works to focus an item --- src/components/ha-sidebar-panel-config.ts | 4 ++ src/components/ha-sidebar-panel-ext-config.ts | 1 + src/components/ha-sidebar-panel-user.ts | 3 + src/components/ha-sidebar-panel.ts | 4 ++ src/components/ha-sidebar-panels.ts | 62 +++++++++++++++++++ src/components/ha-sidebar.ts | 16 ++--- src/resources/button-handlers.ts | 12 ++++ src/resources/styles.ts | 6 ++ 8 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 src/resources/button-handlers.ts diff --git a/src/components/ha-sidebar-panel-config.ts b/src/components/ha-sidebar-panel-config.ts index 2f8b1630b930..372c9267da2e 100644 --- a/src/components/ha-sidebar-panel-config.ts +++ b/src/components/ha-sidebar-panel-config.ts @@ -9,6 +9,7 @@ import { updateCanInstall, UpdateEntity } from "../data/update"; import { haStyleSidebarItem } from "../resources/styles"; import { HomeAssistant } from "../types"; import "./ha-svg-icon"; +import { keydown, keyup } from "../resources/button-handlers"; const styles = css` :host { @@ -41,8 +42,11 @@ class HaSidebarPanelConfig extends LitElement { const notices = this._updatesCount + this._issuesCount; return html` (e.currentTarget as HTMLElement).click())} + @keyup=${keyup((e) => (e.currentTarget as HTMLElement).click())} > diff --git a/src/components/ha-sidebar-panel-ext-config.ts b/src/components/ha-sidebar-panel-ext-config.ts index 52f2c090eeb1..c73e60ed4475 100644 --- a/src/components/ha-sidebar-panel-ext-config.ts +++ b/src/components/ha-sidebar-panel-ext-config.ts @@ -28,6 +28,7 @@ class HaSidebarPanelExtConfig extends LitElement { protected render() { return html`