diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts new file mode 100644 index 0000000000..a275e3e4f5 --- /dev/null +++ b/src/components/ha-control-select-menu.ts @@ -0,0 +1,255 @@ +import { Ripple } from "@material/mwc-ripple"; +import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; +import { SelectBase } from "@material/mwc-select/mwc-select-base"; +import { css, html, nothing } from "lit"; +import { + customElement, + eventOptions, + query, + queryAsync, + state, +} from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { ifDefined } from "lit/directives/if-defined"; +import { debounce } from "../common/util/debounce"; +import { nextRender } from "../common/util/render-status"; + +@customElement("ha-control-select-menu") +export class HaControlSelectMenu extends SelectBase { + @query(".select") protected mdcRoot!: HTMLElement; + + @query(".select-anchor") protected anchorElement!: HTMLDivElement | null; + + @queryAsync("mwc-ripple") private _ripple!: Promise; + + @state() private _shouldRenderRipple = false; + + public override render() { + const classes = { + "select-disabled": this.disabled, + "select-required": this.required, + "select-invalid": !this.isUiValid, + "select-no-value": !this.selectedText, + }; + + const labelledby = this.label ? "label" : undefined; + + return html` +
+ + +
+
+ +
+
+

${this.label}

+ ${this.selectedText + ? html`

${this.selectedText}

` + : nothing} +
+ ${this._shouldRenderRipple && !this.disabled + ? html` ` + : nothing} +
+ ${this.renderMenu()} +
+ `; + } + + protected onFocus() { + this.handleRippleFocus(); + super.onFocus(); + } + + protected onBlur() { + this.handleRippleBlur(); + super.onBlur(); + } + + private _rippleHandlers: RippleHandlers = new RippleHandlers(() => { + this._shouldRenderRipple = true; + return this._ripple; + }); + + @eventOptions({ passive: true }) + private handleRippleActivate(evt?: Event) { + this._rippleHandlers.startPress(evt); + } + + private handleRippleDeactivate() { + this._rippleHandlers.endPress(); + } + + private handleRippleMouseEnter() { + this._rippleHandlers.startHover(); + } + + private handleRippleMouseLeave() { + this._rippleHandlers.endHover(); + } + + private handleRippleFocus() { + this._rippleHandlers.startFocus(); + } + + private handleRippleBlur() { + this._rippleHandlers.endFocus(); + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("translations-updated", this._translationsUpdated); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener( + "translations-updated", + this._translationsUpdated + ); + } + + private _translationsUpdated = debounce(async () => { + await nextRender(); + this.layoutOptions(); + }, 500); + + static override styles = [ + css` + :host { + display: inline-block; + --control-select-menu-text-color: var(--primary-text-color); + --control-select-menu-background-color: var(--disabled-color); + --control-select-menu-background-opacity: 0.2; + --control-select-menu-border-radius: 16px; + --control-select-menu-min-width: 120px; + --control-select-menu-max-width: 200px; + --control-select-menu-width: 100%; + --mdc-icon-size: 24px; + color: var(--primary-text-color); + -webkit-tap-highlight-color: transparent; + } + .select-anchor { + color: var(--control-select-menu-text-color); + height: 56px; + padding: 8px 12px; + overflow: hidden; + position: relative; + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + border-radius: var(--control-select-menu-border-radius); + box-sizing: border-box; + outline: none; + overflow: hidden; + background: none; + --mdc-ripple-color: var(--control-select-menu-background-color); + /* For safari border-radius overflow */ + z-index: 0; + font-size: inherit; + transition: color 180ms ease-in-out; + color: var(--control-text-icon-color); + gap: 12px; + min-width: var(--control-select-menu-min-width); + max-width: var(--control-select-menu-max-width); + width: var(--control-select-menu-width); + user-select: none; + } + .content { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + flex: 1; + overflow: hidden; + } + + .content p { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 0; + width: 100%; + margin: auto; + } + + .label { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + } + + .select-no-value .label { + font-size: 16px; + line-height: 24px; + letter-spacing: 0.5px; + } + + .value { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.5px; + } + + .select-anchor::before { + content: ""; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: var(--control-select-menu-background-color); + transition: + background-color 180ms ease-in-out, + opacity 180ms ease-in-out; + opacity: var(--control-select-menu-background-opacity); + } + + mwc-menu { + --mdc-shape-medium: 8px; + } + mwc-list { + --mdc-list-vertical-padding: 0; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-control-select-menu": HaControlSelectMenu; + } +} diff --git a/src/dialogs/more-info/components/ha-more-info-control-style.ts b/src/dialogs/more-info/components/ha-more-info-control-style.ts index 19d8ae47a2..fb18016a28 100644 --- a/src/dialogs/more-info/components/ha-more-info-control-style.ts +++ b/src/dialogs/more-info/components/ha-more-info-control-style.ts @@ -22,6 +22,35 @@ export const moreInfoControlStyle = css` margin-bottom: 24px; } + .secondary-controls { + display: flex; + flex-direction: row; + justify-content: center; + } + .secondary-controls-scroll { + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 12px; + margin: auto; + overflow: auto; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + margin: 0 -24px; + padding: 0 24px; + } + .secondary-controls-scroll::-webkit-scrollbar { + display: none; + } + + /* Don't use scroll on device without touch support */ + @media (hover: hover) { + .secondary-controls-scroll { + justify-content: center; + flex-wrap: wrap; + } + } + .buttons { display: flex; align-items: center; diff --git a/src/dialogs/more-info/controls/more-info-humidifier.ts b/src/dialogs/more-info/controls/more-info-humidifier.ts index d3b8a14e54..cfea49e2b4 100644 --- a/src/dialogs/more-info/controls/more-info-humidifier.ts +++ b/src/dialogs/more-info/controls/more-info-humidifier.ts @@ -1,3 +1,4 @@ +import { mdiCircleMedium, mdiPower, mdiTuneVariant } from "@mdi/js"; import { CSSResultGroup, LitElement, @@ -7,7 +8,6 @@ import { nothing, } from "lit"; import { property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { @@ -18,7 +18,9 @@ import { computeStateDisplay } from "../../../common/entity/compute_state_displa import { supportsFeature } from "../../../common/entity/supports-feature"; import { formatNumber } from "../../../common/number/format_number"; import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; +import "../../../components/ha-control-select-menu"; import { + HUMIDIFIER_MODE_ICONS, HumidifierEntity, HumidifierEntityFeature, } from "../../../data/humidifier"; @@ -58,100 +60,111 @@ class MoreInfoHumidifier extends LitElement { const currentHumidity = this.stateObj.attributes.current_humidity; return html` - ${currentHumidity - ? html`
- ${currentHumidity != null - ? html` -
-

- ${computeAttributeNameDisplay( - this.hass.localize, - this.stateObj, - this.hass.entities, - "current_humidity" - )} -

-

- ${formatNumber( - currentHumidity, - this.hass.locale - )}${blankBeforePercent(this.hass.locale)}% -

-
- ` - : nothing} -
` - : nothing} +
+ ${currentHumidity != null + ? html` +
+

+ ${computeAttributeNameDisplay( + this.hass.localize, + this.stateObj, + this.hass.entities, + "current_humidity" + )} +

+

+ ${formatNumber( + currentHumidity, + this.hass.locale + )}${blankBeforePercent(this.hass.locale)}% +

+
+ ` + : nothing} +
+
-
- - - ${computeStateDisplay( - this.hass.localize, - this.stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities, - "off" - )} - - - ${computeStateDisplay( - this.hass.localize, - this.stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities, - "on" - )} - - - ${supportModes - ? html` - - ${stateObj.attributes.available_modes!.map( - (mode) => html` - - ${computeAttributeValueDisplay( - hass.localize, - stateObj!, - hass.locale, - hass.config, - hass.entities, - "mode", - mode - )} - - ` - )} - - ` - : nothing} +
+
+ + + + ${computeStateDisplay( + this.hass.localize, + this.stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities, + "off" + )} + + + ${computeStateDisplay( + this.hass.localize, + this.stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities, + "on" + )} + + + + ${supportModes + ? html` + + + ${stateObj.attributes.available_modes!.map( + (mode) => html` + + + ${computeAttributeValueDisplay( + hass.localize, + stateObj!, + hass.locale, + hass.config, + hass.entities, + "mode", + mode + )} + + ` + )} + + ` + : nothing} +
`; } @@ -243,7 +256,6 @@ class MoreInfoHumidifier extends LitElement { :host { color: var(--primary-text-color); } - .current { display: flex; flex-direction: row; @@ -277,11 +289,6 @@ class MoreInfoHumidifier extends LitElement { font-weight: 500; line-height: 28px; } - - ha-select { - width: 100%; - margin-top: 8px; - } `, ]; }