Fix visual map issues (#20541)

* Fix visual map issues (#15587)

- hover background color of zoom control in dark mode
- map light mode when dark theme is used
- background color of zoom control with map dark mode when light theme is used

* Fix breaking change (#15587)

* Change theme mode selection to use dropdown (#15587)

- Additionally fixed unpleasant horizontal scrollbar in map editor

* Add yaml migration, fix force light/dark options

---------

Co-authored-by: Christoph Wen <wen.christoph@gmail.com>
This commit is contained in:
karwosts 2024-04-24 02:35:37 -07:00 committed by GitHub
parent 5b7ab1bfcb
commit 713763fc21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 149 additions and 76 deletions

View File

@ -133,7 +133,7 @@ export class HaLocationsEditor extends LitElement {
.layers=${this._getLayers(this._circles, this._locationMarkers)} .layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom} .zoom=${this.zoom}
.autoFit=${this.autoFit} .autoFit=${this.autoFit}
?darkMode=${this.darkMode} ?forceDarkMode=${this.darkMode}
></ha-map> ></ha-map>
${this.helper ${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@ -69,7 +69,9 @@ export class HaMap extends ReactiveElement {
@property({ type: Boolean }) public fitZones = false; @property({ type: Boolean }) public fitZones = false;
@property({ type: Boolean }) public darkMode = false; @property({ type: Boolean }) public forceDarkMode = false;
@property({ type: Boolean }) public forceLightMode = false;
@property({ type: Number }) public zoom = 14; @property({ type: Number }) public zoom = 14;
@ -154,7 +156,8 @@ export class HaMap extends ReactiveElement {
} }
if ( if (
!changedProps.has("darkMode") && !changedProps.has("forceDarkMode") &&
!changedProps.has("forceLightMode") &&
(!changedProps.has("hass") || (!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode)) (oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
) { ) {
@ -164,11 +167,13 @@ export class HaMap extends ReactiveElement {
} }
private _updateMapStyle(): void { private _updateMapStyle(): void {
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false); const darkMode =
const forcedDark = this.darkMode; !this.forceLightMode &&
(this.forceDarkMode || (this.hass.themes.darkMode ?? false));
const map = this.renderRoot.querySelector("#map"); const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("dark", darkMode); map!.classList.toggle("dark", darkMode);
map!.classList.toggle("forced-dark", forcedDark); map!.classList.toggle("forced-dark", this.forceDarkMode);
map!.classList.toggle("forced-light", this.forceLightMode);
} }
private async _loadMap(): Promise<void> { private async _loadMap(): Promise<void> {
@ -398,8 +403,13 @@ export class HaMap extends ReactiveElement {
"--dark-primary-color" "--dark-primary-color"
); );
const className = const className = this.forceLightMode
this.darkMode || this.hass.themes.darkMode ? "dark" : "light"; ? "light"
: this.forceDarkMode
? "dark"
: this.hass.themes.darkMode
? "dark"
: "light";
for (const entity of this.entities) { for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)]; const stateObj = hass.states[getEntityId(entity)];
@ -543,27 +553,30 @@ export class HaMap extends ReactiveElement {
background: #090909; background: #090909;
} }
#map.forced-dark { #map.forced-dark {
color: #ffffff;
--map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5) --map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5)
contrast(1.2) saturate(0.3); contrast(1.2) saturate(0.3);
} }
#map.forced-light {
background: #ffffff;
color: #000000;
--map-filter: invert(0);
}
#map:active { #map:active {
cursor: grabbing; cursor: grabbing;
cursor: -moz-grabbing; cursor: -moz-grabbing;
cursor: -webkit-grabbing; cursor: -webkit-grabbing;
} }
.light {
color: #000000;
}
.dark {
color: #ffffff;
}
.leaflet-tile-pane { .leaflet-tile-pane {
filter: var(--map-filter); filter: var(--map-filter);
} }
.dark .leaflet-bar a { .dark .leaflet-bar a {
background-color: var(--card-background-color, #1c1c1c); background-color: #1c1c1c;
color: #ffffff; color: #ffffff;
} }
.dark .leaflet-bar a:hover {
background-color: #313131;
}
.leaflet-marker-draggable { .leaflet-marker-draggable {
cursor: move !important; cursor: move !important;
} }

View File

@ -138,7 +138,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
includeDomains includeDomains
); );
return { type: "map", entities: foundEntities }; return { type: "map", entities: foundEntities, theme_mode: "auto" };
} }
protected render() { protected render() {
@ -151,6 +151,14 @@ class HuiMapCard extends LitElement implements LovelaceCard {
(${this._error.code}) (${this._error.code})
</ha-alert>`; </ha-alert>`;
} }
const isDarkMode =
this._config.dark_mode || this._config.theme_mode === "dark"
? true
: this._config.theme_mode === "light"
? false
: this.hass.themes.darkMode;
return html` return html`
<ha-card id="card" .header=${this._config.title}> <ha-card id="card" .header=${this._config.title}>
<div id="root"> <div id="root">
@ -161,7 +169,9 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.paths=${this._getHistoryPaths(this._config, this._stateHistory)} .paths=${this._getHistoryPaths(this._config, this._stateHistory)}
.autoFit=${this._config.auto_fit || false} .autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones} .fitZones=${this._config.fit_zones}
?darkMode=${this._config.dark_mode} ?forceDarkMode=${this._config.theme_mode === "dark" ||
this._config.dark_mode}
?forceLightMode=${this._config.theme_mode === "light"}
interactiveZones interactiveZones
renderPassive renderPassive
></ha-map> ></ha-map>
@ -170,6 +180,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.map.reset_focus" "ui.panel.lovelace.cards.map.reset_focus"
)} )}
.path=${mdiImageFilterCenterFocus} .path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap} @click=${this._fitMap}
tabindex="0" tabindex="0"
></ha-icon-button> ></ha-icon-button>

