Compare commits

...

104 Commits

Author SHA1 Message Date
karwosts
ebff35d17f
Fix more-info media source select (#29400) 2026-02-04 17:18:49 +01:00
Wendelin
aec4a06156
migrate ha-select to ha-dropdown (#29392)
* migrate ha-select to ha-dropdown

* remove ha-menu

* review

* Fix eslint error

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-02-04 13:47:15 +00:00
Dominik Bruhn
917f2b4434
Add tag-id column in tag table (#29383) 2026-02-04 14:35:56 +01:00
Paul Bottein
79ec6b972e
Change default icon for blank area if not icon configured (#29394) 2026-02-04 14:33:27 +01:00
Paul Bottein
9e35befa99
Remove old lovelace overview from pickers (#29390) 2026-02-04 12:03:29 +01:00
Paul Bottein
75160d67d3
Load domain translation when integration page load (#29391) 2026-02-04 11:58:27 +01:00
Tom Carpenter
b145d09041
Fix Horizontal Scrolling on System Logs Page (#29375) 2026-02-04 08:32:10 +00:00
Aidan Timson
f3f7a1e46a
Migrate siren advanced controls to wa-dialog (more info) (#29369)
* Migrate siren advanced controls to wa (more info)

* Fix footer

* Update src/dialogs/more-info/components/siren/ha-more-info-siren-advanced-controls.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-04 06:45:50 +00:00
Simon Lamon
091315d9a9
Fixup dev container (#29376)
* Fixup dev container

* Fix yarn installation command in bootstrap script

* Fast restart
2026-02-04 08:33:39 +02:00
renovate[bot]
75b830cdf9
Update dependency globals to v17.3.0 (#29385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 08:22:40 +02:00
renovate[bot]
e4b8352832
Update formatjs monorepo (#29386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 06:14:54 +01:00
karwosts
4e193187f9
Don't shrink ha-dropdown checkboxes (#29387) 2026-02-04 06:14:39 +01:00
Paul Bottein
5394b3b8cf
Add translations for new overview dialog (#29382) 2026-02-03 23:37:24 +01:00
ildar170975
2ab867986a
Data tables: standardize columns (#29155)
* Create data-table-columns.ts

* Update data-table-columns.ts

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* fix a translation key

* move commonly used translations to generic

* remove a translation for another PR

* restore "domain" translation for while

* resolving conflicts

* resolve conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* fix conflicts

* fix conflicts

* fix import

* fix import
2026-02-03 21:37:53 +01:00
renovate[bot]
a1c3a6c662
Update babel monorepo to v7.29.0 (#29379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 21:37:34 +01:00
Benedikt Johannes
11296adbd4
Coversation -> Conversation (#29378)
* Coversation -> Conversation

* Update en.json
2026-02-03 20:22:32 +01:00
Paul Bottein
4e04f4284e
Use area icon for area empty state (#29371) 2026-02-03 17:58:37 +01:00
Paul Bottein
a0cc0d9cca
Improve other devices page in home dashboard (#29370) 2026-02-03 15:50:49 +00:00
uptimeZERO_
c925053bb8
Animate app side bar (#29026) 2026-02-03 14:55:10 +00:00
Aidan Timson
22a7aa8f8e
Add default view transition to edit badge and card (#29360) 2026-02-03 14:33:20 +00:00
Petar Petrov
3a5f719a3e
Fix chart theme colors in Lovelace edit mode (#29361)
When edit mode is toggled, existing cards are moved into edit mode
wrappers. This triggers connectedCallback which was calling _setupChart
synchronously before the browser recalculated CSS inheritance. The
chart would read stale CSS custom properties, resulting in low-contrast
axis labels in dark theme.

Defer _setupChart using afterNextRender to allow the browser to complete
layout and CSS recalculation first. Guard conditions prevent issues with
rapid connect/disconnect cycles.
2026-02-03 16:27:17 +02:00
Aidan Timson
7b7182c147
Migrate state card select/input_select to select menu (#29362)
* Migrate state card input select to select menu

* Sort

* Migrate state card select to select menu
2026-02-03 16:26:41 +02:00
Paul Bottein
0eb7229819
Hide edit and delete actions for YAML dashboards in config (#29368)
YAML dashboards are defined in configuration files and cannot be
modified or deleted through the UI. This change ensures the edit
and delete actions are only shown for storage-mode dashboards.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:25:40 +02:00
Petar Petrov
fcc6f1b5e9
Move dialog scrim to pseudo-element (#29357) 2026-02-03 14:28:51 +01:00
Paul Bottein
19dc2a5865
Add missing danger variant in dropdown item (#29359) 2026-02-03 11:37:42 +00:00
Wendelin
ae63530123
Migrate ha-control-select-menu to use ha-dropdown (#29350) 2026-02-03 10:30:51 +00:00
uptimeZERO_
97e1f47af9
Add transition for more-info-dialog when toggling expand state (#29341) 2026-02-03 09:21:29 +00:00
Paul Bottein
346d916944
Add an disable view transition as it crashes chrome browser when using chrome dev tools (#29339) 2026-02-03 09:10:16 +01:00
Kristel
ab326b3277
Refactor _applyFilters for Scenes and Scripts pages (#29116)
* proper functionnames

* refactor _applyFilters on scenes and script page

* Apply suggestions from code review

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-03 07:50:02 +00:00
Tom
55e251c04f
Expose target_humidity_step value to climate (#28834)
Add target_humidity_step to climate
2026-02-03 09:34:53 +02:00
Darryn Capes-Davis
b902f3e6f5
Fix CSS minification issue for ha-card (#29354) 2026-02-03 09:19:06 +02:00
ildar170975
9daac6c49f
Remove "type" where "attribute: false" (#29356)
remove "type" when "attribute: false"
2026-02-03 09:17:33 +02:00
Benedikt Johannes
65ea0c9121
Fix "unamed" -> "unnamed" (potential crash / non registered translation information bug) (#29351)
* Fix typo

* Update home-area-view-strategy.ts

* Update home-other-devices-view-strategy.ts
2026-02-03 05:55:11 +00:00
Denis Shulyaka
505cc698f6
Fallback data flow section label translation (#29352) 2026-02-03 05:54:18 +00:00
karwosts
b0f9b31dae
Fix missing imports in devtools-statistics (#29355) 2026-02-03 06:45:39 +01:00
Norbert Rittel
2b2966a214
Make explanation of "search command(s)" shortcut consistent (#29345)
Change "search command" to "search commands"
2026-02-02 17:49:45 +01:00
renovate[bot]
558b251e32
Update dependency @codemirror/view to v6.39.12 (#29349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:41:06 +01:00
renovate[bot]
df28eaa99e
Update dependency tar to v7.5.7 [SECURITY] (#29348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:40:45 +01:00
Petar Petrov
40342c9cfd
Remove redundant dialog backdrop color (#29337) 2026-02-02 17:12:27 +01:00
Paul Bottein
c6573b9c1b
Fix "Reload resources" menu for YAML resource mode (#29346) 2026-02-02 16:07:10 +00:00
karwosts
54c46ad362
Entity diagnostic - handle entity not in the registry (#29344) 2026-02-02 17:00:26 +01:00
Wendelin
3376036392
Fix dropdown width in datatables (#29340) 2026-02-02 13:49:54 +00:00
Paul Bottein
ff5ecc047a
Fix type error for missing hass.themes race condition in themes mixin (#29338) 2026-02-02 13:44:24 +00:00
Jeremy Cook
a8393cddd4
Improved HTML presentation table support to ha-markdown, compatible w… (#29108)
Improved HTML presentation table support to ha-markdown, compatible with companion app on android and iOS

- Support valign attribute for vertical alignment (top, middle, bottom, baseline)
- Support border attribute for cell borders (0, 1, 2, 3px)
- Set default styling for presentation tables (no borders, no padding)
- Use CSS variables for customization
- Include future-proof @supports rule for attr() function
- Maintain backward compatibility with existing markdown tables
2026-02-02 15:17:12 +02:00
Tom Carpenter
edfc33039b
Merge Long Term Statistics for Power Sensors in Energy Dashboard (#29319)
* Merge Long Term Statistics for Power Sensors in Energy Dashboard

When using 5minute data for the power sources chart, data would be missing if the selected range was beyond the short term statistics limit. This change takes data from long term statistics and merges it in to the power sources data if the short term statistics doesn't extend far enough back for the selection.

* Skip for zero-length power statistics

Prevent out of bounds array access if power statistics has no entries.
2026-02-02 15:11:52 +02:00
ildar170975
14f8d982a9
hui-gauge-card-editor: use imported code for actions (#29326)
use imported code for actions
2026-02-02 15:02:22 +02:00
Marcin Bauer
847a040fa7
Keep focus on search field when clicking filter chips (#29249)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:45:54 +01:00
Aidan Timson
8611359481
Ensure template renderer overflows on overflow (#29335) 2026-02-02 12:07:49 +00:00
Kristel
f473ebf18c
bugfix: add eventlistener for exposed-entities-changed to Entities page (#29299) 2026-02-02 11:32:32 +00:00
Wendelin
9010898742
Fix missing ha-md-menu in config/labels (#29334) 2026-02-02 11:15:33 +00:00
Petar Petrov
1e30394bf3
Append current entity state to history and statistics charts (#29273) 2026-02-02 09:34:50 +00:00
Paulus Schoutsen
bca2cb0c1e
Conditionally show HTML5 push notifications (#29227) 2026-02-02 09:24:42 +00:00
Aidan Timson
29317eb842
Show hint only if keyboard shortcuts is enabled (#29332)
Enabled by default, must be explicity disabled
2026-02-02 10:59:14 +02:00
Wendelin
a8327ef59a
Revert "Fix automation sidebar ui supported check" (#29331) 2026-02-02 08:57:19 +00:00
Petar Petrov
01c8832024
Add area dashboard link to navigation picker (#29264) 2026-02-02 09:51:03 +01:00
Linus Rath
a8c633e627
Update untracked consumption threshold to 1W (#29310) 2026-02-02 08:16:34 +00:00
Simon Lamon
b659671814
Bump codespace to Python 3.14 (#29316) 2026-02-02 09:52:07 +02:00
dependabot[bot]
3060cdf355
Bump github/codeql-action from 4.31.11 to 4.32.0 (#29329)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.11 to 4.32.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](19b2f06db2...b20883b0cd)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 08:44:06 +02:00
dependabot[bot]
215241df56
Bump actions/cache from 5.0.2 to 5.0.3 (#29328)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](8b402f58fb...cdf6c1fa76)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 08:43:49 +02:00
renovate[bot]
f6852894b0
Update dependency @lokalise/node-api to v15.6.1 (#29322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 06:33:24 +01:00
renovate[bot]
86d7205a3a
Update dependency @braintree/sanitize-url to v7.1.2 (#29325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 06:33:03 +01:00
Norbert Rittel
9c16ceda71
Also replace "Sensor type" with "Type of power measurement" (#29317)
Replace "Sensor type" with "Type of power measurement"
2026-02-01 10:44:47 +01:00
karwosts
288789a604
Use ha-form for condition template (#29301) 2026-02-01 10:18:27 +01:00
Norbert Rittel
09139d5bec
Replace "Power sensor type" with "Type of power measurement" (#29305) 2026-02-01 09:54:56 +01:00
ildar170975
2d90be9af3
ha-config-device-page: fix placement for tooltip (#29302)
fix placement for tooltip
2026-02-01 09:39:08 +01:00
renovate[bot]
1c6464663e
Update dependency @rsdoctor/rspack-plugin to v1.5.1 (#29297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 09:37:46 +01:00
renovate[bot]
189f0b9472
Update dependency node-vibrant to v4.0.4 (#29293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 09:04:52 +02:00
renovate[bot]
771f5eaff4
Update dependency @vibrant/color to v4.0.4 (#29292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 08:49:36 +02:00
renovate[bot]
a4cb3b5b01
Update dependency globals to v17.2.0 (#29288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 16:51:11 +00:00
Norbert Rittel
eb1ae99a1f
Replace "home page" with "Overview page" for preferences panel (#29284)
Replace "home page" with "Overview page"
2026-01-30 17:46:34 +01:00
Aidan Timson
655643eb3f
Cleanup padding on matter dashboard expansion panel (#29286)
Cleanup padding on matter config expansion panel
2026-01-30 17:43:32 +01:00
Aidan Timson
02eb1e6832
Fix scrolling for labs page (#29287) 2026-01-30 17:41:40 +01:00
karwosts
62f7a2eea1
Fix areas cannot be deleted (#29285) 2026-01-30 14:01:31 +00:00
Aidan Timson
a7f9b93018
Fix type error for missing hass.config race condition in themes mixin (#29280) 2026-01-30 12:54:08 +01:00
Paul Bottein
3e7011e2c8
Fix demo because of new default panel (#29279) 2026-01-30 11:48:47 +01:00
Aidan Timson
97f89bd983
Add missing settings nav items for quick search (#29278)
* Add missing repairs quick search item

* Add voice assistants
2026-01-30 12:11:09 +02:00
renovate[bot]
9aedaeabbf
Update dependency @rspack/core to v1.7.4 (#29275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 11:58:31 +02:00
Aidan Timson
7e839dc895
Change default shortcut tip in Quick Search to mod+k, add tip to settings (#29253) 2026-01-30 09:48:03 +00:00
Wendelin
1f6c916d11
Fix multi select in quick search (#29272)
Add item selection state management to QuickBar component
2026-01-30 10:14:32 +01:00
Wendelin
71bd12bb90
Fix --wa-color-text-normal (#29271)
Update normal text color variable in wa.globals.ts
2026-01-30 09:48:52 +02:00
Simon Lamon
e741c14482
Remove duplicated text (#29265) 2026-01-30 08:46:18 +01:00
Wendelin
a496448ed9
Fix device download diagnostic via overflow (#29269)
fix diagnostic download link handling to simplify URL signing
2026-01-30 09:42:40 +02:00
renovate[bot]
c772285358
Update dependency typescript-eslint to v8.54.0 (#29267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 08:13:50 +02:00
renovate[bot]
4fcdd09935
Update dependency @bundle-stats/plugin-webpack-filter to v4.21.9 (#29266)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 08:13:30 +02:00
Paul Bottein
157e89e5e7
Fix default lovelace yaml loading (#29240) 2026-01-29 23:26:39 +01:00
Wendelin
c223f932f3
Implement fallback for dialog close event in Quick Search (#29260) 2026-01-29 17:41:12 +01:00
Wendelin
fcd63e7cba
Prevent quick search to close from hot keys (#29251) 2026-01-29 17:20:26 +01:00
Paul Bottein
c1bab376c8
Remove default title for new dashboards (#29259) 2026-01-29 16:05:44 +00:00
Aidan Timson
01f5df6671
Add protocols to quick search (#29248)
Add protocols to quick search, extract logic and translations
2026-01-29 17:37:09 +02:00
TheJulianJES
241a655765
Fix Matter dashboard using disabled and ignored config entries (#29254) 2026-01-29 15:27:49 +00:00
Paul Bottein
9841a6341a
Remove unused theme option in distribution card (#29250) 2026-01-29 15:26:46 +00:00
Paul Bottein
44c917b4b7
Prevent action in tile container (#29257) 2026-01-29 15:25:55 +00:00
uptimeZERO_
bff785ca68
Move theme settings to user settings (#29255) 2026-01-29 15:14:34 +00:00
Paul Bottein
1f15724024
Fix actions in dashboard overflow menu (#29256) 2026-01-29 15:46:10 +01:00
Paul Bottein
ceaa3b8c17
Stop click propagation when clicking item in icon overflow (#29252) 2026-01-29 15:16:37 +01:00
Aidan Timson
7ed3ac1e24
Fixes for picker combo box scrolling and selection (#29242) 2026-01-29 15:11:30 +01:00
ildar170975
cfcb649a6f
computeGroupEntitiesState(): fix condition (#29234)
* fix condition

* fix condition

* prettier
2026-01-29 15:35:53 +02:00
Aidan Timson
ae036f4084
Remove unused "app" item from quick search (#29244) 2026-01-29 12:18:54 +00:00
Wendelin
6cca48e79d
Fix quick search apps (#29238) 2026-01-29 11:30:09 +00:00
Paul Bottein
feb9ce421d
Fix default yaml lovelace panel loading (#29230) 2026-01-28 23:32:46 +01:00
Marcin Bauer
9639403865
Reorder profile settings to prioritize user preferences (#29202)
Reorganizes the User settings card on the profile page to show more
frequently used settings first:

- Default dashboard
- Customize sidebar
- Advanced mode (admin)
- Entity ID picker (admin)

Then adds a divider followed by localization settings:

- Language, number format, time format, date format, time zone, first weekday

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:56:45 -05:00
Aidan Timson
8e8cb095b1
Add meta+click/enter support to quick search (#29220)
* Allow meta+click event from combobox

* Handle new tab events for navigations

* Add mod+enter support for new tab

* Helper function
2026-01-28 18:52:15 +01:00
Paul Bottein
60079ce999
Add welcome banner for new overview dashboard (#29223) 2026-01-28 18:46:22 +01:00
Petar Petrov
4f3196adb9
Add non standard power sensor support (#28845)
* Add non standard power sensor support

* remove useless code

* GridPowerSourceInput type for grid power source saving
2026-01-28 18:20:35 +02:00
285 changed files with 5484 additions and 4799 deletions

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.13
FROM mcr.microsoft.com/devcontainers/python:3.14
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
node_modules/.cache/prettier

View File

@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0

View File

@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.13"
PYTHON_VERSION: "3.14"
NODE_OPTIONS: --max_old_space_size=6144
permissions:

View File

@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.13"
PYTHON_VERSION: "3.14"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@ -84,7 +84,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@2025.12.0
with:
abi: cp313
abi: cp314
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}

View File

@ -9,11 +9,14 @@ import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockDeviceRegistry } from "./stubs/device_registry";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
import { mockLabelRegistry } from "./stubs/label_registry";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl {
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",

View File

@ -27,4 +27,25 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS(
"frontend/subscribe_system_data",
(_msg, currentHass, onChange) => {
onChange?.({
value: currentHass.systemData,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
}
);
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
onChange?.({
preview_feature: _msg.preview_feature,
domain: _msg.domain,
enabled: false,
is_built_in: true,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
};

View File

@ -7,8 +7,18 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
})
);
hass.mockWS("render_template", (msg, _hass, onChange) => {
let result = msg.template;
// Simple variable substitution for demo purposes
if (msg.variables) {
for (const [key, value] of Object.entries(msg.variables)) {
result = result.replace(
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
String(value)
);
}
}
onChange!({
result: msg.template,
result,
listeners: { all: false, domains: [], entities: [], time: false },
});
// eslint-disable-next-line @typescript-eslint/no-empty-function

View File

@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"

View File

@ -27,25 +27,25 @@
"type": "module",
"dependencies": {
"@babel/runtime": "7.28.6",
"@braintree/sanitize-url": "7.1.1",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/language": "6.12.1",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.39.11",
"@codemirror/view": "6.39.12",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.2.0",
"@formatjs/intl-displaynames": "7.2.0",
"@formatjs/intl-durationformat": "0.10.0",
"@formatjs/intl-getcanonicallocales": "3.2.0",
"@formatjs/intl-listformat": "8.2.0",
"@formatjs/intl-locale": "5.2.0",
"@formatjs/intl-numberformat": "9.2.1",
"@formatjs/intl-pluralrules": "6.2.1",
"@formatjs/intl-relativetimeformat": "12.2.1",
"@formatjs/intl-datetimeformat": "7.2.1",
"@formatjs/intl-displaynames": "7.2.1",
"@formatjs/intl-durationformat": "0.10.1",
"@formatjs/intl-getcanonicallocales": "3.2.1",
"@formatjs/intl-listformat": "8.2.1",
"@formatjs/intl-locale": "5.2.1",
"@formatjs/intl-numberformat": "9.2.2",
"@formatjs/intl-pluralrules": "6.2.2",
"@formatjs/intl-relativetimeformat": "12.2.2",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@ -71,7 +71,6 @@
"@material/mwc-icon-button": "0.27.0",
"@material/mwc-linear-progress": "0.27.0",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
@ -89,7 +88,7 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vibrant/color": "4.0.0",
"@vibrant/color": "4.0.4",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@ -112,7 +111,7 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.1.1",
"intl-messageformat": "11.1.2",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@ -122,7 +121,7 @@
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
@ -146,17 +145,17 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.28.6",
"@babel/core": "7.29.0",
"@babel/helper-define-polyfill-provider": "0.6.6",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.6",
"@bundle-stats/plugin-webpack-filter": "4.21.8",
"@lokalise/node-api": "15.6.0",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@bundle-stats/plugin-webpack-filter": "4.21.9",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.0",
"@rspack/core": "1.7.3",
"@rsdoctor/rspack-plugin": "1.5.1",
"@rspack/core": "1.7.4",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@ -211,11 +210,11 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.1",
"tar": "7.5.6",
"tar": "7.5.7",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.53.1",
"typescript-eslint": "8.54.0",
"vite-tsconfig-paths": "6.0.5",
"vitest": "4.0.18",
"webpack-stats-plugin": "1.1.3",
@ -229,7 +228,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"globals": "17.1.0",
"globals": "17.3.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"

View File

@ -12,7 +12,7 @@ readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.13.0"
requires-python = ">=3.14.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"

View File

@ -16,6 +16,12 @@ if [[ -n "$DEVCONTAINER" ]]; then
nvm install --reinstall-packages-from="$nodeCurrent" --default
nvm uninstall "$nodeCurrent"
fi
# install yarn if not already available
if ! command -v yarn &> /dev/null; then
npm install -g corepack
yes | yarn
fi
fi
if ! command -v yarn &> /dev/null; then

View File

@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"

View File

@ -0,0 +1,29 @@
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../types";
import { canShowPage } from "./can_show_page";
export interface NavigationFilterOptions {
/** Whether there are Bluetooth config entries (pre-fetched by caller) */
hasBluetoothConfigEntries?: boolean;
}
/**
* Filters navigation pages based on visibility rules.
* Handles special cases like Bluetooth (requires config entries)
* and external app configuration.
*/
export const filterNavigationPages = (
hass: HomeAssistant,
pages: PageNavigation[],
options: NavigationFilterOptions = {}
): PageNavigation[] =>
pages.filter((page) => {
if (page.path === "#external-app-configuration") {
return hass.auth.external?.config.hasSettingsScreen;
}
// Only show Bluetooth page if there are Bluetooth config entries
if (page.component === "bluetooth") {
return options.hasBluetoothConfigEntries ?? false;
}
return canShowPage(hass, page);
});

View File

@ -8,7 +8,9 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
return UNAVAILABLE;
}
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
const validState = states.some(
(stateObj) => !isUnavailableState(stateObj.state)
);
if (!validState) {
return UNAVAILABLE;

View File

@ -16,6 +16,7 @@ export interface ShortcutConfig {
* Default is false to avoid interrupting copy/paste.
*/
allowWhenTextSelected?: boolean;
allowInInput?: boolean;
}
/**
@ -29,7 +30,10 @@ function registerShortcuts(
Object.entries(shortcuts).forEach(([key, config]) => {
wrappedShortcuts[key] = (event: KeyboardEvent) => {
if (!canOverrideAlphanumericInput(event.composedPath())) {
if (
!config.allowInInput &&
!canOverrideAlphanumericInput(event.composedPath())
) {
return;
}
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {

View File

@ -1,3 +1,15 @@
let isViewTransitionDisabled = false;
try {
isViewTransitionDisabled =
window.localStorage.getItem("disableViewTransition") === "true";
} catch {
// ignore
}
export const setViewTransitionDisabled = (disabled: boolean): void => {
isViewTransitionDisabled = disabled;
};
/**
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
*
@ -14,7 +26,7 @@
export const withViewTransition = (
callback: (viewTransitionAvailable: boolean) => void
): Promise<void> => {
if (!document.startViewTransition) {
if (!document.startViewTransition || isViewTransitionDisabled) {
callback(false);
return Promise.resolve();
}

View File

@ -19,6 +19,7 @@ import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
@ -27,6 +28,7 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
@ -92,10 +94,18 @@ export class HaChartBase extends LitElement {
private _resizeAnimationDuration?: number;
private _suspendResize = false;
private _layoutTransitionActive = false;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => {
if (this.chart) {
if (this._suspendResize) {
this._shouldResizeChart = true;
return;
}
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = true;
} else {
@ -113,8 +123,11 @@ export class HaChartBase extends LitElement {
private _originalZrFlush?: () => void;
private _pendingSetup = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
}
@ -126,7 +139,13 @@ export class HaChartBase extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._setupChart();
this._pendingSetup = true;
afterNextRender(() => {
if (this.isConnected && this._pendingSetup) {
this._pendingSetup = false;
this._setupChart();
}
});
}
this._listeners.push(
@ -181,6 +200,26 @@ export class HaChartBase extends LitElement {
() => window.removeEventListener("keyup", handleKeyUp)
);
}
const handleLayoutTransition: EventListener = (ev) => {
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
this._layoutTransitionActive = Boolean(event.detail?.active);
this.toggleAttribute(
"layout-transition-active",
this._layoutTransitionActive
);
this._suspendResize = this._layoutTransitionActive;
if (!this._suspendResize) {
this._resizeChartIfNeeded();
}
};
window.addEventListener("hass-layout-transition", handleLayoutTransition);
this._listeners.push(() =>
window.removeEventListener(
"hass-layout-transition",
handleLayoutTransition
)
);
}
protected firstUpdated() {
@ -988,19 +1027,29 @@ export class HaChartBase extends LitElement {
}
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize({
animation:
this._reducedMotion ||
typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
this._resizeChartIfNeeded();
};
private _resizeChartIfNeeded() {
if (!this.chart || !this._shouldResizeChart) {
return;
}
if (this._suspendResize) {
return;
}
if (!this.chart.getZr().animation.isFinished()) {
return;
}
this.chart.resize({
animation:
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
@ -1022,11 +1071,18 @@ export class HaChartBase extends LitElement {
display: block;
position: relative;
letter-spacing: normal;
overflow: visible;
}
:host([layout-transition-active]),
:host([layout-transition-active]) .container,
:host([layout-transition-active]) .chart-container {
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
position: relative;
overflow: visible;
}
.container.has-height {
max-height: var(--chart-max-height, 350px);
@ -1034,6 +1090,7 @@ export class HaChartBase extends LitElement {
.chart-container {
width: 100%;
max-height: var(--chart-max-height, 350px);
overflow: visible;
}
.has-height .chart-container {
flex: 1;

View File

@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property({ type: String, attribute: false }) public valueFormatter?: (
@property({ attribute: false }) public valueFormatter?: (
value: number
) => string;

View File

@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement {
@property({ attribute: false }) public data?: SunburstNode;
@property({ type: String, attribute: false }) public valueFormatter?: (
@property({ attribute: false }) public valueFormatter?: (
value: number
) => string;

View File

@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ attribute: false }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ attribute: false }) public chartIndex?;
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@ -716,6 +716,18 @@ export class StateHistoryChartLine extends LitElement {
// Add an entry for final values
pushData(endTime, prevValues);
// For sensors, append current state if viewing recent data
const now = new Date();
// allow 1s of leeway for "now"
const isUpToNow = now.getTime() - endTime.getTime() <= 1000;
if (domain === "sensor" && isUpToNow && data.length === 1) {
const stateObj = this.hass.states[states.entity_id];
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
if (currentValue !== null) {
data[0].data!.push([now, currentValue]);
}
}
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});

View File

@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ attribute: false }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ attribute: false }) public chartIndex?;
@property({ attribute: "hide-reset-button", type: Boolean })
public hideResetButton?: boolean;

View File

@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ attribute: false, type: Number }) public hoursToShow?: number;
@property({ attribute: false }) public hoursToShow?: number;
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;

View File

@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement {
@property({ attribute: false }) public endTime?: Date;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@ -605,6 +605,57 @@ export class StatisticsChart extends LitElement {
}
});
// Append current state if viewing recent data
const now = new Date();
// allow 10m of leeway for "now", because stats are 5 minute aggregated
const isUpToNow = now.getTime() - endTime.getTime() <= 600000;
if (isUpToNow) {
// Skip external statistics (they have ":" in the ID)
if (!statistic_id.includes(":")) {
const stateObj = this.hass.states[statistic_id];
if (stateObj) {
const currentValue = parseFloat(stateObj.state);
if (
isFinite(currentValue) &&
!this._hiddenStats.has(statistic_id)
) {
// First, close out the last stat segment at prevEndTime
const lastEndTime = prevEndTime;
const lastValues = prevValues;
if (lastEndTime && lastValues) {
statDataSets.forEach((d, i) => {
d.data!.push(
this._transformDataValue([lastEndTime, ...lastValues[i]!])
);
});
}
// Then push the current state at now
statTypes.forEach((type, i) => {
const val: (number | null)[] = [];
if (type === "sum" || type === "change") {
// Skip cumulative types - need special calculation
val.push(null);
} else if (
type === bandTop &&
this.chartType === "line" &&
drawBands &&
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
) {
// For band chart, current value is both min and max, so diff is 0
val.push(0);
val.push(currentValue);
} else {
val.push(currentValue);
}
statDataSets[i].data!.push(
this._transformDataValue([now, ...val])
);
});
}
}
}
}
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
Array.prototype.push.apply(legendData, statLegendData);

View File

@ -130,9 +130,9 @@ export class HaDataTable extends LitElement {
// eslint-disable-next-line lit/no-native-attributes
@property({ type: String }) public id = "id";
@property({ attribute: false, type: String }) public noDataText?: string;
@property({ attribute: false }) public noDataText?: string;
@property({ attribute: false, type: String }) public searchLabel?: string;
@property({ attribute: false }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;

View File

@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement {
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
/**
* Show only devices with entities from specific domains.

View File

@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement {
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
@property({ type: Boolean })
public reorder = false;

View File

@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
/**
* Show entities from specific domains.

View File

@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public statisticIds?: StatisticsMetaData[];
@property({ attribute: false }) public helpMissingEntityUrl =

View File

@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ attribute: false, type: Array }) public statisticIds?: string[];
@property({ attribute: false }) public statisticIds?: string[];
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";

View File

@ -48,7 +48,6 @@ export class HaAnsiToHtml extends LitElement {
static styles = css`
pre {
overflow-x: auto;
margin: 0;
}
pre.wrap {

View File

@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement {
@property({ type: Boolean, attribute: "disable-speech" })
public disableSpeech = false;
@property({ type: Boolean, attribute: false })
@property({ attribute: false })
public startListening?: boolean;
@query("#message-input") private _messageInput!: HaTextField;

View File

@ -2,14 +2,12 @@ import type { PropertyValueMap } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import type { AssistPipeline } from "../data/assist_pipeline";
import { listAssistPipelines } from "../data/assist_pipeline";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import type { HaSelectOption } from "./ha-select";
const PREFERRED = "preferred";
const LAST_USED = "last_used";
@ -41,6 +39,31 @@ export class HaAssistPipelinePicker extends LitElement {
return nothing;
}
const value = this.value ?? this._default;
const options: HaSelectOption[] = [
{
value: PREFERRED,
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
}),
},
];
if (this.includeLastUsed) {
options.unshift({
value: LAST_USED,
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
});
}
options.push(
...this._pipelines.map((pipeline) => ({
value: pipeline.id,
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
}))
);
return html`
<ha-select
.label=${this.label ||
@ -49,33 +72,8 @@ export class HaAssistPipelinePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${this.includeLastUsed
? html`
<ha-list-item .value=${LAST_USED}>
${this.hass!.localize(
"ui.components.pipeline-picker.last_used"
)}
</ha-list-item>
`
: null}
<ha-list-item .value=${PREFERRED}>
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
})}
</ha-list-item>
${this._pipelines.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline.id}>
${pipeline.name}
(${formatLanguageCode(pipeline.language, this.hass.locale)})
</ha-list-item>`
)}
</ha-select>
`;
}
@ -96,17 +94,17 @@ export class HaAssistPipelinePicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === this._default)
value === "" ||
value === this.value ||
(this.value === undefined && value === this._default)
) {
return;
}
this.value = target.value === this._default ? undefined : target.value;
this.value = value === this._default ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@ -4,10 +4,8 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-list-item";
import "./ha-select";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -260,14 +258,10 @@ export class HaBaseTimeInput extends LitElement {
.required=${this.required}
.value=${this.amPm}
.disabled=${this.disabled}
name="amPm"
naturalMenuWidth
fixedMenuPosition
.name=${"amPm"}
@selected=${this._valueChanged}
@closed=${stopPropagation}
.options=${["AM", "PM"]}
>
<ha-list-item value="AM">AM</ha-list-item>
<ha-list-item value="PM">PM</ha-list-item>
</ha-select>`}
</div>
${this.helper
@ -282,10 +276,12 @@ export class HaBaseTimeInput extends LitElement {
fireEvent(this, "value-changed");
}
private _valueChanged(ev: InputEvent) {
private _valueChanged(ev: InputEvent | CustomEvent<{ value: string }>): void {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
textField.name === "amPm" ? textField.value : Number(textField.value);
textField.name === "amPm"
? (ev as CustomEvent<{ value: string }>).detail.value
: Number(textField.value);
const value: TimeChangedEvent = {
hours: this.hours,
minutes: this.minutes,
@ -366,10 +362,6 @@ export class HaBaseTimeInput extends LitElement {
ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium);
}
ha-select {
--mdc-shape-small: 0;
width: 85px;
}
:host([clearable]) .mdc-select__anchor {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}

View File

@ -2,12 +2,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { stringCompare } from "../common/string/compare";
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
import { fetchBlueprints } from "../data/blueprint";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
@customElement("ha-blueprint-picker")
@ -55,20 +53,16 @@ class HaBluePrintPicker extends LitElement {
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}
.disabled=${this.disabled}
@selected=${this._blueprintChanged}
@closed=${stopPropagation}
>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<ha-list-item .value=${blueprint.path}>
${blueprint.name}
</ha-list-item>
`
.options=${this._processedBlueprints(this.blueprints).map(
(blueprint) => ({
value: blueprint.path,
label: blueprint.name,
})
)}
>
</ha-select>
`;
}
@ -82,8 +76,8 @@ class HaBluePrintPicker extends LitElement {
}
}
private _blueprintChanged(ev) {
const newValue = ev.target.value;
private _blueprintChanged(ev: CustomEvent<{ value: string }>) {
const newValue = ev.detail.value;
if (newValue !== this.value) {
this.value = newValue;

View File

@ -51,6 +51,7 @@ export class HaCard extends LitElement {
font-weight: var(--ha-font-weight-normal);
}
/* clean-css ignore:start */
:host
::slotted(
.card-content:not(:nth-child(1 of .card-content, .card-header))
@ -59,6 +60,7 @@ export class HaCard extends LitElement {
padding-top: 0;
margin-top: calc(var(--ha-space-2) * -1);
}
/* clean-css ignore:end */
:host ::slotted(.card-content) {
padding: var(--ha-space-4);

View File

@ -1,24 +1,31 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, nothing } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon";
import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon";
import "./ha-menu";
export interface SelectOption {
label: string;
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends SelectBase {
@query(".select") protected mdcRoot!: HTMLElement;
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@ -26,95 +33,83 @@ export class HaControlSelectMenu extends SelectBase {
@property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false;
@property() public options;
@property({ type: Boolean })
public disabled = false;
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
}
@property({ type: Boolean })
public required = false;
@property()
public label?: string;
@property()
public value?: string;
@property({ attribute: false }) public options: SelectOption[] = [];
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
const classes = {
"select-disabled": this.disabled,
"select-required": this.required,
"select-invalid": !this.isUiValid,
"select-no-value": !this.selectedText,
};
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
const labelAttribute =
this.label && this.hideLabel ? this.label : undefined;
if (this.disabled) {
return this._renderTrigger();
}
return html`
<div class="select ${classMap(classes)}">
<input
class="formElement"
.name=${this.name}
.value=${this.value}
hidden
?disabled=${this.disabled}
?required=${this.required}
/>
<!-- @ts-ignore -->
<div
class="select-anchor"
aria-autocomplete="none"
role="combobox"
aria-expanded=${this.menuOpen}
aria-invalid=${!this.isUiValid}
aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required}
aria-controls="listbox"
@focus=${this.onFocus}
@blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown}
>
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${this.selectedText
? html`<p class="value">${this.selectedText}</p>`
: nothing}
</div>
${this._renderArrow()}
<ha-ripple .disabled=${this.disabled}></ha-ripple>
</div>
${this.renderMenu()}
</div>
<ha-dropdown placement="bottom" @wa-show=${this._showDropdown}>
${this._renderTrigger()} ${this.options.map(this._renderOption)}
</ha-dropdown>
`;
}
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
private _renderTrigger() {
const selectedText = this.getValueObject(this.options, this.value)?.label;
const classes = {
"select-disabled": this.disabled,
"select-required": this.required,
"select-no-value": !selectedText,
};
return html`<button
?disabled=${this.disabled}
slot="trigger"
class="select-anchor ${classMap(classes)}"
>
${this.renderMenuContent()}
</ha-menu>`;
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${selectedText ? html`<p class="value">${selectedText}</p>` : nothing}
</div>
${this._renderArrow()}
</button>`;
}
private _renderOption = (option: SelectOption) =>
html`<ha-dropdown-item
.value=${option.value}
class=${this.value === option.value ? "selected" : ""}
>${option.iconPath
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
: option.attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${option.attributeIcon.stateObj}
.attribute=${option.attributeIcon.attribute}
.attributeValue=${option.attributeIcon.attributeValue}
></ha-attribute-icon>`
: nothing}
${option.label}</ha-dropdown-item
>`;
private _renderArrow() {
if (!this.showArrow) return nothing;
if (!this.showArrow) {
return nothing;
}
return html`
<div class="icon">
@ -124,47 +119,42 @@ export class HaControlSelectMenu extends SelectBase {
}
private _renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined;
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const defaultIcon = this.querySelector("[slot='icon']");
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
| HaSvgIcon
| HaIcon
| null;
if (!defaultIcon && !icon) {
return null;
}
return html`
<div class="icon">
${icon && icon.localName === "ha-svg-icon" && "path" in icon
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
: icon && icon.localName === "ha-icon" && "icon" in icon
? html`<ha-icon .path=${icon.icon}></ha-icon>`
: html`<slot name="icon"></slot>`}
${iconPath
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
: icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${attributeIcon.stateObj}
.attribute=${attributeIcon.attribute}
.attributeValue=${attributeIcon.attributeValue}
></ha-attribute-icon>`
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
</div>
`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
private _showDropdown() {
this.style.setProperty(
"--control-select-menu-width",
`${this._triggerButton.offsetWidth}px`
);
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
private getValueObject = memoizeOne(
(options: SelectOption[], value?: string) =>
options.find((option) => option.value === value)
);
static override styles = [
css`
@ -186,6 +176,8 @@ export class HaControlSelectMenu extends SelectBase {
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;
@ -211,6 +203,12 @@ export class HaControlSelectMenu extends SelectBase {
font-weight: var(--ha-font-weight-normal);
letter-spacing: 0.25px;
}
.select-anchor:hover {
--control-select-menu-background-color: var(
--ha-color-on-neutral-quiet
);
}
.content {
display: flex;
flex-direction: column;
@ -230,16 +228,19 @@ export class HaControlSelectMenu extends SelectBase {
}
.label {
font-size: 0.85em;
font-size: var(--ha-font-size-s);
letter-spacing: 0.4px;
}
.select-no-value .label {
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
.content .value {
font-size: var(--ha-font-size-m);
}
.select-anchor:focus-visible {
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
}
@ -258,10 +259,23 @@ export class HaControlSelectMenu extends SelectBase {
opacity: var(--control-select-menu-background-opacity);
}
.select-disabled .select-anchor {
.select-disabled.select-anchor {
cursor: not-allowed;
color: var(--disabled-color);
}
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown::part(menu) {
min-width: var(--control-select-menu-width);
}
`,
];
}

View File

@ -24,10 +24,10 @@ export class HaControlSwitch extends LitElement {
@property({ type: Boolean }) public checked = false;
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
@property({ attribute: false, type: String }) pathOn?: string;
@property({ attribute: false }) pathOn?: string;
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
@property({ attribute: false, type: String }) pathOff?: string;
@property({ attribute: false }) pathOff?: string;
@property({ type: String })
public label?: string;

View File

@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
@ -14,9 +13,8 @@ import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@ -73,37 +71,35 @@ export class HaConversationAgentPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._agents.map((agent) => ({
value: agent.id,
label: agent.name,
disabled:
agent.supported_languages !== "*" &&
agent.supported_languages.length === 0,
}));
if (!this.required) {
options.unshift({
value: NONE,
label: this.hass.localize(
"ui.components.conversation-agent-picker.none"
),
});
}
return html`
<ha-select
.label=${this.label ||
this.hass!.localize(
"ui.components.coversation-agent-picker.conversation_agent"
"ui.components.conversation-agent-picker.conversation_agent"
)}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize(
"ui.components.coversation-agent-picker.none"
)}
</ha-list-item>`
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item
.value=${agent.id}
.disabled=${agent.supported_languages !== "*" &&
agent.supported_languages.length === 0}
>
${agent.name}
</ha-list-item>`
)}</ha-select
.options=${options}
></ha-select
>${(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
@ -238,17 +234,17 @@ export class HaConversationAgentPicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
this.value = value === NONE ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._agents!.find((agent) => agent.id === this.value)

View File

@ -76,6 +76,18 @@ export class HaDialog extends DialogBase {
var(--divider-color)
);
z-index: var(--dialog-z-index, 8);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
-webkit-backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
@ -84,9 +96,9 @@ export class HaDialog extends DialogBase {
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog .mdc-dialog__scrim {
background-color: var(--mdc-dialog-scrim-color, none);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);

View File

@ -4,6 +4,18 @@ import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@ -15,6 +27,30 @@ export class HaDrawer extends DrawerBase {
private _rtlStyle?: HTMLElement;
private _sidebarTransitionActive = false;
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = true;
fireEvent(window, "hass-layout-transition", {
active: true,
reason: "sidebar",
});
};
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = false;
fireEvent(window, "hass-layout-transition", {
active: false,
reason: "sidebar",
});
};
protected createAdapter() {
return {
...super.createAdapter(),
@ -63,6 +99,38 @@ export class HaDrawer extends DrawerBase {
}
}
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
@ -90,6 +158,16 @@ export class HaDrawer extends DrawerBase {
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
inset-inline-start: 0 !important;
inset-inline-end: initial !important;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
@ -103,6 +181,15 @@ export class HaDrawer extends DrawerBase {
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
.mdc-drawer,
.mdc-drawer-app-content {
transition: none;
}
}
`,
];

View File

@ -1,9 +1,9 @@
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
import { css, type CSSResultGroup, html } from "lit";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
/**
* Home Assistant dropdown item component
@ -37,6 +37,7 @@ export class HaDropdownItem extends DropdownItem {
#check {
visibility: visible;
flex-shrink: 0;
}
#icon ::slotted(*) {

View File

@ -1,6 +1,9 @@
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import type { HaDropdownItem } from "./ha-dropdown-item";
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
/**
* Home Assistant dropdown component

View File

@ -31,6 +31,7 @@ import "./ha-expansion-panel";
import "./ha-icon";
import "./ha-list";
import "./ha-list-item";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
@customElement("ha-filter-categories")
export class HaFilterCategories extends SubscribeMixin(LitElement) {
@ -174,7 +175,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}
}
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
private _handleAction(ev: HaDropdownSelectEvent) {
const categoryId = (ev.currentTarget as any).categoryId;
const action = ev.detail.item.value;
switch (action) {

View File

@ -11,8 +11,7 @@ import "../ha-formfield";
import "../ha-icon-button";
import "../ha-picker-field";
import type { HaDropdown } from "../ha-dropdown";
import type { HaDropdownItem } from "../ha-dropdown-item";
import type { HaDropdown, HaDropdownSelectEvent } from "../ha-dropdown";
import type {
HaFormElement,
HaFormMultiSelectData,
@ -108,7 +107,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
`;
}
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
protected _toggleItem(ev: HaDropdownSelectEvent) {
ev.preventDefault(); // keep the dropdown open
const value = ev.detail.item.value;
const action = (ev.detail.item as any).action;

View File

@ -17,6 +17,7 @@ import type {
HaFormOptionalActionsSchema,
HaFormSchema,
} from "./types";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
const NO_ACTIONS = [];
@ -142,7 +143,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
`;
}
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
private _handleAddAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
this._displayActions = [...(this._displayActions ?? []), action];
}

View File

@ -30,7 +30,7 @@ export class HaGauge extends LitElement {
@property({ attribute: false })
public formatOptions?: Intl.NumberFormatOptions;
@property({ attribute: false, type: String }) public valueText?: string;
@property({ attribute: false }) public valueText?: string;
@property({ attribute: false }) public locale!: FrontendLocaleData;

View File

@ -48,7 +48,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
section?: string
) => (PickerComboBoxItem | string)[] | undefined;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })

View File

@ -113,13 +113,13 @@ class HaHsColorPicker extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Number, attribute: false })
@property({ attribute: false })
public renderSize?: number;
@property({ type: Array })
public value?: [number, number];
@property({ attribute: false, type: Number })
@property({ attribute: false })
public colorBrightness?: number;
@property({ type: Number })
@ -131,10 +131,10 @@ class HaHsColorPicker extends LitElement {
@property({ type: Number })
public ww?: number;
@property({ attribute: false, type: Number })
@property({ attribute: false })
public minKelvin?: number;
@property({ attribute: false, type: Number })
@property({ attribute: false })
public maxKelvin?: number;
@query("#canvas") private _canvas!: HTMLCanvasElement;

View File

@ -19,7 +19,7 @@ export interface HaIconButtonToolbarItem {
@customElement("ha-icon-button-toolbar")
export class HaIconButtonToolbar extends LitElement {
@property({ type: Array, attribute: false })
@property({ attribute: false })
public items: (HaIconButtonToolbarItem | string)[] = [];
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];

View File

@ -3,6 +3,7 @@ import { mdiDotsVertical } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../common/dom/stop_propagation";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-dropdown";
@ -39,7 +40,10 @@ export class HaIconOverflowMenu extends LitElement {
return html`
${this.narrow
? html` <!-- Collapsed representation for small screens -->
<ha-dropdown @wa-show=${this._handleIconOverflowMenuOpened}>
<ha-dropdown
@wa-show=${this._handleIconOverflowMenuOpened}
@click=${stopPropagation}
>
<ha-icon-button
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}

View File

@ -107,9 +107,6 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;

View File

@ -134,16 +134,51 @@ export class HaMarkdown extends LitElement {
}
table[role="presentation"] {
--markdown-table-border-collapse: separate;
--markdown-table-border-width: attr(border, 0);
--markdown-table-border-width: 0;
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th {
vertical-align: attr(valign, middle);
}
th,
td {
vertical-align: middle;
}
}
table[role="presentation"] td[valign="top"],
table[role="presentation"] th[valign="top"] {
vertical-align: top;
}
table[role="presentation"] td[valign="middle"],
table[role="presentation"] th[valign="middle"] {
vertical-align: middle;
}
table[role="presentation"] td[valign="bottom"],
table[role="presentation"] th[valign="bottom"] {
vertical-align: bottom;
}
table[role="presentation"] td[valign="baseline"],
table[role="presentation"] th[valign="baseline"] {
vertical-align: baseline;
}
@supports (border-width: attr(border px, 0)) {
table[role="presentation"] {
--markdown-table-border-width: attr(border px, 0);
}
table[role="presentation"] th,
table[role="presentation"] td {
vertical-align: attr(valign, middle);
}
}
table[role="presentation"][border="0"] {
--markdown-table-border-width: 0;
}
table[role="presentation"][border="1"] {
--markdown-table-border-width: 1px;
}
table[role="presentation"][border="2"] {
--markdown-table-border-width: 2px;
}
table[role="presentation"][border="3"] {
--markdown-table-border-width: 3px;
}
table {
border-collapse: var(--markdown-table-border-collapse, collapse);
}

View File

@ -1,45 +0,0 @@
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
import { styles } from "@material/mwc-menu/mwc-menu.css";
import { html } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "./ha-list";
@customElement("ha-menu")
export class HaMenu extends MenuBase {
protected get listElement() {
if (!this.listElement_) {
this.listElement_ = this.renderRoot.querySelector("ha-list");
return this.listElement_;
}
return this.listElement_;
}
protected renderList() {
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
const classes = this.renderListClasses();
return html`<ha-list
rootTabbable
.innerAriaLabel=${this.innerAriaLabel}
.innerRole=${this.innerRole}
.multi=${this.multi}
class=${classMap(classes)}
.itemRoles=${itemRoles}
.wrapFocus=${this.wrapFocus}
.activatable=${this.activatable}
@action=${this.onAction}
>
<slot></slot>
</ha-list>`;
}
static styles = styles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-menu": HaMenu;
}
}

View File

@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { SupervisorMounts } from "../data/supervisor/mounts";
import {
@ -17,7 +16,7 @@ import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import type { HaSelectOption } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@ -52,60 +51,54 @@ class HaMountPicker extends LitElement {
if (!this._mounts) {
return nothing;
}
const dataDiskOption = html`<ha-list-item
graphic="icon"
.value=${_BACKUP_DATA_DISK_}
>
<span>
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup"}
</span>
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
</ha-list-item>`;
const options: HaSelectOption[] = this._filterMounts(
this._mounts,
this.usage
).map((mount) => ({
value: mount.name,
label: mount.name,
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
}`,
iconPath:
mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore,
}));
if (this.usage === SupervisorMountUsage.BACKUP) {
const dataDiskOption = {
value: _BACKUP_DATA_DISK_,
iconPath: mdiHarddisk,
label:
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup",
};
if (
!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
) {
options.unshift(dataDiskOption);
} else {
options.push(dataDiskOption);
}
}
return html`
<ha-select
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.mount-picker.mount")
: this.label}
.value=${this._value}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
@selected=${this._mountChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${this.usage === SupervisorMountUsage.BACKUP &&
(!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
? dataDiskOption
: nothing}
${this._filterMounts(this._mounts, this.usage).map(
(mount) =>
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
<span>${mount.name}</span>
<span slot="secondary"
>${mount.server}${mount.port
? `:${mount.port}`
: nothing}${mount.type === SupervisorMountType.NFS
? mount.path
: `:${mount.share}`}</span
>
<ha-svg-icon
slot="graphic"
.path=${mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore}
></ha-svg-icon>
</ha-list-item>`
)}
${this.usage === SupervisorMountUsage.BACKUP &&
this._mounts.default_backup_mount
? dataDiskOption
: nothing}
</ha-select>
`;
}
@ -153,16 +146,10 @@ class HaMountPicker extends LitElement {
}
}
private get _value() {
return this.value || "";
}
private _mountChanged(ev: CustomEvent<{ value: string }>) {
const newValue = ev.detail.value;
private _mountChanged(ev: Event) {
ev.stopPropagation();
const target = ev.target as HaSelect;
const newValue = target.value;
if (newValue !== this._value) {
if (newValue !== this.value) {
this._setValue(newValue);
}
}

View File

@ -11,6 +11,7 @@ import { fetchConfig } from "../data/lovelace/config/types";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import { findRelated, type RelatedResult } from "../data/search";
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
@ -25,8 +26,9 @@ import {
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
const RELATED_SORT_PREFIX = {
area: "0_area",
device: "1_device",
area_view: "0_area_view",
area: "1_area_settings",
device: "2_device",
} as const;
interface NavigationItem extends PickerComboBoxItem {
@ -260,12 +262,7 @@ export class HaNavigationPicker extends LitElement {
const viewConfigs = await Promise.all(
lovelacePanels.map((panel) =>
fetchConfig(
this.hass!.connection,
// path should be null to fetch default lovelace panel
panel.url_path === "lovelace" ? null : panel.url_path,
true
)
fetchConfig(this.hass!.connection, panel.url_path, true)
.then((config) => [panel.id, config] as [string, typeof config])
.catch((_) => [panel.id, undefined] as [string, undefined])
)
@ -437,10 +434,31 @@ export class HaNavigationPicker extends LitElement {
for (const areaId of relatedAreaIds) {
const area = this.hass.areas[areaId];
const primary = area?.name ?? areaId;
// Area dashboard view
const viewPath = `/home/${computeAreaPath(areaId)}`;
relatedItems.push({
id: viewPath,
primary,
secondary: viewPath,
icon: area?.icon ?? undefined,
icon_path: area?.icon ? undefined : mdiTextureBox,
sorting_label: createSortingLabel(
RELATED_SORT_PREFIX.area_view,
primary,
viewPath
),
group: "related",
});
// Area settings
const path = `/config/areas/area/${areaId}`;
relatedItems.push({
id: path,
primary,
primary: this.hass.localize(
"ui.components.navigation-picker.area_settings",
{ area: primary }
),
secondary: path,
icon: area?.icon ?? undefined,
icon_path: area?.icon ? undefined : mdiTextureBox,

View File

@ -89,7 +89,7 @@ export class HaPasswordField extends LitElement {
@property({ type: Boolean }) readOnly = false;
// eslint-disable-next-line lit/no-native-attributes
@property({ attribute: false, type: String }) autocapitalize = "";
@property({ attribute: false }) autocapitalize = "";
@state() private _unmaskedPassword = false;

View File

@ -22,6 +22,7 @@ import {
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import { isTouch } from "../util/is_touch";
import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
import "./ha-combo-box-item";
@ -56,6 +57,11 @@ export interface PickerComboBoxItem {
icon?: string;
}
export interface PickerComboBoxIndexSelectedDetail {
index: number;
newTab?: boolean;
}
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
const PADDING_ID = "___padding___";
@ -113,7 +119,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
section?: string
) => PickerComboBoxItem[] | undefined;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })
@ -279,6 +285,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
section === "separator"
? html`<div class="separator"></div>`
: html`<ha-filter-chip
@mousedown=${isTouch ? undefined : this._preventBlur}
@click=${this._toggleSection}
.section-id=${section.id}
.selected=${this._selectedSection === section.id}
@ -417,18 +424,19 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return this.value || "";
}
private _valueSelected = (ev: Event) => {
private _valueSelected = (ev: MouseEvent) => {
ev.stopPropagation();
const value = (ev.currentTarget as any).value as string;
const index = Number((ev.currentTarget as any).index);
const newValue = value?.trim();
const newTab = ev.ctrlKey || ev.metaKey;
this._fireSelectedEvents(newValue, index);
this._fireSelectedEvents(newValue, index, newTab);
};
private _fireSelectedEvents(value: string, index: number) {
private _fireSelectedEvents(value: string, index: number, newTab = false) {
fireEvent(this, "value-changed", { value });
fireEvent(this, "index-selected", { index });
fireEvent(this, "index-selected", { index, newTab });
}
private _clearSearch = () => {
@ -500,6 +508,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._valuePinned = true;
};
private _preventBlur(ev: Event) {
ev.preventDefault();
}
private _toggleSection(ev: Event) {
ev.stopPropagation();
this._resetSelectedItem();
@ -517,9 +529,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._items = this._getItems();
// Reset scroll position when filter changes
if (this.virtualizerElement) {
this.virtualizerElement.scrollToIndex(0);
}
this.virtualizerElement?.element(0)?.scrollIntoView();
}
private _registerKeyboardShortcuts() {
@ -529,15 +539,42 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
Home: this._selectFirstItem,
End: this._selectLastItem,
Enter: this._pickSelectedItem,
"$mod+Enter": this._pickSelectedItemNewTab,
});
}
private _focusList() {
if (this._selectedItemIndex === -1) {
this._selectNextItem();
this._initializeSelectedIndex();
}
}
/**
* Initialize keyboard selection to the currently selected value,
* or fall back to the first item when searching (skipping section titles).
*/
private _initializeSelectedIndex(): void {
if (!this.virtualizerElement?.items?.length) {
return;
}
const initialIndex = this._getInitialSelectedIndex();
// Only initialize to first item if searching, otherwise require a selected value
if (initialIndex === 0 && !this._search) {
return;
}
let index = initialIndex;
// Skip section titles (strings)
if (typeof this.virtualizerElement.items[index] === "string") {
index += 1;
}
// Bounds check: ensure index is valid after skipping section title
if (index >= this.virtualizerElement.items.length) {
return;
}
this._selectedItemIndex = index;
this._scrollToSelectedItem();
}
private _selectNextItem = (ev?: KeyboardEvent) => {
ev?.stopPropagation();
ev?.preventDefault();
@ -556,6 +593,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return;
}
// If no item is selected yet, start from the currently selected value
if (this._selectedItemIndex === -1) {
this._initializeSelectedIndex();
if (this._selectedItemIndex !== -1) {
return;
}
}
const nextIndex =
maxItems === this._selectedItemIndex
? this._selectedItemIndex
@ -647,7 +692,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
?.querySelector(".selected")
?.classList.remove("selected");
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
this.virtualizerElement
?.element(this._selectedItemIndex)
?.scrollIntoView({ block: "nearest" });
requestAnimationFrame(() => {
this.virtualizerElement
@ -657,6 +704,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
};
private _pickSelectedItem = (ev: KeyboardEvent) => {
this._pickItem(ev, false);
};
private _pickSelectedItemNewTab = (ev: KeyboardEvent) => {
this._pickItem(ev, true);
};
private _pickItem = (ev: KeyboardEvent, newTab: boolean) => {
ev.stopPropagation();
if (
this.virtualizerElement?.items?.length !== undefined &&
@ -668,14 +723,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
).forEach((item, index) => {
if (typeof item !== "string") {
this._fireSelectedEvents(item.id, index);
this._fireSelectedEvents(item.id, index, newTab);
}
});
return;
}
if (this._selectedItemIndex === -1) {
return;
this._initializeSelectedIndex();
if (this._selectedItemIndex === -1) {
return;
}
}
// if filter button is focused
@ -685,7 +743,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._selectedItemIndex
] as PickerComboBoxItem;
if (item) {
this._fireSelectedEvents(item.id, this._selectedItemIndex);
this._fireSelectedEvents(item.id, this._selectedItemIndex, newTab);
}
};
@ -891,6 +949,6 @@ declare global {
}
interface HASSDomEvents {
"index-selected": { index: number };
"index-selected": PickerComboBoxIndexSelectedDetail;
}
}

View File

@ -22,7 +22,7 @@ export class HaQrCode extends LitElement {
@property({ type: Number })
public margin = 4;
@property({ attribute: false, type: Number })
@property({ attribute: false })
public maskPattern?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
@property({ attribute: "center-image" }) public centerImage?: string;

View File

@ -14,8 +14,8 @@ import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-dropdown";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
import "./ha-dropdown-item";
import type { HaDropdownItem } from "./ha-dropdown-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -259,7 +259,7 @@ class HaQrScanner extends LitElement {
this._qrCodeScanned(this._manualInput!.value);
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
const cameraId = ev.detail?.item?.value;
if (cameraId) {
this._selectedCamera = cameraId;

View File

@ -1,187 +1,209 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { mdiClose } from "@mdi/js";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon-button";
import "./ha-menu";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-picker-field";
import type { HaPickerField } from "./ha-picker-field";
import "./ha-svg-icon";
export interface HaSelectOption {
value: string;
label?: string;
secondary?: string;
iconPath?: string;
disabled?: boolean;
}
@customElement("ha-select")
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon = false;
export class HaSelect extends LitElement {
@property({ type: Boolean }) public clearable = false;
@property({ type: Boolean, reflect: true }) public clearable = false;
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@property() public label?: string;
@property() public options;
@property() public helper?: string;
@property() public value?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public disabled = false;
@state() private _opened = false;
@query("ha-picker-field") private _triggerField!: HaPickerField;
private _getValueLabel = memoizeOne(
(
options: HaSelectOption[] | string[] | undefined,
value: string | undefined
) => {
if (!options || !value) {
return value;
}
for (const option of options) {
if (
(typeof option === "string" && option === value) ||
(typeof option !== "string" && option.value === value)
) {
return typeof option === "string"
? option
: option.label || option.value;
}
}
return value;
}
);
protected override render() {
if (this.disabled) {
return this._renderField();
}
return html`
${super.render()}
${this.clearable && !this.required && !this.disabled && this.value
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-dropdown
placement="bottom"
@wa-select=${this._handleSelect}
@wa-show=${this._handleShow}
@wa-hide=${this._handleHide}
>
${this._renderField()}
${this.options
? this.options.map(
(option) => html`
<ha-dropdown-item
.value=${typeof option === "string" ? option : option.value}
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
class=${this.value ===
(typeof option === "string" ? option : option.value)
? "selected"
: ""}
>
${option.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${option.iconPath}
></ha-svg-icon>`
: nothing}
<div class="content">
${typeof option === "string"
? option
: option.label || option.value}
${option.secondary
? html`<div class="secondary">${option.secondary}</div>`
: nothing}
</div>
</ha-dropdown-item>
`
)
: html`<slot></slot>`}
</ha-dropdown>
`;
}
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
>
${this.renderMenuContent()}
</ha-menu>`;
private _renderField() {
const valueLabel = this._getValueLabel(this.options, this.value);
return html`
<ha-picker-field
slot="trigger"
type="button"
class=${this._opened ? "opened" : ""}
compact
aria-label=${ifDefined(this.label)}
@clear=${this._clearValue}
.label=${this.label}
.helper=${this.helper}
.value=${valueLabel}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${!this.clearable ||
this.required ||
this.disabled ||
!this.value}
>
</ha-picker-field>
`;
}
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
ev.stopPropagation();
const value = ev.detail.item.value;
if (value === this.value) {
return;
}
return html`<span class="mdc-select__icon"
><slot name="icon"></slot
></span>`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
protected async firstUpdated() {
super.firstUpdated();
if (this.inlineArrow) {
this.shadowRoot
?.querySelector(".mdc-select__selected-text-container")
?.classList.add("inline-arrow");
}
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("inlineArrow")) {
const textContainerElement = this.shadowRoot?.querySelector(
".mdc-select__selected-text-container"
);
if (this.inlineArrow) {
textContainerElement?.classList.add("inline-arrow");
} else {
textContainerElement?.classList.remove("inline-arrow");
}
}
if (changedProperties.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
fireEvent(this, "selected", { value });
}
private _clearValue(): void {
if (this.disabled || !this.value) {
return;
}
this.valueSetDirectly = true;
this.select(-1);
this.mdcFoundation.handleChange();
fireEvent(this, "selected", { value: undefined });
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
private _handleShow() {
this.style.setProperty(
"--select-menu-width",
`${this._triggerField.offsetWidth}px`
);
this._opened = true;
}
static override styles = [
styles,
css`
:host([clearable]) {
position: relative;
}
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: var(--ha-space-4);
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: var(--ha-space-4);
padding-inline-end: 0px;
direction: var(--direction);
}
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
}
.mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(
--select-selected-text-padding-end,
var(--ha-space-4)
);
}
ha-icon-button {
position: absolute;
top: 10px;
right: 28px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 28px;
direction: var(--direction);
}
.inline-arrow {
flex-grow: 0;
}
`,
];
private _handleHide() {
this._opened = false;
}
static styles = css`
:host {
position: relative;
}
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown-item .content {
display: flex;
gap: var(--ha-space-1);
flex-direction: column;
}
ha-dropdown-item .secondary {
font-size: var(--ha-font-size-s);
color: var(--ha-color-text-secondary);
}
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
:host ::slotted(ha-dropdown-item.selected),
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
interface HASSDomEvents {
selected: { value: string | undefined };
}
}

View File

@ -5,17 +5,16 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
import "../ha-dropdown-item";
import "../ha-formfield";
import "../ha-generic-picker";
import "../ha-input-helper-text";
import "../ha-list-item";
import "../ha-radio";
import "../ha-select";
import "../ha-select-box";
@ -231,24 +230,15 @@ export class HaSelectSelector extends LitElement {
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label ?? ""}
.value=${this.value ?? ""}
.value=${(this.value as string) ?? ""}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
.required=${this.required}
clearable
@closed=${stopPropagation}
@selected=${this._valueChanged}
.options=${options}
>
${options.map(
(item: SelectOption) => html`
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
>${item.label}</ha-list-item
>
`
)}
</ha-select>
`;
}
@ -295,7 +285,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
if (ev.detail?.index === -1 && this.value !== undefined) {
if (ev.detail?.value === undefined && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
@ -385,7 +375,7 @@ export class HaSelectSelector extends LitElement {
ha-formfield {
display: block;
}
ha-list-item[disabled] {
ha-dropdown-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {

View File

@ -224,6 +224,7 @@ ${typeof this._templateResult.result === "object"
margin-bottom: 0;
direction: ltr;
border-radius: var(--ha-border-radius-sm);
overflow-wrap: break-word;
}
`;
}

View File

@ -108,6 +108,7 @@ export class HaSettingsRow extends LitElement {
white-space: normal;
}
.prefix-wrap {
flex: 1;
display: var(--settings-row-prefix-display);
}
:host([narrow]) .prefix-wrap {

View File

@ -492,19 +492,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="start"
>${this._updatesCount + this._issuesCount}</span
>`
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="start">
${this._updatesCount + this._issuesCount}
</span>
`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("panel.config")}</span
>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>`
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
: nothing}
</ha-md-list-item>
`;
@ -524,13 +527,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
type="button"
>
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
${!this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="start">${notificationCount}</span>`
${notificationCount > 0
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${this.alwaysExpand && notificationCount > 0
${notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
: nothing}
</ha-md-list-item>
@ -739,6 +744,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
font-size: var(--ha-font-size-xl);
align-items: center;
overflow: hidden;
width: calc(56px + var(--safe-area-inset-left, 0px));
padding-left: calc(
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
);
@ -747,6 +754,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
padding-inline-end: initial;
padding-top: var(--safe-area-inset-top, 0px);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) .menu {
width: calc(256px + var(--safe-area-inset-left, 0px));
@ -761,15 +769,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
margin-left: 3px;
margin-inline-start: 3px;
margin-inline-end: initial;
width: 100%;
display: none;
flex: 1;
min-width: 0;
max-width: 0;
opacity: 0;
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .title {
margin: 0;
padding: 0 var(--ha-space-4);
}
:host([expanded]) .title {
display: initial;
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
}
.panels-list {
@ -827,6 +842,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--ha-space-3);
--md-list-item-leading-icon-size: var(--ha-space-6);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item {
width: 248px;
@ -867,11 +883,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
}
ha-md-list-item .item-text {
display: none;
display: block;
max-width: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item .item-text {
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
display: block;
overflow: hidden;
text-overflow: ellipsis;
@ -889,6 +916,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
background-color: var(--accent-color);
padding: 2px 6px;
color: var(--text-accent-color, var(--text-primary-color));
transition:
opacity var(--ha-animation-duration-normal) ease,
transform var(--ha-animation-duration-normal) ease;
}
ha-svg-icon + .badge {
@ -900,6 +930,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
line-height: var(--ha-line-height-expanded);
padding: 0 var(--ha-space-1);
}
:host([expanded]) .badge[slot="start"],
:host(:not([expanded])) .badge[slot="end"] {
opacity: 0;
transform: scale(0.8);
pointer-events: none;
}
ha-md-list-item.user {
--md-list-item-leading-icon-size: var(--ha-space-10);
@ -938,6 +974,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@media (prefers-reduced-motion: reduce) {
.menu,
ha-md-list-item,
ha-md-list-item .item-text,
.title {
transition: none;
}
}
`,
];
}

View File

@ -2,16 +2,14 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@ -61,6 +59,30 @@ export class HaSTTPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.stt-picker.none") || "None",
});
}
return html`
<ha-select
.label=${this.label ||
@ -69,33 +91,8 @@ export class HaSTTPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.stt-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@ -144,17 +141,17 @@ export class HaSTTPicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
this.value = value === NONE ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)

View File

@ -76,7 +76,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public compact = false;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
/**
* Show only targets with entities from specific domains.

View File

@ -1,11 +1,10 @@
import type { TemplateResult } from "lit";
import { css, html, nothing, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { HomeAssistant } from "../types";
import "./ha-select";
import "./ha-list-item";
import type { HaSelectOption } from "./ha-select";
const DEFAULT_THEME = "default";
@ -25,6 +24,26 @@ export class HaThemePicker extends LitElement {
@property({ type: Boolean }) public required = false;
protected render(): TemplateResult {
const options: HaSelectOption[] = Object.keys(
this.hass?.themes.themes || {}
).map((theme) => ({
value: theme,
}));
if (this.includeDefault) {
options.unshift({
value: DEFAULT_THEME,
label: "Home Assistant",
});
}
if (!this.required) {
options.unshift({
value: "remove",
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
});
}
return html`
<ha-select
.label=${this.label ||
@ -33,31 +52,8 @@ export class HaThemePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`
<ha-list-item value="remove">
${this.hass!.localize("ui.components.theme-picker.no_theme")}
</ha-list-item>
`
: nothing}
${this.includeDefault
? html`
<ha-list-item .value=${DEFAULT_THEME}>
Home Assistant
</ha-list-item>
`
: nothing}
${Object.keys(this.hass!.themes.themes)
.sort()
.map(
(theme) =>
html`<ha-list-item .value=${theme}>${theme}</ha-list-item>`
)}
</ha-select>
.options=${options}
></ha-select>
`;
}
@ -67,11 +63,11 @@ export class HaThemePicker extends LitElement {
}
`;
private _changed(ev): void {
if (!this.hass || ev.target.value === "") {
private _changed(ev: CustomEvent<{ value: string }>): void {
if (!this.hass || ev.detail.value === "") {
return;
}
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@ -36,10 +36,19 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .mdc-top-app-bar {
padding-left: var(--safe-area-inset-left);
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
}
}
.mdc-top-app-bar__title {
font-size: var(--ha-font-size-xl);
padding-inline-start: var(--ha-space-6);

View File

@ -2,16 +2,14 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { TTSEngine } from "../data/tts";
import { listTTSEngines } from "../data/tts";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@ -61,6 +59,30 @@ export class HaTTSPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id === value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (!this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.tts-picker.none"),
});
}
return html`
<ha-select
.label=${this.label ||
@ -69,33 +91,8 @@ export class HaTTSPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@ -144,17 +141,17 @@ export class HaTTSPicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
this.value = value === NONE ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)

View File

@ -1,15 +1,13 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { TTSVoice } from "../data/tts";
import { listTTSVoices } from "../data/tts";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@ -31,14 +29,25 @@ export class HaTTSVoicePicker extends LitElement {
@state() _voices?: TTSVoice[] | null;
@query("ha-select") private _select?: HaSelect;
protected render() {
if (!this._voices) {
return nothing;
}
const value =
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
const options: HaSelectOption[] = (this._voices || []).map((voice) => ({
value: voice.voice_id,
label: voice.name,
}));
if (!this.required || !this.value) {
options.unshift({
value: NONE,
label: this.hass!.localize("ui.components.tts-voice-picker.none"),
});
}
return html`
<ha-select
.label=${this.label ||
@ -47,21 +56,8 @@ export class HaTTSVoicePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-voice-picker.none")}
</ha-list-item>`
: nothing}
${this._voices.map(
(voice) =>
html`<ha-list-item .value=${voice.voice_id}>
${voice.name}
</ha-list-item>`
)}
</ha-select>
`;
}
@ -102,34 +98,25 @@ export class HaTTSVoicePicker extends LitElement {
}
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (
changedProperties.has("_voices") &&
this._select?.value !== this.value
) {
this._select?.layoutOptions();
fireEvent(this, "value-changed", { value: this._select?.value });
}
}
static styles = css`
ha-select {
width: 100%;
text-align: start;
display: block;
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
this.value = value === NONE ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@ -48,7 +48,7 @@ export class TopAppBarBaseBase extends BaseElement {
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
@property({ attribute: false, type: Object })
@property({ attribute: false })
get scrollTarget() {
return this._scrollTarget || window;
}
@ -288,10 +288,19 @@ export class TopAppBarBaseBase extends BaseElement {
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .mdc-top-app-bar {
padding-left: var(--safe-area-inset-left);
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
}
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
box-shadow: none;
}

View File

@ -21,8 +21,8 @@ import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog-header";
import "../ha-dropdown";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
import "../ha-dropdown-item";
import type { HaDropdownItem } from "../ha-dropdown-item";
import "../ha-icon-button-arrow-prev";
import "../ha-wa-dialog";
import "./ha-media-manage-button";
@ -177,7 +177,7 @@ class DialogMediaPlayerBrowse extends LitElement {
this.classList.add("opened");
}
private async _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail?.item?.value;
switch (action) {
case "auto":

View File

@ -2,6 +2,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { ActionHandlerOptions } from "../../data/lovelace/action_handler";
import { actionHandler } from "../../panels/lovelace/common/directives/action-handler-directive";
import "../ha-ripple";
@ -14,7 +15,7 @@ export class HaTileContainer extends LitElement {
@property({ type: Boolean })
public vertical = false;
@property({ type: Boolean, attribute: false })
@property({ attribute: false })
public interactive = false;
@property({ attribute: false })
@ -47,7 +48,11 @@ export class HaTileContainer extends LitElement {
>
<ha-ripple .disabled=${!this.interactive}></ha-ripple>
</div>
<div class="container ${containerOrientationClass}">
<div
class="container ${containerOrientationClass}"
@action=${stopPropagation}
@click=${stopPropagation}
>
<div class="content ${classMap(contentClasses)}">
<slot name="icon"></slot>
<slot name="info" id="info"></slot>

View File

@ -53,6 +53,7 @@ export type ClimateEntity = HassEntityBase & {
current_humidity?: number;
target_humidity_low?: number;
target_humidity_high?: number;
target_humidity_step?: number;
min_humidity?: number;
max_humidity?: number;
fan_mode?: string;

View File

@ -35,7 +35,7 @@ export const deserializeFilters = (value: DataTableFilters) => {
// returns true when this filter has *selected* options and the filter's name
// equals the given filterName
export const isUsedFilter = (
export const isFilterUsed = (
key: string,
filter: DataTableFilter,
filterName: string
@ -49,7 +49,7 @@ export const isUsedFilter = (
// which has resulted in a list of items that match these selected opions
// (this list can be empty),
// and the filter's name equals (one of) the given filterName(s)
export const isUsedRelatedItemsFilter = (
export const isRelatedItemsFilterUsed = (
key: string,
filter: DataTableFilter,
filterName: string | string[]

View File

@ -131,11 +131,30 @@ export interface FlowToGridSourceEnergyPreference {
number_energy_price: number | null;
}
export interface GridPowerSourceEnergyPreference {
// W meter
stat_rate: string;
export interface PowerConfig {
stat_rate?: string; // Standard single sensor
stat_rate_inverted?: string; // Inverted single sensor
stat_rate_from?: string; // Battery: discharge / Grid: consumption
stat_rate_to?: string; // Battery: charge / Grid: return
}
export interface GridPowerSourceEnergyPreference {
stat_rate: string;
power_config?: PowerConfig;
}
/**
* Input type for saving grid power sources.
* Core requires EITHER stat_rate (legacy) OR power_config (new format).
* When reading from backend, stat_rate is always populated.
*/
export type GridPowerSourceInput = Omit<
GridPowerSourceEnergyPreference,
"stat_rate"
> & {
stat_rate?: string;
};
export interface GridSourceTypeEnergyPreference {
type: "grid";
@ -159,6 +178,7 @@ export interface BatterySourceTypeEnergyPreference {
stat_energy_from: string;
stat_energy_to: string;
stat_rate?: string;
power_config?: PowerConfig;
}
export interface GasSourceTypeEnergyPreference {
type: "gas";
@ -491,6 +511,15 @@ const getEnergyData = async (
"mean",
])
: {};
// If power stats 5 minute data is selected, then also fetch hourly data which
// will be used to back-fill any missing data points in the 5 minute data when
// the requested range is beyond the limit of short term statistics.
const _powerStatsHour: Statistics | Promise<Statistics> =
powerStatIds.length && finePeriod === "5minute"
? fetchStatistics(hass!, start, end, powerStatIds, "hour", powerUnits, [
"mean",
])
: {};
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
@ -599,6 +628,7 @@ const getEnergyData = async (
const [
energyStats,
powerStats,
powerStatsHour,
waterStats,
energyStatsCompare,
waterStatsCompare,
@ -607,12 +637,44 @@ const getEnergyData = async (
] = await Promise.all([
_energyStats,
_powerStats,
_powerStatsHour,
_waterStats,
_energyStatsCompare,
_waterStatsCompare,
_fossilEnergyConsumption,
_fossilEnergyConsumptionCompare,
]);
// Back-fill any missing power statistics from hourly data if present
if (Object.keys(powerStatsHour).length) {
powerStatIds.forEach((powerId) => {
if (powerId in powerStatsHour) {
// If we have extra hourly power statistics for an ID, we may need to
// insert data into statistics
if (powerId in powerStats && powerStats[powerId].length) {
// We have 5-minute data. Only insert hourly values for time periods
// before the first 5-minute value.
const powerStatFirst = powerStats[powerId][0];
const powerStatHour = powerStatsHour[powerId];
let powerStatHourLast = 0;
for (const powerStat of powerStatHour) {
if (powerStat.end > powerStatFirst.start) {
break;
}
powerStatHourLast++;
}
powerStats[powerId] = [
...powerStatHour.slice(0, powerStatHourLast),
...powerStats[powerId],
];
} else {
// There was no 5-minute data, so simply insert full hourly data
powerStats[powerId] = powerStatsHour[powerId];
}
}
});
}
const stats = { ...energyStats, ...waterStats, ...powerStats };
if (compare) {
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };

View File

@ -13,10 +13,13 @@ export interface SidebarFrontendUserData {
export interface CoreFrontendSystemData {
default_panel?: string;
onboarded_version?: string;
onboarded_date?: string;
}
export interface HomeFrontendSystemData {
favorite_entities?: string[];
welcome_banner_dismissed?: boolean;
}
declare global {

View File

@ -1,6 +1,10 @@
import type { Connection } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
export interface LovelaceInfo {
resource_mode: "yaml" | "storage";
}
export interface LovelaceResource {
id: string;
type: "css" | "js" | "module" | "html";
@ -42,3 +46,8 @@ export const deleteResource = (hass: HomeAssistant, id: string) =>
type: "lovelace/resources/delete",
resource_id: id,
});
export const fetchLovelaceInfo = (hass: HomeAssistant): Promise<LovelaceInfo> =>
hass.callWS({
type: "lovelace/info",
});

View File

@ -15,16 +15,26 @@ import type { LocalizeKeys } from "../common/translations/localize";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "home";
export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean =>
Boolean(hass.panels.lovelace?.config);
export const getLegacyDefaultPanelUrlPath = (): string | null => {
const defaultPanel = window.localStorage.getItem("defaultPanel");
return defaultPanel ? JSON.parse(defaultPanel) : null;
};
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
hass.userData?.default_panel ||
hass.systemData?.default_panel ||
getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL;
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
const defaultPanel =
hass.userData?.default_panel ||
hass.systemData?.default_panel ||
getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL;
// If default panel is lovelace and no old overview exists, fall back to home
if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) {
return DEFAULT_PANEL;
}
return defaultPanel;
};
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
const panel = getDefaultPanelUrlPath(hass);

View File

@ -1,22 +1,24 @@
import {
mdiKeyboard,
mdiNavigationVariant,
mdiPuzzle,
mdiReload,
mdiServerNetwork,
mdiStorePlus,
} from "@mdi/js";
import { canShowPage } from "../common/config/can_show_page";
import {
filterNavigationPages,
type NavigationFilterOptions,
} from "../common/config/filter_navigation_pages";
import { componentsWithService } from "../common/config/components_with_service";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import { configSections } from "../panels/config/ha-panel-config";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import type { HassioAddonInfo } from "./hassio/addon";
import { domainToName } from "./integration";
import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
export interface NavigationComboBoxItem extends PickerComboBoxItem {
path: string;
@ -27,6 +29,7 @@ export interface NavigationComboBoxItem extends PickerComboBoxItem {
export interface BaseNavigationCommand {
path: string;
primary: string;
secondary?: string;
icon_path?: string;
iconPath?: string;
iconColor?: string;
@ -45,11 +48,14 @@ export interface NavigationInfo extends PageNavigation {
const generateNavigationPanelCommands = (
localize: HomeAssistant["localize"],
panels: HomeAssistant["panels"],
addons?: HassioAddonInfo[]
apps?: HassioAddonInfo[]
): BaseNavigationCommand[] =>
Object.entries(panels)
.filter(
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
([panelKey]) =>
panelKey !== "_my_redirect" &&
panelKey !== "hassio" &&
panelKey !== "app"
)
.map(([_panelKey, panel]) => {
const translationKey = getPanelNameTranslationKey(panel);
@ -59,12 +65,10 @@ const generateNavigationPanelCommands = (
let image: string | undefined;
if (addons) {
const addon = addons.find(({ slug }) => slug === panel.url_path);
if (addon) {
image = addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined;
if (apps) {
const app = apps.find(({ slug }) => slug === panel.url_path);
if (app) {
image = app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined;
}
}
@ -98,33 +102,30 @@ const getNavigationInfoFromConfig = (
};
const generateNavigationConfigSectionCommands = (
hass: HomeAssistant
hass: HomeAssistant,
filterOptions: NavigationFilterOptions = {}
): BaseNavigationCommand[] => {
if (!hass.user?.is_admin) {
return [];
}
const items: NavigationInfo[] = [];
const allPages = Object.values(configSections).flat();
const visiblePages = filterNavigationPages(hass, allPages, filterOptions);
Object.values(configSections).forEach((sectionPages) => {
sectionPages.forEach((page) => {
if (!canShowPage(hass, page)) {
return;
}
for (const page of visiblePages) {
const info = getNavigationInfoFromConfig(hass.localize, page);
const info = getNavigationInfoFromConfig(hass.localize, page);
if (!info) {
continue;
}
// Add to list, but only if we do not already have an entry for the same path and component
if (items.some((e) => e.path === info.path)) {
continue;
}
if (!info) {
return;
}
// Add to list, but only if we do not already have an entry for the same path and component
if (items.some((e) => e.path === info.path)) {
return;
}
items.push(info);
});
});
items.push(info);
}
return items;
};
@ -140,7 +141,7 @@ const finalizeNavigationCommands = (
return {
id: `navigation_${index}_${item.path}`,
icon_path: item.iconPath || mdiNavigationVariant,
secondary,
secondary: item.secondary || secondary,
sorting_label: `${item.primary}_${secondary}`,
...item,
};
@ -148,41 +149,42 @@ const finalizeNavigationCommands = (
export const generateNavigationCommands = (
hass: HomeAssistant,
addons?: HassioAddonInfo[]
apps?: HassioAddonInfo[],
filterOptions: NavigationFilterOptions = {}
): NavigationComboBoxItem[] => {
const panelItems = generateNavigationPanelCommands(
hass.localize,
hass.panels,
addons
apps
);
const sectionItems = generateNavigationConfigSectionCommands(hass);
const supervisorItems: BaseNavigationCommand[] = [];
const sectionItems = generateNavigationConfigSectionCommands(
hass,
filterOptions
);
const appItems: BaseNavigationCommand[] = [];
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
supervisorItems.push({
path: "/hassio/store",
appItems.push({
path: "/config/apps/available",
icon_path: mdiStorePlus,
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_store"
"ui.dialogs.quick-bar.commands.navigation.app_store"
),
iconColor: "#F1C447",
});
supervisorItems.push({
path: "/hassio/dashboard",
icon_path: mdiPuzzle,
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
),
});
if (addons) {
for (const addon of addons.filter((a) => a.version)) {
supervisorItems.push({
path: `/hassio/addon/${addon.slug}`,
image: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
if (apps) {
for (const app of apps.filter((a) => a.version)) {
appItems.push({
path: `/config/app/${app.slug}`,
image: app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined,
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_info",
{ addon: addon.name }
"ui.dialogs.quick-bar.commands.navigation.app_info",
{ app: app.name }
),
secondary: hass.localize(
"ui.dialogs.quick-bar.commands.types.app_settings"
),
iconColor: "#F1C447",
});
}
}
@ -201,7 +203,7 @@ export const generateNavigationCommands = (
return finalizeNavigationCommands(hass.localize, [
...panelItems,
...sectionItems,
...supervisorItems,
...appItems,
...additionalItems,
]);
};
@ -265,16 +267,16 @@ const generateServerControlCommands = (
return serverActions.map((action, index) => {
const primary = hass.localize(
"ui.dialogs.quick-bar.commands.server_control.perform_action",
"ui.dialogs.quick-bar.commands.home_assistant_control.perform_action",
{
action: hass.localize(
`ui.dialogs.quick-bar.commands.server_control.${action}`
`ui.dialogs.quick-bar.commands.home_assistant_control.${action}`
),
}
);
const secondary = hass.localize(
"ui.dialogs.quick-bar.commands.types.server_control"
"ui.dialogs.quick-bar.commands.types.home_assistant_control"
);
return {

View File

@ -13,7 +13,6 @@ import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-date-input";
import "../../../components/ha-humidifier-state";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-time-input";
@ -296,17 +295,11 @@ class EntityPreviewRow extends LitElement {
.label=${computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${isUnavailableState(stateObj.state)}
naturalMenuWidth
.options=${stateObj.attributes.options?.map((option) => ({
value: option,
label: this.hass!.formatEntityState(stateObj, option),
})) || []}
>
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
</ha-list-item>
`
)
: ""}
</ha-select>
`;
}

View File

@ -91,9 +91,11 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
) || field.name
);
}

View File

@ -95,9 +95,11 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
) || field.name
);
}

View File

@ -86,9 +86,11 @@ export const showSubConfigFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
) || field.name
);
}

View File

@ -1,23 +1,18 @@
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
import { mdiPlay, mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-button";
import "../../../../components/ha-control-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
getMobileCloseToBottomAnimation,
getMobileOpenFromBottomAnimation,
} from "../../../../components/ha-md-dialog";
import "../../../../components/ha-select";
import "../../../../components/ha-textfield";
import "../../../../components/ha-wa-dialog";
import { SirenEntityFeature } from "../../../../data/siren";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@ -28,23 +23,25 @@ class MoreInfoSirenAdvancedControls extends LitElement {
@state() _stateObj?: HassEntity;
@state() private _open = false;
@state() _tone?: string;
@state() _volume?: number;
@state() _duration?: number;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog({ stateObj }: { stateObj: HassEntity }) {
this._stateObj = stateObj;
this._open = true;
}
public closeDialog(): void {
this._dialog?.close();
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._stateObj = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -65,50 +62,33 @@ class MoreInfoSirenAdvancedControls extends LitElement {
SirenEntityFeature.DURATION
);
return html`
<ha-md-dialog
open
<ha-wa-dialog
.open=${this._open}
.hass=${this.hass}
header-title=${this.hass.localize(
"ui.components.siren.advanced_controls"
)}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this.hass.localize(
"ui.components.siren.advanced_controls"
)}</span
>
</ha-dialog-header>
<div slot="content">
<div>
<div class="options">
${supportsTones
? html`
<ha-select
.label=${this.hass.localize("ui.components.siren.tone")}
@closed=${stopPropagation}
@change=${this._handleToneChange}
@selected=${this._handleToneChange}
.value=${this._tone}
.options=${Object.entries(
this._stateObj!.attributes.available_tones
).map(([toneId, toneName]) => ({
value: Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId,
label: toneName,
}))}
>
${Object.entries(
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item
.value=${Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId}
>${toneName}</ha-list-item
>
`
)}
</ha-select>
`
: nothing}
@ -153,17 +133,21 @@ class MoreInfoSirenAdvancedControls extends LitElement {
</ha-control-button>
</div>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</ha-button>
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private _handleToneChange(ev) {
this._tone = ev.target.value;
private _handleToneChange(ev: CustomEvent<{ value: string }>) {
this._tone = ev.detail.value;
}
private _handleVolumeChange(ev) {

View File

@ -8,9 +8,7 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
@ -29,6 +27,7 @@ import "../../../state-control/climate/ha-state-control-climate-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
type MainControl = "temperature" | "humidity";
@ -169,182 +168,112 @@ class MoreInfoClimate extends LitElement {
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.climate.mode")}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
${html`
<ha-svg-icon
slot="icon"
.path=${climateHvacModeIcon(stateObj.state)}
></ha-svg-icon>
`}
${stateObj.attributes.hvac_modes
.options=${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${climateHvacModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
.map((mode) => ({
value: mode,
iconPath: climateHvacModeIcon(mode),
label: this.hass.formatEntityState(stateObj, mode),
}))}
@wa-select=${this._handleOperationModeChanged}
>
<ha-svg-icon
slot="icon"
.path=${climateHvacModeIcon(stateObj.state)}
></ha-svg-icon>
</ha-control-select-menu>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
@wa-select=${this._handlePresetmodeChanged}
.options=${stateObj.attributes.preset_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "preset_mode",
attributeValue: mode,
},
}))}
>
${stateObj.attributes.preset_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${stateObj.attributes.preset_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
@wa-select=${this._handleFanModeChanged}
.options=${stateObj.attributes.fan_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "fan_mode",
attributeValue: mode,
},
}))}
>
${stateObj.attributes.fan_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${stateObj.attributes.fan_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
`}
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
)}
</ha-list-item>
`
)}
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
@wa-select=${this._handleSwingmodeChanged}
.options=${stateObj.attributes.swing_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: mode,
},
}))}
>
${stateObj.attributes.swing_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${stateObj.attributes.swing_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
`}
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
)}
</ha-list-item>
`
)}
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
@ -352,52 +281,34 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.swing_horizontal_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_horizontal_mode"
)}
.value=${stateObj.attributes.swing_horizontal_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingHorizontalmodeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.swing_horizontal_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${stateObj.attributes
.swing_horizontal_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
`}
${stateObj.attributes.swing_horizontal_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
)}
</ha-list-item>
`
@wa-select=${this._handleSwingHorizontalmodeChanged}
.options=${stateObj.attributes.swing_horizontal_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_horizontal_mode",
attributeValue: mode,
},
})
)}
>
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
@ -410,8 +321,8 @@ class MoreInfoClimate extends LitElement {
this._mainControl = ev.currentTarget.control;
}
private _handleFanModeChanged(ev) {
const newVal = ev.target.value;
private _handleFanModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
this._callServiceHelper(
this.stateObj!.attributes.fan_mode,
newVal,
@ -420,15 +331,15 @@ class MoreInfoClimate extends LitElement {
);
}
private _handleOperationModeChanged(ev) {
const newVal = ev.target.value;
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", {
hvac_mode: newVal,
});
}
private _handleSwingmodeChanged(ev) {
const newVal = ev.target.value;
private _handleSwingmodeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
this._callServiceHelper(
this.stateObj!.attributes.swing_mode,
newVal,
@ -437,8 +348,8 @@ class MoreInfoClimate extends LitElement {
);
}
private _handleSwingHorizontalmodeChanged(ev) {
const newVal = ev.target.value;
private _handleSwingHorizontalmodeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
this._callServiceHelper(
this.stateObj!.attributes.swing_horizontal_mode,
newVal,
@ -447,8 +358,8 @@ class MoreInfoClimate extends LitElement {
);
}
private _handlePresetmodeChanged(ev) {
const newVal = ev.target.value || null;
private _handlePresetmodeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value || null;
if (newVal) {
this._callServiceHelper(
this.stateObj!.attributes.preset_mode,

View File

@ -9,7 +9,6 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
@ -31,6 +30,7 @@ import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-fan")
class MoreInfoFan extends LitElement {
@ -48,8 +48,8 @@ class MoreInfoFan extends LitElement {
});
};
private _handleDirection(ev) {
const newVal = ev.target.value;
private _handleDirection(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
const oldVal = this.stateObj?.attributes.direction;
if (!newVal || oldVal === newVal) return;
@ -60,8 +60,8 @@ class MoreInfoFan extends LitElement {
});
}
private _handlePresetMode(ev) {
const newVal = ev.target.value;
private _handlePresetMode(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
const oldVal = this._presetMode;
if (!newVal || oldVal === newVal) return;
@ -73,8 +73,8 @@ class MoreInfoFan extends LitElement {
});
}
private _handleOscillating(ev) {
const newVal = ev.target.value === "true";
private _handleOscillating(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value === "true";
const oldVal = this.stateObj?.attributes.oscillating;
if (oldVal === newVal) return;
@ -176,65 +176,64 @@ class MoreInfoFan extends LitElement {
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"preset_mode"
)}
.value=${this.stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetMode}
@closed=${stopPropagation}
>
${this.stateObj.attributes.preset_mode
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${this.stateObj.attributes.preset_mode}
></ha-attribute-icon>`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${this.stateObj.attributes.preset_modes?.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
)}
</ha-list-item>
`
@wa-select=${this._handlePresetMode}
.options=${this.stateObj.attributes.preset_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "preset_mode",
attributeValue: mode,
}
: undefined,
})
)}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
${supportsDirection
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"direction"
)}
.value=${this.stateObj.attributes.direction}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleDirection}
@closed=${stopPropagation}
@wa-select=${this._handleDirection}
.options=${["forward", "reverse"].map((direction) => ({
value: direction,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
direction
)
: direction,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "direction",
attributeValue: direction,
}
: undefined,
}))}
>
<ha-attribute-icon
slot="icon"
@ -243,40 +242,13 @@ class MoreInfoFan extends LitElement {
attribute="direction"
.attributeValue=${this.stateObj.attributes.direction}
></ha-attribute-icon>
<ha-list-item value="forward" graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
attributeValue="forward"
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"forward"
)}
</ha-list-item>
<ha-list-item value="reverse" graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
attributeValue="reverse"
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"reverse"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
${supportsOscillate
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating"
@ -285,37 +257,26 @@ class MoreInfoFan extends LitElement {
? "true"
: "false"}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOscillating}
@closed=${stopPropagation}
@wa-select=${this._handleOscillating}
.options=${["true", "false"].map((val) => ({
value: val,
iconPath:
val === "true"
? mdiArrowOscillating
: mdiArrowOscillatingOff,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
val === "true"
)
: val,
}))}
>
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillatingOff}
></ha-svg-icon>
<ha-list-item value="true" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiArrowOscillating}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
true
)}
</ha-list-item>
<ha-list-item value="false" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiArrowOscillatingOff}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
false
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}

View File

@ -2,7 +2,6 @@ import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
@ -14,6 +13,7 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@ -74,68 +74,48 @@ class MoreInfoHumidifier extends LitElement {
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${hass}
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
@wa-select=${this._handleStateChanged}
.options=${["off", "on"].map((fanState) => ({
value: fanState,
label: this.stateObj
? this.hass.formatEntityState(this.stateObj, fanState)
: fanState,
}))}
>
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off">
${this.hass.formatEntityState(this.stateObj, "off")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.formatEntityState(this.stateObj, "on")}
</ha-list-item>
</ha-control-select-menu>
${supportModes
? html`
<ha-control-select-menu
.hass=${hass}
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${stateObj.attributes.mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${stateObj.attributes.available_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj!,
@wa-select=${this._handleModeChanged}
.options=${stateObj.attributes.available_modes?.map((mode) => ({
value: mode,
label: stateObj
? this.hass.formatEntityAttributeValue(
stateObj,
"mode",
mode
)}
</ha-list-item>
`
)}
)
: mode,
attributeIcon: stateObj
? {
stateObj,
attribute: "mode",
attributeValue: mode,
}
: undefined,
})) || []}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
@ -143,8 +123,8 @@ class MoreInfoHumidifier extends LitElement {
`;
}
private _handleStateChanged(ev) {
const newVal = ev.target.value || null;
private _handleStateChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value || null;
this._callServiceHelper(
this.stateObj!.state,
newVal,
@ -153,8 +133,8 @@ class MoreInfoHumidifier extends LitElement {
);
}
private _handleModeChanged(ev) {
const newVal = ev.target.value || null;
private _handleModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
this._mode = newVal;
this._callServiceHelper(
this.stateObj!.attributes.mode,

View File

@ -9,7 +9,6 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
@ -38,6 +37,7 @@ import "../components/lights/ha-more-info-light-favorite-colors";
import "../components/lights/light-color-rgb-picker";
import "../components/lights/light-color-temp-picker";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
type MainControl = "brightness" | "color_temp" | "color";
@ -253,47 +253,35 @@ class MoreInfoLight extends LitElement {
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"effect"
)}
.value=${this.stateObj.attributes.effect}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleEffect}
@closed=${stopPropagation}
>
${this.stateObj.attributes.effect
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${this.stateObj.attributes.effect}
></ha-attribute-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiCreation}
></ha-svg-icon>`}
${this.stateObj.attributes.effect_list?.map(
(effect) => html`
<ha-list-item .value=${effect} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${effect}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"effect",
effect
)}
</ha-list-item>
`
@wa-select=${this._handleEffect}
.options=${this.stateObj.attributes.effect_list.map(
(effect) => ({
value: effect,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
this.stateObj,
"effect",
effect
)
: effect,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "effect",
attributeValue: effect,
}
: undefined,
})
)}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
@ -317,8 +305,8 @@ class MoreInfoLight extends LitElement {
});
};
private _handleEffect(ev) {
const newVal = ev.target.value;
private _handleEffect(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
const oldVal = this._effect;
if (!newVal || oldVal === newVal) return;

View File

@ -25,8 +25,8 @@ import { VolumeSliderController } from "../../../common/util/volume-slider";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-marquee-text";
@ -202,12 +202,11 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
return html`<ha-dropdown>
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(`ui.card.media_player.source`)}
.path=${mdiLoginVariant}
@wa-select=${this._handleSourceChange}
>
</ha-icon-button>
${this.stateObj.attributes.source_list!.map(
@ -743,7 +742,7 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSourceChange(e: CustomEvent<{ item: HaDropdownItem }>) {
private _handleSourceChange(e: HaDropdownSelectEvent) {
const source = e.detail.item.value;
if (!source || this.stateObj!.attributes.source === source) {
return;
@ -755,7 +754,7 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSoundModeChange(ev: CustomEvent<{ item: HaDropdownItem }>) {
private _handleSoundModeChange(ev: HaDropdownSelectEvent) {
const soundMode = ev.detail.item.value;
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
return;

View File

@ -1,12 +1,10 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-select";
import type { RemoteEntity } from "../../../data/remote";
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-select";
import "../../../components/ha-list-item";
@customElement("more-info-remote")
class MoreInfoRemote extends LitElement {
@ -30,30 +28,24 @@ class MoreInfoRemote extends LitElement {
)}
.value=${stateObj.attributes.current_activity || ""}
@selected=${this._handleActivityChanged}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
.options=${stateObj.attributes.activity_list?.map((activity) => ({
value: activity,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"activity",
activity
),
}))}
>
${stateObj.attributes.activity_list?.map(
(activity) => html`
<ha-list-item .value=${activity}>
${this.hass.formatEntityAttributeValue(
stateObj,
"activity",
activity
)}
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
`;
}
private _handleActivityChanged(ev) {
private _handleActivityChanged(ev: CustomEvent<{ value: string }>) {
const oldVal = this.stateObj!.attributes.current_activity;
const newVal = ev.target.value;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) {
return;

View File

@ -11,13 +11,11 @@ import {
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry";
@ -172,21 +170,17 @@ class MoreInfoVacuum extends LitElement {
.disabled=${stateObj.state === UNAVAILABLE}
.value=${stateObj.attributes.fan_speed}
@selected=${this._handleFanSpeedChanged}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
${stateObj.attributes.fan_speed_list!.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_speed",
mode
)}
</ha-list-item>
`
.options=${stateObj.attributes.fan_speed_list!.map(
(mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"fan_speed",
mode
),
})
)}
>
</ha-select>
<div
style="justify-content: center; align-self: center; padding-top: 1.3em"
@ -291,9 +285,9 @@ class MoreInfoVacuum extends LitElement {
});
}
private _handleFanSpeedChanged(ev) {
private _handleFanSpeedChanged(ev: CustomEvent<{ value: string }>) {
const oldVal = this.stateObj!.attributes.fan_speed;
const newVal = ev.target.value;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) {
return;

View File

@ -2,9 +2,10 @@ import { mdiAccount, mdiAccountArrowRight, mdiWaterBoiler } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-list-item";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { WaterHeaterEntity } from "../../../data/water_heater";
@ -16,7 +17,6 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import "../../../components/ha-attribute-icon";
@customElement("more-info-water_heater")
class MoreInfoWaterHeater extends LitElement {
@ -74,32 +74,25 @@ class MoreInfoWaterHeater extends LitElement {
${supportOperationMode && stateObj.attributes.operation_list
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.water_heater.mode")}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
${stateObj.attributes.operation_list
@wa-select=${this._handleOperationModeChanged}
.options=${stateObj.attributes.operation_list
.concat()
.sort(compareWaterHeaterOperationMode)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="operation_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
.map((mode) => ({
value: mode,
label: this.hass.formatEntityState(stateObj, mode),
attributeIcon: {
stateObj,
attribute: "operation_mode",
attributeValue: mode,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>
`
: nothing}
@ -112,31 +105,18 @@ class MoreInfoWaterHeater extends LitElement {
)}
.value=${stateObj.attributes.away_mode}
.disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleAwayModeChanged}
@closed=${stopPropagation}
@wa-select=${this._handleAwayModeChanged}
.options=${["on", "off"].map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
mode
),
iconPath: mode === "on" ? mdiAccountArrowRight : mdiAccount,
}))}
>
<ha-svg-icon slot="icon" .path=${mdiAccount}></ha-svg-icon>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiAccountArrowRight}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
"on"
)}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiAccount}></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
"off"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
@ -144,8 +124,8 @@ class MoreInfoWaterHeater extends LitElement {
`;
}
private _handleOperationModeChanged(ev) {
const newVal = ev.target.value;
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
this._callServiceHelper(
this.stateObj!.state,
newVal,
@ -156,8 +136,8 @@ class MoreInfoWaterHeater extends LitElement {
);
}
private _handleAwayModeChanged(ev) {
const newVal = ev.target.value === "on";
private _handleAwayModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value === "on";
const oldVal = this.stateObj!.attributes.away_mode === "on";
this._callServiceHelper(oldVal, newVal, "set_away_mode", {

View File

@ -25,11 +25,11 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
getEntityContext,
getEntityEntryContext,
@ -37,11 +37,12 @@ import {
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import { computeRTL } from "../../common/util/compute_rtl";
import { withViewTransition } from "../../common/util/view-transition";
import "../../components/ha-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev";
import "../../components/ha-related-items";
@ -317,7 +318,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._setView("related");
}
private _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
private _handleMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail?.item?.value;
switch (action) {
case "device":
@ -770,7 +771,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
}
private _enlarge() {
this.large = !this.large;
withViewTransition(() => {
this.large = !this.large;
});
}
private _handleOpened() {

View File

@ -1,8 +1,10 @@
import { mdiDevices } from "@mdi/js";
import Fuse from "fuse.js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { NavigationFilterOptions } from "../../common/config/filter_navigation_pages";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
@ -15,6 +17,7 @@ import "../../components/ha-icon";
import "../../components/ha-picker-combo-box";
import type {
HaPickerComboBox,
PickerComboBoxIndexSelectedDetail,
PickerComboBoxItem,
} from "../../components/ha-picker-combo-box";
import "../../components/ha-spinner";
@ -48,8 +51,10 @@ import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../../resources/fuseMultiTerm";
import { buttonLinkStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { isIosApp } from "../../util/is_ios";
import { isMac } from "../../util/is_mac";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar";
@ -64,7 +69,7 @@ export class QuickBar extends LitElement {
@state() private _loading = true;
@state() private _hint?: string;
@state() private _showHint = false;
@state() private _selectedSection?: QuickBarSection;
@ -80,8 +85,12 @@ export class QuickBar extends LitElement {
private _addons?: HassioAddonInfo[];
private _navigationFilterOptions: NavigationFilterOptions = {};
private _translationsLoaded = false;
private _itemSelected = false;
// #region lifecycle
public async showDialog(params: QuickBarParams) {
if (!this._translationsLoaded) {
@ -90,7 +99,7 @@ export class QuickBar extends LitElement {
}
this._initialize();
this._selectedSection = params.mode;
this._hint = params.hint;
this._showHint = params.showHint ?? false;
this._open = true;
}
@ -104,6 +113,12 @@ export class QuickBar extends LitElement {
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
// Derive Bluetooth config entries status for navigation filtering
this._navigationFilterOptions = {
hasBluetoothConfigEntries: configEntries.some(
(entry) => entry.domain === "bluetooth"
),
};
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error fetching config entries for quick bar", err);
@ -152,15 +167,28 @@ export class QuickBar extends LitElement {
this._selectedSection = undefined;
this._opened = false;
this._open = false;
this._itemSelected = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
};
// fallback in case the closed event is not fired
private _dialogCloseStarted = () => {
setTimeout(
() => {
if (this._opened) {
this._dialogClosed();
}
},
350 // close animation timeout is 300ms
);
};
// #endregion lifecycle
// #region render
protected render() {
if (!this._open) {
if (!this._open && !this._opened) {
return nothing;
}
@ -210,6 +238,7 @@ export class QuickBar extends LitElement {
hideActions
@wa-show=${this._showTriggered}
@wa-after-show=${this._dialogOpened}
@wa-hide=${this._dialogCloseStarted}
@closed=${this._dialogClosed}
>
${!this._loading && this._opened
@ -230,9 +259,17 @@ export class QuickBar extends LitElement {
clearable
></ha-picker-combo-box>`
: nothing}
${this._hint
${this._showHint
? html`<ha-tip slot="footer" .hass=${this.hass}
>${this._hint}</ha-tip
>${this.hass.localize("ui.tips.key_shortcut_quick_search", {
keyboard_shortcut: html`<button
class="link"
@click=${this._openShortcutDialog}
>
${this.hass.localize("ui.tips.keyboard_shortcut")}
</button>`,
modifier: isMac ? "⌘" : "Ctrl",
})}</ha-tip
>`
: nothing}
</ha-adaptive-dialog>
@ -281,6 +318,9 @@ export class QuickBar extends LitElement {
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.image}
style=${"iconColor" in item && item.iconColor
? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px`
: ""}
/>
`
: item.icon
@ -393,7 +433,8 @@ export class QuickBar extends LitElement {
if (!section || section === "navigate") {
let navigateItems = this._generateNavigationCommandsMemoized(
this.hass,
this._addons
this._addons,
this._navigationFilterOptions
).sort(this._sortBySortingLabel);
if (filter) {
@ -559,7 +600,11 @@ export class QuickBar extends LitElement {
);
private _generateNavigationCommandsMemoized = memoizeOne(
generateNavigationCommands
(
hass: HomeAssistant,
apps: HassioAddonInfo[] | undefined,
filterOptions: NavigationFilterOptions
) => generateNavigationCommands(hass, apps, filterOptions)
);
private _generateActionCommandsMemoized = memoizeOne(generateActionCommands);
@ -613,13 +658,29 @@ export class QuickBar extends LitElement {
// #region interaction
private async _handleItemSelected(ev: CustomEvent<{ index: number }>) {
if (this._comboBox && this._comboBox.virtualizerElement) {
const index = ev.detail.index;
private _navigate(path: string, newTab = false) {
if (newTab) {
window.open(path, "_blank", "noreferrer");
} else {
navigate(path);
}
}
private async _handleItemSelected(
ev: CustomEvent<PickerComboBoxIndexSelectedDetail>
) {
if (
!this._itemSelected &&
this._comboBox &&
this._comboBox.virtualizerElement
) {
const { index, newTab } = ev.detail;
const item = this._comboBox.virtualizerElement.items[
index
] as PickerComboBoxItem;
this._itemSelected = true;
// entity selected
if (item && "stateObj" in item) {
this.closeDialog();
@ -631,15 +692,17 @@ export class QuickBar extends LitElement {
// device selected
if (item && item.id.startsWith(`device${SEPARATOR}`)) {
const path = `/config/devices/device/${item.id.split(SEPARATOR)[1]}`;
this.closeDialog();
navigate(`/config/devices/device/${item.id.split(SEPARATOR)[1]}`);
this._navigate(path, newTab);
return;
}
// area selected
if (item && item.id.startsWith(`area${SEPARATOR}`)) {
const path = `/config/areas/area/${item.id.split(SEPARATOR)[1]}`;
this.closeDialog();
navigate(`/config/areas/area/${item.id.split(SEPARATOR)[1]}`);
this._navigate(path, newTab);
return;
}
@ -693,53 +756,65 @@ export class QuickBar extends LitElement {
return;
}
navigate((item as NavigationComboBoxItem).path);
const path = (item as NavigationComboBoxItem).path;
this._navigate(path, newTab);
}
}
}
private _openShortcutDialog(ev: Event): void {
ev.preventDefault();
showShortcutsDialog(this);
this.closeDialog();
}
// #endregion interaction
// #region styles
static styles = css`
:host {
--dialog-surface-margin-top: var(--ha-space-10);
--ha-dialog-min-height: 620px;
--ha-bottom-sheet-height: calc(
100vh - max(var(--safe-area-inset-top), 48px)
);
--ha-bottom-sheet-height: calc(
100dvh - max(var(--safe-area-inset-top), 48px)
);
--ha-bottom-sheet-max-height: calc(
100vh - max(var(--safe-area-inset-top), 48px)
);
--ha-bottom-sheet-max-height: calc(
100dvh - max(var(--safe-area-inset-top), 48px)
);
--dialog-content-padding: 0;
--safe-area-inset-bottom: 0px;
}
static get styles(): CSSResultGroup {
return [
buttonLinkStyle,
css`
:host {
--dialog-surface-margin-top: var(--ha-space-10);
--ha-dialog-min-height: 620px;
--ha-bottom-sheet-height: calc(
100vh - max(var(--safe-area-inset-top), 48px)
);
--ha-bottom-sheet-height: calc(
100dvh - max(var(--safe-area-inset-top), 48px)
);
--ha-bottom-sheet-max-height: calc(
100vh - max(var(--safe-area-inset-top), 48px)
);
--ha-bottom-sheet-max-height: calc(
100dvh - max(var(--safe-area-inset-top), 48px)
);
--dialog-content-padding: 0;
--safe-area-inset-bottom: 0px;
}
ha-tip {
display: flex;
justify-content: center;
align-items: center;
color: var(--secondary-text-color);
gap: var(--ha-space-1);
}
ha-tip {
display: flex;
justify-content: center;
align-items: center;
color: var(--secondary-text-color);
gap: var(--ha-space-1);
}
ha-tip a {
color: var(--primary-color);
}
ha-tip a {
color: var(--primary-color);
}
@media all and (max-width: 450px), all and (max-height: 690px) {
ha-tip {
display: none;
}
}
`;
@media all and (max-width: 450px), all and (max-height: 690px) {
ha-tip {
display: none;
}
}
`,
];
}
// #endregion styles
}

View File

@ -1,4 +1,5 @@
import { fireEvent } from "../../common/dom/fire_event";
import { closeDialog } from "../make-dialog-manager";
export type QuickBarSection =
| "entity"
@ -10,7 +11,7 @@ export type QuickBarSection =
export interface QuickBarParams {
entityFilter?: string;
mode?: QuickBarSection;
hint?: string;
showHint?: boolean;
}
export const loadQuickBar = () => import("./ha-quick-bar");
@ -26,3 +27,7 @@ export const showQuickBar = (
addHistory: false,
});
};
export const closeQuickBar = (): void => {
closeDialog("ha-quick-bar");
};

View File

@ -9,8 +9,8 @@ import { formatLanguageCode } from "../../common/language/format_language";
import "../../components/chips/ha-assist-chip";
import "../../components/ha-dialog";
import "../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
import { getLanguageOptions } from "../../components/ha-language-picker";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { fetchAssistSatelliteConfiguration } from "../../data/assist_satellite";
@ -328,7 +328,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
}
}
private _handlePickLanguage(ev: CustomEvent<{ item: HaDropdownItem }>) {
private _handlePickLanguage(ev: HaDropdownSelectEvent) {
this._language = ev.detail.item.value;
}

Some files were not shown because too many files have changed in this diff Show More