Compare commits

...
Author SHA1 Message Date
WendelinandClaude Opus 5.5 b37560aaa9 Move the target picker entity pictures to their own branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 11:19:07 +01:00
Wendelin 4ff8ca4f83 Refactor close-picker event handling to support afterClose callback and improve entity action bar with new icon 2026-10-28 10:52:03 +01:00
Wendelin 68d6f80f2b Enhance entity action bar with localized labels and state management 2026-10-28 10:33:36 +01:00
Claude 4fe356576a Show entity pictures and an entity action bar in the pickers
Target picker rows now show the entity picture, e.g. of a person, like
the automation targets do since the entity pictures fix.

The generic picker gets a footer slot below its list. When the picker
opens for a selected entity, the entity and target pickers put an action
bar there to open the more info dialog or copy the entity ID.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018Kv7eE9FbWRSwWDetkaL85
2026-10-28 08:40:25 +00:00
5 changed files with 174 additions and 3 deletions

No files matched your search

@@ -0,0 +1,117 @@
import type { ContextType } from "@lit/context";
import { mdiContentCopy } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consume } from "../../common/decorators/consume";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import { copyToClipboard } from "../../common/util/copy-clipboard";
import {
formattersContext,
internationalizationContext,
} from "../../data/context";
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
import { showToast } from "../../util/toast";
import { HaActionBar } from "../ha-action-bar";
import "../ha-button";
import "../ha-icon-next";
import "../ha-svg-icon";
/**
* @element ha-entity-action-bar
*
* @summary Action bar for a selected entity, placed below the list of an
* entity or target picker. Opens the more info dialog of the entity or copies
* its entity ID.
*
* @cssprop --ha-action-bar-border-top - Top border of the bar. Defaults to `var(--ha-border-width-sm) solid var(--divider-color)`.
*
* @fires close-picker - Fired after an action, so the picker around the bar closes.
*/
@customElement("ha-entity-action-bar")
export class HaEntityActionBar extends HaActionBar {
@property({ attribute: "entity-id" }) public entityId?: string;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state()
@consumeEntityState({ entityIdPath: ["entityId"] })
private _stateObj?: HassEntity;
protected render() {
const name = this._stateObj
? this._formatters.formatEntityName(this._stateObj)
: undefined;
const viewLabel = name
? this._i18n.localize(
"ui.components.entity.entity-action-bar.view_entity",
{ name }
)
: nothing;
const copyIdLabel = name
? this._i18n.localize(
"ui.components.entity.entity-action-bar.copy_entity_id",
{ name }
)
: nothing;
return html`
<ha-button
appearance="filled"
variant="neutral"
size="s"
title=${viewLabel}
accessible-label=${viewLabel}
@click=${this._showMoreInfo}
>
${this._i18n.localize("ui.components.entity.entity-action-bar.view")}
<ha-icon-next slot="end"></ha-icon-next>
</ha-button>
<ha-button
appearance="filled"
variant="neutral"
size="s"
title=${copyIdLabel}
accessible-label=${copyIdLabel}
@click=${this._copyEntityId}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this._i18n.localize("ui.components.entity.entity-action-bar.copy_id")}
</ha-button>
`;
}
private _showMoreInfo() {
if (!this.entityId) {
return;
}
const entityId = this.entityId;
fireEvent(this, "close-picker", {
afterClose: () => showMoreInfoDialog(this, { entityId }),
});
}
private async _copyEntityId() {
if (!this.entityId) {
return;
}
await copyToClipboard(this.entityId);
showToast(this, {
message: this._i18n.localize("ui.common.copied_clipboard"),
});
fireEvent(this, "close-picker");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-action-bar": HaEntityActionBar;
}
}
@@ -40,6 +40,7 @@ import type { HaGenericPicker } from "../ha-generic-picker";
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
import "../ha-svg-icon";
import "./ha-entity-action-bar";
import "./state-badge";
const CREATE_ID = "___create-new-entity___";
@@ -481,6 +482,14 @@ export class HaEntityPicker extends LitElement {
)}
@value-changed=${this._valueChanged}
>
${
!this.addButton && this.value && this._states[this.value]
? html`<ha-entity-action-bar
slot="footer"
.entityId=${this.value}
></ha-entity-action-bar>`
: nothing
}
</ha-generic-picker>
`;
}
+32 -3
View File
@@ -16,7 +16,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { tinykeys } from "tinykeys";
import { consume } from "../common/decorators/consume";
import { fireEvent } from "../common/dom/fire_event";
import { fireEvent, type HASSDomEvent } from "../common/dom/fire_event";
import { configContext } from "../data/context";
import { PickerMixin } from "../mixins/picker-mixin";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
@@ -34,6 +34,11 @@ import type {
import "./ha-picker-field";
import "./ha-svg-icon";
export interface ClosePickerParams {
// Runs after the picker is closed
afterClose?: () => void;
}
@customElement("ha-generic-picker")
export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: "aria-label" })
@@ -134,6 +139,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@state() private _openedNarrow = false;
// Runs once the picker is closed and the browser has restored the focus
private _afterClose?: () => void;
@state() private _unknownValue = false;
@state() private _selectedValue?: string;
@@ -236,7 +244,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
aria-modal="true"
aria-label=${this.ariaLabel || this.label || "Select option"}
>
${this._renderComboBox(true)}
${this._renderComboBox(true)} ${this._renderFooter()}
</ha-bottom-sheet>
`
: html`
@@ -260,7 +268,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
aria-modal="true"
aria-label=${this.ariaLabel || this.label || "Select option"}
>
${this._renderComboBox()}
${this._renderComboBox()} ${this._renderFooter()}
</wa-popover>
`
: nothing
@@ -298,6 +306,13 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
`;
}
// The wrapper keeps the bottom sheet's footer styles off the slotted content.
private _renderFooter() {
return html`<div>
<slot name="footer" @close-picker=${this._handleClosePicker}></slot>
</div>`;
}
private _setUnknownValue = () => {
const items = this.getItems();
if (
@@ -384,6 +399,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
this._selectedValue = undefined;
this._unsubscribeTinyKeys?.();
fireEvent(this, "picker-closed");
const afterClose = this._afterClose;
this._afterClose = undefined;
afterClose?.();
}
private _valueChanged(ev: CustomEvent) {
@@ -425,6 +443,16 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
});
}
public close() {
this._pickerWrapperOpen = false;
}
private _handleClosePicker(ev: HASSDomEvent<ClosePickerParams | undefined>) {
ev.stopPropagation();
this._afterClose = ev.detail?.afterClose;
this.close();
}
connectedCallback() {
super.connectedCallback();
this._handleResize();
@@ -540,5 +568,6 @@ declare global {
interface HASSDomEvents {
"picker-closed": undefined;
"picker-opened": undefined;
"close-picker": ClosePickerParams | undefined;
}
}
+10
View File
@@ -60,6 +60,7 @@ import {
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./entity/ha-entity-action-bar";
import "./ha-button";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
@@ -556,6 +557,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
)}
.getAdditionalItems=${this._getAdditionalItems}
>
${
this._replaceTarget?.type === "entity" &&
this.hass.states[this._replaceTarget.id]
? html`<ha-entity-action-bar
slot="footer"
.entityId=${this._replaceTarget.id}
></ha-entity-action-bar>`
: nothing
}
</ha-generic-picker>
${
showExclude
+6
View File
@@ -843,6 +843,12 @@
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n }helper",
"unknown": "Unknown entity selected"
},
"entity-action-bar": {
"view": "View",
"view_entity": "View {name}",
"copy_id": "Copy ID",
"copy_entity_id": "Copy ID of {name}"
},
"entity-name-picker": {
"types": {
"floor": "Floor",