Skip to content

Commit 0daf94e

Browse files
wendevlinsilamon
andauthored
Quick bar: new design and area search (#28678)
* Add "Commands" title to quick bar translations in English * Enhance QuickBar dialog handling and localize commands title * add nav icons * Add icons and styles and separate navigation from commands * handle non admin * Add areas * Fix import and shortcuts * Restructure * remove area sort * move keys * area search keys review * Fix adaptive dialog slots without header * Design review * Review marcin * Fix safe area bottom * Fix ios focus * Make it clearable --------- Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
1 parent 00a3237 commit 0daf94e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1389
-1384
lines changed

demo/src/stubs/area_registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
1+
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
22
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
33

44
export const mockAreaRegistry = (

gallery/src/pages/components/ha-form.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
1010
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
1111
import "../../../../src/components/ha-form/ha-form";
1212
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
13-
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
13+
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
1414
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
1515
import { getEntity } from "../../../../src/fake_data/entity";
1616
import { provideHass } from "../../../../src/fake_data/provide_hass";

gallery/src/pages/components/ha-selector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
1111
import "../../../../src/components/ha-formfield";
1212
import "../../../../src/components/ha-selector/ha-selector";
1313
import "../../../../src/components/ha-settings-row";
14-
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
14+
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
1515
import type { BlueprintInput } from "../../../../src/data/blueprint";
1616
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
1717
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";

src/common/areas/areas-floor-hierarchy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AreaRegistryEntry } from "../../data/area_registry";
1+
import type { AreaRegistryEntry } from "../../data/area/area_registry";
22
import type { FloorRegistryEntry } from "../../data/floor_registry";
33

44
export interface AreasFloorHierarchy {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AreaRegistryEntry } from "../../data/area_registry";
1+
import type { AreaRegistryEntry } from "../../data/area/area_registry";
22

33
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
44
area.name?.trim();

src/common/entity/context/get_area_context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AreaRegistryEntry } from "../../../data/area_registry";
1+
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
22
import type { FloorRegistryEntry } from "../../../data/floor_registry";
33
import type { HomeAssistant } from "../../../types";
44

src/common/entity/context/get_device_context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AreaRegistryEntry } from "../../../data/area_registry";
1+
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
22
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
33
import type { FloorRegistryEntry } from "../../../data/floor_registry";
44
import type { HomeAssistant } from "../../../types";

