From 24f334bfe27c92ab0827e1a542d47fa9b27a8f4a Mon Sep 17 00:00:00 2001 From: Donnie Date: Sat, 24 Oct 2020 19:29:34 -0700 Subject: [PATCH] Refactor sidebar to use mwc-list --- src/components/ha-clickable-list-item.ts | 52 ++++ src/components/ha-clickable-ripple.ts | 70 +++++ src/components/ha-sidebar.ts | 291 +++++++++++------- .../ha-sortable-style-ha-clickable.ts | 99 ++++++ src/resources/ha-sortable-style-mwc.ts | 99 ++++++ 5 files changed, 495 insertions(+), 116 deletions(-) create mode 100644 src/components/ha-clickable-list-item.ts create mode 100644 src/components/ha-clickable-ripple.ts create mode 100644 src/resources/ha-sortable-style-ha-clickable.ts create mode 100644 src/resources/ha-sortable-style-mwc.ts diff --git a/src/components/ha-clickable-list-item.ts b/src/components/ha-clickable-list-item.ts new file mode 100644 index 0000000000..fee471c8f4 --- /dev/null +++ b/src/components/ha-clickable-list-item.ts @@ -0,0 +1,52 @@ +import "./ha-clickable-ripple"; +import { ListItem } from "@material/mwc-list/mwc-list-item"; +// import { ListItemBase } from "@material/mwc-list/mwc-list-item-base"; +import { css, CSSResult, customElement } from "lit-element"; +import { html } from "lit-html"; + +@customElement("ha-clickable-list-item") +export class HaClickableListItem extends ListItem { + public href?: string; + + public render() { + const r = super.render(); + const href = this.href ? `/${this.href}` : ""; + + return html` ${this.renderRipple()} + + ${r} + `; + } + + static get styles(): CSSResult[] { + return [ + super.styles, + css` + :host { + padding-left: 0px; + padding-right: 0px; + } + + :host([graphic="avatar"]:not([twoLine])), + :host([graphic="icon"]:not([twoLine])) { + height: 48px; + } + + a { + width: 100%; + height: 100%; + display: flex; + align-items: center; + padding-left: var(--mdc-list-side-padding, 16px); + padding-right: var(--mdc-list-side-padding, 16px); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-clickable-list-item": HaClickableListItem; + } +} diff --git a/src/components/ha-clickable-ripple.ts b/src/components/ha-clickable-ripple.ts new file mode 100644 index 0000000000..e33d6c106a --- /dev/null +++ b/src/components/ha-clickable-ripple.ts @@ -0,0 +1,70 @@ +import { ListItem } from "@material/mwc-list/mwc-list-item"; +import { Ripple } from "@material/mwc-ripple"; +import { RippleBase } from "@material/mwc-ripple/mwc-ripple-base"; +// import { ListItemBase } from "@material/mwc-list/mwc-list-item-base"; +import { css, CSSResult, customElement } from "lit-element"; +import { html } from "lit-html"; + +@customElement("ha-clickable-ripple") +export class HaClickableRipple extends RippleBase { + public href?: string; + + // public render() { + // const r = super.render(); + // // prettier-ignore + // return html` + // + // ${r} + // `; + // } + + public render() { + /** @classMap */ + const classes = { + "mdc-ripple-upgraded--unbounded": this.unbounded, + "mdc-ripple-upgraded--background-focused": this.bgFocused, + "mdc-ripple-upgraded--foreground-activation": super.fgActivation, + "mdc-ripple-upgraded--foreground-deactivation": super.fgDeactivation, + hover: super.hovering, + primary: this.primary, + accent: this.accent, + disabled: this.disabled, + activated: this.activated, + selected: this.selected, + }; + return html`
`; + } + + static get styles(): CSSResult[] { + return [ + super.styles, + css` + /* a { + width: 100%; + height: 100%; + border: 1px solid black; + vertical-align: middle; + } */ + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-clickable-ripple": HaClickableRipple; + } +} diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 83502db730..652da7a534 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -1,3 +1,6 @@ +import "./ha-clickable-list-item"; +import "@material/mwc-list/mwc-list-item"; +import "@material/mwc-list/mwc-list"; import "@material/mwc-button/mwc-button"; import "@material/mwc-icon-button"; import { @@ -9,10 +12,6 @@ import { mdiPlus, 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 { css, CSSResult, @@ -210,10 +209,10 @@ class HaSidebar extends LitElement { return html` ${this._renderHeader()} ${this._renderAllPanels()} - ${this._renderDivider()} + ${this._renderUtilityPanels()} ${this._renderNotifications()} ${this._renderUserItem()} -
+ ${this._renderSpacer()}
`; } @@ -338,10 +337,9 @@ class HaSidebar extends LitElement { // prettier-ignore return html` - + `; } @@ -375,13 +373,14 @@ class HaSidebar extends LitElement { if (!panel) { return ""; } - return html` - `; + `; })} ${this._renderSpacer()}` : ""}`; } - private _renderDivider() { - return html`
`; - } - private _renderSpacer() { - return html`
`; + return html``; } private _renderNotifications() { @@ -419,20 +414,19 @@ class HaSidebar extends LitElement { } } - return html`
- - + ${!this.expanded && notificationCount > 0 ? html` - + ${notificationCount} ` @@ -441,45 +435,48 @@ class HaSidebar extends LitElement { ${this.hass.localize("ui.notification_drawer.title")} ${this.expanded && notificationCount > 0 - ? html` ${notificationCount} ` + ? html` + ${notificationCount} + ` : ""} - -
`; + + `; } private _renderUserItem() { - return html` - - + - - ${this.hass.user ? this.hass.user.name : ""} - - - `; + + ${this.hass.user ? this.hass.user.name : ""} + + `; } private _renderExternalConfiguration() { return html`${this._externalConfig && this._externalConfig.hasSettingsScreen ? html` - - - - - ${this.hass.localize("ui.sidebar.external_app_configuration")} - - - + + + ${this.hass.localize("ui.sidebar.external_app_configuration")} + + ` : ""}`; } @@ -520,7 +513,7 @@ class HaSidebar extends LitElement { if (!Sortable) { const [sortableImport, sortStylesImport] = await Promise.all([ import("sortablejs/modular/sortable.core.esm"), - import("../resources/ha-sortable-style"), + import("../resources/ha-sortable-style-ha-clickable"), ]); const style = document.createElement("style"); @@ -541,8 +534,9 @@ class HaSidebar extends LitElement { this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), { animation: 150, fallbackClass: "sortable-fallback", + // fallbackTolerance: 15, dataIdAttr: "data-panel", - handle: "paper-icon-item", + handle: "ha-clickable-list-item", onSort: async () => { this._panelOrder = this._sortable.toArray(); }, @@ -604,7 +598,7 @@ class HaSidebar extends LitElement { clearTimeout(this._mouseLeaveTimeout); this._mouseLeaveTimeout = undefined; } - this._showTooltip(ev.currentTarget as PaperIconItemElement); + this._showTooltip(ev.currentTarget); } private _itemMouseLeave() { @@ -620,7 +614,7 @@ class HaSidebar extends LitElement { if (this.expanded || ev.target.nodeName !== "A") { return; } - this._showTooltip(ev.target.querySelector("paper-icon-item")); + this._showTooltip(ev.target.querySelector("ha-clickable-list-item")); } private _listboxFocusOut() { @@ -644,13 +638,13 @@ class HaSidebar extends LitElement { this._recentKeydownActiveUntil = new Date().getTime() + 100; } - private _showTooltip(item: PaperIconItemElement) { + private _showTooltip(item) { if (this._tooltipHideTimeout) { clearTimeout(this._tooltipHideTimeout); this._tooltipHideTimeout = undefined; } const tooltip = this._tooltip; - const listbox = this.shadowRoot!.querySelector("paper-listbox")!; + const listbox = this.shadowRoot!.querySelector("mwc-list")!; let top = item.offsetTop + 11; if (listbox.contains(item)) { top -= listbox.scrollTop; @@ -689,21 +683,77 @@ class HaSidebar extends LitElement { fireEvent(this, "hass-toggle-menu"); } - 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 - ? mdiViewDashboard - : undefined - ) + private _isUtilityPanel(panel: PanelInfo) { + return ( + panel.component_name === "developer-tools" || + panel.component_name === "config" || + panel.component_name === "external-config" ); } + 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 _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 + ) + )} + ${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 + ) + )} + + + `; + } + private _renderPanel( urlPath: string, title: string | null, @@ -711,23 +761,21 @@ class HaSidebar extends LitElement { iconPath?: string | null ) { return html` - - - ${iconPath - ? html`` - : html``} - ${title} - + ${iconPath + ? html`` + : html``} + + ${title} + ${this.editMode ? html` ` : ""} - + `; } @@ -806,7 +854,6 @@ class HaSidebar extends LitElement { } :host([narrow]) .title { margin: 0; - padding: 0 16px; } :host([expanded]) .title { display: initial; @@ -822,21 +869,22 @@ class HaSidebar extends LitElement { display: none; } - paper-listbox { + mwc-list.ha-scrollbar { + --mdc-list-vertical-padding: 4px 0; 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) - 350px); height: calc( - 100% - var(--header-height) - 132px - env(safe-area-inset-bottom) + 100% - var(--header-height) - 350px - env(safe-area-inset-bottom) ); overflow-x: hidden; background: none; margin-left: env(safe-area-inset-left); } - :host([rtl]) paper-listbox { + :host([rtl]) mwc-list { margin-left: initial; margin-right: env(safe-area-inset-right); } @@ -847,32 +895,36 @@ class HaSidebar extends LitElement { font-weight: 500; font-size: 14px; position: relative; - display: block; outline: 0; + border: 1px solid black; } - paper-icon-item { + mwc-list-item { box-sizing: border-box; margin: 4px; - padding-left: 12px; border-radius: 4px; - --paper-item-min-height: 40px; width: 48px; + --mdc-list-item-graphic-margin: 16px; + --mdc-list-item-meta-size: 32px; } :host([expanded]) paper-icon-item { width: 248px; } - :host([rtl]) paper-icon-item { + :host([rtl]) mwc-list-item { padding-left: auto; padding-right: 12px; } - ha-icon[slot="item-icon"], - ha-svg-icon[slot="item-icon"] { + ha-icon[slot="graphic"], + ha-svg-icon[slot="graphic"] { color: var(--sidebar-icon-color); } - .iron-selected paper-icon-item::before, + [slot="graphic"] { + width: 100%; + } + + .iron-selected mwc-list-item::before, a:not(.iron-selected):focus::before { border-radius: 4px; position: absolute; @@ -885,7 +937,7 @@ class HaSidebar extends LitElement { transition: opacity 15ms linear; will-change: opacity; } - .iron-selected paper-icon-item::before { + .iron-selected mwc-list-item::before { background-color: var(--sidebar-selected-icon-color); opacity: 0.12; } @@ -894,23 +946,24 @@ class HaSidebar extends LitElement { opacity: var(--dark-divider-opacity); margin: 4px 8px; } - .iron-selected paper-icon-item:focus::before, - .iron-selected:focus paper-icon-item::before { + .iron-selected mwc-list-item:focus::before, + .iron-selected:focus mwc-list-item::before { opacity: 0.2; } - .iron-selected paper-icon-item[pressed]:before { + .iron-selected mwc-list-item[pressed]:before { opacity: 0.37; } - paper-icon-item span { + mwc-list-item span { color: var(--sidebar-text-color); font-weight: 500; font-size: 14px; + width: 100%; } - a.iron-selected paper-icon-item ha-icon, - a.iron-selected paper-icon-item 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); } @@ -918,22 +971,21 @@ class HaSidebar extends LitElement { color: var(--sidebar-selected-text-color); } - paper-icon-item .item-text { + mwc-list-item mwc-list-item .item-text, + mwc-list-item .item-text { display: none; max-width: calc(100% - 56px); } - :host([expanded]) paper-icon-item .item-text { + :host([expanded]) mwc-list-item .item-text { display: block; } .divider { - bottom: 112px; padding: 10px 0; } .divider::before { - content: " "; display: block; - height: 1px; + height: 100px; background-color: var(--divider-color); } .notifications-container { @@ -957,10 +1009,10 @@ class HaSidebar extends LitElement { margin-left: initial; margin-right: env(safe-area-inset-right); } - .profile paper-icon-item { + .profile mwc-list-item { padding-left: 4px; } - :host([rtl]) .profile paper-icon-item { + :host([rtl]) .profile mwc-list-item { padding-left: auto; padding-right: 4px; } @@ -972,15 +1024,16 @@ class HaSidebar extends LitElement { } .notification-badge { - min-width: 20px; + /* min-width: 20px; box-sizing: border-box; border-radius: 50%; font-weight: 400; background-color: var(--accent-color); - line-height: 20px; + line-height: 30px; text-align: center; - padding: 0px 6px; + padding: 0px 4px; color: var(--text-accent-color, var(--text-primary-color)); + font-size: 14px; */ } ha-svg-icon + .notification-badge { position: absolute; @@ -994,6 +1047,12 @@ class HaSidebar extends LitElement { pointer-events: none; } + .bottom-spacer { + flex: 1; + pointer-events: none; + height: 100%; + } + .subheader { color: var(--sidebar-text-color); font-weight: 500; diff --git a/src/resources/ha-sortable-style-ha-clickable.ts b/src/resources/ha-sortable-style-ha-clickable.ts new file mode 100644 index 0000000000..c3529c9030 --- /dev/null +++ b/src/resources/ha-sortable-style-ha-clickable.ts @@ -0,0 +1,99 @@ +import { css } from "lit-element"; + +export const sortableStyles = css` + #sortable ha-clickable-list-item:nth-of-type(2n) { + animation-name: keyframes1; + animation-iteration-count: infinite; + transform-origin: 50% 10%; + animation-delay: -0.75s; + animation-duration: 0.25s; + } + + #sortable ha-clickable-list-item:nth-of-type(2n-1) { + animation-name: keyframes2; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + animation-delay: -0.5s; + animation-duration: 0.33s; + } + + #sortable a { + height: 48px; + display: flex; + } + + #sortable { + outline: none; + display: block !important; + } + + .hidden-panel { + display: flex !important; + } + + .sortable-fallback { + display: none; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-fallback { + opacity: 0; + } + + @keyframes keyframes1 { + 0% { + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } + } + + @keyframes keyframes2 { + 0% { + transform: rotate(1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } + } + + .show-panel, + .hide-panel { + display: none; + position: absolute; + top: 0; + right: 0; + --mdc-icon-button-size: 40px; + } + + .hide-panel { + top: 4px; + right: 8px; + } + + :host([expanded]) .hide-panel { + display: block; + } + + :host([expanded]) .show-panel { + display: inline-flex; + } + + ha-clickable-list-item.hidden-panel, + ha-clickable-list-item.hidden-panel span, + ha-clickable-list-item.hidden-panel ha-icon[slot="item-icon"] { + color: var(--secondary-text-color); + cursor: pointer; + } +`; diff --git a/src/resources/ha-sortable-style-mwc.ts b/src/resources/ha-sortable-style-mwc.ts new file mode 100644 index 0000000000..f4f92a57d5 --- /dev/null +++ b/src/resources/ha-sortable-style-mwc.ts @@ -0,0 +1,99 @@ +import { css } from "lit-element"; + +export const sortableStyles = css` + #sortable mwc-list-item:nth-of-type(2n) { + animation-name: keyframes1; + animation-iteration-count: infinite; + transform-origin: 50% 10%; + animation-delay: -0.75s; + animation-duration: 0.25s; + } + + #sortable mwc-list-item:nth-of-type(2n-1) { + animation-name: keyframes2; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + animation-delay: -0.5s; + animation-duration: 0.33s; + } + + #sortable mwc-list-item { + height: 48px; + display: flex; + } + + #sortable { + outline: none; + display: block !important; + } + + .hidden-panel { + display: flex !important; + } + + .sortable-fallback { + display: none; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-fallback { + opacity: 0; + } + + @keyframes keyframes1 { + 0% { + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } + } + + @keyframes keyframes2 { + 0% { + transform: rotate(1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } + } + + .show-panel, + .hide-panel { + display: none; + position: absolute; + top: 0; + right: 0; + --mdc-icon-button-size: 40px; + } + + .hide-panel { + top: 4px; + right: 8px; + } + + :host([expanded]) .hide-panel { + display: block; + } + + :host([expanded]) .show-panel { + display: inline-flex; + } + + mwc-list-item.hidden-panel, + mwc-list-item.hidden-panel span, + mwc-list-item.hidden-panel ha-icon[slot="item-icon"] { + color: var(--secondary-text-color); + cursor: pointer; + } +`;