View File

@ -3,7 +3,7 @@ import { ActionConfig } from "../../../data/lovelace/config/action";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { Statistic, StatisticType } from "../../../data/recorder"; import { Statistic, StatisticType } from "../../../data/recorder";
import { ForecastType } from "../../../data/weather"; import { ForecastType } from "../../../data/weather";
import { FullCalendarView, TranslationDict } from "../../../types"; import { FullCalendarView, ThemeMode, TranslationDict } from "../../../types";
import { LovelaceCardFeatureConfig } from "../card-features/types"; import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LegacyStateFilter } from "../common/evaluate-filter"; import { LegacyStateFilter } from "../common/evaluate-filter";
import { Condition, LegacyCondition } from "../common/validate-condition"; import { Condition, LegacyCondition } from "../common/validate-condition";
@ -314,6 +314,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
hours_to_show?: number; hours_to_show?: number;
geo_location_sources?: string[]; geo_location_sources?: string[];
dark_mode?: boolean; dark_mode?: boolean;
theme_mode?: ThemeMode;
} }
export interface MarkdownCardConfig extends LovelaceCardConfig { export interface MarkdownCardConfig extends LovelaceCardConfig {

View File

@ -1,3 +1,4 @@
import { mdiPalette } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { import {
@ -11,6 +12,7 @@ import {
string, string,
union, union,
} from "superstruct"; } from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { hasLocation } from "../../../../common/entity/has_location"; import { hasLocation } from "../../../../common/entity/has_location";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
@ -28,6 +30,7 @@ import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EntitiesEditorEvent } from "../types"; import { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { LocalizeFunc } from "../../../../common/translations/localize";
export const mapEntitiesConfigStruct = union([ export const mapEntitiesConfigStruct = union([
object({ object({
@ -50,30 +53,11 @@ const cardConfigStruct = assign(
hours_to_show: optional(number()), hours_to_show: optional(number()),
geo_location_sources: optional(array(string())), geo_location_sources: optional(array(string())),
auto_fit: optional(boolean()), auto_fit: optional(boolean()),
theme_mode: optional(string()),
}) })
); );
const SCHEMA = [ const themeModes = ["auto", "light", "dark"] as const;
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{
name: "default_zoom",
default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } },
},
{ name: "dark_mode", selector: { boolean: {} } },
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 0 } },
},
],
},
] as const;
@customElement("hui-map-card-editor") @customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@ -83,8 +67,68 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@state() private _configEntities?: EntityConfig[]; @state() private _configEntities?: EntityConfig[];
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "expandable",
iconPath: mdiPalette,
title: localize(`ui.panel.lovelace.editor.card.map.appearance`),
schema: [
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{
name: "default_zoom",
default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } },
},
{
name: "theme_mode",
default: "auto",
selector: {
select: {
mode: "dropdown",
options: themeModes.map((themeMode) => ({
value: themeMode,
label: localize(
`ui.panel.lovelace.editor.card.map.theme_modes.${themeMode}`
),
})),
},
},
},
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 0 } },
},
],
},
],
},
] as const
);
public setConfig(config: MapCardConfig): void { public setConfig(config: MapCardConfig): void {
assert(config, cardConfigStruct); assert(config, cardConfigStruct);
// Migrate legacy dark_mode to theme_mode
if (!this._config && !("theme_mode" in config)) {
config = { ...config };
if (config.dark_mode) {
config.theme_mode = "dark";
} else {
config.theme_mode = "auto";
}
delete config.dark_mode;
fireEvent(this, "config-changed", { config: config });
}
this._config = config; this._config = config;
this._configEntities = config.entities this._configEntities = config.entities
? processEditorEntities(config.entities) ? processEditorEntities(config.entities)
@ -104,33 +148,32 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${this._config} .data=${this._config}
.schema=${SCHEMA} .schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<div class="card-config">
<hui-entity-editor <hui-entity-editor
.hass=${this.hass} .hass=${this.hass}
.entities=${this._configEntities} .entities=${this._configEntities}
.entityFilter=${hasLocation} .entityFilter=${hasLocation}
@entities-changed=${this._entitiesValueChanged} @entities-changed=${this._entitiesValueChanged}
></hui-entity-editor> ></hui-entity-editor>
<h3>
${this.hass.localize( <h3>
"ui.panel.lovelace.editor.card.map.geo_location_sources" ${this.hass.localize(
)} "ui.panel.lovelace.editor.card.map.geo_location_sources"
</h3> )}
<div class="geo_location_sources"> </h3>
<hui-input-list-editor
.inputLabel=${this.hass.localize( <hui-input-list-editor
"ui.panel.lovelace.editor.card.map.source" .inputLabel=${this.hass.localize(
)} "ui.panel.lovelace.editor.card.map.source"
.hass=${this.hass} )}
.value=${this._geo_location_sources} .hass=${this.hass}
@value-changed=${this._geoSourcesChanged} .value=${this._geo_location_sources}
></hui-input-list-editor> @value-changed=${this._geoSourcesChanged}
</div> ></hui-input-list-editor>
</div>
`; `;
} }
@ -170,9 +213,14 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
fireEvent(this, "config-changed", { config: ev.detail.value }); fireEvent(this, "config-changed", { config: ev.detail.value });
} }
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => { private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) { switch (schema.name) {
case "dark_mode": case "theme_mode":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}`
);
case "default_zoom": case "default_zoom":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}` `ui.panel.lovelace.editor.card.map.${schema.name}`
@ -185,16 +233,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
}; };
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [configElementStyle, css``];
configElementStyle,
css`
.geo_location_sources {
padding-left: 20px;
padding-inline-start: 20px;
direction: var(--direction);
}
`,
];
} }
} }

View File

@ -5835,7 +5835,14 @@
"name": "Map", "name": "Map",
"geo_location_sources": "Geolocation sources", "geo_location_sources": "Geolocation sources",
"dark_mode": "Dark mode?", "dark_mode": "Dark mode?",
"default_zoom": "Default zoom", "appearance": "Appearance",
"theme_mode": "Theme Mode",
"theme_modes": {
"auto": "Auto",
"light": "Light",
"dark": "Dark"
},
"default_zoom": "Default Zoom",
"source": "Source", "source": "Source",
"description": "The Map card that allows you to display entities on a map." "description": "The Map card that allows you to display entities on a map."
}, },

View File

@ -139,6 +139,8 @@ export type FullCalendarView =
| "dayGridDay" | "dayGridDay"
| "listWeek"; | "listWeek";
export type ThemeMode = "auto" | "light" | "dark";
export interface ToggleButton { export interface ToggleButton {
label: string; label: string;
iconPath?: string; iconPath?: string;