Compare commits

...

18 Commits

Author SHA1 Message Date
Petar Petrov a973291d1a Document hass-to-context migration in frontend instructions 2026-06-04 18:37:04 +03:00
Bram Kragten ee755ff58a Use zone location as location of person if no gps is available (#52415) 2026-06-04 17:49:23 +03:00
Aidan Timson 635a6442b4 Update agents on button size after #52391 (#52411)
* Update agents button size after #52391

* Add updated sizes
2026-06-04 17:41:37 +03:00
Paul Bottein 3608156e83 Show media player playback controls as disabled instead of hiding them (#52370)
* Show media player playback controls as disabled instead of hiding them

* Add on/off power by default
2026-06-04 15:55:05 +03:00
Bram Kragten 8529980bdd Show template errors in traces (#52412) 2026-06-04 13:54:08 +01:00
Paul Bottein 9ff508259f Migrate deprecated Web Awesome size values to short form (s/m/l) (#52391) 2026-06-04 13:32:33 +01:00
Petar Petrov 1ec4dc6c79 Trim redundancy in AI coding instructions (#52409) 2026-06-04 13:30:13 +01:00
Aidan Timson 39c88e573d Match the card style of apps repo to installed (#52407) 2026-06-04 13:49:13 +02:00
renovate[bot] 99eb752a68 Update vitest monorepo to v4.1.8 (#52405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-04 11:39:55 +02:00
Jan-Philipp Benecke 9cc63c1f53 Use context instead of hass property in ha-area-picker (#52394)
* Use context instead of hass property in ha-area-picker

* Use state() on _areas
2026-06-04 11:42:54 +03:00
Aidan Timson d2188f600f Add related context event to trace pages (#52392)
* Add trace related context

* Review
2026-06-04 10:47:19 +03:00
Petar Petrov d69d4c592d Update AI coding instructions to match current frontend code (#52403)
Update AI coding instructions to match current frontend APIs

Fix outdated references in .github/copilot-instructions.md (symlinked as
CLAUDE.md and AGENTS.md): showAlertDialog import path, ESLint flat config,
ha-dialog width presets, ha-alert slots/props, ha-button variant/appearance
guidance, Web Awesome direction, and the querySelector pitfall.
2026-06-04 09:46:21 +02:00
Aidan Timson a97df0409c Fix editor context (#52393) 2026-06-04 09:24:05 +03:00
Aidan Timson 468756bd2f Add related context to area page (#52383) 2026-06-04 08:30:02 +03:00
ildar170975 cc43caa87b hui-card-picker: fix a path to "content" (#52398)
fix a path to "content"
2026-06-04 08:29:00 +03:00
renovate[bot] 1fb3efadfa Update dependency js-yaml to v4.2.0 (#52401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-04 08:02:56 +03:00
Wendelin 69599352a3 Introduce list-virtualized (#52354)
* Fix keyboard nav and focus

* Simplify

* use virtualized list in zha

* update jsdocs

* Fix ha-domain-integration

* Update docs and clean up

* fix

* review

* Add translation

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-06-04 07:58:59 +03:00
Simon Lamon 5c0f2feac1 Remove explicit netlify version (#52386) 2026-06-03 20:50:55 +02:00
209 changed files with 2520 additions and 1645 deletions
+84 -64
View File
@@ -8,6 +8,7 @@ You are an assistant helping with development of the Home Assistant frontend. Th
- [Quick Reference](#quick-reference)
- [Core Architecture](#core-architecture)
- [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
- [Development Standards](#development-standards)
- [Component Library](#component-library)
- [Common Patterns](#common-patterns)
@@ -40,7 +41,7 @@ script/develop # Development server
```typescript
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { showAlertDialog } from "../dialogs/generic/show-alert-dialog";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
```
## Core Architecture
@@ -52,13 +53,64 @@ The Home Assistant frontend is a modern web application that:
- Communicates with the backend via WebSocket API
- Provides comprehensive theming and internationalization
## State Access: Contexts Instead of `hass`
Every component used to take the whole `hass: HomeAssistant` object — a god-object that re-renders on any unrelated `hass` change, forces tests to mock everything, and hides what a component actually reads. We're moving leaf components to **fine-grained [Lit context](https://lit.dev/docs/data/context/)**: consume only the slice you need and re-render only when it changes.
For new code, consume the matching context instead of adding a `hass` property. `hass` stays for container components that own it and feed the providers; the canonical migration is [`hui-button-card.ts`](src/panels/lovelace/cards/hui-button-card.ts). Infrastructure: contexts in [`src/data/context/index.ts`](src/data/context/index.ts), the `consume…` helpers in [`src/common/decorators/consume-context-entry.ts`](src/common/decorators/consume-context-entry.ts), and `@transform` in [`src/common/decorators/transform.ts`](src/common/decorators/transform.ts). Providers are wired automatically by `contextMixin` on `HassBaseEl` — you only consume.
### Contexts
Consume the narrowest context that covers your reads:
| Context | Replaces |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `statesContext` | `hass.states` |
| `entitiesContext` / `devicesContext` / `areasContext` / `floorsContext` | `hass.entities` / `.devices` / `.areas` / `.floors` (or `registriesContext` for all four) |
| `servicesContext` | `hass.services` |
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
| `formattersContext` | `hass.formatEntityName`, `hass.formatEntityState`, `hass.formatEntityAttributeName`, … |
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.hassUrl` |
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
| `uiContext` | `hass.themes`, `hass.selectedTheme`, `hass.panels`, `hass.dockedSidebar`, … |
| `narrowViewportContext` | narrow-layout boolean |
Lazy contexts (subscribe on first consumer, tear down after the last): `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`. The single-field contexts (`localizeContext`, `themesContext`, `userContext`, …) are **deprecated** — use the grouped ones above.
### Consuming
Use the `consume…` helpers for entity-scoped and `localize` reads. `entityIdPath` is resolved against `this`, so these watch `this._config.entity`:
```ts
@state() @consumeEntityState({ entityIdPath: ["_config", "entity"] })
private _stateObj?: HassEntity; // consumeEntityStates(...) for a record of several
@state() @consumeEntityRegistryEntry({ entityIdPath: ["_config", "entity"] })
private _entity?: EntityRegistryDisplayEntry;
@state() @consumeLocalize()
private _localize!: LocalizeFunc;
```
For any other single field, pair `@consume` with `@transform`:
```ts
@state()
@consume({ context: uiContext, subscribe: true })
@transform<HomeAssistantUI, Themes>({ transformer: ({ themes }) => themes })
private _themes!: Themes;
```
`@transform`'s `watch` option re-runs the transformer when a host prop changes — needed when an entity id is computed, since `consumeEntityState` only watches the first path segment. To consume a whole group untransformed, drop `@transform` and type it `ContextType<typeof statesContext>`.
## Development Standards
### Code Quality Requirements
**Linting and Formatting (Enforced by Tools)**
- ESLint config extends Airbnb, TypeScript strict, Lit, Web Components, Accessibility
- ESLint config (flat config) extends TypeScript strict, Lit, Web Components, Accessibility (lit-a11y), and import-x
- Prettier with ES5 trailing commas enforced
- No console statements (`no-console: "error"`) - use proper logging
- Import organization: No unused imports, consistent type imports
@@ -136,6 +188,7 @@ export class HaMyComponent extends LitElement {
### Data Management
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
- **Prefer contexts over `hass`**: For state reads, consume the relevant Lit context instead of taking the whole `hass` object — see [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
- **Cache appropriately**: Use collections and caching for frequently accessed data
- **Handle errors gracefully**: All API calls should have error handling
- **Update real-time**: Subscribe to state changes for live updates
@@ -160,7 +213,7 @@ try {
- Defined in `src/resources/theme/core.globals.ts`
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
- **Mobile-first responsive**: Design for mobile, enhance for desktop
- **Follow Material Design**: Use Material Web Components where appropriate
- **Prefer `ha-*` components**: Build on the Home Assistant component library (many now wrap Web Awesome components); avoid new use of legacy Material Web Components (`mwc-*`), which are being phased out
- **Support RTL**: Ensure all layouts work in RTL languages
```typescript
@@ -267,15 +320,22 @@ fireEvent(this, "show-dialog", {
**Dialog Sizing:**
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (580px - default), `"large"` (1024px), or `"full"`
- Custom sizing is NOT recommended - use the standard width presets
**Button Appearance Guidelines:**
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
`ha-button` (wraps the Web Awesome button — see `src/components/ha-button.ts`) has two independent axes plus size:
- **`variant`** (color): `"brand"` (default), `"neutral"`, `"danger"`, `"warning"`, `"success"`
- **`appearance`** (fill style): `"accent"`, `"filled"`, `"outlined"`, `"plain"`
- **`size`**: `"xs"` (extra small, 40px), `"s"` (small, 32px), `"m"` (medium, 40px - default), `"l"` (large, 48px), `"xl"` (extra large, 40px)
Common patterns:
- **Primary action**: `appearance="filled"` for emphasis (or the default appearance for a lighter look)
- **Secondary action**: `appearance="plain"` for cancel/dismiss actions
- **Destructive actions**: `variant="danger"` for delete/remove operations (the generic confirmation dialog uses `variant="danger"` for its confirm button — see `src/dialogs/generic/dialog-box.ts`)
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
**Gallery Documentation:**
@@ -308,7 +368,8 @@ fireEvent(this, "show-dialog", {
### Alert Component (ha-alert)
- Types: `error`, `warning`, `info`, `success`
- Properties: `title`, `alert-type`, `dismissable`, `icon`, `action`, `rtl`
- Properties: `title`, `alert-type`, `dismissable`, `narrow`
- Slots: `icon` (override the leading icon), `action` (custom action content)
- Content announced by screen readers when dynamically displayed
```html
@@ -449,7 +510,7 @@ this.hass.localize("ui.panel.config.updates.update_available", {
### Common Pitfalls to Avoid
- Don't use `querySelector` - Use refs or component properties
- Don't manually query the DOM with `querySelector` - use the `@query`/`@queryAll` decorators or component properties
- Don't manipulate DOM directly - Let Lit handle rendering
- Don't use global styles - Scope styles to components
- Don't block the main thread - Use web workers for heavy computation
@@ -540,35 +601,24 @@ When creating a pull request, you **must** use the PR template located at `.gith
#### Translation Considerations
- **Add translation keys**: All user-facing text must be translatable
- **Use placeholders**: Support dynamic content in translations
All user-facing text must be translatable — see the **Internationalization** section (under Common Patterns) for the `localize` API and placeholder usage. From a copy perspective:
- **Keep context**: Provide enough context for translators
```typescript
// Good
this.hass.localize("ui.panel.config.automation.delete_confirm", {
name: automation.alias,
});
// Bad - hardcoded text
("Are you sure you want to delete this automation?");
```
- **Avoid concatenation**: Prefer full localized strings with placeholders over stitching translated fragments together
### Common Review Issues (From PR Analysis)
Recurring, easy-to-miss problems surfaced in real PR reviews. These complement the standards above rather than repeating them — items already covered earlier (loading states, error handling, mobile layout, theming, import hygiene) are intentionally not duplicated here.
#### User Experience and Accessibility
- **Form validation**: Always provide proper field labels and validation feedback
- **Form accessibility**: Prevent password managers from incorrectly identifying fields
- **Loading states**: Show clear progress indicators during async operations
- **Error handling**: Display meaningful error messages when operations fail
- **Mobile responsiveness**: Ensure components work well on small screens
- **Hit targets**: Make clickable areas large enough for touch interaction
- **Visual feedback**: Provide clear indication of interactive states
- **Visual feedback**: Provide clear indication of interactive states (hover, active, focus)
#### Dialog and Modal Patterns
- **Dialog width constraints**: Respect minimum and maximum width requirements
- **Interview progress**: Show clear progress for multi-step operations
- **State persistence**: Handle dialog state properly during background operations
- **Cancel behavior**: Ensure cancel/close buttons work consistently
@@ -580,15 +630,12 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
- **Visual hierarchy**: Ensure proper font sizes and spacing ratios
- **Grid alignment**: Components should align to the design grid system
- **Badge placement**: Position badges and indicators consistently
- **Color theming**: Respect theme variables and design system colors
#### Code Quality Issues
- **Null checking**: Always check if entities exist before accessing properties
- **TypeScript safety**: Handle potentially undefined array/object access
- **Import organization**: Remove unused imports and use proper type imports
- **Event handling**: Properly subscribe and unsubscribe from events
- **Memory leaks**: Clean up subscriptions and event listeners
- **Event handling and cleanup**: Subscribe/unsubscribe correctly and remove listeners to avoid memory leaks
#### Configuration and Props
@@ -599,39 +646,12 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
## Review Guidelines
### Core Requirements Checklist
Final pre-submission checklist. Linting and formatting are enforced by tooling, so this focuses on what tools can't catch rather than restating every rule above.
- [ ] TypeScript strict mode passes (`yarn lint:types`)
- [ ] No ESLint errors or warnings (`yarn lint:eslint`)
- [ ] Prettier formatting applied (`yarn lint:prettier`)
- [ ] Lit analyzer passes (`yarn lint:lit`)
- [ ] Component follows Lit best practices
- [ ] Proper error handling implemented
- [ ] Loading states handled
- [ ] Mobile responsive
- [ ] Theme variables used
- [ ] Translations added
- [ ] Accessible to screen readers
- [ ] Tests added (where applicable)
- [ ] No console statements (use proper logging)
- [ ] Unused imports removed
- [ ] Proper naming conventions
### Text and Copy Checklist
- [ ] Follows terminology guidelines (Delete vs Remove, Create vs Add)
- [ ] Localization keys added for all user-facing text
- [ ] Uses "Home Assistant" (never "HA" or "HASS")
- [ ] Sentence case for ALL text (titles, headings, buttons, labels)
- [ ] American English spelling
- [ ] Friendly, informational tone
- [ ] Avoids abbreviations and jargon
- [ ] Correct terminology (integration not component)
### Component-Specific Checks
- [ ] ha-alert used correctly for messages
- [ ] ha-form uses proper schema structure
- [ ] `yarn lint` passes (TypeScript, ESLint, Prettier, Lit analyzer) and `yarn test` is green
- [ ] Tests added for new data processing/utilities (where applicable)
- [ ] All user-facing text is localized and follows the Text and Copy guidelines (sentence case, "Home Assistant" in full, Delete/Remove + Create/Add)
- [ ] Components handle all states (loading, error, unavailable)
- [ ] Entity existence checked before property access
- [ ] Event subscriptions properly cleaned up
- [ ] Event/subscription listeners cleaned up (no memory leaks)
- [ ] Accessible to screen readers and keyboard
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev
npx -y netlify-cli deploy --dir=cast/dist --alias dev
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
@@ -82,7 +82,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod
npx -y netlify-cli deploy --dir=cast/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
+2 -2
View File
@@ -47,7 +47,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
@@ -83,7 +83,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod
npx -y netlify-cli deploy --dir=gallery/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
- name: Deploy preview to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
--json > deploy_output.json
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+2 -2
View File
@@ -78,13 +78,13 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action",
type: "error",
actionSlot: html`<ha-button size="small" slot="action">restart</ha-button>`,
actionSlot: html`<ha-button size="s" slot="action">restart</ha-button>`,
},
{
title: "Unsaved data",
description: "You have unsaved data",
type: "warning",
actionSlot: html`<ha-button size="small" slot="action">save</ha-button>`,
actionSlot: html`<ha-button size="s" slot="action">save</ha-button>`,
},
{
title: "Slotted icon",
@@ -26,7 +26,7 @@ title: Button
filled button
</ha-button>
<ha-button size="small">
<ha-button size="s">
small
</ha-button>
</div>
@@ -34,7 +34,7 @@ title: Button
```html
<ha-button> simple button </ha-button>
<ha-button size="small"> small </ha-button>
<ha-button size="s"> small </ha-button>
```
### API
@@ -57,7 +57,7 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| size | "small"/"medium"/"large" | "medium" | Sets the button size. |
| size | "xs"/"s"/"m"/"l"/"xl" | "m" | Sets the button size. |
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
| disabled | Boolean | false | Disables the button and prevents user interaction. |
+2 -2
View File
@@ -49,7 +49,7 @@ export class DemoHaButton extends LitElement {
<ha-button
.appearance=${appearance}
.variant=${variant}
size="small"
size="s"
>
${titleCase(`${variant} ${appearance}`)}
</ha-button>
@@ -100,7 +100,7 @@ export class DemoHaButton extends LitElement {
<ha-button
.variant=${variant}
.appearance=${appearance}
size="small"
size="s"
disabled
>
${titleCase(`${appearance}`)}
@@ -62,10 +62,11 @@ host reflects `aria-multiselectable`.
**Events**
- `ha-list-selected`selection changed. Detail
`{ index: number | Set<number>, diff: { added: Set<number>, removed: Set<number> } }`.
`index` is a `number` in single mode (`-1` when nothing selected) and a
`Set<number>` in multi mode.
- `ha-list-item-selected`an option was selected. Detail is the option's
index (`number`). In single mode this is the only selection event; in multi
mode it fires for each option added to the selection.
- `ha-list-item-deselected` — an option was deselected (multi mode only). Detail
is the option's index (`number`).
**Methods / getters**
+53 -13
View File
@@ -20,7 +20,6 @@ import "../../../../src/components/item/ha-list-item-option";
import "../../../../src/components/list/ha-list-base";
import "../../../../src/components/list/ha-list-nav";
import "../../../../src/components/list/ha-list-selectable";
import type { HaListSelectedDetail } from "../../../../src/components/list/types";
type Appearance = "line" | "checkbox";
type Position = "start" | "end";
@@ -185,7 +184,7 @@ export class DemoHaList extends LitElement {
<ha-card header="Single select, appearance=line">
<ha-list-selectable
aria-label="Single select"
@ha-list-selected=${this._onSingle}
@ha-list-item-selected=${this._onSingle}
>
${this._options.map(
(o, i) => html`
@@ -205,7 +204,8 @@ export class DemoHaList extends LitElement {
<ha-list-selectable
multi
aria-label="Multi select line"
@ha-list-selected=${this._onMultiLine}
@ha-list-item-selected=${this._onMultiLineSelected}
@ha-list-item-deselected=${this._onMultiLineDeselected}
>
${this._options.map(
(o, i) => html`
@@ -227,7 +227,8 @@ export class DemoHaList extends LitElement {
<ha-list-selectable
multi
aria-label="Multi checkbox start"
@ha-list-selected=${this._onMultiCheckStart}
@ha-list-item-selected=${this._onMultiCheckStartSelected}
@ha-list-item-deselected=${this._onMultiCheckStartDeselected}
>
${this._options.map(
(o, i) => html`
@@ -253,7 +254,8 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
<ha-list-selectable
multi
aria-label="Multi checkbox end"
@ha-list-selected=${this._onMultiCheckEnd}
@ha-list-item-selected=${this._onMultiCheckEndSelected}
@ha-list-item-deselected=${this._onMultiCheckEndDeselected}
>
${this._options.map(
(o, i) => html`
@@ -347,20 +349,58 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
this._buttonClicks++;
};
private _onSingle = (ev: CustomEvent<HaListSelectedDetail>) => {
this._single = ev.detail.index;
private _withIndex(
value: number | Set<number>,
index: number,
selected: boolean
): Set<number> {
const next = new Set(value instanceof Set ? value : []);
if (selected) {
next.add(index);
} else {
next.delete(index);
}
return next;
}
private _onSingle = (ev: CustomEvent<number>) => {
this._single = ev.detail;
};
private _onMultiLine = (ev: CustomEvent<HaListSelectedDetail>) => {
this._multiLine = ev.detail.index;
private _onMultiLineSelected = (ev: CustomEvent<number>) => {
this._multiLine = this._withIndex(this._multiLine, ev.detail, true);
};
private _onMultiCheckStart = (ev: CustomEvent<HaListSelectedDetail>) => {
this._multiCheckStart = ev.detail.index;
private _onMultiLineDeselected = (ev: CustomEvent<number>) => {
this._multiLine = this._withIndex(this._multiLine, ev.detail, false);
};
private _onMultiCheckEnd = (ev: CustomEvent<HaListSelectedDetail>) => {
this._multiCheckEnd = ev.detail.index;
private _onMultiCheckStartSelected = (ev: CustomEvent<number>) => {
this._multiCheckStart = this._withIndex(
this._multiCheckStart,
ev.detail,
true
);
};
private _onMultiCheckStartDeselected = (ev: CustomEvent<number>) => {
this._multiCheckStart = this._withIndex(
this._multiCheckStart,
ev.detail,
false
);
};
private _onMultiCheckEndSelected = (ev: CustomEvent<number>) => {
this._multiCheckEnd = this._withIndex(this._multiCheckEnd, ev.detail, true);
};
private _onMultiCheckEndDeselected = (ev: CustomEvent<number>) => {
this._multiCheckEnd = this._withIndex(
this._multiCheckEnd,
ev.detail,
false
);
};
static styles = css`
@@ -17,13 +17,13 @@ subtitle: A slider component for selecting a value from a range.
### Example Usage
<div class="wrapper">
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="medium"></ha-slider>
<ha-slider size="s" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="m"></ha-slider>
</div>
```html
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="medium"></ha-slider>
<ha-slider size="s" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="m"></ha-slider>
```
### API
+2 -2
View File
@@ -29,7 +29,7 @@ export class DemoHaSlider extends LitElement {
></ha-slider>
<span>Small</span>
<ha-slider
size="small"
size="s"
min="0"
max="8"
value="4"
@@ -37,7 +37,7 @@ export class DemoHaSlider extends LitElement {
></ha-slider>
<span>Medium</span>
<ha-slider
size="medium"
size="m"
min="0"
max="8"
value="4"
+3 -3
View File
@@ -95,7 +95,7 @@
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.4",
"intl-messageformat": "11.2.7",
"js-yaml": "4.1.1",
"js-yaml": "4.2.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
@@ -153,7 +153,7 @@
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@vitest/coverage-v8": "4.1.7",
"@vitest/coverage-v8": "4.1.8",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -196,7 +196,7 @@
"typescript": "6.0.3",
"typescript-eslint": "8.60.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.7",
"vitest": "4.1.8",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
+59
View File
@@ -0,0 +1,59 @@
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
export interface EntityLocation {
latitude: number;
longitude: number;
gpsAccuracy?: number;
}
const findFirstActiveZone = (
inZones: readonly string[],
states: HassEntities
): HassEntity | undefined => {
for (const zoneId of inZones) {
const zone = states[zoneId];
if (
zone &&
!zone.attributes.passive &&
typeof zone.attributes.latitude === "number" &&
typeof zone.attributes.longitude === "number"
) {
return zone;
}
}
return undefined;
};
export const getEntityLocation = (
stateObj: HassEntity,
states: HassEntities
): EntityLocation | undefined => {
const {
latitude,
longitude,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (typeof latitude === "number" && typeof longitude === "number") {
return { latitude, longitude, gpsAccuracy };
}
if (computeStateDomain(stateObj) !== "person") {
return undefined;
}
const inZones = stateObj.attributes.in_zones;
if (!Array.isArray(inZones) || inZones.length === 0) {
return undefined;
}
const zone = findFirstActiveZone(inZones, states);
if (!zone) {
return undefined;
}
return {
latitude: zone.attributes.latitude,
longitude: zone.attributes.longitude,
};
};
+1 -1
View File
@@ -167,7 +167,7 @@ export class StateHistoryCharts extends LitElement {
)}`}
${this.syncCharts && this._hasZoomedCharts
? html`<ha-button
size="large"
size="l"
class="reset-button"
@click=${this._handleGlobalZoomReset}
>
@@ -117,7 +117,7 @@ export class HaEntityNamePicker extends LitElement {
<div class="header">
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-button-toggle-group
size="small"
size="s"
.buttons=${modeButtons}
.active=${this._mode}
.disabled=${this.disabled}
+66 -36
View File
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import { mdiPlus, mdiTextureBox } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
@@ -10,10 +11,19 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
import { createAreaRegistryEntry } from "../data/area/area_registry";
import {
apiContext,
areasContext,
devicesContext,
entitiesContext,
floorsContext,
internationalizationContext,
statesContext,
} from "../data/context";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box-item";
import "./ha-generic-picker";
@@ -27,8 +37,6 @@ const ADD_NEW_ID = "___ADD_NEW___";
@customElement("ha-area-picker")
export class HaAreaPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@@ -84,16 +92,37 @@ export class HaAreaPicker extends LitElement {
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
@consume({ context: entitiesContext, subscribe: true })
private _entities!: ContextType<typeof entitiesContext>;
@consume({ context: devicesContext, subscribe: true })
private _devices!: ContextType<typeof devicesContext>;
@consume({ context: areasContext, subscribe: true })
private _areas!: ContextType<typeof areasContext>;
@consume({ context: floorsContext, subscribe: true })
private _floors!: ContextType<typeof floorsContext>;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
@state() private _pendingAreaId?: string;
protected willUpdate(changedProperties: PropertyValues<this>) {
protected willUpdate(changedProperties: PropertyValues) {
if (
this._pendingAreaId &&
changedProperties.has("hass") &&
this.hass.areas !== changedProperties.get("hass")?.areas &&
this.hass.areas[this._pendingAreaId]
changedProperties.has("_areas") &&
this._areas[this._pendingAreaId]
) {
this._setValue(this._pendingAreaId);
this._pendingAreaId = undefined;
@@ -107,11 +136,11 @@ export class HaAreaPicker extends LitElement {
private _getAreasMemoized = memoizeOne(
(
haAreas: HomeAssistant["areas"],
haFloors: HomeAssistant["floors"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
haStates: HomeAssistant["states"],
haAreas: ContextType<typeof areasContext>,
haFloors: ContextType<typeof floorsContext>,
haDevices: ContextType<typeof devicesContext>,
haEntities: ContextType<typeof entitiesContext>,
haStates: ContextType<typeof statesContext>,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
@@ -131,9 +160,9 @@ export class HaAreaPicker extends LitElement {
// Recompute value renderer when the areas change
private _computeValueRenderer = memoizeOne(
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
(haAreas: ContextType<typeof areasContext>): PickerValueRenderer =>
(value) => {
const area = this.hass.areas[value];
const area = haAreas[value];
if (!area) {
return html`
@@ -142,7 +171,7 @@ export class HaAreaPicker extends LitElement {
`;
}
const { floor } = getAreaContext(area, this.hass.floors);
const { floor } = getAreaContext(area, this._floors);
const areaName = area ? computeAreaName(area) : undefined;
const floorName = floor ? computeFloorName(floor) : undefined;
@@ -166,11 +195,11 @@ export class HaAreaPicker extends LitElement {
private _getItems = () =>
this._getAreasMemoized(
this.hass.areas,
this.hass.floors,
this.hass.devices,
this.hass.entities,
this.hass.states,
this._areas,
this._floors,
this._devices,
this._entities,
this._states,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
@@ -180,7 +209,7 @@ export class HaAreaPicker extends LitElement {
);
private _allAreaNames = memoizeOne(
(areas: HomeAssistant["areas"]) =>
(areas: ContextType<typeof areasContext>) =>
Object.values(areas)
.map((area) => computeAreaName(area)?.toLowerCase())
.filter(Boolean) as string[]
@@ -193,13 +222,13 @@ export class HaAreaPicker extends LitElement {
return [];
}
const allAreas = this._allAreaNames(this.hass.areas);
const allAreas = this._allAreaNames(this._areas);
if (searchString && !allAreas.includes(searchString.toLowerCase())) {
return [
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
primary: this._i18n.localize(
"ui.components.area-picker.add_new_suggestion",
{
name: searchString,
@@ -213,7 +242,7 @@ export class HaAreaPicker extends LitElement {
return [
{
id: ADD_NEW_ID,
primary: this.hass.localize("ui.components.area-picker.add_new"),
primary: this._i18n.localize("ui.components.area-picker.add_new"),
icon_path: mdiPlus,
},
];
@@ -221,15 +250,17 @@ export class HaAreaPicker extends LitElement {
protected render(): TemplateResult {
const baseLabel =
this.label ?? this.hass.localize("ui.components.area-picker.area");
const valueRenderer = this._computeValueRenderer(this.hass.areas);
this.label ?? this._i18n.localize("ui.components.area-picker.area");
const areas = this._areas;
const floors = this._floors;
const valueRenderer = this._computeValueRenderer(areas);
// Only show label if there's no floor
let label: string | undefined = baseLabel;
if (this.value && baseLabel) {
const area = this.hass.areas[this.value];
const area = areas[this.value];
if (area) {
const { floor } = getAreaContext(area, this.hass.floors);
const { floor } = getAreaContext(area, floors);
if (floor) {
label = undefined;
}
@@ -238,12 +269,11 @@ export class HaAreaPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${label}
.helper=${this.helper}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
.emptyLabel=${this._i18n.localize("ui.components.area-picker.no_areas")}
.disabled=${this.disabled}
.required=${this.required}
.value=${this.value}
@@ -252,7 +282,7 @@ export class HaAreaPicker extends LitElement {
.valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel}
.searchKeys=${areaComboBoxKeys}
.unknownItemText=${this.hass.localize(
.unknownItemText=${this._i18n.localize(
"ui.components.area-picker.unknown"
)}
@value-changed=${this._valueChanged}
@@ -271,7 +301,7 @@ export class HaAreaPicker extends LitElement {
}
if (value.startsWith(ADD_NEW_ID)) {
this.hass.loadFragmentTranslation("config");
this._i18n.loadFragmentTranslation("config");
const suggestedName = value.substring(ADD_NEW_ID.length);
@@ -279,15 +309,15 @@ export class HaAreaPicker extends LitElement {
suggestedName: suggestedName,
createEntry: async (values) => {
try {
const area = await createAreaRegistryEntry(this.hass, values);
if (this.hass.areas[area.area_id]) {
const area = await createAreaRegistryEntry(this._api, values);
if (this._areas[area.area_id]) {
this._setValue(area.area_id);
} else {
this._pendingAreaId = area.area_id;
}
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
title: this._i18n.localize(
"ui.components.area-picker.failed_create_area"
),
text: err.message,
@@ -308,7 +338,7 @@ export class HaAreaPicker extends LitElement {
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.area-picker.no_match", {
this._i18n.localize("ui.components.area-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
-2
View File
@@ -85,7 +85,6 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
<ha-area-picker
.curValue=${area}
.noAdd=${this.noAdd}
.hass=${this.hass}
.value=${area}
.label=${this.pickedAreaLabel}
.includeDomains=${this.includeDomains}
@@ -112,7 +111,6 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
<div>
<ha-area-picker
.noAdd=${this.noAdd}
.hass=${this.hass}
.label=${this.pickAreaLabel}
.helper=${this.helper}
.includeDomains=${this.includeDomains}
+2 -2
View File
@@ -15,7 +15,7 @@ import "./ha-svg-icon";
*
* @attr {ToggleButton[]} buttons - the button config
* @attr {string} active - The value of the currently active button.
* @attr {("small"|"medium")} size - The size of the buttons in the group.
* @attr {("s"|"m")} size - The size of the buttons in the group.
* @attr {("brand"|"neutral"|"success"|"warning"|"danger")} variant - The variant of the buttons in the group.
*
* @fires value-changed - Dispatched when the active button changes.
@@ -26,7 +26,7 @@ export class HaButtonToggleGroup extends LitElement {
@property() public active?: string;
@property({ reflect: true }) size: "small" | "medium" = "medium";
@property({ reflect: true }) size: "s" | "m" = "m";
@property({ type: Boolean, reflect: true, attribute: "no-wrap" })
public nowrap = false;
+3 -3
View File
@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @cssprop --ha-button-height - The height of the button.
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
*
* @attr {("small"|"medium"|"large")} size - Sets the button size.
* @attr {("xs"|"s"|"m"|"l"|"xl")} size - Sets the button size.
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
* @attr {("accent"|"filled"|"plain")} appearance - Sets the button appearance.
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
@@ -65,7 +65,7 @@ export class HaButton extends Button {
box-shadow: var(--ha-button-box-shadow);
}
:host([size="small"]) .button {
:host([size="s"]) .button {
--wa-form-control-height: var(
--ha-button-height,
var(--button-height, 32px)
@@ -74,7 +74,7 @@ export class HaButton extends Button {
--wa-form-control-padding-inline: var(--ha-space-3);
}
:host([size="large"]) .button {
:host([size="l"]) .button {
--wa-form-control-height: var(
--ha-button-height,
var(--button-height, 48px)
+1 -1
View File
@@ -61,7 +61,7 @@ export class HaDurationInput extends LitElement {
${this.allowNegative
? html`
<ha-button-toggle-group
size="small"
size="s"
.buttons=${[
{ label: "+", iconPath: mdiPlusThick, value: "+" },
{ label: "-", iconPath: mdiMinusThick, value: "-" },
+1
View File
@@ -107,6 +107,7 @@ export class HaExpansionPanel extends LitElement {
}
const newExpanded = !this.expanded;
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
this._container.style.overflow = "hidden";
if (newExpanded) {
+1 -1
View File
@@ -120,7 +120,7 @@ export class HaFileUpload extends LitElement {
@dragend=${this._handleDragEnd}
>${!this.value
? html`<ha-button
size="small"
size="s"
appearance="filled"
@click=${this._openFilePicker}
>
+136 -132
View File
@@ -1,5 +1,5 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -9,14 +9,18 @@ import { stringCompare } from "../common/string/compare";
import { deepEqual } from "../common/util/deep-equal";
import type { RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-list";
import "./input/ha-input-search";
import type { HaInputSearch } from "./input/ha-input-search";
import "./item/ha-list-item-option";
import "./list/ha-list-selectable-virtualized";
import type { HaListSelectableVirtualized } from "./list/ha-list-selectable-virtualized";
import type { HaListVirtualizedItem } from "./list/ha-list-virtualized";
interface HaFilterDevicesItem extends HaListVirtualizedItem {
name: string;
}
@customElement("ha-filter-devices")
export class HaFilterDevices extends LitElement {
@@ -34,15 +38,12 @@ export class HaFilterDevices extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
@query("ha-list-selectable-virtualized")
private _listElement?: HaListSelectableVirtualized;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
if (!this.hasUpdated) {
loadVirtualizer();
}
if (
properties.has("value") &&
!deepEqual(this.value, properties.get("value"))
@@ -51,6 +52,20 @@ export class HaFilterDevices extends LitElement {
}
}
protected updated(changed: PropertyValues<this>) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded || !this._listElement) {
return;
}
this._listElement.style.height = `${this.clientHeight - 49 - 4 - 38}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 38px - height of the search input
}, 300);
}
}
protected render() {
return html`
<ha-expansion-panel
@@ -66,6 +81,7 @@ export class HaFilterDevices extends LitElement {
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
@keydown=${this._handleClearFilterKeydown}
></ha-icon-button>`
: nothing}
</div>
@@ -74,75 +90,45 @@ export class HaFilterDevices extends LitElement {
appearance="outlined"
.value=${this._filter}
@input=${this._handleSearchChange}
@keydown=${this._handleSearchKeydown}
>
</ha-input-search>
<ha-list class="ha-scrollbar" multi>
<lit-virtualizer
.items=${this._devices(
this.hass.devices,
this._filter || "",
this.value
)}
.keyFunction=${this._keyFunction}
.renderItem=${this._renderItem}
@click=${this._handleItemClick}
@keydown=${this._handleItemKeydown}
>
</lit-virtualizer>
</ha-list>`
<ha-list-selectable-virtualized
multi
.rows=${this._devices(this.hass.devices, this._filter || "")}
.rowRenderer=${this._renderItem}
@ha-list-item-selected=${this._handleAdded}
@ha-list-item-deselected=${this._handleRemoved}
></ha-list-selectable-virtualized>`
: nothing}
</ha-expansion-panel>
`;
}
private _keyFunction = (device) => device?.id;
private _renderItem = (device) =>
!device
private _renderItem = (item?: HaFilterDevicesItem) =>
!item
? nothing
: html`<ha-check-list-item
tabindex="0"
.value=${device.id}
.selected=${this.value?.includes(device.id) ?? false}
: html`<ha-list-item-option
style="width: 100%;"
appearance="checkbox"
selection-position="end"
.value=${item.id}
.selected=${this.value?.includes(item.id) ?? false}
>
${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)}
</ha-check-list-item>`;
<span slot="headline">${item.name}</span>
</ha-list-item-option>`;
private _handleItemKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
this._handleItemClick(ev);
}
private _handleAdded(ev: CustomEvent<number>) {
this.value = [
...(this.value ?? []),
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
];
}
private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item");
const value = listItem?.value;
if (!value) {
return;
}
if (this.value?.includes(value)) {
this.value = this.value?.filter((val) => val !== value);
} else {
this.value = [...(this.value || []), value];
}
listItem.selected = this.value?.includes(value);
}
protected updated(changed: PropertyValues<this>) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
}, 300);
}
private _handleRemoved(ev: CustomEvent<number>) {
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
.id;
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
}
private _expandedWillChange(ev) {
@@ -155,30 +141,38 @@ export class HaFilterDevices extends LitElement {
private _handleSearchChange(ev: InputEvent) {
const target = ev.target as HaInputSearch;
this._filter = (target.value ?? "").toLowerCase();
this._filter = target.value ?? "";
}
private _handleSearchKeydown(ev: KeyboardEvent) {
if (ev.key === "ArrowDown" && this._listElement) {
ev.preventDefault();
this._listElement.focus();
}
}
private _devices = memoizeOne(
(devices: HomeAssistant["devices"], filter: string, _value) => {
(
devices: HomeAssistant["devices"],
filter: string
): HaFilterDevicesItem[] => {
const values = Object.values(devices);
return values
.map((device) => ({
id: device.id,
interactive: true,
name: computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
),
}))
.filter(
(device) =>
!filter ||
computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
.toLowerCase()
.includes(filter)
({ name }) =>
!filter || name.toLowerCase().includes(filter.toLowerCase())
)
.sort((a, b) =>
stringCompare(
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
this.hass.locale.language
)
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
@@ -217,6 +211,13 @@ export class HaFilterDevices extends LitElement {
});
}
private _handleClearFilterKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.stopPropagation();
this._clearFilter(ev);
}
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
@@ -224,58 +225,61 @@ export class HaFilterDevices extends LitElement {
value: undefined,
items: undefined,
});
this._listElement?.clearSelection();
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
border-bottom: 1px solid var(--divider-color);
}
:host([expanded]) {
flex: 1;
height: 0;
}
static styles = css`
:host {
border-bottom: 1px solid var(--divider-color);
}
:host([expanded]) {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
}
ha-expansion-panel {
--ha-card-border-radius: var(--ha-border-radius-square);
--expansion-panel-content-padding: 0;
}
.header {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: 0;
min-width: 16px;
box-sizing: border-box;
border-radius: var(--ha-border-radius-circle);
font-size: var(--ha-font-size-xs);
font-weight: var(--ha-font-weight-normal);
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
}
ha-check-list-item {
width: 100%;
}
ha-input-search {
display: block;
padding: var(--ha-space-1) var(--ha-space-2) 0;
}
`,
];
}
ha-expansion-panel {
--ha-card-border-radius: var(--ha-border-radius-square);
--expansion-panel-content-padding: 0;
}
:host([expanded]) ha-expansion-panel {
flex: 1;
min-height: 0;
}
ha-list-selectable-virtualized {
flex: 1;
min-height: 0;
}
.header {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: 0;
min-width: 16px;
box-sizing: border-box;
border-radius: var(--ha-border-radius-circle);
font-size: var(--ha-font-size-xs);
font-weight: var(--ha-font-weight-normal);
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
}
ha-input-search {
display: block;
padding: var(--ha-space-1) var(--ha-space-2) 0;
}
`;
}
declare global {
+48 -38
View File
@@ -23,7 +23,6 @@ import "./item/ha-list-item-option";
import type { HaListItemOption } from "./item/ha-list-item-option";
import "./list/ha-list-selectable";
import type { HaListSelectable } from "./list/ha-list-selectable";
import type { HaListSelectedDetail } from "./list/types";
@customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends LitElement {
@@ -42,7 +41,7 @@ export class HaFilterFloorAreas extends LitElement {
@state() private _shouldRender = false;
@query("ha-list-selectable") private _list?: HTMLElement;
@query("ha-list-selectable") private _list?: HaListSelectable;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -75,6 +74,7 @@ export class HaFilterFloorAreas extends LitElement {
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
@keydown=${this._handleClearFilterKeydown}
></ha-icon-button>`
: nothing}
</div>
@@ -83,7 +83,8 @@ export class HaFilterFloorAreas extends LitElement {
<ha-list-selectable
class="ha-scrollbar"
multi
@ha-list-selected=${this._handleListChanged}
@ha-list-item-selected=${this._handleAdded}
@ha-list-item-deselected=${this._handleRemoved}
aria-label=${this.hass.localize(
"ui.panel.config.areas.caption"
)}
@@ -163,46 +164,47 @@ export class HaFilterFloorAreas extends LitElement {
`;
}
private _handleListChanged(ev: CustomEvent<HaListSelectedDetail>) {
if (!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size) {
private _handleAdded(ev: CustomEvent<number>) {
if (!this.value) {
this.value = {};
}
const addedItem = (ev.currentTarget as HaListSelectable).items[
ev.detail
] as HaListItemOption & { type: string; value: string };
if (!addedItem) {
return;
}
if (ev.detail.diff?.added.size) {
const addedIndex = ev.detail.diff.added.values().next().value;
if (addedIndex === undefined) {
return;
}
const addedItem = (ev.currentTarget as HaListSelectable).items[
addedIndex
] as HaListItemOption & { type: string; value: string };
this.value = {
...this.value,
[addedItem.type]: [
...(this.value[addedItem.type] || []),
addedItem.value,
],
};
}
if (!this.value) {
this.value = {};
}
this.value = {
...this.value,
[addedItem.type]: [
...(this.value[addedItem.type] || []),
addedItem.value,
],
};
} else {
const removedIndex = ev.detail.diff?.removed.values().next().value;
if (removedIndex === undefined) {
return;
}
const removedItem = (ev.currentTarget as HaListSelectable).items[
removedIndex
] as HaListItemOption & { type: string; value: string };
this.value = {
...this.value,
[removedItem.type]: this.value![removedItem.type].filter(
(val) => val !== removedItem.value
),
};
private _handleRemoved(ev: CustomEvent<number>) {
if (!this.value) {
return;
}
const removedItem = (ev.currentTarget as HaListSelectable).items[
ev.detail
] as HaListItemOption & { type: string; value: string };
if (!removedItem) {
return;
}
this.value = {
...this.value,
[removedItem.type]: this.value![removedItem.type].filter(
(val) => val !== removedItem.value
),
};
}
protected updated(changed: PropertyValues<this>) {
@@ -286,6 +288,13 @@ export class HaFilterFloorAreas extends LitElement {
});
}
private _handleClearFilterKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.stopPropagation();
this._clearFilter(ev);
}
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
@@ -293,6 +302,7 @@ export class HaFilterFloorAreas extends LitElement {
value: undefined,
items: undefined,
});
this._list?.clearSelection();
}
static get styles(): CSSResultGroup {
@@ -129,7 +129,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
@wa-select=${this._handleAddAction}
@closed=${stopPropagation}
>
<ha-button slot="trigger" appearance="filled" size="small">
<ha-button slot="trigger" appearance="filled" size="s">
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.localize?.("ui.components.form-optional-actions.add") ||
"Add interaction"}
+1 -1
View File
@@ -174,7 +174,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
<slot name="field">
${this.addButtonLabel && !this.value
? html`<ha-button
size="small"
size="s"
appearance="filled"
@click=${this.open}
.disabled=${this.disabled}
+1 -1
View File
@@ -171,7 +171,7 @@ export class HaLabelsPicker extends LitElement {
: nothing}
<ha-button
id="picker"
size="small"
size="s"
appearance="filled"
@click=${this._openPicker}
.disabled=${this.disabled}
@@ -53,7 +53,7 @@ class HaLawnMowerActionButton extends LitElement {
appearance="plain"
@click=${this.callService}
.service=${action.service}
size="small"
size="s"
>
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
</ha-button>
+1 -1
View File
@@ -102,7 +102,7 @@ export class HaPictureUpload extends LitElement {
<div>
<ha-button
appearance="plain"
size="small"
size="s"
variant="danger"
@click=${this._handleChangeClick}
>
@@ -91,7 +91,6 @@ export class HaAreaSelector extends LitElement {
if (!this.selector.area?.multiple) {
return html`
<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -63,7 +63,7 @@ export class HaChooseSelector extends LitElement {
return html`<div class="multi-header">
<span>${this.label}${this.required ? "*" : ""}</span>
<ha-button-toggle-group
size="small"
size="s"
.buttons=${this._toggleButtons(
this.selector.choose.choices,
this.selector.choose.translation_key,
@@ -190,7 +190,7 @@ export class HaMediaSelector extends LitElement {
? html`<div>
<ha-button
appearance="plain"
size="small"
size="s"
variant="danger"
@click=${this._clearValue}
>
@@ -307,7 +307,7 @@ export class HaNumericThresholdSelector extends LitElement {
>`
: nothing}
<ha-button-toggle-group
size="small"
size="s"
.buttons=${choiceToggleButtons}
.active=${activeChoice}
.disabled=${this.disabled}
+3 -3
View File
@@ -5,7 +5,7 @@ import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-slider")
export class HaSlider extends Slider {
@property({ reflect: true }) size: "small" | "medium" = "small";
@property({ reflect: true }) size: "s" | "m" = "s";
@property({ type: Boolean, attribute: "with-tooltip" }) withTooltip = true;
@@ -110,12 +110,12 @@ export class HaSlider extends Slider {
);
}
:host([size="medium"]) {
:host([size="m"]) {
--thumb-width: 20px;
--thumb-height: 20px;
}
:host([size="small"]) {
:host([size="s"]) {
--thumb-width: 16px;
--thumb-height: 16px;
}
+1 -1
View File
@@ -43,7 +43,7 @@ class HaTracePicker extends LitElement {
slot="field"
appearance="filled"
variant="neutral"
size="small"
size="s"
@click=${this._openPicker}
>
${this._renderTracePickerValue(this.value!)}
@@ -108,7 +108,6 @@ export class HaVacuumSegmentAreaMapper extends LitElement {
<span class="segment-name">${segment.name}</span>
<ha-svg-icon class="arrow" .path=${mdiArrowRightThin}></ha-svg-icon>
<ha-area-picker
.hass=${this.hass}
.value=${mappedAreas}
.label=${this.hass.localize(
"ui.dialogs.vacuum_segment_mapping.area_label"
+1 -1
View File
@@ -58,7 +58,7 @@ export class HaVacuumState extends LitElement {
return html`
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._callService}
.disabled=${!interceptable}
>
+1 -1
View File
@@ -99,7 +99,7 @@ export class HaInputCopy extends LitElement {
: nothing}
</ha-input>
</div>
<ha-button @click=${this._copy} appearance="plain" size="small">
<ha-button @click=${this._copy} appearance="plain" size="s">
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this._i18n.localize("ui.common.copy")}
</ha-button>
+1 -1
View File
@@ -130,7 +130,7 @@ class HaInputMulti extends LitElement {
</ha-sortable>
<div class="layout horizontal">
<ha-button
size="small"
size="s"
appearance="filled"
@click=${this._addItem}
.disabled=${this.disabled ||
+3
View File
@@ -38,6 +38,9 @@ export class HaListItemBase extends HaRowItem {
public connectedCallback(): void {
super.connectedCallback();
if (!this.hasAttribute("ha-list-item")) {
this.setAttribute("ha-list-item", "");
}
if (!this.hasAttribute("role")) {
this.setAttribute("role", this.defaultRole);
}
+149 -91
View File
@@ -1,9 +1,9 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { tinykeys } from "tinykeys";
import { compareNodeOrder } from "../../common/dom/compare-node-order";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { haStyleScrollbar } from "../../resources/styles";
import type { HaListItemBase } from "../item/ha-list-item-base";
import "./types";
import type { HaListItemRegistrationDetail } from "./types";
@@ -45,13 +45,13 @@ export class HaListBase extends LitElement {
/** Host `role` attribute. Empty string means no role is set. */
protected readonly hostRole: string = "list";
private _activeItemIndex = -1;
protected activeItemIndex = -1;
private _firstFocusableIndex = -1;
protected firstFocusableIndex = -1;
private _lastFocusableIndex = -1;
protected lastFocusableIndex = -1;
private _hasFocusableItem = false;
protected hasFocusableItem = false;
private _unbindKeys?: () => void;
@@ -63,22 +63,28 @@ export class HaListBase extends LitElement {
if (!this.hasAttribute("role") && this.hostRole) {
this.setAttribute("role", this.hostRole);
}
this._unbindKeys = tinykeys(this, {
ArrowDown: this._onForward,
ArrowUp: this._onBack,
Home: this._onHome,
End: this._onEnd,
Enter: this._onActivate,
Space: this._onActivate,
});
this.addEventListener("focusin", this._onFocusIn);
this._unbindKeys = tinykeys(
this,
{
ArrowDown: this._onForward,
ArrowUp: this._onBack,
Home: this._onHome,
End: this._onEnd,
PageDown: this._onPageDown,
PageUp: this._onPageUp,
Enter: this.onActivate,
Space: this.onActivate,
},
{ ignore: this._ignoreKeyEvent }
);
this.addEventListener("focusin", this.onFocusIn);
this.addEventListener(
"ha-list-item-register",
this._onItemRegister as EventListener
this.onItemRegister as EventListener
);
this.addEventListener(
"ha-list-item-unregister",
this._onItemUnregister as EventListener
this.onItemUnregister as EventListener
);
}
@@ -86,25 +92,23 @@ export class HaListBase extends LitElement {
super.disconnectedCallback();
this._unbindKeys?.();
this._unbindKeys = undefined;
this.removeEventListener("focusin", this._onFocusIn);
this.removeEventListener("focusin", this.onFocusIn);
this.removeEventListener(
"ha-list-item-register",
this._onItemRegister as EventListener
this.onItemRegister as EventListener
);
this.removeEventListener(
"ha-list-item-unregister",
this._onItemUnregister as EventListener
this.onItemUnregister as EventListener
);
}
public focus(options?: FocusOptions) {
if (!this.items.length) {
if (!this.itemCount) {
super.focus(options);
return;
}
this.focusItemAtIndex(
this._activeItemIndex >= 0 ? this._activeItemIndex : 0
);
this.focusItemAtIndex(this.activeItemIndex >= 0 ? this.activeItemIndex : 0);
}
public focusItemAtIndex(index: number) {
@@ -115,19 +119,19 @@ export class HaListBase extends LitElement {
}
public getActiveItemIndex(): number {
return this._activeItemIndex;
return this.activeItemIndex;
}
public setActiveItemIndex(index: number, focusItem = false) {
if (!this._hasFocusableItem) {
this._activeItemIndex = -1;
if (!this.hasFocusableItem) {
this.activeItemIndex = -1;
return;
}
this._activeItemIndex = Math.max(0, Math.min(this.items.length - 1, index));
if (!this._isFocusable(this._activeItemIndex)) {
this._activeItemIndex = this._firstFocusableIndex;
this.activeItemIndex = Math.max(0, Math.min(this.itemCount - 1, index));
if (!this.isFocusable(this.activeItemIndex)) {
this.activeItemIndex = this.firstFocusableIndex;
}
this._applyActive(focusItem);
this.applyActive(focusItem);
}
/**
@@ -135,18 +139,18 @@ export class HaListBase extends LitElement {
* to layer in extra bookkeeping (e.g. selection state sync).
*/
public updateListItems() {
this._recomputeFocusableIndexes();
this.recomputeFocusableIndexes();
if (
this._activeItemIndex >= this.items.length ||
!this._hasFocusableItem ||
this._activeItemIndex < 0
this.activeItemIndex >= this.itemCount ||
!this.hasFocusableItem ||
this.activeItemIndex < 0
) {
this._activeItemIndex = this._firstFocusableIndex;
this.activeItemIndex = this.firstFocusableIndex;
}
this._applyActive(false);
this.applyActive(false);
}
private _onItemRegister = (
protected onItemRegister = (
ev: HASSDomEvent<HaListItemRegistrationDetail>
) => {
ev.stopPropagation();
@@ -160,7 +164,7 @@ export class HaListBase extends LitElement {
this.updateListItems();
};
private _onItemUnregister = (
protected onItemUnregister = (
ev: HASSDomEvent<HaListItemRegistrationDetail>
) => {
ev.stopPropagation();
@@ -172,136 +176,190 @@ export class HaListBase extends LitElement {
this.updateListItems();
};
private _recomputeFocusableIndexes() {
protected recomputeFocusableIndexes() {
let first = -1;
let last = -1;
for (let i = 0; i < this.items.length; i++) {
if (this._isFocusable(i)) {
for (let i = 0; i < this.itemCount; i++) {
if (this.isFocusable(i)) {
if (first === -1) {
first = i;
}
last = i;
}
}
this._firstFocusableIndex = first;
this._lastFocusableIndex = last;
this._hasFocusableItem = first !== -1;
this.firstFocusableIndex = first;
this.lastFocusableIndex = last;
this.hasFocusableItem = first !== -1;
}
protected render(): TemplateResult {
return html`<div part="base" class="base">
protected render(): TemplateResult | typeof nothing {
return html`<div part="base" class="base ha-scrollbar">
<slot></slot>
</div>`;
}
private _isFocusable(index: number): boolean {
protected isFocusable(index: number): boolean {
const item = this.items[index];
return !!item && item.interactive && !item.disabled;
}
private _applyActive(focusItem: boolean) {
protected applyActive(focusItem: boolean) {
this.items.forEach((item, i) => {
if (!item.interactive || item.disabled) {
item.removeAttribute("tabindex");
return;
}
item.tabIndex = i === this._activeItemIndex ? 0 : -1;
item.tabIndex = i === this.activeItemIndex ? 0 : -1;
});
if (focusItem && this._activeItemIndex >= 0) {
this.items[this._activeItemIndex]?.focus();
if (focusItem && this.activeItemIndex >= 0) {
this.items[this.activeItemIndex]?.focus();
}
}
private _onFocusIn = (ev: FocusEvent) => {
protected onFocusIn = (ev: FocusEvent) => {
const path = ev.composedPath();
for (let i = 0; i < this.items.length; i++) {
if (path.includes(this.items[i])) {
if (i !== this._activeItemIndex) {
this._activeItemIndex = i;
this._applyActive(false);
if (i !== this.activeItemIndex) {
this.activeItemIndex = i;
this.applyActive(false);
}
return;
}
}
};
private _ignoreKeyEvent = (ev: KeyboardEvent): boolean => {
if (ev.repeat && (ev.key === "Enter" || ev.key === " ")) {
return true;
}
if (ev.isComposing) {
return true;
}
const target = ev.target as HTMLElement | null;
// Allow held arrow/Home/End to repeat for continuous navigation
return (
!!target &&
target !== ev.currentTarget &&
target.matches("[contenteditable],input,select,textarea")
);
};
private _onForward = (ev: KeyboardEvent) => {
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, 1));
this.moveFocus(ev, this._stepIndex(this.activeItemIndex, 1));
};
private _onBack = (ev: KeyboardEvent) => {
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, -1));
this.moveFocus(ev, this._stepIndex(this.activeItemIndex, -1));
};
private _onHome = (ev: KeyboardEvent) => {
this._moveFocus(ev, this._firstFocusableIndex);
this.moveFocus(ev, this.firstFocusableIndex);
};
private _onEnd = (ev: KeyboardEvent) => {
this._moveFocus(ev, this._lastFocusableIndex);
this.moveFocus(ev, this.lastFocusableIndex);
};
private _onActivate = (ev: KeyboardEvent) => {
if (!this._isFocusable(this._activeItemIndex)) {
private _onPageDown = (ev: KeyboardEvent) => {
this.moveFocus(
ev,
this._stepIndex(this.activeItemIndex, 1, this.getPageSize())
);
};
private _onPageUp = (ev: KeyboardEvent) => {
this.moveFocus(
ev,
this._stepIndex(this.activeItemIndex, -1, this.getPageSize())
);
};
/**
* Number of items to jump for PageUp/PageDown. Defaults to 10 (per WAI-ARIA
* Authoring Practices: "moves focus a manageable number of nodes,
* typically 10"). Subclasses with a known viewport (e.g. virtualized lists)
* can override to use the visible page size.
*/
protected getPageSize(): number {
return 10;
}
protected onActivate = (ev: KeyboardEvent) => {
if (!this.isFocusable(this.activeItemIndex)) {
return;
}
ev.preventDefault();
const active = this.items[this._activeItemIndex];
const active = this.items[this.activeItemIndex];
active.activate();
fireEvent(this, "ha-list-activated", {
index: this._activeItemIndex,
index: this.activeItemIndex,
item: active,
});
};
private _moveFocus(ev: KeyboardEvent, next: number) {
if (!this._hasFocusableItem || next < 0 || next === this._activeItemIndex) {
protected moveFocus(ev: KeyboardEvent, next: number) {
if (!this.hasFocusableItem) {
return;
}
ev.preventDefault();
this._activeItemIndex = next;
this._applyActive(true);
if (next < 0 || next === this.activeItemIndex) {
return;
}
this.activeItemIndex = next;
this.applyActive(true);
}
protected get itemCount(): number {
return this.items.length;
}
/**
* Step from `from` by `delta`, skipping non-interactive and disabled items.
* Returns `from` when no other focusable item can be reached (honouring
* `wrapFocus`).
* Pass `count` > 1 to advance by multiple focusable items (PageUp/Down).
* Returns the last focusable index reached, or `from` when none is.
*/
private _stepIndex(from: number, delta: 1 | -1): number {
const n = this.items.length;
if (!n || !this._hasFocusableItem) {
private _stepIndex(from: number, delta: 1 | -1, count = 1): number {
const n = this.itemCount;
if (!n || !this.hasFocusableItem) {
return from;
}
let last = from;
let i = from;
for (let step = 0; step < n; step++) {
let landed = 0;
for (let step = 0; step < n && landed < count; step++) {
i += delta;
if (i < 0 || i >= n) {
if (!this.wrapFocus) {
return from;
return last;
}
i = (i + n) % n;
}
if (this._isFocusable(i)) {
return i;
if (this.isFocusable(i)) {
last = i;
landed++;
}
}
return from;
return last;
}
static styles: CSSResultGroup = css`
:host {
display: block;
}
.base {
display: flex;
flex-direction: column;
gap: var(--ha-list-gap, 0);
padding: var(--ha-list-padding, 0);
margin: 0;
list-style: none;
}
`;
static styles = [
haStyleScrollbar,
css`
:host {
display: block;
}
.base {
display: flex;
flex-direction: column;
gap: var(--ha-list-gap, 0);
padding: var(--ha-list-padding, 0);
margin: 0;
list-style: none;
overflow-x: hidden;
}
`,
];
}
declare global {
+1 -1
View File
@@ -30,7 +30,7 @@ export class HaListNav extends HaListBase {
part="nav"
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
>
<div part="base" class="base" role="list">
<div part="base" class="base ha-scrollbar" role="list">
<slot></slot>
</div>
</nav>`;
@@ -0,0 +1,85 @@
import { property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { Constructor } from "../../types";
import { HaListItemOption } from "../item/ha-list-item-option";
import type { HaListBase } from "./ha-list-base";
export const SelectableMixin = <T extends Constructor<HaListBase>>(
superClass: T
) => {
class SelectableClass extends superClass {
@property({ type: Boolean, reflect: true }) public multi = false;
protected override readonly hostRole = "listbox";
public connectedCallback(): void {
super.connectedCallback();
this.addEventListener("click", this._onOptionClick);
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener("click", this._onOptionClick);
}
public updated(changed: Map<string, unknown>) {
super.updated(changed);
if (changed.has("multi")) {
this.setAttribute(
"aria-multiselectable",
this.multi ? "true" : "false"
);
}
}
/** Hook: index of a clicked option element, or `-1` if it's not ours. */
protected optionIndexOf(opt: HaListItemOption): number {
return this.items.indexOf(opt);
}
public clearSelection() {
(this.items as HaListItemOption[]).forEach((opt) => {
if (opt.selected) {
opt.toggleAttribute("selected", false);
}
});
}
private _onOptionClick = (ev: Event) => {
const path = ev.composedPath();
for (const el of path) {
if (el === this) {
return;
}
if (el instanceof HaListItemOption) {
if (el.disabled) {
return;
}
const index = this.optionIndexOf(el);
if (index < 0) {
return;
}
if (this.multi) {
fireEvent(
this,
`ha-list-item-${el.selected ? "deselected" : "selected"}`,
index
);
el.toggleAttribute("selected");
return;
}
if (!el.selected) {
fireEvent(this, "ha-list-item-selected", index);
// deselect the other optional selected item
this.clearSelection();
el.toggleAttribute("selected", true);
}
}
}
};
}
return SelectableClass;
};
@@ -0,0 +1,66 @@
import { customElement } from "lit/decorators";
import { HaListItemOption } from "../item/ha-list-item-option";
import { SelectableMixin } from "./ha-list-selectable-mixin";
import { HaListVirtualized } from "./ha-list-virtualized";
/**
* @element ha-list-selectable-virtualized
* @extends {HaListVirtualized}
*
* @summary
* Virtualized selection list (role `listbox`). Rows must render
* `<ha-list-item-option>` as their top-level element. Selection is index-based:
* clicking a row fires `ha-list-item-selected` / `ha-list-item-deselected` with
* the row's index, and the row's `selected` attribute is toggled. Consumers own
* the source of truth set each row's `selected` from their own state (for
* example, keyed by the option's `value`) and update it in the event handlers.
*
* Because selection is tracked per-row by the consumer, filtering the visible
* `rows` doesn't affect selections for items outside the current view.
*
* @attr {boolean} multi - Whether multiple options can be selected at once. In
* single-select mode, selecting a row clears any previous selection.
*
* @fires ha-list-item-selected - Fires when the user selects a row.
* `detail` is the row's index (number).
* @fires ha-list-item-deselected - Fires when the user deselects a row (multi-select only).
* `detail` is the row's index (number).
*/
@customElement("ha-list-selectable-virtualized")
export class HaListSelectableVirtualized extends SelectableMixin(
HaListVirtualized
) {
/**
* Hook: maps a clicked option to its absolute index by offsetting its
* position among the rendered (virtualized) children by `rangeStart`.
* Returns `-1` if it's not one of our rows or nothing is rendered yet.
*/
protected optionIndexOf(opt: HaListItemOption): number {
if (!this.virtualizerElement || this.rangeStart === -1) {
return -1;
}
const index = Array.from(this.virtualizerElement.children).indexOf(opt);
if (index === -1) {
return -1;
}
return this.rangeStart + index;
}
/** Deselects every currently rendered (visible) option. */
public clearSelection() {
if (!this.virtualizerElement || this.rangeStart === -1) {
return;
}
Array.from(this.virtualizerElement.children).forEach((opt) => {
if (opt instanceof HaListItemOption && opt.selected) {
opt.toggleAttribute("selected", false);
}
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-list-selectable-virtualized": HaListSelectableVirtualized;
}
}
+5 -192
View File
@@ -1,8 +1,6 @@
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HaListItemOption } from "../item/ha-list-item-option";
import { customElement } from "lit/decorators";
import { HaListBase } from "./ha-list-base";
import type { HaListSelectedDetail } from "./types";
import { SelectableMixin } from "./ha-list-selectable-mixin";
/**
* @element ha-list-selectable
@@ -14,196 +12,11 @@ import type { HaListSelectedDetail } from "./types";
*
* @attr {boolean} multi - Whether multiple options can be selected at once.
*
* @fires ha-list-selected - Fired when the selection changes. `detail: HaListSelectedDetail`.
* @fires ha-list-item-selected - An option was selected. `detail: number` (option index).
* @fires ha-list-item-deselected - An option was deselected (multi mode only). `detail: number` (option index).
*/
@customElement("ha-list-selectable")
export class HaListSelectable extends HaListBase {
@property({ type: Boolean, reflect: true }) public multi = false;
protected override readonly hostRole = "listbox";
private _selectedIndices?: Set<number>;
public connectedCallback(): void {
super.connectedCallback();
this.addEventListener("click", this._onOptionClick);
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener("click", this._onOptionClick);
}
public updated(changed: Map<string, unknown>) {
super.updated(changed);
if (changed.has("multi")) {
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
if (!this.multi && (this._selectedIndices?.size ?? 0) > 1) {
const first = Math.min(...this._selectedIndices!);
this._setSelection(new Set([first]));
}
}
}
/**
* Returns the current selection. `number` (or `-1` if nothing) when single,
* `Set<number>` when multi.
*/
public get selected(): number | Set<number> {
if (this.multi) {
return new Set(this._selectedIndices);
}
return (this._selectedIndices?.size ?? 0) === 0
? -1
: this._selectedIndices!.values().next().value!;
}
public get selectedItems(): HaListItemOption[] {
return this._sortedSelectedIndices()
.map((i) => this.items[i] as HaListItemOption | undefined)
.filter((it): it is HaListItemOption => !!it);
}
/** Replace the entire selection. */
public setSelected(indices: number | number[] | Set<number>): void {
const next =
typeof indices === "number"
? indices < 0
? new Set<number>()
: new Set([indices])
: new Set(indices);
if (!this.multi && next.size > 1) {
const first = Math.min(...next);
this._setSelection(new Set([first]));
return;
}
this._setSelection(next);
}
public select(index: number): void {
if (index < 0) {
return;
}
if (this.multi) {
const next = new Set(this._selectedIndices);
next.add(index);
this._setSelection(next);
} else {
this._setSelection(new Set([index]));
}
}
public toggle(index: number, force?: boolean): void {
if (index < 0) {
return;
}
if (this.multi) {
const next = new Set(this._selectedIndices);
const isSelected = next.has(index);
const shouldSelect = force !== undefined ? force : !isSelected;
if (shouldSelect) {
next.add(index);
} else {
next.delete(index);
}
this._setSelection(next);
} else {
const isSelected = this._selectedIndices!.has(index);
const shouldSelect = force !== undefined ? force : !isSelected;
this._setSelection(shouldSelect ? new Set([index]) : new Set());
}
}
public clearSelection(): void {
this._setSelection(new Set());
}
public updateListItems() {
super.updateListItems();
this._syncItemSelectedState(true);
}
private _sortedSelectedIndices(): number[] {
return [...this._selectedIndices!].sort((a, b) => a - b);
}
private _syncItemSelectedState(reset = false): void {
if (!this._selectedIndices || reset) {
this._selectedIndices = new Set<number>();
this.items.forEach((item, i) => {
const opt = item as HaListItemOption;
if (opt.selected) {
this._selectedIndices!.add(i);
}
});
return;
}
this.items.forEach((item, i) => {
const opt = item as HaListItemOption;
const shouldBe = this._selectedIndices!.has(i);
if (opt.selected !== shouldBe) {
opt.selected = shouldBe;
}
});
}
private _setSelection(next: Set<number>): void {
const prev = this._selectedIndices!;
const added = new Set<number>();
const removed = new Set<number>();
next.forEach((i) => {
if (!prev.has(i)) {
added.add(i);
}
});
prev.forEach((i) => {
if (!next.has(i)) {
removed.add(i);
}
});
if (!added.size && !removed.size) {
return;
}
this._selectedIndices = next;
this._syncItemSelectedState();
const detail: HaListSelectedDetail = this.multi
? { index: new Set(next), diff: { added, removed } }
: {
index: next.size === 0 ? -1 : next.values().next().value!,
diff: { added, removed },
};
fireEvent(this, "ha-list-selected", detail);
}
private _onOptionClick = (ev: Event) => {
const path = ev.composedPath();
for (const el of path) {
if (el === this) {
return;
}
if (el instanceof HaListItemOption) {
const index = this.items.indexOf(el);
if (index < 0) {
return;
}
const item = this.items[index];
if (item.disabled) {
return;
}
if (this.multi) {
this.toggle(index);
} else {
this.select(index);
}
return;
}
}
};
}
export class HaListSelectable extends SelectableMixin(HaListBase) {}
declare global {
interface HTMLElementTagNameMap {
+356
View File
@@ -0,0 +1,356 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize.js";
import {
css,
html,
nothing,
type PropertyValues,
type TemplateResult,
} from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { loadVirtualizer } from "../../resources/virtualizer";
import { HaListItemBase } from "../item/ha-list-item-base";
import { HaListBase } from "./ha-list-base";
import type { HaListItemRegistrationDetail } from "./types";
/**
* A single row in a {@link HaListVirtualized}. Identified by a stable `id`
* used as the virtualizer key. Extra fields are passed through to the
* `rowRenderer`.
*/
export interface HaListVirtualizedItem {
/** Stable key used by the virtualizer to track the row across re-renders. */
id: string;
/** Whether the row can be focused and activated. Defaults to `false`. */
interactive?: boolean;
disabled?: boolean;
[key: string]: unknown;
}
/**
* @element ha-list-virtualized
* @extends {HaListBase}
*
* @summary
* Virtualized list. Renders only the rows currently in view to keep large
* lists performant, while preserving the roving-tabindex keyboard navigation
* of {@link HaListBase}.
*
* @csspart base - The scrollable outer container (`<div>`).
*
* @attr {number} pin-index - Row index to scroll to when the list first
* renders. Cleared once the user scrolls.
* @attr {string} pin-block - Block alignment for `pin-index`: `start`,
* `center` (default), `end`, or `nearest`.
*
* @fires ha-list-activated - Fired when a row is activated via Enter/Space. `detail: { index, item }`.
*/
@customElement("ha-list-virtualized")
export class HaListVirtualized extends HaListBase {
@state() private _virtualizerReady = false;
/**
* The list data. Each item is rendered by `rowRenderer`; its `interactive`
* and `disabled` flags determine whether the row is focusable.
*/
@property({ attribute: false })
public rows!: HaListVirtualizedItem[];
/** Renders a single row from its data and index. */
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<HaListVirtualizedItem>;
/** Row index to scroll to on first render (the "pinned" row). */
@property({ attribute: "pin-index", type: Number }) public pinIndex?: number;
/** Block alignment used when scrolling to `pinIndex`. */
@property({ attribute: "pin-block" }) public pinBlock:
| "start"
| "center"
| "end"
| "nearest" = "center";
@state() private _unpinned = false;
@query("lit-virtualizer")
protected virtualizerElement?: LitVirtualizer<HaListVirtualizedItem>;
protected rangeStart = -1;
protected rangeEnd = -1;
private _activeItemFocus = false;
private _scrollToActiveItem = false;
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._loadVirtualizer();
}
if (changedProps.has("rows")) {
this.recomputeFocusableIndexes();
this.activeItemIndex = this.firstFocusableIndex;
}
}
private async _loadVirtualizer() {
await loadVirtualizer();
this._virtualizerReady = true;
}
protected override render(): TemplateResult | typeof nothing {
if (!this._virtualizerReady) {
return nothing;
}
return html`<div part="base" class="base ha-scrollbar">
<lit-virtualizer
.keyFunction=${this._keyFunction}
tabindex="-1"
scroller
.items=${this.rows}
.renderItem=${this.rowRenderer}
style="min-height: 36px; height: 100%;"
.layout=${!this._unpinned && this.pinIndex !== undefined
? {
pin: {
index: this.pinIndex,
block: this.pinBlock,
},
}
: undefined}
@unpinned=${this._handleUnpinned}
@rangeChanged=${this._handleRangeChanged}
>
</lit-virtualizer>
</div>`;
}
/**
* Sets the active (roving-tabindex) row. If the row is outside the rendered
* range it is scrolled into view first, then activated/focused once the
* virtualizer has laid it out.
* @param index - Row index to make active; clamped to the valid range.
* @param focusItem - Whether to move DOM focus to the row.
*/
public setActiveItemIndex(index: number, focusItem = false) {
if (!this.hasFocusableItem) {
this.activeItemIndex = -1;
return;
}
this.activeItemIndex = Math.max(0, Math.min(this.rows.length - 1, index));
if (!this.isFocusable(this.activeItemIndex)) {
this.activeItemIndex = this.firstFocusableIndex;
}
if (
this.activeItemIndex >= this.rangeStart &&
this.activeItemIndex <= this.rangeEnd
) {
this.applyActive(focusItem);
} else {
this._activeItemFocus = focusItem;
this._scrollToActiveItem = true;
this.virtualizerElement
?.element(index)
?.scrollIntoView({ block: "nearest" });
}
}
/**
* Focuses the row at `index`, scrolling it into view if needed. No-op until
* the virtualizer is ready or when `index` is negative.
*/
public override focusItemAtIndex(index: number) {
if (!this._virtualizerReady || index < 0) {
return;
}
this.setActiveItemIndex(index, true);
}
protected override applyActive(focusItem: boolean) {
if (this.virtualizerElement && this.rangeStart > -1) {
Array.from(this.virtualizerElement.children).forEach((child, index) => {
const el = child as HTMLElement;
if (index + this.rangeStart === this.activeItemIndex) {
el.tabIndex = 0;
if (focusItem) {
el.focus();
}
} else {
el.removeAttribute("tabindex");
}
});
}
}
@eventOptions({ passive: true })
private async _handleRangeChanged(ev: { first: number; last: number }) {
this.rangeStart = ev.first;
this.rangeEnd = ev.last;
await this.virtualizerElement?.layoutComplete;
this._applySetSize();
if (!this.virtualizerElement) {
return;
}
const inRange =
this.activeItemIndex >= this.rangeStart &&
this.activeItemIndex <= this.rangeEnd;
const focus = this._scrollToActiveItem && inRange && this._activeItemFocus;
this.applyActive(focus);
if (this._scrollToActiveItem && inRange) {
this._activeItemFocus = false;
this._scrollToActiveItem = false;
}
}
// Expose total count + position to assistive tech, since only a slice of
// items is in the DOM at any time.
private _applySetSize() {
if (!this.virtualizerElement || this.rangeStart < 0) {
return;
}
const total = this.rows?.length ?? 0;
Array.from(this.virtualizerElement.children).forEach((child, index) => {
const el = child as HTMLElement;
el.setAttribute("aria-setsize", String(total));
el.setAttribute("aria-posinset", String(this.rangeStart + index + 1));
});
}
protected onFocusIn = (ev: FocusEvent) => {
if (
!this.virtualizerElement ||
this.rangeStart === -1 ||
this.rangeEnd === -1
) {
return;
}
const path = ev.composedPath();
const children = Array.from(this.virtualizerElement.children);
for (let i = this.rangeStart; i <= this.rangeEnd; i++) {
if (path.includes(children[i - this.rangeStart])) {
if (i !== this.activeItemIndex) {
this.activeItemIndex = i;
if (i < this.rangeStart || i > this.rangeEnd) {
this._activeItemFocus = true;
this._scrollToActiveItem = true;
this.virtualizerElement
?.element(this.activeItemIndex)
?.scrollIntoView({ block: "nearest" });
} else {
this.applyActive(false);
}
}
return;
}
}
};
protected override onActivate = (ev: KeyboardEvent) => {
if (!this.isFocusable(this.activeItemIndex)) {
return;
}
if (
this.virtualizerElement &&
this.activeItemIndex >= this.rangeStart &&
this.activeItemIndex <= this.rangeEnd
) {
const active = this.virtualizerElement?.children[
this.activeItemIndex - this.rangeStart
] as HaListItemBase | undefined;
if (active && active instanceof HaListItemBase) {
ev.preventDefault();
active.activate();
fireEvent(this, "ha-list-activated", {
index: this.activeItemIndex,
item: active,
});
}
}
};
protected isFocusable(index: number): boolean {
const item = this.rows[index];
if (!item) {
return false;
}
const { disabled = false, interactive = false } = this.rows[index];
return interactive && !disabled;
}
protected override get itemCount(): number {
return this.rows?.length ?? 0;
}
protected override moveFocus(ev: KeyboardEvent, next: number) {
if (!this.hasFocusableItem) {
return;
}
ev.preventDefault();
if (next < 0 || next === this.activeItemIndex) {
return;
}
this.activeItemIndex = next;
if (next < this.rangeStart || next > this.rangeEnd) {
this._activeItemFocus = true;
this._scrollToActiveItem = true;
this.virtualizerElement?.element(this.activeItemIndex)?.scrollIntoView({
block: "nearest",
});
} else {
this.applyActive(true);
}
}
protected override getPageSize(): number {
if (this.rangeStart < 0 || this.rangeEnd < 0) {
return super.getPageSize();
}
return Math.max(1, this.rangeEnd - this.rangeStart + 1);
}
private _keyFunction = (item: HaListVirtualizedItem) => item.id;
@eventOptions({ passive: true })
private _handleUnpinned() {
this._unpinned = true;
}
protected override onItemRegister = (
ev: HASSDomEvent<HaListItemRegistrationDetail>
) => {
ev.stopPropagation();
};
protected override onItemUnregister = (
ev: HASSDomEvent<HaListItemRegistrationDetail>
) => {
ev.stopPropagation();
// ignore
};
static styles = [
...HaListBase.styles,
css`
.base {
height: 100%;
}
[ha-list-item] {
width: 100%;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-list-virtualized": HaListVirtualized;
}
}
+2 -7
View File
@@ -1,11 +1,5 @@
import type { HaListItemBase } from "../item/ha-list-item-base";
export interface HaListSelectedDetail {
index: number | Set<number>;
diff?: { added: Set<number>; removed: Set<number> };
value?: string | string[];
}
export interface HaListActivatedDetail {
index: number;
item: HaListItemBase;
@@ -17,7 +11,8 @@ export interface HaListItemRegistrationDetail {
declare global {
interface HASSDomEvents {
"ha-list-selected": HaListSelectedDetail;
"ha-list-item-selected": number;
"ha-list-item-deselected": number;
"ha-list-activated": HaListActivatedDetail;
"ha-list-item-register": HaListItemRegistrationDetail;
"ha-list-item-unregister": HaListItemRegistrationDetail;
+4 -4
View File
@@ -23,6 +23,7 @@ import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityLocation } from "../../common/entity/get_entity_location";
import { DecoratedMarker } from "../../common/map/decorated_marker";
import { filterXSS } from "../../common/util/xss";
import type { HomeAssistant, ThemeMode } from "../../types";
@@ -584,18 +585,17 @@ export class HaMap extends ReactiveElement {
const customTitle = typeof entity !== "string" ? entity.name : undefined;
const title = customTitle ?? computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (!(latitude && longitude)) {
const location = getEntityLocation(stateObj, hass.states);
if (!location) {
continue;
}
const { latitude, longitude, gpsAccuracy } = location;
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
@@ -38,7 +38,7 @@ class MediaManageButton extends LitElement {
return nothing;
}
return html`
<ha-button appearance="filled" size="small" @click=${this._manage}>
<ha-button appearance="filled" size="s" @click=${this._manage}>
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.components.media-browser.file_management.manage"
+26 -2
View File
@@ -150,8 +150,15 @@ export class HaTracePathDetails extends LitElement {
parts.push(
data.map((trace, idx) => {
const { path, timestamp, result, error, changed_variables, ...rest } =
trace as any;
const {
path,
timestamp,
result,
error,
template_errors,
changed_variables,
...rest
} = trace as any;
if (result?.enabled === false) {
return html`${this.hass!.localize(
@@ -240,6 +247,18 @@ export class HaTracePathDetails extends LitElement {
)}
</div>`
: nothing}
${template_errors?.length
? html`<div class="error">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.template_errors"
)}
<ul>
${template_errors.map(
(templateError: string) => html`<li>${templateError}</li>`
)}
</ul>
</div>`
: nothing}
${result
? html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.result"
@@ -406,6 +425,11 @@ export class HaTracePathDetails extends LitElement {
color: var(--error-color);
}
.error ul {
margin: var(--ha-space-1) 0;
padding-left: var(--ha-space-6);
}
ha-tab-group {
background-color: var(--primary-background-color);
border-top: 1px solid var(--divider-color);
+2 -2
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../types";
import type { DeviceRegistryEntry } from "../device/device_registry";
import type {
EntityRegistryDisplayEntry,
@@ -39,7 +39,7 @@ export interface AreaRegistryEntryMutableParams {
}
export const createAreaRegistryEntry = (
hass: HomeAssistant,
hass: HomeAssistantApi,
values: AreaRegistryEntryMutableParams
) =>
hass.callWS<AreaRegistryEntry>({
+1
View File
@@ -194,6 +194,7 @@ export interface ControlButton {
icon: string;
// Used as key for action as well as tooltip and aria-label translation key
action: keyof TranslationDict["ui"]["card"]["media_player"];
disabled?: boolean;
}
export interface MediaPlayerItem {
+1
View File
@@ -11,6 +11,7 @@ interface BaseTraceStep {
path: string;
timestamp: string;
error?: string;
template_errors?: string[];
changed_variables?: Record<string, unknown>;
}
@@ -114,7 +114,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "button") {
return html`
<ha-button appearance="plain" size="small" .disabled=${disabled}>
<ha-button appearance="plain" size="s" .disabled=${disabled}>
${this.hass.localize("ui.card.button.press")}
</ha-button>
`;
@@ -237,7 +237,7 @@ class EntityPreviewRow extends LitElement {
.disabled=${disabled}
class="text-content"
appearance="plain"
size="small"
size="s"
>
${stateObj.state === "locked"
? this.hass!.localize("ui.card.lock.unlock")
@@ -182,7 +182,6 @@ class StepFlowCreateEntry extends LitElement {
.device=${device.id}
></ha-input>
<ha-area-picker
.hass=${this.hass}
.device=${device.id}
.value=${this._deviceUpdate[device.id]?.area ??
device.area_id ??
@@ -214,7 +214,7 @@ export class HaMoreInfoViewVacuumCleanAreas extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._openSegmentMapping}
>
<ha-svg-icon
@@ -31,7 +31,7 @@ class MoreInfoAutomation extends LitElement {
<div class="actions">
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._runActions}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
@@ -22,7 +22,7 @@ class MoreInfoCounter extends LitElement {
<div class="actions">
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"increment"}
@click=${this._handleActionClick}
.disabled=${disabled ||
@@ -32,7 +32,7 @@ class MoreInfoCounter extends LitElement {
</ha-button>
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"decrement"}
@click=${this._handleActionClick}
.disabled=${disabled ||
@@ -42,7 +42,7 @@ class MoreInfoCounter extends LitElement {
</ha-button>
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"reset"}
@click=${this._handleActionClick}
.disabled=${disabled}
@@ -431,7 +431,7 @@ class MoreInfoMediaPlayer extends LitElement {
? html`<ha-button
variant="brand"
appearance="filled"
size="medium"
size="m"
action=${action}
@click=${this._handleClick}
class="center-control"
@@ -3,6 +3,7 @@ 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 { getEntityLocation } from "../../../common/entity/get_entity_location";
import "../../../components/ha-button";
import "../../../components/map/ha-map";
import { showZoneEditor } from "../../../data/zone";
@@ -21,8 +22,13 @@ class MoreInfoPerson extends LitElement {
return nothing;
}
const location = getEntityLocation(this.stateObj, this.hass.states);
const hasOwnCoordinates =
typeof this.stateObj.attributes.latitude === "number" &&
typeof this.stateObj.attributes.longitude === "number";
return html`
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
${location
? html`
<ha-map
.hass=${this.hass}
@@ -31,15 +37,12 @@ class MoreInfoPerson extends LitElement {
></ha-map>
`
: ""}
${!__DEMO__ &&
this.hass.user?.is_admin &&
this.stateObj.attributes.latitude &&
this.stateObj.attributes.longitude
${!__DEMO__ && this.hass.user?.is_admin && hasOwnCoordinates
? html`
<div class="actions">
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._handleAction}
>
${this.hass.localize(
@@ -52,7 +52,7 @@ class MoreInfoSiren extends LitElement {
${allowAdvanced
? html`<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._showAdvancedControlsDialog}
>
${this.hass.localize("ui.components.siren.advanced_controls")}
@@ -21,7 +21,7 @@ class MoreInfoTimer extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"start"}
@click=${this._handleActionClick}
>
@@ -33,7 +33,7 @@ class MoreInfoTimer extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"pause"}
@click=${this._handleActionClick}
>
@@ -45,7 +45,7 @@ class MoreInfoTimer extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"cancel"}
@click=${this._handleActionClick}
>
@@ -53,7 +53,7 @@ class MoreInfoTimer extends LitElement {
</ha-button>
<ha-button
appearance="plain"
size="small"
size="s"
.action=${"finish"}
@click=${this._handleActionClick}
>
@@ -31,10 +31,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
"ui.panel.config.voice_assistants.satellite_wizard.area.secondary"
)}
</p>
<ha-area-picker
.hass=${this.hass}
.value=${device.area_id}
></ha-area-picker>
<ha-area-picker .value=${device.area_id}></ha-area-picker>
</div>
<div class="footer">
<ha-button @click=${this._setArea}
@@ -95,10 +95,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
"ui.panel.config.voice_assistants.satellite_wizard.local.failed_secondary"
)}
</p>
<ha-button
appearance="plain"
size="small"
@click=${this._prevStep}
<ha-button appearance="plain" size="s" @click=${this._prevStep}
>${this.hass.localize("ui.common.back")}</ha-button
>
<ha-button
@@ -108,7 +105,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
)}
target="_blank"
rel="noreferrer noopener"
size="small"
size="s"
appearance="plain"
>
<ha-svg-icon .path=${mdiOpenInNew} slot="start"></ha-svg-icon>
@@ -131,10 +128,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
"ui.panel.config.voice_assistants.satellite_wizard.local.not_supported_secondary"
)}
</p>
<ha-button
appearance="plain"
size="small"
@click=${this._prevStep}
<ha-button appearance="plain" size="s" @click=${this._prevStep}
>${this.hass.localize("ui.common.back")}</ha-button
>
<ha-button
@@ -145,7 +139,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
target="_blank"
rel="noreferrer noopener"
appearance="plain"
size="small"
size="s"
>
<ha-svg-icon .path=${mdiOpenInNew} slot="start"></ha-svg-icon>
${this.hass.localize(
@@ -131,7 +131,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
></ha-select>
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._testWakeWord}
>
<ha-svg-icon
@@ -166,7 +166,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
</ha-select>
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._openPipeline}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
@@ -186,11 +186,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
@value-changed=${this._voicePicked}
@closed=${stopPropagation}
></ha-tts-voice-picker>
<ha-button
appearance="plain"
size="small"
@click=${this._testTts}
>
<ha-button appearance="plain" size="s" @click=${this._testTts}>
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.success.try_tts"
@@ -150,7 +150,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
? html`<div class="footer centered">
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._changeWakeWord}
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
@@ -117,7 +117,7 @@ export class HaVoiceCommandDialog extends LitElement {
slot="trigger"
appearance="plain"
variant="neutral"
size="small"
size="s"
.loading=${!this._pipelines}
>
${this._pipeline?.name}
+1 -1
View File
@@ -23,7 +23,7 @@ class HaInitPage extends LitElement {
<p class="retry-text">
Retrying in ${this._retryInSeconds} seconds...
</p>
<ha-button size="small" appearance="plain" @click=${this._retry}
<ha-button size="s" appearance="plain" @click=${this._retry}
>Retry now</ha-button
>
${location.host.includes("ui.nabu.casa")
+1 -1
View File
@@ -41,7 +41,7 @@ class HassErrorScreen extends LitElement {
<div class="content">
<ha-alert alert-type="error">${this.error}</ha-alert>
<slot>
<ha-button appearance="plain" size="small" @click=${this._handleBack}>
<ha-button appearance="plain" size="s" @click=${this._handleBack}>
${this.hass?.localize("ui.common.back")}
</ha-button>
</slot>
+1 -1
View File
@@ -97,7 +97,7 @@ class NotificationManager extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
slot="action"
@click=${this._buttonClicked}
>
@@ -132,7 +132,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
href="https://www.home-assistant.io/installation/#advanced-installation-methods"
target="_blank"
rel="noreferrer noopener"
size="small"
size="s"
>
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.learn_more"
+5 -5
View File
@@ -147,7 +147,7 @@ export class HAFullCalendar extends LitElement {
<div class="navigation">
<ha-button
appearance="filled"
size="small"
size="s"
class="today"
@click=${this._handleToday}
>${this.hass.localize(
@@ -171,7 +171,7 @@ export class HAFullCalendar extends LitElement {
<ha-button-toggle-group
.buttons=${viewToggleButtons}
.active=${this._activeView}
size="small"
size="s"
no-wrap
@value-changed=${this._handleView}
></ha-button-toggle-group>
@@ -197,7 +197,7 @@ export class HAFullCalendar extends LitElement {
<div class="controls buttons">
<ha-button
appearance="plain"
size="small"
size="s"
class="today"
@click=${this._handleToday}
>${this.hass.localize(
@@ -207,7 +207,7 @@ export class HAFullCalendar extends LitElement {
<ha-button-toggle-group
.buttons=${viewToggleButtons}
.active=${this._activeView}
size="small"
size="s"
no-wrap
@value-changed=${this._handleView}
></ha-button-toggle-group>
@@ -219,7 +219,7 @@ export class HAFullCalendar extends LitElement {
<div id="calendar"></div>
${this.addFab && this._hasMutableCalendars
? html`<ha-button size="large" slot="fab" @click=${this._createEvent}>
? html`<ha-button size="l" slot="fab" @click=${this._createEvent}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.components.calendar.event.add")}
</ha-button>`
@@ -175,7 +175,7 @@ export class HaConfigApplicationCredentials extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._deleteSelected}
variant="danger"
>${this.hass.localize(
@@ -199,11 +199,7 @@ export class HaConfigApplicationCredentials extends LitElement {
</ha-help-tooltip>
`}
</div>
<ha-button
slot="fab"
size="large"
@click=${this._addApplicationCredential}
>
<ha-button slot="fab" size="l" @click=${this._addApplicationCredential}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.application_credentials.picker.add_application_credential"
@@ -156,7 +156,7 @@ export class HaConfigAppsInstalled extends LitElement {
)}
</div>
<ha-button size="large" href="/config/apps/available">
<ha-button size="l" href="/config/apps/available">
<ha-svg-icon slot="start" .path=${mdiStorePlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.apps.installed.add_app")}
</ha-button>
@@ -295,7 +295,7 @@ export class HaConfigAppsInstalled extends LitElement {
cursor: pointer;
}
ha-button[size="large"] {
ha-button[size="l"] {
position: fixed;
right: calc(var(--ha-space-4) + var(--safe-area-inset-right));
bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
@@ -116,7 +116,7 @@ export class HaConfigAppsRegistries extends LitElement {
id="registry"
has-fab
></ha-data-table>
<ha-button size="large" @click=${this._showAddRegistryDialog}>
<ha-button size="l" @click=${this._showAddRegistryDialog}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.apps.registries.add")}
</ha-button>
@@ -184,7 +184,7 @@ export class HaConfigAppsRegistries extends LitElement {
ha-icon-button.delete {
color: var(--error-color);
}
ha-button[size="large"] {
ha-button[size="l"] {
position: fixed;
right: calc(var(--ha-space-4) + var(--safe-area-inset-right));
bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
@@ -195,7 +195,7 @@ export class HaConfigAppsRepositories extends LitElement {
id="slug"
has-fab
></ha-data-table>
<ha-button size="large" @click=${this._showAddRepositoryDialog}>
<ha-button size="l" @click=${this._showAddRepositoryDialog}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.apps.repositories.add")}
</ha-button>
@@ -292,7 +292,7 @@ export class HaConfigAppsRepositories extends LitElement {
ha-icon-button.delete {
color: var(--error-color);
}
ha-button[size="large"] {
ha-button[size="l"] {
position: fixed;
right: calc(var(--ha-space-4) + var(--safe-area-inset-right));
bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
@@ -169,6 +169,9 @@ export class SupervisorAppsRepositoryEl extends LitElement {
cursor: pointer;
overflow: hidden;
}
ha-card:hover {
background-color: var(--ha-color-fill-neutral-quiet-resting);
}
.card-content.has-footer {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
}
@@ -210,7 +210,6 @@ class DialogFloorDetail extends LitElement {
</p>`}
<ha-area-picker
no-add
.hass=${this.hass}
@value-changed=${this._addArea}
.excludeAreas=${areas.map((a) => a.area_id)}
.addButtonLabel=${this.hass.localize(
@@ -22,6 +22,10 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -33,7 +37,6 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
@@ -240,6 +243,10 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
super.updated(changedProps);
if (changedProps.has("areaId")) {
this._findRelated();
fireEvent(this, "hass-related-context", {
itemType: "area",
itemId: this.areaId,
});
}
}
@@ -318,7 +318,7 @@ export class HaConfigAreasDashboard extends LitElement {
: nothing}
</div>
<ha-dropdown slot="fab" @wa-select=${this._handleCreateAction}>
<ha-button slot="trigger" id="fab" size="large">
<ha-button slot="trigger" id="fab" size="l">
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.common.add")}
</ha-button>
@@ -120,7 +120,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
.disabled=${this.disabled}
@click=${this._addActionDialog}
.appearance=${this.root ? "accent" : "filled"}
.size=${this.root ? "medium" : "small"}
.size=${this.root ? "m" : "s"}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass.localize(
@@ -728,7 +728,7 @@ class DialogAddAutomationElement
active-variant="brand"
.buttons=${tabButtons}
.active=${this._tab}
size="small"
size="s"
full-width
@value-changed=${this._switchTab}
></ha-button-toggle-group>`
@@ -194,7 +194,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
${this._visibleOptionals.includes("area")
? html` <ha-area-picker
id="area"
.hass=${this.hass}
.value=${this._entryUpdates.area}
@value-changed=${this._registryEntryChanged}
></ha-area-picker>`
@@ -35,7 +35,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
)}
<ha-button
appearance="plain"
size="small"
size="s"
slot="action"
@click=${this._enable}
>
@@ -57,7 +57,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
<ha-button
slot="fab"
size="large"
size="l"
class=${this.dirty ? "dirty" : ""}
.disabled=${this.saving}
@click=${this._saveAutomation}
@@ -294,7 +294,7 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
.disabled=${this.disabled}
@click=${this._addConditionDialog}
.appearance=${this.root ? "accent" : "filled"}
.size=${this.root ? "medium" : "small"}
.size=${this.root ? "m" : "s"}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass.localize(
@@ -237,7 +237,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
? html`
<ha-button
appearance="plain"
size="small"
size="s"
@click=${this._showTrace}
slot="toolbar-icon"
>
@@ -490,7 +490,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
)}
<ha-button
appearance="filled"
size="small"
size="s"
variant="warning"
slot="action"
@click=${this._duplicate}
@@ -508,7 +508,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
"ui.panel.config.automation.editor.disabled"
)}
<ha-button
size="small"
size="s"
slot="action"
@click=${this._toggle}
>
@@ -533,7 +533,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
)}
<ha-button
appearance="filled"
size="small"
size="s"
slot="action"
@click=${this._toggle}
>
@@ -553,7 +553,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
></ha-yaml-editor>
<ha-button
slot="fab"
size="large"
size="l"
class=${this.dirty ? "dirty" : ""}
.disabled=${this.saving}
@click=${this._handleSaveAutomation}
@@ -23,11 +23,7 @@ export class HaAutomationNote extends LitElement {
"ui.panel.config.automation.editor.note.label"
)}
</span>
<ha-button
@click=${this._handleClick}
size="small"
appearance="plain"
>
<ha-button @click=${this._handleClick} size="s" appearance="plain">
${this._i18n.localize("ui.common.edit")}
</ha-button>
</div>
@@ -695,14 +695,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
target="_blank"
appearance="plain"
rel="noreferrer"
size="small"
size="s"
>
${this.hass.localize("ui.panel.config.common.learn_more")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}> </ha-svg-icon>
</ha-button>
</div>`
: nothing}
<ha-button slot="fab" size="large" @click=${this._createNew}>
<ha-button slot="fab" size="l" @click=${this._createNew}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
@@ -142,21 +142,42 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _relatedContextAreaId?: string;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("registryEntry")) {
const areaId = this.registryEntry?.area_id;
if (areaId) {
fireEvent(this, "hass-related-context", {
itemType: "area",
itemId: areaId,
});
} else {
fireEvent(this, "hass-related-context", undefined);
}
if (
changedProps.has("currentEntityId") ||
changedProps.has("entityRegistry")
) {
this._setRelatedContext();
}
}
private _setRelatedContext(): void {
const areaId = this.currentEntityId
? this.entityRegistry?.find(
({ entity_id }) => entity_id === this.currentEntityId
)?.area_id || undefined
: undefined;
if (areaId === this._relatedContextAreaId) {
return;
}
this._relatedContextAreaId = areaId;
fireEvent(
this,
"hass-related-context",
areaId
? {
itemType: "area",
itemId: areaId,
}
: undefined
);
}
protected renderLoading(): TemplateResult {
return html`
<ha-fade-in .delay=${500}>
@@ -1,4 +1,5 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiDotsVertical,
mdiDownload,
@@ -33,6 +34,8 @@ import type {
NodeInfo,
} from "../../../components/trace/hat-script-graph";
import type { AutomationEntity } from "../../../data/automation";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { LogbookEntry } from "../../../data/logbook";
import { getLogbookDataForContext } from "../../../data/logbook";
import type {
@@ -63,6 +66,10 @@ export class HaAutomationTrace extends LitElement {
@property({ attribute: false }) public route!: Route;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityRegistry?: EntityRegistryEntry[];
@state() private _entityId?: string;
@state() private _traces?: AutomationTrace[];
@@ -110,7 +117,7 @@ export class HaAutomationTrace extends LitElement {
? html`
<ha-button
appearance="plain"
size="small"
size="s"
class="trace-link"
@click=${this._navigateToAutomation}
slot="toolbar-icon"
@@ -343,7 +350,7 @@ export class HaAutomationTrace extends LitElement {
}
if (
changedProps.has("automations") &&
(changedProps.has("automationId") || changedProps.has("automations")) &&
this.automationId &&
!this._entityId
) {
@@ -352,6 +359,32 @@ export class HaAutomationTrace extends LitElement {
);
this._entityId = automation?.entity_id;
}
if (
changedProps.has("automationId") ||
changedProps.has("_entityId") ||
changedProps.has("_entityRegistry")
) {
this._setRelatedContext();
}
}
private _setRelatedContext() {
const areaId = this._entityId
? this._entityRegistry?.find(
(entry) => entry.entity_id === this._entityId
)?.area_id
: undefined;
fireEvent(
this,
"hass-related-context",
areaId
? {
itemType: "area",
itemId: areaId,
}
: undefined
);
}
private _pickOlderTrace() {
@@ -114,7 +114,7 @@ export const ManualEditorMixin = <TConfig>(
<div class="fab-positioner">
<ha-button
slot="fab"
size="large"
size="l"
class=${this.dirty ? "dirty" : ""}
.disabled=${this.saving}
@click=${this.saveConfig}
@@ -92,7 +92,7 @@ export default class HaAutomationOption extends AutomationSortableListMixin<Opti
<div class="buttons">
<ha-button
appearance="filled"
size="small"
size="s"
.disabled=${this.disabled}
@click=${this._addOption}
>
@@ -104,7 +104,7 @@ export default class HaAutomationOption extends AutomationSortableListMixin<Opti
${!this.showDefaultActions
? html`<ha-button
appearance="plain"
size="small"
size="s"
.disabled=${this.disabled}
@click=${this._showDefaultActions}
>
@@ -179,7 +179,7 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
.disabled=${this.disabled}
@click=${this._addTriggerDialog}
.appearance=${this.root ? "accent" : "filled"}
.size=${this.root ? "medium" : "small"}
.size=${this.root ? "m" : "s"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
@@ -40,7 +40,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
appearance="plain"
slot="end"
@click=${this._download}
size="small"
size="s"
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
@@ -63,7 +63,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
appearance="plain"
slot="end"
@click=${this._show}
size="small"
size="s"
>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.show_encryption_key_action"
@@ -84,7 +84,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
<ha-button
appearance="plain"
variant="danger"
size="small"
size="s"
slot="end"
@click=${this._change}
>
@@ -156,7 +156,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-button[size="small"] ha-svg-icon {
ha-button[size="s"] ha-svg-icon {
--mdc-icon-size: 16px;
}
`;
@@ -379,7 +379,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
)}
</span>
<ha-button
size="small"
size="s"
appearance="plain"
slot="end"
@click=${this._downloadKey}
@@ -146,7 +146,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
)}
</span>
<ha-button
size="small"
size="s"
appearance="plain"
slot="end"
@click=${this._download}
@@ -533,7 +533,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
? html`
<ha-button
slot="fab"
size="large"
size="l"
?disabled=${backupInProgress}
@click=${this._newBackup}
>
@@ -161,7 +161,7 @@ class HaConfigBackupDetails extends LitElement {
slot="end"
rel="noreferrer noopener"
appearance="plain"
size="small"
size="s"
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
@@ -323,7 +323,7 @@ class HaConfigBackupOverview extends LitElement {
<ha-button
slot="fab"
size="large"
size="l"
.loading=${backupInProgress}
@click=${this._newBackup}
>

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