mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-19 11:48:08 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c08fdb623 | ||
|
|
434b2a1db2 |
@@ -1,100 +1,69 @@
|
||||
import { AssistChip } from "@material/web/chips/internal/assist-chip";
|
||||
import { styles } from "@material/web/chips/internal/assist-styles.cssresult.js";
|
||||
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
|
||||
|
||||
import { css, html } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-assist-chip")
|
||||
// @ts-ignore
|
||||
export class HaAssistChip extends AssistChip {
|
||||
export class HaAssistChip extends HaChipBase {
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
|
||||
@property({ type: Boolean }) active = false;
|
||||
@property({ type: Boolean, reflect: true }) active = false;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
elevatedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-assist-chip-container-shape: var(
|
||||
--ha-assist-chip-container-shape,
|
||||
16px
|
||||
);
|
||||
--md-assist-chip-outline-color: var(--outline-color);
|
||||
--md-assist-chip-label-text-weight: 400;
|
||||
}
|
||||
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
||||
.filled {
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
background-color: var(--ha-assist-chip-filled-container-color);
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]),
|
||||
::slotted([slot="trailing-icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
font-size: var(--_label-text-size) !important;
|
||||
}
|
||||
@property({ type: Boolean, reflect: true }) elevated = false;
|
||||
|
||||
.trailing.icon ::slotted(*),
|
||||
.trailing.icon svg {
|
||||
margin-inline-end: unset;
|
||||
margin-inline-start: var(--_icon-label-space);
|
||||
}
|
||||
::before {
|
||||
background: var(--ha-assist-chip-container-color, transparent);
|
||||
opacity: var(--ha-assist-chip-container-opacity, 1);
|
||||
}
|
||||
:where(.active)::before {
|
||||
background: var(--ha-assist-chip-active-container-color);
|
||||
opacity: var(--ha-assist-chip-active-container-opacity);
|
||||
}
|
||||
.label {
|
||||
font-family: var(--ha-font-family-body);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected override renderOutline() {
|
||||
if (this.filled) {
|
||||
return html`<span class="filled"></span>`;
|
||||
}
|
||||
|
||||
return super.renderOutline();
|
||||
}
|
||||
|
||||
protected override getContainerClasses() {
|
||||
return {
|
||||
...super.getContainerClasses(),
|
||||
active: this.active,
|
||||
};
|
||||
}
|
||||
|
||||
protected override renderPrimaryContent() {
|
||||
return html`
|
||||
<span class="leading icon" aria-hidden="true">
|
||||
${this.renderLeadingIcon()}
|
||||
</span>
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="touch"></span>
|
||||
<span class="trailing leading icon" aria-hidden="true">
|
||||
${this.renderTrailingIcon()}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderTrailingIcon() {
|
||||
return html`<slot name="trailing-icon"></slot>`;
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-chip-label-color: var(
|
||||
--md-assist-chip-label-text-color,
|
||||
var(--md-sys-color-on-surface, var(--primary-text-color))
|
||||
);
|
||||
--ha-button-border-radius: var(
|
||||
--ha-assist-chip-container-shape,
|
||||
16px
|
||||
);
|
||||
--ha-chip-container-color: var(
|
||||
--ha-assist-chip-container-color,
|
||||
transparent
|
||||
);
|
||||
--ha-chip-container-opacity: var(
|
||||
--ha-assist-chip-container-opacity,
|
||||
1
|
||||
);
|
||||
--ha-chip-outline-color: var(
|
||||
--md-assist-chip-outline-color,
|
||||
var(--outline-color)
|
||||
);
|
||||
--ha-chip-icon-label-space: var(
|
||||
--md-assist-chip-icon-label-space,
|
||||
var(--ha-space-2)
|
||||
);
|
||||
}
|
||||
:host([filled]) {
|
||||
--ha-chip-container-color: var(
|
||||
--ha-assist-chip-filled-container-color
|
||||
);
|
||||
--ha-chip-outline-color: transparent;
|
||||
}
|
||||
:host([active]) {
|
||||
--ha-chip-container-color: var(
|
||||
--ha-assist-chip-active-container-color
|
||||
);
|
||||
--ha-chip-container-opacity: var(
|
||||
--ha-assist-chip-active-container-opacity,
|
||||
1
|
||||
);
|
||||
}
|
||||
.primary {
|
||||
padding-inline-end: var(
|
||||
--md-assist-chip-trailing-space,
|
||||
var(--ha-space-4)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
import { LocalizeController } from "@home-assistant/webawesome/dist/utilities/localize.js";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaButton } from "../ha-button";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
export interface ChipFocusOptions extends FocusOptions {
|
||||
trailing?: boolean;
|
||||
}
|
||||
|
||||
export class HaChipBase extends HaButton {
|
||||
@property() label = "";
|
||||
|
||||
@property({ type: Boolean }) removable = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "remove-only" }) removeOnly = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "soft-disabled" })
|
||||
softDisabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "always-focusable" })
|
||||
alwaysFocusable = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "has-icon" })
|
||||
hasIcon = false;
|
||||
|
||||
@property({ type: Number, reflect: true, attribute: "tabindex" })
|
||||
override tabIndex = 0;
|
||||
|
||||
@property({ attribute: "aria-label-remove" }) ariaLabelRemove: string | null =
|
||||
null;
|
||||
|
||||
@query(".primary") private _primary?: HTMLButtonElement | HTMLAnchorElement;
|
||||
|
||||
@query(".remove") private _remove?: HTMLButtonElement;
|
||||
|
||||
@state() private _trailingFocused = false;
|
||||
|
||||
@state() private _hasTrailingIcon = false;
|
||||
|
||||
private _chipLocalize = new LocalizeController(this);
|
||||
|
||||
override variant: HaButton["variant"] = "neutral";
|
||||
|
||||
override appearance: HaButton["appearance"] = "outlined";
|
||||
|
||||
override size: HaButton["size"] = "s";
|
||||
|
||||
protected get pressed(): boolean | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get focusable() {
|
||||
return !this.disabled || this.alwaysFocusable;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("click", this._blockDisabledClick);
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
}
|
||||
|
||||
override focus(options?: ChipFocusOptions) {
|
||||
if (!this.focusable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(options?.trailing || (this.removeOnly && !this.href)) &&
|
||||
this._remove
|
||||
) {
|
||||
this._remove.focus(options);
|
||||
} else {
|
||||
this._primary?.focus(options);
|
||||
}
|
||||
}
|
||||
|
||||
override click() {
|
||||
if (this.removeOnly && !this.href) {
|
||||
this._remove?.click();
|
||||
} else {
|
||||
this._primary?.click();
|
||||
}
|
||||
}
|
||||
|
||||
override blur() {
|
||||
this._primary?.blur();
|
||||
this._remove?.blur();
|
||||
}
|
||||
|
||||
protected override updated(changed: PropertyValues<this>) {
|
||||
super.updated(changed);
|
||||
|
||||
if (changed.has("disabled") || changed.has("alwaysFocusable")) {
|
||||
fireEvent(this, "update-focus", undefined, { composed: false });
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class=${classMap({ container: true, "has-remove": this.removable })}>
|
||||
${this._renderPrimaryAction()}
|
||||
${
|
||||
this.removable
|
||||
? html`
|
||||
<span id="remove-label" hidden>
|
||||
${this._chipLocalize.term("remove")}
|
||||
</span>
|
||||
<span id="accessible-label" hidden>${this.ariaLabel}</span>
|
||||
<button
|
||||
class="button remove trailing action"
|
||||
part="remove-button"
|
||||
type="button"
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
aria-disabled=${this.softDisabled ? "true" : nothing}
|
||||
aria-label=${this.ariaLabelRemove || nothing}
|
||||
aria-labelledby=${
|
||||
this.ariaLabelRemove
|
||||
? nothing
|
||||
: this.ariaLabel
|
||||
? "remove-label accessible-label"
|
||||
: "remove-label label"
|
||||
}
|
||||
tabindex=${this.removeOnly && !this.href ? this.tabIndex : -1}
|
||||
@click=${this._handleRemove}
|
||||
@focus=${this._handleRemoveFocus}
|
||||
@blur=${this._handleRemoveBlur}
|
||||
>
|
||||
<span class="icon" aria-hidden="true">
|
||||
<slot name="remove-trailing-icon">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</slot>
|
||||
</span>
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPrimaryAction() {
|
||||
if (this.href) {
|
||||
return html`<a
|
||||
class="button primary action"
|
||||
part="base"
|
||||
title=${this.title || nothing}
|
||||
href=${this.href}
|
||||
target=${this.target || nothing}
|
||||
download=${this.download || nothing}
|
||||
rel=${this.rel || nothing}
|
||||
aria-label=${this.ariaLabel || nothing}
|
||||
tabindex=${this._trailingFocused ? -1 : this.tabIndex}
|
||||
@click=${this.handlePrimaryClick}
|
||||
>${this.renderPrimaryContent()}</a
|
||||
>`;
|
||||
}
|
||||
|
||||
if (this.removeOnly) {
|
||||
return html`<span class="content" title=${this.title || nothing}>
|
||||
${this.renderPrimaryContent()}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
return html`<button
|
||||
class=${classMap({
|
||||
button: true,
|
||||
primary: true,
|
||||
action: true,
|
||||
disabled: this.disabled || this.softDisabled,
|
||||
})}
|
||||
part="base"
|
||||
type="button"
|
||||
title=${this.title || nothing}
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
aria-disabled=${
|
||||
this.softDisabled || (this.disabled && this.alwaysFocusable)
|
||||
? "true"
|
||||
: nothing
|
||||
}
|
||||
aria-label=${this.ariaLabel || nothing}
|
||||
aria-pressed=${this.pressed ?? nothing}
|
||||
tabindex=${this._trailingFocused ? -1 : this.tabIndex}
|
||||
@click=${this.handlePrimaryClick}
|
||||
>
|
||||
${this.renderPrimaryContent()}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
protected renderPrimaryContent() {
|
||||
return html`
|
||||
${this.renderLeadingIcon()}
|
||||
<span class="label" id="label" part="label">
|
||||
${this.label || html`<slot></slot>`}
|
||||
</span>
|
||||
<span class="icon" aria-hidden="true" ?hidden=${!this._hasTrailingIcon}>
|
||||
<slot name="trailing-icon" @slotchange=${this._iconChanged}></slot>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderLeadingIcon(): TemplateResult | typeof nothing {
|
||||
return html`<span class="icon leading" aria-hidden="true">
|
||||
<slot name="icon" @slotchange=${this._iconChanged}></slot>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
protected handlePrimaryClick(event: MouseEvent) {
|
||||
this._blockDisabledClick(event);
|
||||
}
|
||||
|
||||
private _iconChanged(event: Event) {
|
||||
if (!(event.target instanceof HTMLSlotElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasIcon = event.target.assignedElements({ flatten: true }).length > 0;
|
||||
|
||||
if (event.target.name === "icon") {
|
||||
this.hasIcon = hasIcon;
|
||||
} else {
|
||||
this._hasTrailingIcon = hasIcon;
|
||||
}
|
||||
}
|
||||
|
||||
private _blockDisabledClick(event: MouseEvent) {
|
||||
if (
|
||||
this.softDisabled ||
|
||||
(this.disabled && (!this.href || this.alwaysFocusable))
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRemove(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.disabled || this.softDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!fireEvent(this, "remove", undefined, {
|
||||
bubbles: false,
|
||||
composed: false,
|
||||
cancelable: true,
|
||||
}).defaultPrevented
|
||||
) {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRemoveFocus() {
|
||||
this._trailingFocused = true;
|
||||
}
|
||||
|
||||
private _handleRemoveBlur() {
|
||||
this._trailingFocused = false;
|
||||
}
|
||||
|
||||
private _handleKeyDown(event: KeyboardEvent) {
|
||||
if (
|
||||
(event.key !== "ArrowLeft" && event.key !== "ArrowRight") ||
|
||||
!this._primary ||
|
||||
!this._remove
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forwards =
|
||||
(event.key === "ArrowRight") !==
|
||||
(getComputedStyle(this).direction === "rtl");
|
||||
|
||||
if (
|
||||
(forwards && this.shadowRoot?.activeElement === this._primary) ||
|
||||
(!forwards && this.shadowRoot?.activeElement === this._remove)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
(forwards ? this._remove : this._primary).focus();
|
||||
}
|
||||
}
|
||||
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
--ha-button-height: 32px;
|
||||
--ha-button-border-radius: var(--ha-border-radius-pill);
|
||||
--wa-font-weight-action: var(--ha-font-weight-normal);
|
||||
--wa-button-transform-hover: none;
|
||||
--wa-button-transform-active: none;
|
||||
}
|
||||
:host([variant]) {
|
||||
--wa-color-on-quiet: var(
|
||||
--ha-chip-label-color,
|
||||
var(--primary-text-color)
|
||||
);
|
||||
--wa-color-fill-quiet: color-mix(
|
||||
in srgb,
|
||||
var(--ha-chip-label-color, var(--primary-text-color)) 8%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
height: var(--ha-button-height);
|
||||
position: relative;
|
||||
border-radius: var(--ha-button-border-radius);
|
||||
}
|
||||
.container::before,
|
||||
.container::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.container::before {
|
||||
background: var(--ha-chip-container-color, transparent);
|
||||
opacity: var(--ha-chip-container-opacity, 1);
|
||||
}
|
||||
.container::after {
|
||||
border: var(--ha-chip-outline-width, 1px) solid
|
||||
var(--ha-chip-outline-color, var(--outline-color));
|
||||
}
|
||||
.button,
|
||||
.content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ha-chip-icon-label-space, var(--ha-space-2));
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
color: var(--ha-chip-label-color, var(--primary-text-color));
|
||||
background: transparent;
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
padding-inline: var(--ha-space-4);
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
}
|
||||
.button.primary,
|
||||
.content {
|
||||
flex: 1;
|
||||
height: var(--ha-button-height);
|
||||
min-height: var(--ha-button-height);
|
||||
}
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
font-size: var(--ha-chip-label-size, 0.875rem);
|
||||
font-weight: var(
|
||||
--ha-chip-label-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
line-height: var(--ha-chip-label-line-height, 1.25rem);
|
||||
color: var(--ha-chip-label-color, var(--primary-text-color));
|
||||
}
|
||||
.button.remove {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
width: calc(var(--ha-chip-icon-size, 18px) + 2 * var(--ha-space-2));
|
||||
height: var(--ha-button-height);
|
||||
min-height: var(--ha-button-height);
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
.has-remove .primary,
|
||||
.has-remove .content {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
:host([has-icon]) .primary,
|
||||
:host([has-icon]) .content {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
.button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -8px 0;
|
||||
}
|
||||
slot:not([name]),
|
||||
slot[name="icon"],
|
||||
slot[name="trailing-icon"],
|
||||
slot[name="selected-icon"],
|
||||
slot[name="remove-trailing-icon"] {
|
||||
display: contents;
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
flex: none;
|
||||
color: var(--ha-chip-label-color, var(--primary-text-color));
|
||||
}
|
||||
:host(:not([has-icon])) .leading {
|
||||
display: none;
|
||||
}
|
||||
.icon[hidden] {
|
||||
display: none;
|
||||
}
|
||||
ha-svg-icon,
|
||||
::slotted([slot]) {
|
||||
flex: none;
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--ha-chip-icon-size, 18px);
|
||||
}
|
||||
:host([disabled]),
|
||||
:host([soft-disabled]) {
|
||||
opacity: 1;
|
||||
}
|
||||
:host([disabled]) .label,
|
||||
:host([disabled]) .icon,
|
||||
:host([soft-disabled]) .label,
|
||||
:host([soft-disabled]) .icon {
|
||||
opacity: 0.38;
|
||||
}
|
||||
:host([disabled][selected]) .container::before,
|
||||
:host([soft-disabled][selected]) .container::before {
|
||||
background-color: var(--primary-text-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
:host([elevated]) .container {
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
remove: undefined;
|
||||
"update-focus": undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,92 @@
|
||||
import { MdChipSet } from "@material/web/chips/chip-set";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, queryAssignedElements } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-chip-set")
|
||||
export class HaChipSet extends MdChipSet {}
|
||||
export class HaChipSet extends LitElement {
|
||||
@queryAssignedElements({ flatten: true }) private _children!: HTMLElement[];
|
||||
|
||||
get chips(): HaChipBase[] {
|
||||
return this._children.filter(
|
||||
(child): child is HaChipBase => child instanceof HaChipBase
|
||||
);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute("role", "toolbar");
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
this.addEventListener("focusin", this._updateTabIndices);
|
||||
this.addEventListener("update-focus", this._updateTabIndices);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("focusin", this._updateTabIndices);
|
||||
this.removeEventListener("update-focus", this._updateTabIndices);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<slot @slotchange=${this._updateTabIndices}></slot>`;
|
||||
}
|
||||
|
||||
private _updateTabIndices() {
|
||||
const chips = this.chips;
|
||||
|
||||
const active =
|
||||
chips.find((chip) => chip.focusable && chip.shadowRoot?.activeElement) ||
|
||||
chips.find((chip) => chip.focusable);
|
||||
|
||||
for (const chip of chips) {
|
||||
chip.tabIndex = chip === active ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleKeyDown(event: KeyboardEvent) {
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chips = this.chips.filter((chip) => chip.focusable);
|
||||
|
||||
if (!chips.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const forwards =
|
||||
(event.key === "ArrowRight") !==
|
||||
(getComputedStyle(this).direction === "rtl");
|
||||
|
||||
const current = chips.findIndex((chip) => chip.shadowRoot?.activeElement);
|
||||
|
||||
const index =
|
||||
event.key === "Home"
|
||||
? 0
|
||||
: event.key === "End"
|
||||
? chips.length - 1
|
||||
: current === -1
|
||||
? forwards
|
||||
? 0
|
||||
: chips.length - 1
|
||||
: (current + (forwards ? 1 : -1) + chips.length) % chips.length;
|
||||
|
||||
chips[index].focus({
|
||||
trailing: event.key === "End" || (event.key !== "Home" && !forwards),
|
||||
});
|
||||
this._updateTabIndices();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -1,54 +1,81 @@
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
|
||||
import { FilterChip } from "@material/web/chips/internal/filter-chip";
|
||||
import { styles } from "@material/web/chips/internal/filter-styles.cssresult.js";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
|
||||
import { css, html } from "lit";
|
||||
import { mdiCheck } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-filter-chip")
|
||||
export class HaFilterChip extends FilterChip {
|
||||
export class HaFilterChip extends HaChipBase {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) elevated = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "no-leading-icon" })
|
||||
noLeadingIcon = false;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
elevatedStyles,
|
||||
trailingIconStyles,
|
||||
selectableStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-filter-chip-container-shape: var(--ha-border-radius-md);
|
||||
--md-filter-chip-outline-color: var(--outline-color);
|
||||
--md-filter-chip-selected-container-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--_label-text-font: var(--ha-font-family-body);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected getContainerClasses() {
|
||||
const classes = super.getContainerClasses();
|
||||
if (this.noLeadingIcon) {
|
||||
classes["has-icon"] = false;
|
||||
}
|
||||
return classes;
|
||||
protected override get pressed() {
|
||||
return this.selected;
|
||||
}
|
||||
|
||||
protected renderLeadingIcon() {
|
||||
protected override renderLeadingIcon() {
|
||||
if (this.noLeadingIcon) {
|
||||
// eslint-disable-next-line lit/prefer-nothing
|
||||
return html``;
|
||||
return nothing;
|
||||
}
|
||||
return super.renderLeadingIcon();
|
||||
|
||||
return this.selected
|
||||
? html`<span class="icon" aria-hidden="true">
|
||||
<slot name="selected-icon">
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
</slot>
|
||||
</span>`
|
||||
: super.renderLeadingIcon();
|
||||
}
|
||||
|
||||
protected override handlePrimaryClick(event: MouseEvent) {
|
||||
if (this.disabled || this.softDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = this.selected;
|
||||
this.selected = !this.selected;
|
||||
event.stopPropagation();
|
||||
|
||||
// Dispatch from the chip so listeners see the new selection before deciding
|
||||
// whether to cancel it. Preserve pointer and modifier-key information.
|
||||
const click =
|
||||
typeof PointerEvent !== "undefined" && event instanceof PointerEvent
|
||||
? new PointerEvent("click", event)
|
||||
: new MouseEvent("click", event);
|
||||
|
||||
if (!this.dispatchEvent(click)) {
|
||||
this.selected = previous;
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-button-border-radius: var(--ha-border-radius-md);
|
||||
--ha-chip-label-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
:host([selected]) {
|
||||
--ha-chip-outline-width: var(
|
||||
--md-filter-chip-selected-outline-width,
|
||||
0px
|
||||
);
|
||||
--ha-chip-container-color: var(
|
||||
--md-filter-chip-selected-container-color,
|
||||
rgba(var(--rgb-primary-text-color), 0.15)
|
||||
);
|
||||
}
|
||||
:host([selected]:not([no-leading-icon])) .primary {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import { InputChip } from "@material/web/chips/internal/input-chip";
|
||||
import { styles } from "@material/web/chips/internal/input-styles.cssresult.js";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-input-chip")
|
||||
export class HaInputChip extends InputChip {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
trailingIconStyles,
|
||||
selectableStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
max-width: 100%;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-input-chip-container-shape: 16px;
|
||||
--md-input-chip-outline-color: var(--outline-color);
|
||||
--md-input-chip-selected-container-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-input-chip-selected-container-opacity: 1;
|
||||
--md-input-chip-label-text-font: Roboto, sans-serif;
|
||||
--md-input-chip-label-text-weight: 400;
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
.selected::before {
|
||||
opacity: var(--ha-input-chip-selected-container-opacity);
|
||||
}
|
||||
`,
|
||||
];
|
||||
export class HaInputChip extends HaChipBase {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) avatar = false;
|
||||
|
||||
override removable = true;
|
||||
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-button-border-radius: 16px;
|
||||
--ha-chip-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
:host([selected]) {
|
||||
--ha-chip-container-color: var(
|
||||
--md-input-chip-selected-container-color,
|
||||
rgba(var(--rgb-primary-text-color), 0.15)
|
||||
);
|
||||
--ha-chip-outline-width: var(
|
||||
--md-input-chip-selected-outline-width,
|
||||
0px
|
||||
);
|
||||
--ha-chip-container-opacity: var(
|
||||
--ha-input-chip-selected-container-opacity,
|
||||
1
|
||||
);
|
||||
}
|
||||
:host([avatar]) ::slotted([slot="icon"]) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -18,9 +18,14 @@ vi.mock("../../../src/components/ha-dialog", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-button", () => {
|
||||
customElements.define("ha-button", class extends HTMLElement {});
|
||||
return {};
|
||||
vi.mock("../../../src/components/ha-button", async () => {
|
||||
const lit = await import("lit");
|
||||
|
||||
class HaButton extends lit.LitElement {}
|
||||
|
||||
customElements.define("ha-button", HaButton);
|
||||
|
||||
return { HaButton };
|
||||
});
|
||||
|
||||
vi.mock("../../../src/components/ha-dialog-footer", () => {
|
||||
|
||||
Reference in New Issue
Block a user