src/common/entity/context/get_entity_context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { HassEntity } from "home-assistant-js-websocket";
2-
import type { AreaRegistryEntry } from "../../../data/area_registry";
2+
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
33
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
44
import type {
55
EntityRegistryDisplayEntry,

src/components/ha-adaptive-dialog.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { mdiClose } from "@mdi/js";
2-
import { css, html, LitElement } from "lit";
2+
import { css, html, LitElement, nothing } from "lit";
33
import { customElement, property, state } from "lit/decorators";
4-
import type { HomeAssistant } from "../types";
54
import { listenMediaQuery } from "../common/dom/media_query";
5+
import type { HomeAssistant } from "../types";
66
import "./ha-bottom-sheet";
77
import "./ha-dialog-header";
88
import "./ha-icon-button";
@@ -88,6 +88,9 @@ export class HaAdaptiveDialog extends LitElement {
8888
@property({ type: Boolean, attribute: "block-mode-change" })
8989
public blockModeChange = false;
9090

91+
@property({ type: Boolean, attribute: "without-header" })
92+
public withoutHeader = false;
93+
9194
@state() private _mode: DialogSheetMode = "dialog";
9295

9396
private _unsubMediaQuery?: () => void;
@@ -118,27 +121,33 @@ export class HaAdaptiveDialog extends LitElement {
118121
if (this._mode === "bottom-sheet") {
119122
return html`
120123
<ha-bottom-sheet .open=${this.open} flexcontent>
121-
<ha-dialog-header
122-
slot="header"
123-
.subtitlePosition=${this.headerSubtitlePosition}
124-
>
125-
<slot name="headerNavigationIcon" slot="navigationIcon">
126-
<ha-icon-button
127-
data-drawer="close"
128-
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
129-
.path=${mdiClose}
130-
></ha-icon-button>
131-
</slot>
132-
${this.headerTitle !== undefined
133-
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
134-
${this.headerTitle}
135-
</span>`
136-
: html`<slot name="headerTitle" slot="title"></slot>`}
137-
${this.headerSubtitle !== undefined
138-
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
139-
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
140-
<slot name="headerActionItems" slot="actionItems"></slot>
141-
</ha-dialog-header>
124+
${!this.withoutHeader
125+
? html`<ha-dialog-header
126+
slot="header"
127+
.subtitlePosition=${this.headerSubtitlePosition}
128+
>
129+
<slot name="headerNavigationIcon" slot="navigationIcon">
130+
<ha-icon-button
131+
data-drawer="close"
132+
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
133+
.path=${mdiClose}
134+
></ha-icon-button>
135+
</slot>
136+
${this.headerTitle !== undefined
137+
? html`<span
138+
slot="title"
139+
class="title"
140+
id="ha-wa-dialog-title"
141+
>
142+
${this.headerTitle}
143+
</span>`
144+
: html`<slot name="headerTitle" slot="title"></slot>`}
145+
${this.headerSubtitle !== undefined
146+
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
147+
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
148+
<slot name="headerActionItems" slot="actionItems"></slot>
149+
</ha-dialog-header>`
150+
: nothing}
142151
<slot></slot>
143152
<slot name="footer" slot="footer"></slot>
144153
</ha-bottom-sheet>
@@ -156,6 +165,7 @@ export class HaAdaptiveDialog extends LitElement {
156165
.headerSubtitle=${this.headerSubtitle}
157166
.headerSubtitlePosition=${this.headerSubtitlePosition}
158167
flexcontent
168+
.withoutHeader=${this.withoutHeader}
159169
>
160170
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
161171
<ha-icon-button

src/components/ha-area-picker.ts

Lines changed: 8 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,10 @@ import { customElement, property, query } from "lit/decorators";
66
import memoizeOne from "memoize-one";
77
import { fireEvent } from "../common/dom/fire_event";
88
import { computeAreaName } from "../common/entity/compute_area_name";
9-
import { computeDomain } from "../common/entity/compute_domain";
109
import { computeFloorName } from "../common/entity/compute_floor_name";
1110
import { getAreaContext } from "../common/entity/context/get_area_context";
12-
import { createAreaRegistryEntry } from "../data/area_registry";
13-
import type {
14-
DeviceEntityDisplayLookup,
15-
DeviceRegistryEntry,
16-
} from "../data/device/device_registry";
17-
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
18-
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
11+
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
12+
import { createAreaRegistryEntry } from "../data/area/area_registry";
1913
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
2014
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
2115
import type { HomeAssistant, ValueChangedEvent } from "../types";
@@ -30,12 +24,6 @@ import "./ha-svg-icon";
3024

3125
const ADD_NEW_ID = "___ADD_NEW___";
3226

33-
const SEARCH_KEYS = [
34-
{ name: "search_labels.areaName", weight: 10 },
35-
{ name: "search_labels.aliases", weight: 8 },
36-
{ name: "search_labels.floorName", weight: 6 },
37-
{ name: "search_labels.id", weight: 3 },
38-
];
3927
@customElement("ha-area-picker")
4028
export class HaAreaPicker extends LitElement {
4129
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -102,6 +90,8 @@ export class HaAreaPicker extends LitElement {
10290
await this._picker?.open();
10391
}
10492

93+
private _getAreasMemoized = memoizeOne(getAreas);
94+
10595
// Recompute value renderer when the areas change
10696
private _computeValueRenderer = memoizeOne(
10797
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
@@ -137,183 +127,13 @@ export class HaAreaPicker extends LitElement {
137127
}
138128
);
139129

140-
private _getAreas = memoizeOne(
141-
(
142-
haAreas: HomeAssistant["areas"],
143-
haDevices: HomeAssistant["devices"],
144-
haEntities: HomeAssistant["entities"],
145-
includeDomains: this["includeDomains"],
146-
excludeDomains: this["excludeDomains"],
147-
includeDeviceClasses: this["includeDeviceClasses"],
148-
deviceFilter: this["deviceFilter"],
149-
entityFilter: this["entityFilter"],
150-
excludeAreas: this["excludeAreas"]
151-
): PickerComboBoxItem[] => {
152-
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
153-
let inputDevices: DeviceRegistryEntry[] | undefined;
154-
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
155-
156-
const areas = Object.values(haAreas);
157-
const devices = Object.values(haDevices);
158-
const entities = Object.values(haEntities);
159-
160-
if (
161-
includeDomains ||
162-
excludeDomains ||
163-
includeDeviceClasses ||
164-
deviceFilter ||
165-
entityFilter
166-
) {
167-
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
168-
inputDevices = devices;
169-
inputEntities = entities.filter((entity) => entity.area_id);
170-
171-
if (includeDomains) {
172-
inputDevices = inputDevices!.filter((device) => {
173-
const devEntities = deviceEntityLookup[device.id];
174-
if (!devEntities || !devEntities.length) {
175-
return false;
176-
}
177-
return deviceEntityLookup[device.id].some((entity) =>
178-
includeDomains.includes(computeDomain(entity.entity_id))
179-
);
180-
});
181-
inputEntities = inputEntities!.filter((entity) =>
182-
includeDomains.includes(computeDomain(entity.entity_id))
183-
);
184-
}
185-
186-
if (excludeDomains) {
187-
inputDevices = inputDevices!.filter((device) => {
188-
const devEntities = deviceEntityLookup[device.id];
189-
if (!devEntities || !devEntities.length) {
190-
return true;
191-
}
192-
return entities.every(
193-
(entity) =>
194-
!excludeDomains.includes(computeDomain(entity.entity_id))
195-
);
196-
});
197-
inputEntities = inputEntities!.filter(
198-
(entity) =>
199-
!excludeDomains.includes(computeDomain(entity.entity_id))
200-
);
201-
}
202-
203-
if (includeDeviceClasses) {
204-
inputDevices = inputDevices!.filter((device) => {
205-
const devEntities = deviceEntityLookup[device.id];
206-
if (!devEntities || !devEntities.length) {
207-
return false;
208-
}
209-
return deviceEntityLookup[device.id].some((entity) => {
210-
const stateObj = this.hass.states[entity.entity_id];
211-
if (!stateObj) {
212-
return false;
213-
}
214-
return (
215-
stateObj.attributes.device_class &&
216-
includeDeviceClasses.includes(stateObj.attributes.device_class)
217-
);
218-
});
219-
});
220-
inputEntities = inputEntities!.filter((entity) => {
221-
const stateObj = this.hass.states[entity.entity_id];
222-
return (
223-
stateObj.attributes.device_class &&
224-
includeDeviceClasses.includes(stateObj.attributes.device_class)
225-
);
226-
});
227-
}
228-
229-
if (deviceFilter) {
230-
inputDevices = inputDevices!.filter((device) =>
231-
deviceFilter!(device)
232-
);
233-
}
234-
235-
if (entityFilter) {
236-
inputDevices = inputDevices!.filter((device) => {
237-
const devEntities = deviceEntityLookup[device.id];
238-
if (!devEntities || !devEntities.length) {
239-
return false;
240-
}
241-
return deviceEntityLookup[device.id].some((entity) => {
242-
const stateObj = this.hass.states[entity.entity_id];
243-
if (!stateObj) {
244-
return false;
245-
}
246-
return entityFilter(stateObj);
247-
});
248-
});
249-
inputEntities = inputEntities!.filter((entity) => {
250-
const stateObj = this.hass.states[entity.entity_id];
251-
if (!stateObj) {
252-
return false;
253-
}
254-
return entityFilter!(stateObj);
255-
});
256-
}
257-
}
258-
259-
let outputAreas = areas;
260-
261-
let areaIds: string[] | undefined;
262-
263-
if (inputDevices) {
264-
areaIds = inputDevices
265-
.filter((device) => device.area_id)
266-
.map((device) => device.area_id!);
267-
}
268-
269-
if (inputEntities) {
270-
areaIds = (areaIds ?? []).concat(
271-
inputEntities
272-
.filter((entity) => entity.area_id)
273-
.map((entity) => entity.area_id!)
274-
);
275-
}
276-
277-
if (areaIds) {
278-
outputAreas = outputAreas.filter((area) =>
279-
areaIds!.includes(area.area_id)
280-
);
281-
}
282-
283-
if (excludeAreas) {
284-
outputAreas = outputAreas.filter(
285-
(area) => !excludeAreas!.includes(area.area_id)
286-
);
287-
}
288-
289-
const items = outputAreas.map<PickerComboBoxItem>((area) => {
290-
const { floor } = getAreaContext(area, this.hass.floors);
291-
const floorName = floor ? computeFloorName(floor) : undefined;
292-
const areaName = computeAreaName(area);
293-
return {
294-
id: area.area_id,
295-
primary: areaName || area.area_id,
296-
secondary: floorName,
297-
icon: area.icon || undefined,
298-
icon_path: area.icon ? undefined : mdiTextureBox,
299-
search_labels: {
300-
areaName: areaName || null,
301-
floorName: floorName || null,
302-
id: area.area_id,
303-
aliases: area.aliases.join(" "),
304-
},
305-
};
306-
});
307-
308-
return items;
309-
}
310-
);
311-
312130
private _getItems = () =>
313-
this._getAreas(
131+
this._getAreasMemoized(
314132
this.hass.areas,
133+
this.hass.floors,
315134
this.hass.devices,
316135
this.hass.entities,
136+
this.hass.states,
317137
this.includeDomains,
318138
this.excludeDomains,
319139
this.includeDeviceClasses,
@@ -394,7 +214,7 @@ export class HaAreaPicker extends LitElement {
394214
.getAdditionalItems=${this._getAdditionalItems}
395215
.valueRenderer=${valueRenderer}
396216
.addButtonLabel=${this.addButtonLabel}
397-
.searchKeys=${SEARCH_KEYS}
217+
.searchKeys=${areaComboBoxKeys}
398218
.unknownItemText=${this.hass.localize(
399219
"ui.components.area-picker.unknown"
400220
)}

0 commit comments

Comments
 (0)