Use Material 3 ripple (#20751)

* Use material web ripple component

* Improve button style

* Use css animation instead of ripple for action

* Use ha ripple in all components

* Remove unused label
This commit is contained in:
Paul Bottein 2024-05-07 15:30:45 +02:00 committed by GitHub
parent 505d7b6ddb
commit 4a77359a06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 145 additions and 409 deletions

View File

@ -70,7 +70,6 @@
"@material/mwc-list": "0.27.0", "@material/mwc-list": "0.27.0",
"@material/mwc-menu": "0.27.0", "@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0", "@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0", "@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0", "@material/mwc-snackbar": "0.27.0",
"@material/mwc-switch": "0.27.0", "@material/mwc-switch": "0.27.0",

View File

@ -1,14 +1,7 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-control-button") @customElement("ha-control-button")
export class HaControlButton extends LitElement { export class HaControlButton extends LitElement {
@ -16,10 +9,6 @@ export class HaControlButton extends LitElement {
@property() public label?: string; @property() public label?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<button <button
@ -28,54 +17,13 @@ export class HaControlButton extends LitElement {
aria-label=${ifDefined(this.label)} aria-label=${ifDefined(this.label)}
title=${ifDefined(this.label)} title=${ifDefined(this.label)}
.disabled=${Boolean(this.disabled)} .disabled=${Boolean(this.disabled)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
> >
<slot></slot> <slot></slot>
${this._shouldRenderRipple && !this.disabled <ha-ripple .disabled=${this.disabled}></ha-ripple>
? html`<mwc-ripple></mwc-ripple>`
: ""}
</button> </button>
`; `;
} }
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();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
@ -86,6 +34,7 @@ export class HaControlButton extends LitElement {
--control-button-border-radius: 10px; --control-button-border-radius: 10px;
--control-button-padding: 8px; --control-button-padding: 8px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
color: var(--primary-text-color); color: var(--primary-text-color);
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -113,12 +62,14 @@ export class HaControlButton extends LitElement {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--control-button-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
} }
.button:focus-visible {
--control-button-background-opacity: 0.4;
}
.button::before { .button::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@ -1,22 +1,14 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js"; import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { import { customElement, property, query } from "lit/decorators";
customElement,
eventOptions,
property,
query,
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import "./ha-icon"; import "./ha-icon";
import type { HaIcon } from "./ha-icon"; import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon"; import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon"; import type { HaSvgIcon } from "./ha-svg-icon";
@ -32,10 +24,6 @@ export class HaControlSelectMenu extends SelectBase {
@property({ type: Boolean, attribute: "hide-label" }) @property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false; public hideLabel = false;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
public override render() { public override render() {
const classes = { const classes = {
"select-disabled": this.disabled, "select-disabled": this.disabled,
@ -69,17 +57,10 @@ export class HaControlSelectMenu extends SelectBase {
aria-labelledby=${ifDefined(labelledby)} aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)} aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required} aria-required=${this.required}
@click=${this.onClick}
@focus=${this.onFocus} @focus=${this.onFocus}
@blur=${this.onBlur} @blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown} @keydown=${this.onKeydown}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
> >
${this.renderIcon()} ${this.renderIcon()}
<div class="content"> <div class="content">
@ -91,9 +72,7 @@ export class HaControlSelectMenu extends SelectBase {
: nothing} : nothing}
</div> </div>
${this.renderArrow()} ${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled <ha-ripple .disabled=${this.disabled}></ha-ripple>
? html` <mwc-ripple></mwc-ripple> `
: nothing}
</div> </div>
${this.renderMenu()} ${this.renderMenu()}
</div> </div>
@ -135,46 +114,6 @@ export class HaControlSelectMenu extends SelectBase {
`; `;
} }
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() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated); window.addEventListener("translations-updated", this._translationsUpdated);
@ -204,6 +143,7 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-height: 48px; --control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px; --control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
width: auto; width: auto;
@ -224,7 +164,6 @@ export class HaControlSelectMenu extends SelectBase {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
transition: color 180ms ease-in-out; transition: color 180ms ease-in-out;
@ -264,6 +203,10 @@ export class HaControlSelectMenu extends SelectBase {
letter-spacing: inherit; letter-spacing: inherit;
} }
.select-anchor:focus-visible {
--control-select-menu-background-opacity: 0.4;
}
.select-anchor::before { .select-anchor::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@ -1,6 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "@material/web/ripple/ripple";
@customElement("ha-label") @customElement("ha-label")
class HaLabel extends LitElement { class HaLabel extends LitElement {
@ -11,7 +10,6 @@ class HaLabel extends LitElement {
<span class="content"> <span class="content">
<slot name="icon"></slot> <slot name="icon"></slot>
<slot></slot> <slot></slot>
<md-ripple></md-ripple>
</span> </span>
`; `;
} }
@ -27,7 +25,6 @@ class HaLabel extends LitElement {
0.15 0.15
); );
--ha-label-background-opacity: 1; --ha-label-background-opacity: 1;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
display: inline-flex; display: inline-flex;

View File

@ -0,0 +1,63 @@
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
import { MdRipple } from "@material/web/ripple/ripple";
import "element-internals-polyfill";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-ripple")
export class HaRipple extends MdRipple {
private readonly attachableTouchController = new AttachableController(
this,
this.onTouchControlChange.bind(this)
);
attach(control: HTMLElement) {
super.attach(control);
this.attachableTouchController.attach(control);
}
detach() {
super.detach();
this.attachableTouchController.detach();
}
private _handleTouchEnd = () => {
if (!this.disabled) {
// @ts-ignore
super.endPressAnimation();
}
};
private onTouchControlChange(
prev: HTMLElement | null,
next: HTMLElement | null
) {
// Add touchend event to clean ripple on touch devices using action handler
prev?.removeEventListener("touchend", this._handleTouchEnd);
next?.addEventListener("touchend", this._handleTouchEnd);
}
static override styles = [
...super.styles,
css`
:host {
--md-ripple-hover-opacity: var(--ha-ripple-hover-opacity, 0.08);
--md-ripple-pressed-opacity: var(--ha-ripple-pressed-opacity, 0.12);
--md-ripple-hover-color: var(
--ha-ripple-hover-color,
var(--ha-ripple-color, var(--secondary-text-color))
);
--md-ripple-pressed-color: var(
--ha-ripple-pressed-color,
var(--ha-ripple-color, var(--secondary-text-color))
);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-ripple": HaRipple;
}
}

View File

@ -1,15 +1,7 @@
import type { Ripple } from "@material/mwc-ripple";
import "@material/mwc-ripple/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-tab") @customElement("ha-tab")
export class HaTab extends LitElement { export class HaTab extends LitElement {
@ -19,10 +11,6 @@ export class HaTab extends LitElement {
@property() public name?: string; @property() public name?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div <div
@ -30,60 +18,21 @@ export class HaTab extends LitElement {
role="tab" role="tab"
aria-selected=${this.active} aria-selected=${this.active}
aria-label=${ifDefined(this.name)} aria-label=${ifDefined(this.name)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
> >
${this.narrow ? html`<slot name="icon"></slot>` : ""} ${this.narrow ? html`<slot name="icon"></slot>` : ""}
<span class="name">${this.name}</span> <span class="name">${this.name}</span>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""} <ha-ripple></ha-ripple>
</div> </div>
`; `;
} }
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private _handleKeyDown(ev: KeyboardEvent): void { private _handleKeyDown(ev: KeyboardEvent): void {
if (ev.key === "Enter") { if (ev.key === "Enter") {
(ev.target as HTMLElement).click(); (ev.target as HTMLElement).click();
} }
} }
@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();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
div { div {
@ -126,6 +75,15 @@ export class HaTab extends LitElement {
:host([narrow]) div { :host([narrow]) div {
padding: 0 4px; padding: 0 4px;
} }
div:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
`; `;
} }
} }

View File

@ -1,4 +1,3 @@
import "@material/mwc-ripple";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,

View File

@ -1,15 +1,7 @@
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { import { customElement, property } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import "../components/ha-card"; import "../components/ha-card";
import "../components/ha-ripple";
import "../components/ha-svg-icon"; import "../components/ha-svg-icon";
@customElement("onboarding-welcome-link") @customElement("onboarding-welcome-link")
@ -20,28 +12,15 @@ class OnboardingWelcomeLink extends LitElement {
@property({ type: Boolean }) public noninteractive = false; @property({ type: Boolean }) public noninteractive = false;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card <ha-card
.tabIndex=${this.noninteractive ? "-1" : "0"} .tabIndex=${this.noninteractive ? "-1" : "0"}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
> >
<ha-svg-icon .path=${this.iconPath}></ha-svg-icon> <ha-svg-icon .path=${this.iconPath}></ha-svg-icon>
${this.label} ${this.label}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""} <ha-ripple></ha-ripple>
</ha-card> </ha-card>
`; `;
} }
@ -52,36 +31,6 @@ class OnboardingWelcomeLink extends LitElement {
} }
} }
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
@ -104,6 +53,14 @@ class OnboardingWelcomeLink extends LitElement {
padding: 8px; padding: 8px;
margin-bottom: 16px; margin-bottom: 16px;
} }
ha-card:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
`; `;
} }
} }

View File

@ -1,7 +1,4 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { mdiCloud, mdiPackageVariant } from "@mdi/js"; import { mdiCloud, mdiPackageVariant } from "@mdi/js";
import { import {
CSSResultGroup, CSSResultGroup,
@ -11,18 +8,13 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { import { customElement, property } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { ConfigEntry, ERROR_STATES } from "../../../data/config_entries"; import { ConfigEntry, ERROR_STATES } from "../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../data/device_registry";
@ -54,10 +46,6 @@ export class HaIntegrationCard extends LitElement {
@property({ attribute: false }) public logInfo?: IntegrationLogInfo; @property({ attribute: false }) public logInfo?: IntegrationLogInfo;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
const entryState = this._getState(this.items); const entryState = this._getState(this.items);
@ -79,17 +67,8 @@ export class HaIntegrationCard extends LitElement {
<a <a
href=${`/config/integrations/integration/${this.domain}`} href=${`/config/integrations/integration/${this.domain}`}
class="ripple-anchor" class="ripple-anchor"
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
> >
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""} <ha-ripple></ha-ripple>
<ha-integration-header <ha-integration-header
.hass=${this.hass} .hass=${this.hass}
.domain=${this.domain} .domain=${this.domain}
@ -242,36 +221,6 @@ export class HaIntegrationCard extends LitElement {
} }
); );
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 handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
protected handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
protected handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@ -289,6 +238,15 @@ export class HaIntegrationCard extends LitElement {
.ripple-anchor { .ripple-anchor {
flex-grow: 1; flex-grow: 1;
position: relative; position: relative;
outline: none;
}
.ripple-anchor:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
} }
ha-integration-header { ha-integration-header {
height: 100%; height: 100%;

View File

@ -1,4 +1,3 @@
import "@material/mwc-ripple";
import { import {
mdiFan, mdiFan,
mdiFanOff, mdiFanOff,

View File

@ -1,21 +1,18 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { import {
HassConfig, HassConfig,
HassEntities, HassEntities,
HassEntity, HassEntity,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, eventOptions, queryAsync, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { DOMAINS_TOGGLE } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
@ -27,13 +24,14 @@ import { computeStateDisplaySingleEntity } from "../../../common/entity/compute_
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { import {
stateColorCss,
stateColorBrightness, stateColorBrightness,
stateColorCss,
} from "../../../common/entity/state_color"; } from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { iconColorCSS } from "../../../common/style/icon_color_css"; import { iconColorCSS } from "../../../common/style/icon_color_css";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-ripple";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
import { import {
configContext, configContext,
@ -132,10 +130,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
}) })
_entity?: EntityRegistryDisplayEntry; _entity?: EntityRegistryDisplayEntry;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
private getStateColor(stateObj: HassEntity, config: ButtonCardConfig) { private getStateColor(stateObj: HassEntity, config: ButtonCardConfig) {
const domain = stateObj ? computeStateDomain(stateObj) : undefined; const domain = stateObj ? computeStateDomain(stateObj) : undefined;
return config && (config.state_color ?? domain === "light"); return config && (config.state_color ?? domain === "light");
@ -197,13 +191,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
return html` return html`
<ha-card <ha-card
@action=${this._handleAction} @action=${this._handleAction}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action), hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action), hasDoubleClick: hasAction(this._config!.double_tap_action),
@ -218,6 +205,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
"--state-color": colored ? this._computeColor(stateObj) : undefined, "--state-color": colored ? this._computeColor(stateObj) : undefined,
})} })}
> >
<ha-ripple></ha-ripple>
${this._config.show_icon ${this._config.show_icon
? html` ? html`
<ha-state-icon <ha-state-icon
@ -252,7 +240,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
)} )}
</span>` </span>`
: ""} : ""}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</ha-card> </ha-card>
`; `;
} }
@ -282,31 +269,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
} }
} }
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
@eventOptions({ passive: true })
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
@eventOptions({ passive: true })
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
@eventOptions({ passive: true })
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
iconColorCSS, iconColorCSS,
@ -314,7 +276,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
ha-card { ha-card {
--state-inactive-color: var(--paper-item-icon-color, #44739e); --state-inactive-color: var(--paper-item-icon-color, #44739e);
--state-color: var(--paper-item-icon-color, #44739e); --state-color: var(--paper-item-icon-color, #44739e);
--mdc-ripple-color: var(--state-color); --ha-ripple-color: var(--state-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -340,6 +304,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
color: var(--state-color); color: var(--state-color);
--mdc-icon-size: 100%; --mdc-icon-size: 100%;
transition: transform 180ms ease-in-out; transition: transform 180ms ease-in-out;
pointer-events: none;
} }
ha-state-icon + span { ha-state-icon + span {

View File

@ -1,5 +1,3 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { mdiExclamationThick, mdiHelp } from "@mdi/js"; import { mdiExclamationThick, mdiHelp } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
@ -10,13 +8,7 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { import { customElement, property, state } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@ -29,6 +21,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-badge"; import "../../../components/tile/ha-tile-badge";
@ -313,36 +306,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return this._renderStateContent(stateObj, "state"); return this._renderStateContent(stateObj, "state");
} }
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
if (!this.hasCardAction) return;
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
if (!this.hasCardAction) return;
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
if (!this.hasCardAction) return;
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
if (!this.hasCardAction) return;
this._rippleHandlers.endHover();
}
get hasCardAction() { get hasCardAction() {
return ( return (
!this._config?.tap_action || !this._config?.tap_action ||
@ -420,17 +383,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
role=${ifDefined(this.hasCardAction ? "button" : undefined)} role=${ifDefined(this.hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)} tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
aria-labelledby="info" aria-labelledby="info"
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
> >
${this._shouldRenderRipple <ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
? html`<mwc-ripple></mwc-ripple>`
: nothing}
</div> </div>
<div class="content ${classMap(contentClasses)}"> <div class="content ${classMap(contentClasses)}">
<div <div
@ -494,7 +448,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
box-shadow: var(--shadow-default), var(--shadow-focus); box-shadow: var(--shadow-default), var(--shadow-focus);
} }
ha-card { ha-card {
--mdc-ripple-color: var(--tile-color); --ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%; height: 100%;
transition: transition:
box-shadow 180ms ease-in-out, box-shadow 180ms ease-in-out,

View File

@ -1,6 +1,4 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { noChange } from "lit"; import { noChange } from "lit";
import { import {
AttributePart, AttributePart,
@ -41,8 +39,6 @@ declare global {
class ActionHandler extends HTMLElement implements ActionHandlerType { class ActionHandler extends HTMLElement implements ActionHandlerType {
public holdTime = 500; public holdTime = 500;
public ripple: Ripple;
protected timer?: number; protected timer?: number;
protected held = false; protected held = false;
@ -51,24 +47,21 @@ class ActionHandler extends HTMLElement implements ActionHandlerType {
private dblClickTimeout?: number; private dblClickTimeout?: number;
constructor() {
super();
this.ripple = document.createElement("mwc-ripple");
}
public connectedCallback() { public connectedCallback() {
Object.assign(this.style, { Object.assign(this.style, {
position: "fixed", position: "fixed",
width: isTouch ? "100px" : "50px", width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px", height: isTouch ? "100px" : "50px",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%) scale(0)",
pointerEvents: "none", pointerEvents: "none",
zIndex: "999", zIndex: "999",
background: "var(--primary-color)",
display: null,
opacity: "0.2",
borderRadius: "50%",
transition: "transform 180ms ease-in-out",
}); });
this.appendChild(this.ripple);
this.ripple.primary = true;
[ [
"touchcancel", "touchcancel",
"mouseout", "mouseout",
@ -219,17 +212,16 @@ class ActionHandler extends HTMLElement implements ActionHandlerType {
Object.assign(this.style, { Object.assign(this.style, {
left: `${x}px`, left: `${x}px`,
top: `${y}px`, top: `${y}px`,
display: null, transform: "translate(-50%, -50%) scale(1)",
}); });
this.ripple.disabled = false;
this.ripple.startPress();
this.ripple.unbounded = true;
} }
private stopAnimation() { private stopAnimation() {
this.ripple.endPress(); Object.assign(this.style, {
this.ripple.disabled = true; left: null,
this.style.display = "none"; top: null,
transform: "translate(-50%, -50%) scale(0)",
});
} }
} }

View File

@ -2742,7 +2742,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@material/mwc-ripple@npm:0.27.0, @material/mwc-ripple@npm:^0.27.0": "@material/mwc-ripple@npm:^0.27.0":
version: 0.27.0 version: 0.27.0
resolution: "@material/mwc-ripple@npm:0.27.0" resolution: "@material/mwc-ripple@npm:0.27.0"
dependencies: dependencies:
@ -8967,7 +8967,6 @@ __metadata:
"@material/mwc-list": "npm:0.27.0" "@material/mwc-list": "npm:0.27.0"
"@material/mwc-menu": "npm:0.27.0" "@material/mwc-menu": "npm:0.27.0"
"@material/mwc-radio": "npm:0.27.0" "@material/mwc-radio": "npm:0.27.0"
"@material/mwc-ripple": "npm:0.27.0"
"@material/mwc-select": "npm:0.27.0" "@material/mwc-select": "npm:0.27.0"
"@material/mwc-snackbar": "npm:0.27.0" "@material/mwc-snackbar": "npm:0.27.0"
"@material/mwc-switch": "npm:0.27.0" "@material/mwc-switch": "npm:0.27.0"