mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-27 13:02:51 +00:00
Compare commits
9 Commits
selector-t
...
ha-list-ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3fde7ef00 | ||
|
|
122224ee95 | ||
|
|
88b61b20c8 | ||
|
|
4a79fa56a9 | ||
|
|
5fad0083cd | ||
|
|
03108b801f | ||
|
|
f449c6c1c1 | ||
|
|
d1eb3fd162 | ||
|
|
18c0a7e39d |
188
gallery/src/pages/components/ha-list.markdown
Normal file
188
gallery/src/pages/components/ha-list.markdown
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
title: List
|
||||
---
|
||||
|
||||
# List
|
||||
|
||||
The list family provides accessible, keyboard-navigable list containers and
|
||||
item variants. Pick the container based on semantics, then the item based on
|
||||
interactivity.
|
||||
|
||||
## Containers
|
||||
|
||||
### `<ha-list-base>`
|
||||
|
||||
A styled container with roving-tabindex keyboard navigation. Host role is
|
||||
`list`. Children should be `<ha-list-item-*>`. Arrow keys rove focus;
|
||||
Home/End jump to the first/last enabled item; Enter/Space activates the
|
||||
focused item.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ------- | ------- | ------------------------------ |
|
||||
| `wrap-focus` | Boolean | `false` | Arrow keys wrap past the ends. |
|
||||
| `aria-label` | String | — | Accessible name. |
|
||||
|
||||
**Events**
|
||||
|
||||
- `ha-list-activated` — Enter/Space on a focused item. Detail
|
||||
`{ index: number, item: HaListItemBase }`.
|
||||
|
||||
**Methods**
|
||||
|
||||
- `focus()` — focus the active item (or the first focusable one).
|
||||
- `focusItemAtIndex(index)` — make the item at `index` active and focus it.
|
||||
- `getActiveItemIndex()` — current active index, or `-1`.
|
||||
- `setActiveItemIndex(index, focusItem?)` — move the active index without
|
||||
necessarily focusing.
|
||||
- `updateListItems()` — re-discover slotted items (called automatically on
|
||||
slotchange).
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `base` — the outer `<div role="list">`.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-list-gap` — spacing between items. Defaults to `0`.
|
||||
- `--ha-list-padding` — padding around the list. Defaults to `0`.
|
||||
|
||||
### `<ha-list-selectable>`
|
||||
|
||||
Selectable list. Extends `ha-list-base`. Host role is `listbox`; items must be
|
||||
`<ha-list-item-option>` (role `option`). Set `multi` for multi-select; the
|
||||
host reflects `aria-multiselectable`.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------- | ------- | ------- | -------------------------------------- |
|
||||
| `multi` | Boolean | `false` | Allow multiple options to be selected. |
|
||||
|
||||
**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.
|
||||
|
||||
**Methods / getters**
|
||||
|
||||
- `selected` (getter) — current selection (`number` or `Set<number>`).
|
||||
- `selectedItems` (getter) — selected `HaListItemOption` elements, in index
|
||||
order.
|
||||
- `setSelected(indices)` — replace the entire selection.
|
||||
- `select(index)` — add `index` to the selection (replaces in single mode).
|
||||
- `toggle(index, force?)` — toggle a single index, or force on/off.
|
||||
- `clearSelection()` — clear all.
|
||||
|
||||
### `<ha-list-nav>`
|
||||
|
||||
Same as `ha-list-base`, but wrapped in a `<nav>` landmark
|
||||
(`<nav><div role="list">…</div></nav>`). Use `aria-label` to name the
|
||||
landmark — the value is forwarded to the inner `<nav>`. Items should be
|
||||
`<ha-list-item-button>` with an `href`.
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `nav` — the `<nav>` wrapper.
|
||||
- `base` — the inner `<div role="list">`.
|
||||
|
||||
## Items
|
||||
|
||||
All items inherit from `ha-row-item`, which provides the row layout and the
|
||||
shared slots/attributes below.
|
||||
|
||||
### Shared row layout (`ha-row-item`)
|
||||
|
||||
**Slots**
|
||||
|
||||
- `start` — leading container (icon/avatar).
|
||||
- `end` — trailing container (meta/chevron).
|
||||
- `headline` — primary text (overrides the `headline` attribute).
|
||||
- `supporting-text` — secondary text (overrides the `supporting-text` attribute).
|
||||
- `content` — escape hatch: replaces the entire middle column.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ----------------- | ------- | ------- | --------------------------------------- |
|
||||
| `headline` | String | — | Primary text. Overridden by the slot. |
|
||||
| `supporting-text` | String | — | Secondary text. Overridden by the slot. |
|
||||
| `disabled` | Boolean | `false` | Dims the row and blocks pointer events. |
|
||||
|
||||
**CSS parts**
|
||||
|
||||
`base`, `start`, `content`, `headline`, `supporting-text`, `end`.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-row-item-padding-block` — vertical padding.
|
||||
- `--ha-row-item-padding-inline` — horizontal padding.
|
||||
- `--ha-row-item-gap` — gap between `start`, `content`, and `end`.
|
||||
- `--ha-row-item-min-height` — minimum row height (default `48px`).
|
||||
|
||||
### `<ha-list-item-base>`
|
||||
|
||||
Non-interactive list row. Host role is `listitem`. Inherits everything from
|
||||
`ha-row-item`.
|
||||
|
||||
**Attributes**
|
||||
|
||||
- `interactive` (Boolean, default `false`) — opt this row into the parent
|
||||
list's roving tabindex. Useful for sortable rows that need keyboard focus
|
||||
but no click action. Interactive subclasses set this automatically.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-list-item-focus-radius` — focus outline border-radius.
|
||||
- `--ha-list-item-focus-width` — focus outline width (steady state).
|
||||
- `--ha-list-item-focus-width-start` — focus outline width at the start of
|
||||
the focus-in animation.
|
||||
- `--ha-list-item-focus-offset` — focus outline offset.
|
||||
- `--ha-list-item-focus-background` — background color on keyboard focus.
|
||||
|
||||
### `<ha-list-item-button>`
|
||||
|
||||
Interactive row. Renders an inner `<a>` when `href` is set, otherwise a
|
||||
`<button>`. The full row is the hit target. When placed inside a list using
|
||||
roving tabindex, the host is the tab stop and the inner element carries
|
||||
`tabindex="-1"`.
|
||||
|
||||
**Attributes**
|
||||
|
||||
- `href` (String) — when set, renders an `<a>` instead of a `<button>`.
|
||||
- `target` (String) — anchor `target` (requires `href`).
|
||||
- `rel` (String) — anchor `rel` (requires `href`).
|
||||
- `download` (String) — anchor `download` (requires `href`).
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `ripple` — the ripple effect element.
|
||||
|
||||
### `<ha-list-item-option>`
|
||||
|
||||
Selectable row. Host role is `option`; reflects `aria-selected`. Designed to
|
||||
sit inside `<ha-list-selectable>`, which owns selection state and toggles
|
||||
`selected` on this item — the option itself does not fire selection events.
|
||||
|
||||
**Attributes**
|
||||
|
||||
- `selected` (Boolean, default `false`, reflected) — set by the parent
|
||||
`ha-list-selectable`.
|
||||
- `value` (String) — value identifying the option.
|
||||
- `appearance` (`"line"` | `"checkbox"`, default `"line"`) — `"line"`
|
||||
highlights the row; `"checkbox"` renders a decorative `<ha-checkbox>`.
|
||||
- `selection-position` (`"start"` | `"end"`, default `"start"`) — side the
|
||||
checkbox sits on when `appearance="checkbox"`.
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `checkbox` — wrapper around the `<ha-checkbox>` when `appearance="checkbox"`.
|
||||
- `ripple` — the ripple effect element.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-list-item-selected-background` — background color when selected
|
||||
(`appearance="line"`).
|
||||
415
gallery/src/pages/components/ha-list.ts
Normal file
415
gallery/src/pages/components/ha-list.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiHome,
|
||||
mdiInformationOutline,
|
||||
mdiMapMarker,
|
||||
mdiOpenInNew,
|
||||
mdiViewDashboard,
|
||||
mdiWifi,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/item/ha-list-item-base";
|
||||
import "../../../../src/components/item/ha-list-item-button";
|
||||
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";
|
||||
|
||||
const appearances: Appearance[] = ["line", "checkbox"];
|
||||
const positions: Position[] = ["start", "end"];
|
||||
const selectedStates = [false, true];
|
||||
const disabledStates = [false, true];
|
||||
|
||||
@customElement("demo-components-ha-list")
|
||||
export class DemoHaList extends LitElement {
|
||||
@state() private _buttonClicks = 0;
|
||||
|
||||
@state() private _single: number | Set<number> = -1;
|
||||
|
||||
@state() private _multiLine: number | Set<number> = new Set();
|
||||
|
||||
@state() private _multiCheckStart: number | Set<number> = new Set();
|
||||
|
||||
@state() private _multiCheckEnd: number | Set<number> = new Set();
|
||||
|
||||
private _options = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h2>ha-list-base</h2>
|
||||
<p>
|
||||
Styled container with keyboard focus navigation. Children should be
|
||||
<code>ha-list-item-*</code>.
|
||||
</p>
|
||||
|
||||
<ha-card header="Info list (non-interactive rows)">
|
||||
<ha-list-base aria-label="Device info">
|
||||
<ha-list-item-base
|
||||
headline="IP address"
|
||||
supporting-text="192.168.1.42"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiWifi}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Location" supporting-text="Living room">
|
||||
<ha-svg-icon slot="start" .path=${mdiMapMarker}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Firmware" supporting-text="2026.4.1">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Vertical list (default)">
|
||||
<ha-list-base aria-label="Example list">
|
||||
<ha-list-item-button>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<span slot="headline">First row</span>
|
||||
<span slot="supporting-text">Supporting text</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<ha-svg-icon slot="start" .path=${mdiAccount}></ha-svg-icon>
|
||||
<span slot="headline">Second row</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button disabled>
|
||||
<span slot="headline">Disabled row</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">Fourth row</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Vertical list with wrap-focus">
|
||||
<ha-list-base wrap-focus aria-label="Wrap focus">
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">A</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">B</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">C</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-item-base</h2>
|
||||
<p>Non-interactive base row with slot permutations.</p>
|
||||
|
||||
<ha-card header="Slot permutations">
|
||||
<ha-list-base aria-label="Slot permutations">
|
||||
<ha-list-item-base headline="Headline only"></ha-list-item-base>
|
||||
<ha-list-item-base
|
||||
headline="Headline"
|
||||
supporting-text="Supporting text"
|
||||
></ha-list-item-base>
|
||||
<ha-list-item-base headline="Start + headline">
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Start + headline + end">
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base
|
||||
headline="Full row"
|
||||
supporting-text="All slots filled"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<div slot="content" class="custom-content">
|
||||
<strong>Custom content escape hatch</strong>
|
||||
<span>Replaces the whole middle column</span>
|
||||
</div>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Disabled row" disabled>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-item-button</h2>
|
||||
<p>
|
||||
Interactive row. Renders an inner <code><a></code> when
|
||||
<code>href</code> is set, otherwise a <code><button></code>.
|
||||
</p>
|
||||
|
||||
<ha-card header="Button (default) / link (with href)">
|
||||
<ha-list-base aria-label="Button items">
|
||||
<ha-list-item-button @click=${this._onButtonClick}>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<span slot="headline">Button (clicks: ${this._buttonClicks})</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href="https://www.home-assistant.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
<span slot="headline">Link (opens in new tab)</span>
|
||||
<span slot="supporting-text"
|
||||
>Cmd/Ctrl-click still opens in new tab</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button disabled>
|
||||
<span slot="headline">Disabled button</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button href="#nope" disabled>
|
||||
<span slot="headline">Disabled link</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-selectable + ha-list-item-option</h2>
|
||||
<p>
|
||||
Selectable list (<code>role="listbox"</code>). Items must be
|
||||
<code>ha-list-item-option</code>. Set <code>multi</code> for
|
||||
multi-select.
|
||||
</p>
|
||||
|
||||
<ha-card header="Single select, appearance=line">
|
||||
<ha-list-selectable
|
||||
aria-label="Single select"
|
||||
@ha-list-selected=${this._onSingle}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._single, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>selected: ${JSON.stringify(this._toJson(this._single))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Multi select, appearance=line">
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi select line"
|
||||
@ha-list-selected=${this._onMultiLine}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._multiLine, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>selected: ${JSON.stringify(this._toJson(this._multiLine))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
header='Multi select, appearance=checkbox, selection-position="start"'
|
||||
>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox start"
|
||||
@ha-list-selected=${this._onMultiCheckStart}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="start"
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._multiCheckStart, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
|
||||
>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
header='Multi select, appearance=checkbox, selection-position="end"'
|
||||
>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox end"
|
||||
@ha-list-selected=${this._onMultiCheckEnd}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._multiCheckEnd, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
<span slot="supporting-text">${o.length} characters</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
|
||||
>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Option: all combinations">
|
||||
<div class="grid">
|
||||
${appearances.map((appearance) =>
|
||||
positions.map((position) =>
|
||||
selectedStates.map((selected) =>
|
||||
disabledStates.map(
|
||||
(disabled) => html`
|
||||
<div role="listbox" class="wrap" aria-label="single option">
|
||||
<ha-list-item-option
|
||||
appearance=${appearance}
|
||||
selection-position=${position}
|
||||
?selected=${selected}
|
||||
?disabled=${disabled}
|
||||
>
|
||||
<span slot="headline"
|
||||
>${appearance} / pos=${position}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>selected=${String(selected)}
|
||||
disabled=${String(disabled)}</span
|
||||
>
|
||||
</ha-list-item-option>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-nav</h2>
|
||||
<p>
|
||||
Same as <code>ha-list-base</code> but wrapped in a
|
||||
<code><nav></code> landmark.
|
||||
</p>
|
||||
|
||||
<ha-card header="Sidebar-style navigation">
|
||||
<ha-list-nav aria-label="Primary navigation">
|
||||
${[
|
||||
{ name: "Overview", path: "#overview", icon: mdiHome },
|
||||
{ name: "Dashboards", path: "#dashboards", icon: mdiViewDashboard },
|
||||
{ name: "Map", path: "#map", icon: mdiMapMarker },
|
||||
{ name: "Settings", path: "#settings", icon: mdiCog },
|
||||
].map(
|
||||
(p) => html`
|
||||
<ha-list-item-button .href=${p.path}>
|
||||
<ha-svg-icon slot="start" .path=${p.icon}></ha-svg-icon>
|
||||
<span slot="headline">${p.name}</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list-nav>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isSel(value: number | Set<number>, index: number): boolean {
|
||||
if (typeof value === "number") {
|
||||
return value === index;
|
||||
}
|
||||
return value.has(index);
|
||||
}
|
||||
|
||||
private _toJson(value: number | Set<number>): unknown {
|
||||
return value instanceof Set ? [...value] : value;
|
||||
}
|
||||
|
||||
private _onButtonClick = () => {
|
||||
this._buttonClicks++;
|
||||
};
|
||||
|
||||
private _onSingle = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._single = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiLine = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiLine = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiCheckStart = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckStart = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiCheckEnd = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckEnd = ev.detail.index;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
padding: var(--ha-space-6);
|
||||
}
|
||||
h2 {
|
||||
margin: var(--ha-space-4) 0 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
p {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 560px;
|
||||
}
|
||||
pre {
|
||||
padding: var(--ha-space-4);
|
||||
background: var(--secondary-background-color);
|
||||
margin: 0;
|
||||
}
|
||||
.custom-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--ha-space-3);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.wrap {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-list": DemoHaList;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.41.1",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
@@ -162,7 +162,7 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.4",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
@@ -205,7 +205,7 @@
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.4",
|
||||
"vitest": "4.1.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
|
||||
|
||||
@@ -13,14 +13,17 @@ import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
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 {
|
||||
@@ -75,27 +78,33 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<ha-list class="ha-scrollbar">
|
||||
<ha-list-selectable
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
@ha-list-selected=${this._handleListChanged}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
>
|
||||
${repeat(
|
||||
areas?.floors || [],
|
||||
(floor) => floor.floor_id,
|
||||
(floor) => html`
|
||||
<ha-check-list-item
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${floor.floor_id}
|
||||
.type=${"floors"}
|
||||
.selected=${this.value?.floors?.includes(
|
||||
floor.floor_id
|
||||
) || false}
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
<ha-floor-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.floor=${floor}
|
||||
></ha-floor-icon>
|
||||
${floor.name}
|
||||
</ha-check-list-item>
|
||||
<span slot="headline">${floor.name} </span>
|
||||
</ha-list-item-option>
|
||||
${repeat(
|
||||
floor.areas,
|
||||
(area, index) =>
|
||||
@@ -110,7 +119,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
(area) => area.area_id,
|
||||
(area) => this._renderArea(area)
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
@@ -119,79 +128,83 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
private _renderArea(area, last = false) {
|
||||
const hasFloor = !!area.floor_id;
|
||||
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${area.area_id}
|
||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||
.type=${"areas"}
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
>
|
||||
${hasFloor
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
.end=${last}
|
||||
slot="graphic"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
? html`<ha-tree-indicator
|
||||
slot="start"
|
||||
.end=${last}
|
||||
></ha-tree-indicator>`
|
||||
: nothing}
|
||||
${area.icon
|
||||
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-check-list-item>
|
||||
<span slot="headline">${area.name}</span>
|
||||
</ha-list-item-option>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleItemKeydown(ev) {
|
||||
if (ev.key === " " || ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const listItem = ev.currentTarget;
|
||||
const type = listItem?.type;
|
||||
const value = listItem?.value;
|
||||
|
||||
if (ev.detail.selected === listItem.selected || !value) {
|
||||
private _handleListChanged(ev: CustomEvent<HaListSelectedDetail>) {
|
||||
if (!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.value?.[type]?.includes(value)) {
|
||||
this.value = {
|
||||
...this.value,
|
||||
[type]: this.value[type].filter((val) => val !== value),
|
||||
};
|
||||
} else {
|
||||
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 };
|
||||
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
this.value = {
|
||||
...this.value,
|
||||
[type]: [...(this.value[type] || []), 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
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
listItem.selected = this.value[type]?.includes(value);
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
this.renderRoot.querySelector("ha-list-selectable")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
@@ -317,11 +330,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
}
|
||||
.floor {
|
||||
padding-left: 48px;
|
||||
.floor::part(base) {
|
||||
padding-inline-start: 48px;
|
||||
padding-inline-end: 16px;
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-fade-in";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./list/ha-list-nav";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
@@ -352,12 +352,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
const renderList = (content, cls: string, scrollable: boolean) =>
|
||||
html`<ha-md-list
|
||||
html`<ha-list-nav
|
||||
class=${classMap({
|
||||
"ha-scrollbar": scrollable,
|
||||
[cls]: true,
|
||||
})}
|
||||
>${content}</ha-md-list
|
||||
>${content}</ha-list-nav
|
||||
>`;
|
||||
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
@@ -429,9 +429,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
const iconPath = getPanelIconPath(panel);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
id="sidebar-panel-${urlPath}"
|
||||
class=${classMap({ selected: isSelected })}
|
||||
>
|
||||
@@ -439,7 +438,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand && title
|
||||
? this._renderToolTip(`sidebar-panel-${urlPath}`, title)
|
||||
: nothing}
|
||||
@@ -456,9 +455,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
const isSelected = selectedPanel === "config";
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
class="configuration ${classMap({ selected: isSelected })}"
|
||||
type="button"
|
||||
href="/config"
|
||||
id="sidebar-config"
|
||||
>
|
||||
@@ -480,7 +478,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
"sidebar-config",
|
||||
@@ -496,10 +494,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
: 0;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
class="notifications"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
type="button"
|
||||
id="sidebar-notifications"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
@@ -514,7 +511,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
${notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
"sidebar-notifications",
|
||||
@@ -529,9 +526,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
href="/profile"
|
||||
type="link"
|
||||
id="sidebar-profile"
|
||||
class=${classMap({
|
||||
user: true,
|
||||
@@ -547,7 +543,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand && this.hass.user
|
||||
? this._renderToolTip("sidebar-profile", this.hass.user.name)
|
||||
: nothing}
|
||||
@@ -559,16 +555,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
id="sidebar-external-config"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.sidebar.external_app_configuration")}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
"sidebar-external-config",
|
||||
@@ -713,10 +708,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
ha-list-nav {
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: var(--safe-area-inset-left, 0px);
|
||||
margin-block: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -726,42 +721,38 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
ha-md-list.before-spacer {
|
||||
ha-list-nav.before-spacer {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-md-list.after-spacer {
|
||||
ha-list-nav.after-spacer {
|
||||
padding-top: 0;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
ha-list-item-button {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
margin: var(--ha-space-1);
|
||||
margin: 0 var(--ha-space-1) var(--ha-space-1);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||
--md-list-item-top-space: 0;
|
||||
--md-list-item-bottom-space: 0;
|
||||
--ha-row-item-min-height: var(--ha-space-10);
|
||||
--ha-row-item-padding-block: 0;
|
||||
width: var(--ha-space-12);
|
||||
position: relative;
|
||||
--md-list-item-label-text-color: var(--sidebar-text-color);
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--ha-space-3);
|
||||
--md-list-item-leading-icon-size: var(--ha-space-6);
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item {
|
||||
ha-list-item-button::part(headline) {
|
||||
color: var(--sidebar-text-color);
|
||||
}
|
||||
:host([expanded]) ha-list-item-button {
|
||||
width: 248px;
|
||||
}
|
||||
:host([narrow][expanded]) ha-md-list-item {
|
||||
:host([narrow][expanded]) ha-list-item-button {
|
||||
width: calc(240px - var(--safe-area-inset-left, 0px));
|
||||
}
|
||||
|
||||
ha-md-list-item.selected {
|
||||
--md-list-item-label-text-color: var(--sidebar-selected-icon-color);
|
||||
--md-ripple-hover-color: var(--sidebar-selected-icon-color);
|
||||
ha-list-item-button.selected::part(headline) {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
ha-md-list-item.selected::before {
|
||||
ha-list-item-button.selected::before {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -783,12 +774,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
ha-md-list-item.selected ha-svg-icon[slot="start"],
|
||||
ha-md-list-item.selected ha-icon[slot="start"] {
|
||||
ha-list-item-button.selected ha-svg-icon[slot="start"],
|
||||
ha-list-item-button.selected ha-icon[slot="start"] {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
ha-list-item-button .item-text {
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
@@ -801,7 +792,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
max-width var(--ha-animation-duration-normal) ease,
|
||||
opacity var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
:host([expanded]) ha-list-item-button .item-text {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
@@ -843,13 +834,17 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-md-list-item.user {
|
||||
--md-list-item-leading-icon-size: var(--ha-space-10);
|
||||
--md-list-item-leading-space: var(--ha-space-1);
|
||||
ha-user-badge {
|
||||
width: var(--ha-space-10);
|
||||
height: var(--ha-space-10);
|
||||
}
|
||||
|
||||
ha-md-list-item.user.rtl {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
ha-list-item-button.user {
|
||||
--ha-row-item-padding-inline: var(--ha-space-2) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-list-item-button.user.rtl {
|
||||
--ha-row-item-padding-inline: var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
@@ -869,8 +864,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu,
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
ha-list-item-button,
|
||||
ha-list-item-button .item-text,
|
||||
.title {
|
||||
transition: 1ms;
|
||||
}
|
||||
|
||||
101
src/components/item/ha-list-item-base.ts
Normal file
101
src/components/item/ha-list-item-base.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaRowItem } from "./ha-row-item";
|
||||
|
||||
/**
|
||||
* @element ha-list-item-base
|
||||
* @extends {HaRowItem}
|
||||
*
|
||||
* @summary
|
||||
* Non-interactive list row (role `listitem`). Base class for
|
||||
* `ha-list-item-button`, `ha-list-item-option`.
|
||||
*
|
||||
* @cssprop --ha-list-item-focus-radius - Focus outline border-radius.
|
||||
* @cssprop --ha-list-item-focus-width - Focus outline width (steady state).
|
||||
* @cssprop --ha-list-item-focus-width-start - Focus outline width at the start of the focus-in animation.
|
||||
* @cssprop --ha-list-item-focus-offset - Focus outline offset.
|
||||
* @cssprop --ha-list-item-focus-background - Background color applied on keyboard focus.
|
||||
*
|
||||
* @attr {boolean} interactive - Opts the row into the parent list's roving tabindex. Interactive subclasses set this automatically.
|
||||
*/
|
||||
@customElement("ha-list-item-base")
|
||||
export class HaListItemBase extends HaRowItem {
|
||||
/**
|
||||
* Whether the item takes keyboard focus. Read by the parent list to decide
|
||||
* if it should be part of the roving-tabindex ring. Interactive subclasses
|
||||
* (`ha-list-item-button`, `-option`, `-todo`) override the default to `true`.
|
||||
* For the plain base row, set the `interactive` attribute to opt into focus
|
||||
* (useful for sortable rows where you need keyboard reorder but no click
|
||||
* action).
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) public interactive = false;
|
||||
|
||||
/** Host `role` attribute. Subclasses override. */
|
||||
protected readonly defaultRole: string = "listitem";
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("role")) {
|
||||
this.setAttribute("role", this.defaultRole);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the item (Enter/Space from the parent list). Default dispatches
|
||||
* a click on the host. Subclasses that wrap a native element (e.g. `<a>`)
|
||||
* override this to click the inner element so browser default actions
|
||||
* (like anchor navigation) fire.
|
||||
*/
|
||||
public activate(): void {
|
||||
this.click();
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaRowItem.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-list-item-focus-radius: var(--ha-border-radius-sm);
|
||||
--ha-list-item-focus-width: 2px;
|
||||
--ha-list-item-focus-width-start: var(--ha-space-2);
|
||||
--ha-list-item-focus-offset: -2px;
|
||||
--ha-list-item-focus-background: var(
|
||||
--ha-color-fill-neutral-quiet-hover
|
||||
);
|
||||
}
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
}
|
||||
.base {
|
||||
border-radius: var(--ha-list-item-focus-radius);
|
||||
outline: var(--ha-list-item-focus-width) solid transparent;
|
||||
outline-offset: var(--ha-list-item-focus-offset);
|
||||
transition:
|
||||
outline-color var(--ha-animation-duration-fast) ease-out,
|
||||
background-color var(--ha-animation-duration-fast) ease-out;
|
||||
}
|
||||
@keyframes ha-list-item-focus-in {
|
||||
from {
|
||||
outline-width: var(--ha-list-item-focus-width-start);
|
||||
outline-offset: calc(-1 * var(--ha-list-item-focus-width-start));
|
||||
}
|
||||
to {
|
||||
outline-width: var(--ha-list-item-focus-width);
|
||||
outline-offset: var(--ha-list-item-focus-offset);
|
||||
}
|
||||
}
|
||||
:host(:focus-visible) .base {
|
||||
outline-color: var(--ha-color-focus);
|
||||
background-color: var(--ha-list-item-focus-background);
|
||||
animation: ha-list-item-focus-in var(--ha-animation-duration-normal)
|
||||
ease-in;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-base": HaListItemBase;
|
||||
}
|
||||
}
|
||||
109
src/components/item/ha-list-item-button.ts
Normal file
109
src/components/item/ha-list-item-button.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "../ha-ripple";
|
||||
import { HaListItemBase } from "./ha-list-item-base";
|
||||
|
||||
/**
|
||||
* @element ha-list-item-button
|
||||
* @extends {HaListItemBase}
|
||||
*
|
||||
* @summary
|
||||
* Interactive list row. Renders an inner `<a>` when `href` is set, otherwise
|
||||
* a `<button>`. The full row is the hit target. When placed in a list using
|
||||
* roving tabindex, the host is the tab stop and the inner element carries
|
||||
* `tabindex="-1"`. For a non-interactive row, use `ha-list-item-base`.
|
||||
*
|
||||
* @csspart ripple - The ripple effect element.
|
||||
*
|
||||
* @attr {string} href - URL. When set, renders an `<a>` instead of a `<button>`.
|
||||
* @attr {string} target - Anchor `target` attribute (requires `href`).
|
||||
* @attr {string} rel - Anchor `rel` attribute (requires `href`).
|
||||
* @attr {string} download - Anchor `download` attribute (requires `href`).
|
||||
*/
|
||||
@customElement("ha-list-item-button")
|
||||
export class HaListItemButton extends HaListItemBase {
|
||||
public override interactive = true;
|
||||
|
||||
@property({ type: String }) public href?: string;
|
||||
|
||||
@property({ type: String }) public target?: string;
|
||||
|
||||
@property({ type: String }) public rel?: string;
|
||||
|
||||
@property({ type: String }) public download?: string;
|
||||
|
||||
public override activate(): void {
|
||||
this.renderRoot.querySelector<HTMLElement>("#item")?.click();
|
||||
}
|
||||
|
||||
protected _renderBase(inner: TemplateResult): TemplateResult {
|
||||
if (this.href !== undefined) {
|
||||
return html`<a
|
||||
part="base"
|
||||
class="base interactive"
|
||||
id="item"
|
||||
href=${ifDefined(this.disabled ? undefined : this.href)}
|
||||
target=${ifDefined(this.target)}
|
||||
rel=${ifDefined(this.rel)}
|
||||
download=${ifDefined(this.download)}
|
||||
tabindex="-1"
|
||||
aria-disabled=${this.disabled ? "true" : "false"}
|
||||
>
|
||||
${this._renderRipple()}${inner}
|
||||
</a>`;
|
||||
}
|
||||
return html`<button
|
||||
part="base"
|
||||
class="base interactive"
|
||||
id="item"
|
||||
type="button"
|
||||
?disabled=${this.disabled}
|
||||
tabindex="-1"
|
||||
>
|
||||
${this._renderRipple()}${inner}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
private _renderRipple() {
|
||||
return html`<ha-ripple
|
||||
part="ripple"
|
||||
for="item"
|
||||
?disabled=${this.disabled}
|
||||
></ha-ripple>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemBase.styles,
|
||||
css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
--ha-ripple-color: var(--primary-text-color);
|
||||
}
|
||||
:host([disabled]) {
|
||||
cursor: default;
|
||||
}
|
||||
.base.interactive {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
text-decoration: none;
|
||||
appearance: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
:host([disabled]) .base.interactive {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-button": HaListItemButton;
|
||||
}
|
||||
}
|
||||
132
src/components/item/ha-list-item-option.ts
Normal file
132
src/components/item/ha-list-item-option.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-ripple";
|
||||
import { HaListItemBase } from "./ha-list-item-base";
|
||||
|
||||
export type HaListItemOptionAppearance = "line" | "checkbox";
|
||||
|
||||
export type HaListItemOptionSelectionPosition = "start" | "end";
|
||||
|
||||
/**
|
||||
* @element ha-list-item-option
|
||||
* @extends {HaListItemBase}
|
||||
*
|
||||
* @summary
|
||||
* Selectable list row (role `option`). Selection state is driven by the parent
|
||||
* `ha-list-selectable`; reflects `aria-selected`. When `appearance="checkbox"`, renders
|
||||
* a decorative `<ha-checkbox>` (clicks on the row are handled by the listbox).
|
||||
*
|
||||
* @csspart checkbox - Wrapper around the `<ha-checkbox>` when `appearance="checkbox"`.
|
||||
* @csspart ripple - The ripple effect element.
|
||||
*
|
||||
* @cssprop --ha-list-item-selected-background - Background color when selected (`appearance="line"`).
|
||||
*
|
||||
* @attr {boolean} selected - Whether the option is selected. Set by the parent `ha-list-selectable`.
|
||||
* @attr {string} value - Value identifying the option.
|
||||
* @attr {("line"|"checkbox")} appearance - Visual style. "line" highlights the row; "checkbox" renders an `ha-checkbox`.
|
||||
* @attr {("start"|"end")} selection-position - Side the checkbox sits on when `appearance="checkbox"`.
|
||||
*/
|
||||
@customElement("ha-list-item-option")
|
||||
export class HaListItemOption extends HaListItemBase {
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
@property({ type: String }) public value?: string;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public appearance: HaListItemOptionAppearance = "line";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "selection-position" })
|
||||
public selectionPosition: HaListItemOptionSelectionPosition = "start";
|
||||
|
||||
protected override readonly defaultRole = "option";
|
||||
|
||||
public override interactive = true;
|
||||
|
||||
public update(changed: Map<string, unknown>) {
|
||||
super.update(changed);
|
||||
if (changed.has("selected")) {
|
||||
this.setAttribute("aria-selected", this.selected ? "true" : "false");
|
||||
}
|
||||
if (changed.has("disabled")) {
|
||||
this.setAttribute("aria-disabled", this.disabled ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderBase(inner: TemplateResult): TemplateResult {
|
||||
return html`<div part="base" class="base" id="item">
|
||||
${this._renderRipple()}${this.selectionPosition === "start"
|
||||
? this._renderCheckbox()
|
||||
: nothing}${inner}${this.selectionPosition === "end"
|
||||
? this._renderCheckbox()
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderRipple() {
|
||||
return html`<ha-ripple
|
||||
part="ripple"
|
||||
for="item"
|
||||
?disabled=${this.disabled}
|
||||
></ha-ripple>`;
|
||||
}
|
||||
|
||||
private _renderCheckbox() {
|
||||
if (this.appearance !== "checkbox") {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div part="checkbox" class="checkbox" inert>
|
||||
<ha-checkbox
|
||||
.checked=${this.selected}
|
||||
.disabled=${this.disabled}
|
||||
></ha-checkbox>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemBase.styles,
|
||||
css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
--ha-ripple-color: var(--primary-text-color);
|
||||
--ha-list-item-selected-background: var(
|
||||
--ha-color-fill-primary-quiet-resting,
|
||||
rgba(var(--rgb-primary-color), 0.08)
|
||||
);
|
||||
}
|
||||
:host([disabled]) {
|
||||
cursor: default;
|
||||
}
|
||||
.base {
|
||||
cursor: inherit;
|
||||
}
|
||||
:host([appearance="line"][selected]:not([disabled])) .base,
|
||||
:host([appearance="line"][active]:not([disabled])) .base {
|
||||
background-color: var(--ha-list-item-selected-background);
|
||||
}
|
||||
:host([appearance="line"][selected]:not([disabled])) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
.checkbox ha-checkbox {
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-checkbox::part(base) {
|
||||
gap: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-option": HaListItemOption;
|
||||
}
|
||||
}
|
||||
170
src/components/item/ha-row-item.ts
Normal file
170
src/components/item/ha-row-item.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* @element ha-row-item
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* Generic row layout primitive. Renders a horizontal row with optional
|
||||
* leading/trailing slots and a stacked middle column (headline +
|
||||
* supporting text). Role-agnostic; use `ha-list-item-base` and its
|
||||
* subclasses for list semantics.
|
||||
*
|
||||
* @slot start - Leading container (usually icon/avatar).
|
||||
* @slot end - Trailing container (usually meta/chevron).
|
||||
* @slot headline - Primary text (overrides the `headline` attribute).
|
||||
* @slot supporting-text - Secondary text (overrides the `supporting-text` attribute).
|
||||
* @slot content - Escape hatch: replaces the entire middle column (headline + supporting-text).
|
||||
*
|
||||
* @csspart base - The outer container.
|
||||
* @csspart start - The leading slot wrapper.
|
||||
* @csspart content - The middle column wrapper.
|
||||
* @csspart headline - The headline wrapper.
|
||||
* @csspart supporting-text - The supporting-text wrapper.
|
||||
* @csspart end - The trailing slot wrapper.
|
||||
*
|
||||
* @cssprop --ha-row-item-padding-block - Vertical padding inside the row.
|
||||
* @cssprop --ha-row-item-padding-inline - Horizontal padding inside the row.
|
||||
* @cssprop --ha-row-item-gap - Gap between start, content, and end.
|
||||
* @cssprop --ha-row-item-min-height - Minimum row height.
|
||||
*
|
||||
* @attr {string} headline - Primary text. Overridden by the `headline` slot.
|
||||
* @attr {string} supporting-text - Secondary text. Overridden by the `supporting-text` slot.
|
||||
* @attr {boolean} disabled - Dims the row and blocks pointer events.
|
||||
*/
|
||||
@customElement("ha-row-item")
|
||||
export class HaRowItem extends LitElement {
|
||||
@property({ type: String }) public headline?: string;
|
||||
|
||||
@property({ type: String, attribute: "supporting-text" })
|
||||
public supportingText?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected readonly _slotController = new HasSlotController(
|
||||
this,
|
||||
"start",
|
||||
"end",
|
||||
"headline",
|
||||
"supporting-text",
|
||||
"content"
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._renderBase(this._renderInner());
|
||||
}
|
||||
|
||||
protected _renderBase(inner: TemplateResult): TemplateResult {
|
||||
return html`<div part="base" class="base" id="item">${inner}</div>`;
|
||||
}
|
||||
|
||||
protected _renderInner(): TemplateResult {
|
||||
const hasStart = this._slotController.test("start");
|
||||
const hasEnd = this._slotController.test("end");
|
||||
const hasContent = this._slotController.test("content");
|
||||
|
||||
return html`
|
||||
${hasStart
|
||||
? html`<div part="start" class="start">
|
||||
<slot name="start"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div part="content" class="content">
|
||||
${hasContent
|
||||
? html`<slot name="content"></slot>`
|
||||
: this._renderDefaultContent()}
|
||||
</div>
|
||||
${hasEnd
|
||||
? html`<div part="end" class="end">
|
||||
<slot name="end"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderDefaultContent(): TemplateResult {
|
||||
const hasHeadlineSlot = this._slotController.test("headline");
|
||||
const hasSupportingSlot = this._slotController.test("supporting-text");
|
||||
|
||||
const showHeadline = hasHeadlineSlot || this.headline !== undefined;
|
||||
const showSupporting =
|
||||
hasSupportingSlot || this.supportingText !== undefined;
|
||||
|
||||
return html`
|
||||
${showHeadline
|
||||
? html`<div part="headline" class="headline">
|
||||
<slot name="headline">${this.headline ?? nothing}</slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
${showSupporting
|
||||
? html`<div part="supporting-text" class="supporting">
|
||||
<slot name="supporting-text"
|
||||
>${this.supportingText ?? nothing}</slot
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
--ha-row-item-padding-block: var(--ha-space-3);
|
||||
--ha-row-item-padding-inline: var(--ha-space-4);
|
||||
--ha-row-item-gap: var(--ha-space-4);
|
||||
--ha-row-item-min-height: 48px;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.base {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--ha-row-item-gap);
|
||||
padding-block: var(--ha-row-item-padding-block);
|
||||
padding-inline: var(--ha-row-item-padding-inline);
|
||||
min-height: var(--ha-row-item-min-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.start,
|
||||
.end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.headline {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.supporting {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-row-item": HaRowItem;
|
||||
}
|
||||
}
|
||||
291
src/components/list/ha-list-base.ts
Normal file
291
src/components/list/ha-list-base.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-base
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* Base list container with roving-tabindex keyboard navigation (ArrowUp/Down,
|
||||
* Home/End, optional Enter/Space activation, optional wrap-focus). Discovers
|
||||
* slotted `HaListItemBase` descendants. Subclasses override `hostRole` and/or
|
||||
* `render()` to specialize.
|
||||
*
|
||||
* @slot - List items (`<ha-list-item-*>`).
|
||||
*
|
||||
* @csspart base - The outer container (`<div role="list">`).
|
||||
*
|
||||
* @cssprop --ha-list-gap - Spacing between items. Defaults to `0`.
|
||||
* @cssprop --ha-list-padding - Padding around the list content. Defaults to `0`.
|
||||
*
|
||||
* @attr {boolean} wrap-focus - Whether ArrowUp/Down navigation wraps at the ends.
|
||||
* @attr {string} aria-label - Accessible label for the list.
|
||||
*
|
||||
* @fires ha-list-activated - Fired when an item is activated via Enter/Space. `detail: { index, item }`.
|
||||
*/
|
||||
@customElement("ha-list-base")
|
||||
export class HaListBase extends LitElement {
|
||||
@property({ type: Boolean, attribute: "wrap-focus" })
|
||||
public wrapFocus = false;
|
||||
|
||||
@property({ type: String, attribute: "aria-label", reflect: true })
|
||||
public ariaLabel: string | null = null;
|
||||
|
||||
public items: readonly HaListItemBase[] = [];
|
||||
|
||||
/** Host `role` attribute. Empty string means no role is set. */
|
||||
protected readonly hostRole: string = "list";
|
||||
|
||||
private _activeItemIndex = -1;
|
||||
|
||||
private _firstFocusableIndex = -1;
|
||||
|
||||
private _lastFocusableIndex = -1;
|
||||
|
||||
private _hasFocusableItem = false;
|
||||
|
||||
private _unbindKeys?: () => void;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("ha-list")) {
|
||||
this.setAttribute("ha-list", "");
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unbindKeys?.();
|
||||
this._unbindKeys = undefined;
|
||||
this.removeEventListener("focusin", this._onFocusIn);
|
||||
}
|
||||
|
||||
public firstUpdated(changed: PropertyValues) {
|
||||
super.firstUpdated(changed);
|
||||
this.updateListItems();
|
||||
}
|
||||
|
||||
public focus(options?: FocusOptions) {
|
||||
if (!this.items.length) {
|
||||
super.focus(options);
|
||||
return;
|
||||
}
|
||||
this.focusItemAtIndex(
|
||||
this._activeItemIndex >= 0 ? this._activeItemIndex : 0
|
||||
);
|
||||
}
|
||||
|
||||
public focusItemAtIndex(index: number) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
this.setActiveItemIndex(index, true);
|
||||
}
|
||||
|
||||
public getActiveItemIndex(): number {
|
||||
return this._activeItemIndex;
|
||||
}
|
||||
|
||||
public setActiveItemIndex(index: number, focusItem = false) {
|
||||
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._applyActive(focusItem);
|
||||
}
|
||||
|
||||
public updateListItems() {
|
||||
const next = this._discoverListItems();
|
||||
const changed =
|
||||
next.length !== this.items.length ||
|
||||
next.some((it, i) => it !== this.items[i]);
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
this.items = next;
|
||||
this._recomputeFocusableIndexes();
|
||||
if (
|
||||
this._activeItemIndex >= next.length ||
|
||||
!this._hasFocusableItem ||
|
||||
this._activeItemIndex < 0
|
||||
) {
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
}
|
||||
this._applyActive(false);
|
||||
}
|
||||
|
||||
private _recomputeFocusableIndexes() {
|
||||
let first = -1;
|
||||
let last = -1;
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (this._isFocusable(i)) {
|
||||
if (first === -1) {
|
||||
first = i;
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
this._firstFocusableIndex = first;
|
||||
this._lastFocusableIndex = last;
|
||||
this._hasFocusableItem = first !== -1;
|
||||
}
|
||||
|
||||
public handleSlotChange = () => {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<div part="base" class="base">
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _discoverListItems(): HaListItemBase[] {
|
||||
const slot =
|
||||
this.renderRoot?.querySelector<HTMLSlotElement>("slot:not([name])");
|
||||
if (!slot) {
|
||||
return [];
|
||||
}
|
||||
return slot
|
||||
.assignedElements({ flatten: true })
|
||||
.filter((el): el is HaListItemBase => el instanceof HaListItemBase);
|
||||
}
|
||||
|
||||
private _isFocusable(index: number): boolean {
|
||||
const item = this.items[index];
|
||||
return !!item && item.interactive && !item.disabled;
|
||||
}
|
||||
|
||||
private _applyActive(focusItem: boolean) {
|
||||
this.items.forEach((item, i) => {
|
||||
if (!item.interactive || item.disabled) {
|
||||
item.removeAttribute("tabindex");
|
||||
return;
|
||||
}
|
||||
item.tabIndex = i === this._activeItemIndex ? 0 : -1;
|
||||
});
|
||||
if (focusItem && this._activeItemIndex >= 0) {
|
||||
this.items[this._activeItemIndex]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _onForward = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, 1));
|
||||
};
|
||||
|
||||
private _onBack = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, -1));
|
||||
};
|
||||
|
||||
private _onHome = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._firstFocusableIndex);
|
||||
};
|
||||
|
||||
private _onEnd = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._lastFocusableIndex);
|
||||
};
|
||||
|
||||
private _onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const active = this.items[this._activeItemIndex];
|
||||
active.activate();
|
||||
fireEvent(this, "ha-list-activated", {
|
||||
index: this._activeItemIndex,
|
||||
item: active,
|
||||
});
|
||||
};
|
||||
|
||||
private _moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this._hasFocusableItem || next < 0 || next === this._activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._activeItemIndex = next;
|
||||
this._applyActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step from `from` by `delta`, skipping non-interactive and disabled items.
|
||||
* Returns `from` when no other focusable item can be reached (honouring
|
||||
* `wrapFocus`).
|
||||
*/
|
||||
private _stepIndex(from: number, delta: 1 | -1): number {
|
||||
const n = this.items.length;
|
||||
if (!n || !this._hasFocusableItem) {
|
||||
return from;
|
||||
}
|
||||
let i = from;
|
||||
for (let step = 0; step < n; step++) {
|
||||
i += delta;
|
||||
if (i < 0 || i >= n) {
|
||||
if (!this.wrapFocus) {
|
||||
return from;
|
||||
}
|
||||
i = (i + n) % n;
|
||||
}
|
||||
if (this._isFocusable(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
--ha-list-gap: 0;
|
||||
--ha-list-padding: 0;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap);
|
||||
padding: var(--ha-list-padding);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-base": HaListBase;
|
||||
}
|
||||
}
|
||||
44
src/components/list/ha-list-nav.ts
Normal file
44
src/components/list/ha-list-nav.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
|
||||
/**
|
||||
* @element ha-list-nav
|
||||
* @extends {HaListBase}
|
||||
*
|
||||
* @summary
|
||||
* Navigation list. Wraps the list in a `<nav>` landmark. Items should be
|
||||
* `<ha-list-item-button>` with an `href`. Use `aria-label` to name the landmark.
|
||||
*
|
||||
* @csspart nav - The `<nav>` wrapper.
|
||||
* @csspart base - The inner `<div role="list">`.
|
||||
*/
|
||||
@customElement("ha-list-nav")
|
||||
export class HaListNav extends HaListBase {
|
||||
// No host role — the inner <nav> carries the landmark semantics, and the
|
||||
// inner <div role="list"> preserves the list semantics for screen readers.
|
||||
protected override readonly hostRole = "";
|
||||
|
||||
// The label is forwarded to the inner <nav>
|
||||
@property({ type: String, attribute: "aria-label", reflect: false })
|
||||
public override ariaLabel: string | null = null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<nav
|
||||
part="nav"
|
||||
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
|
||||
>
|
||||
<div part="base" class="base" role="list">
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>
|
||||
</nav>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-nav": HaListNav;
|
||||
}
|
||||
}
|
||||
212
src/components/list/ha-list-selectable.ts
Normal file
212
src/components/list/ha-list-selectable.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
import type { HaListSelectedDetail } from "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-selectable
|
||||
* @extends {HaListBase}
|
||||
*
|
||||
* @summary
|
||||
* Selection list (role `listbox`). Items must be `<ha-list-item-option>`.
|
||||
* Toggle single vs multi selection via the `multi` attribute.
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once.
|
||||
*
|
||||
* @fires ha-list-selected - Fired when the selection changes. `detail: HaListSelectedDetail`.
|
||||
*/
|
||||
@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();
|
||||
}
|
||||
|
||||
private _sortedSelectedIndices(): number[] {
|
||||
return [...this._selectedIndices!].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private _syncItemSelectedState() {
|
||||
if (!this._selectedIndices) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-selectable": HaListSelectable;
|
||||
}
|
||||
}
|
||||
19
src/components/list/types.ts
Normal file
19
src/components/list/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ha-list-selected": HaListSelectedDetail;
|
||||
"ha-list-activated": HaListActivatedDetail;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-svg-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-nav";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-navigation-list")
|
||||
class HaNavigationList extends LitElement {
|
||||
@customElement("ha-config-navigation-list")
|
||||
class HaConfigNavigationList extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -24,16 +23,11 @@ class HaNavigationList extends LitElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-md-list
|
||||
innerRole="menu"
|
||||
itemRoles="menuitem"
|
||||
innerAriaLabel=${ifDefined(this.label)}
|
||||
>
|
||||
<ha-list-nav .ariaLabel=${this.label}>
|
||||
${this.pages.map((page) => {
|
||||
const externalApp = page.path.endsWith("#external-app-configuration");
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
.type=${externalApp ? "button" : "link"}
|
||||
<ha-list-item-button
|
||||
.href=${externalApp ? undefined : page.path}
|
||||
@click=${externalApp ? this._handleExternalApp : undefined}
|
||||
>
|
||||
@@ -55,10 +49,10 @@ class HaNavigationList extends LitElement {
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</ha-list-nav>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -83,14 +77,11 @@ class HaNavigationList extends LitElement {
|
||||
.icon-background ha-svg-icon {
|
||||
color: #fff;
|
||||
}
|
||||
ha-md-list-item {
|
||||
font-size: var(--navigation-list-item-title-font-size);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-navigation-list": HaNavigationList;
|
||||
"ha-config-navigation-list": HaConfigNavigationList;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiPower } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
@@ -8,7 +8,6 @@ import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import type { BackupContent } from "../../../data/backup";
|
||||
import { fetchBackupInfo } from "../../../data/backup";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
@@ -29,6 +28,7 @@ import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@@ -146,7 +146,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
full-width
|
||||
>
|
||||
<ha-card outlined>
|
||||
<ha-navigation-list
|
||||
<ha-config-navigation-list
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
@@ -154,7 +154,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.dashboard.system.main"
|
||||
)}
|
||||
></ha-navigation-list>
|
||||
></ha-config-navigation-list>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
</hass-subpage>
|
||||
@@ -286,10 +286,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
margin-top: -42px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: var(--ha-font-size-l);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.dashboard-alert-title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
|
||||
@customElement("ha-config-navigation")
|
||||
class HaConfigNavigation extends LitElement {
|
||||
@@ -65,20 +65,17 @@ class HaConfigNavigation extends LitElement {
|
||||
<div class="visually-hidden" role="heading" aria-level="2">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>
|
||||
<ha-navigation-list
|
||||
<ha-config-navigation-list
|
||||
has-secondary
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
.label=${this.hass.localize("panel.config")}
|
||||
></ha-navigation-list>
|
||||
></ha-config-navigation-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: var(--ha-font-size-l);
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { getDeviceArea } from "../../../common/entity/context/get_device_context
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/progress/ha-progress-ring";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device/device_registry";
|
||||
@@ -86,7 +86,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
const updates = this.updateEntities;
|
||||
|
||||
return html`
|
||||
<ha-md-list>
|
||||
<ha-list-base
|
||||
aria-label=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
>
|
||||
${updates.map((entity) => {
|
||||
const entityEntry = this.getEntityEntry(entity.entity_id);
|
||||
const deviceEntry =
|
||||
@@ -101,13 +103,12 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
class=${ifDefined(
|
||||
entity.attributes.skipped_version ? "skipped" : undefined
|
||||
)}
|
||||
.entity_id=${entity.entity_id}
|
||||
.hasMeta=${!this.narrow}
|
||||
type="button"
|
||||
@click=${this._openMoreInfo}
|
||||
>
|
||||
<div slot="start">
|
||||
@@ -149,10 +150,10 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
${this._renderUpdateProgress(entity)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -168,10 +169,10 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
.skipped {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
ha-md-list-item {
|
||||
ha-list-item-button {
|
||||
--md-list-item-leading-icon-size: 40px;
|
||||
}
|
||||
ha-icon-next {
|
||||
ha-list-item-button ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
@@ -5,12 +5,12 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../../../common/entity/compute_device_name";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-help-tooltip";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../../../data/device/device_registry";
|
||||
import type {
|
||||
@@ -91,107 +91,103 @@ class DialogZWaveJSNodeStatistics extends LitElement {
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-list noninteractive>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_tx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_tx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta">${this._nodeStatistics?.commands_tx}</span>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_tx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_rx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_rx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta">${this._nodeStatistics?.commands_rx}</span>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_rx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_tx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_tx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta"
|
||||
>${this._nodeStatistics?.commands_dropped_tx}</span
|
||||
>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_dropped_tx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_rx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_rx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta"
|
||||
>${this._nodeStatistics?.commands_dropped_rx}</span
|
||||
>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_dropped_rx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.timeout_response.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.timeout_response.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta">${this._nodeStatistics?.timeout_response}</span>
|
||||
</ha-list-item>
|
||||
<span slot="end">${this._nodeStatistics?.timeout_response}</span>
|
||||
</ha-list-item-base>
|
||||
${this._nodeStatistics?.rtt
|
||||
? html`<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
? html`<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rtt.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rtt.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta">${this._nodeStatistics.rtt}</span>
|
||||
</ha-list-item>`
|
||||
<span slot="end">${this._nodeStatistics.rtt}</span>
|
||||
</ha-list-item-base>`
|
||||
: ``}
|
||||
${this._nodeStatistics?.rssi_translated
|
||||
? html`<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
? html`<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rssi.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rssi.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="meta">${this._nodeStatistics.rssi_translated}</span>
|
||||
</ha-list-item>`
|
||||
<span slot="end">${this._nodeStatistics.rssi_translated}</span>
|
||||
</ha-list-item-base>`
|
||||
: ``}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
${Object.entries(this._workingRoutes).map(([wrKey, wrValue]) =>
|
||||
wrValue
|
||||
? html`
|
||||
@@ -450,10 +446,6 @@ class DialogZWaveJSNodeStatistics extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-list-item {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { StatisticsValidationResult } from "../../../data/recorder";
|
||||
import {
|
||||
@@ -19,9 +19,9 @@ import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-c
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { fixStatisticsIssue } from "../developer-tools/statistics/fix-statistics";
|
||||
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
|
||||
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
|
||||
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
|
||||
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
|
||||
|
||||
@customElement("ha-config-repairs")
|
||||
class HaConfigRepairs extends LitElement {
|
||||
@@ -40,7 +40,9 @@ class HaConfigRepairs extends LitElement {
|
||||
const issues = this.repairsIssues;
|
||||
|
||||
return html`
|
||||
<ha-md-list>
|
||||
<ha-list-base
|
||||
aria-label=${this.hass.localize("ui.panel.config.repairs.caption")}
|
||||
>
|
||||
${issues.map((issue) => {
|
||||
const domainName = domainToName(this.hass.localize, issue.domain);
|
||||
|
||||
@@ -55,12 +57,11 @@ class HaConfigRepairs extends LitElement {
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
.hasMeta=${!this.narrow}
|
||||
.issue=${issue}
|
||||
class=${issue.ignored ? "ignored" : ""}
|
||||
@click=${this._openShowMoreDialog}
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
@@ -92,12 +93,12 @@ class HaConfigRepairs extends LitElement {
|
||||
`ui.panel.config.repairs.${issue.severity}`
|
||||
)}</span
|
||||
>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${(issue.severity === "critical" ||
|
||||
issue.severity === "error") &&
|
||||
issue.created
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: ""}
|
||||
: nothing}
|
||||
${createdBy
|
||||
? html`<span .title=${createdBy}>${createdBy}</span>`
|
||||
: nothing}
|
||||
@@ -106,15 +107,15 @@ class HaConfigRepairs extends LitElement {
|
||||
"ui.panel.config.repairs.dialog.ignored_in_version_short",
|
||||
{ version: issue.dismissed_version }
|
||||
)}`
|
||||
: ""}
|
||||
: nothing}
|
||||
</span>
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -202,11 +203,14 @@ class HaConfigRepairs extends LitElement {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
ha-md-list-item img[slot="start"] {
|
||||
ha-list-item-button img[slot="start"] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
ha-md-list-item span[slot="supporting-text"] {
|
||||
ha-list-item-button ha-icon-next[slot="end"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-list-item-button span[slot="supporting-text"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.error {
|
||||
|
||||
@@ -2,12 +2,14 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/item/ha-row-item";
|
||||
import { isExternal } from "../../data/external";
|
||||
import type { CoreFrontendUserData } from "../../data/frontend";
|
||||
import { subscribeFrontendUserData } from "../../data/frontend";
|
||||
@@ -33,7 +35,6 @@ import "./ha-pick-time-zone-row";
|
||||
import "./ha-push-notifications-row";
|
||||
import "./ha-set-suspend-row";
|
||||
import "./ha-set-vibrate-row";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("ha-profile-section-general")
|
||||
class HaProfileSectionGeneral extends LitElement {
|
||||
@@ -145,29 +146,29 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}</span
|
||||
>
|
||||
<ha-button
|
||||
slot="end"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._customizeSidebar}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}</span
|
||||
>
|
||||
<ha-button
|
||||
slot="end"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._customizeSidebar}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
${this.hass.user!.is_admin
|
||||
? html`
|
||||
<ha-advanced-mode-row
|
||||
|
||||
@@ -7,7 +7,7 @@ import { css } from "lit";
|
||||
*/
|
||||
export const semanticColorStyles = css`
|
||||
html {
|
||||
--ha-color-focus: var(--ha-color-orange-60);
|
||||
--ha-color-focus: var(--ha-color-neutral-60);
|
||||
|
||||
/* text */
|
||||
--ha-color-text-primary: var(--ha-color-neutral-05);
|
||||
|
||||
@@ -62,7 +62,7 @@ export const waColorStyles = css`
|
||||
--wa-panel-border-width: 1px;
|
||||
--wa-color-surface-border: var(--ha-color-border-neutral-quiet);
|
||||
|
||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||
--wa-focus-ring-color: var(--ha-color-focus);
|
||||
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||
|
||||
--wa-form-control-background-color: var(--wa-color-surface-raised);
|
||||
|
||||
132
yarn.lock
132
yarn.lock
@@ -1342,14 +1342,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/search@npm:6.6.0":
|
||||
version: 6.6.0
|
||||
resolution: "@codemirror/search@npm:6.6.0"
|
||||
"@codemirror/search@npm:6.7.0":
|
||||
version: 6.7.0
|
||||
resolution: "@codemirror/search@npm:6.7.0"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.0.0"
|
||||
"@codemirror/view": "npm:^6.37.0"
|
||||
crelt: "npm:^1.0.5"
|
||||
checksum: 10/2947341cf06bde4a682250c245007f70854b7660f7aa29cec2c5cd177fa70deacaadf37c4aa323f669379d32f9912cb958448c93da3dc90b8af5c466ea1cddaf
|
||||
checksum: 10/29b7d8adeb5317ea45bf3c03c6c5ab18210efe800ebc89dd11b4a77827966a8ef85dda082517fe278e470b5ead21e6b29e590d3060775a41d3179bbd74711279
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4762,12 +4762,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/coverage-v8@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/coverage-v8@npm:4.1.4"
|
||||
"@vitest/coverage-v8@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/coverage-v8@npm:4.1.5"
|
||||
dependencies:
|
||||
"@bcoe/v8-coverage": "npm:^1.0.2"
|
||||
"@vitest/utils": "npm:4.1.4"
|
||||
"@vitest/utils": "npm:4.1.5"
|
||||
ast-v8-to-istanbul: "npm:^1.0.0"
|
||||
istanbul-lib-coverage: "npm:^3.2.2"
|
||||
istanbul-lib-report: "npm:^3.0.1"
|
||||
@@ -4777,34 +4777,34 @@ __metadata:
|
||||
std-env: "npm:^4.0.0-rc.1"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
peerDependencies:
|
||||
"@vitest/browser": 4.1.4
|
||||
vitest: 4.1.4
|
||||
"@vitest/browser": 4.1.5
|
||||
vitest: 4.1.5
|
||||
peerDependenciesMeta:
|
||||
"@vitest/browser":
|
||||
optional: true
|
||||
checksum: 10/75c7bfa08d4a410dce09688a7bb1c06b782f90b785a51aea424806621dc90ce21663cf2e8f6f28e3c3c1be708932c5274408cd7cfda51e39dd3e0ae523cca133
|
||||
checksum: 10/378e1d85a1c4670af15a18b544995a43d320460b418c188d7000f96518859e4537e00ea5e38a563c42b6183437252f0ecc92b471ede30c6d43ae87b7c8e09ed3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/expect@npm:4.1.4"
|
||||
"@vitest/expect@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/expect@npm:4.1.5"
|
||||
dependencies:
|
||||
"@standard-schema/spec": "npm:^1.1.0"
|
||||
"@types/chai": "npm:^5.2.2"
|
||||
"@vitest/spy": "npm:4.1.4"
|
||||
"@vitest/utils": "npm:4.1.4"
|
||||
"@vitest/spy": "npm:4.1.5"
|
||||
"@vitest/utils": "npm:4.1.5"
|
||||
chai: "npm:^6.2.2"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/3317bc42e4ee39cfa2102a9f08f0c7975817a74d9503a14e0b1715e5b8c4ab31c5646c07ef8d2d3f71bdf6f1b3053949b175df9c8457e0c0bb3f38b9e031f259
|
||||
checksum: 10/3e94d2d0cf4f7018ed6a7a9394bff971353ea0cc85bcbcff39212279156840b8c533be99e2fd52112e4904c4a5190bdaaf441db7c6b17e356c18577072a3f057
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/mocker@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/mocker@npm:4.1.4"
|
||||
"@vitest/mocker@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/mocker@npm:4.1.5"
|
||||
dependencies:
|
||||
"@vitest/spy": "npm:4.1.4"
|
||||
"@vitest/spy": "npm:4.1.5"
|
||||
estree-walker: "npm:^3.0.3"
|
||||
magic-string: "npm:^0.30.21"
|
||||
peerDependencies:
|
||||
@@ -4815,56 +4815,56 @@ __metadata:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 10/f07f8877635eb03f63981d0d3348bb82fabe7607bbb6b259045bf0b64fae79150b1f399aa7ce42926e4769dc8cde9b7d79d1f665eae2d17b22ecc9ec54663698
|
||||
checksum: 10/949784ba08996543a313459a36a730d4b0847e42ee56cfda07a3e2add67c7adf8acbd59dcf9f75b1e4bc3fe7cc487f9f260905ff9a334866d389478112e5ae82
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/pretty-format@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/pretty-format@npm:4.1.4"
|
||||
"@vitest/pretty-format@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/pretty-format@npm:4.1.5"
|
||||
dependencies:
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/e06d63ce4f797ad578ee19aeec996f72835a7274ee2eb75dce12d7b45debcda72d054f58b6f4e5dac4424681dc13dbad7ac023c6017fc60406cabea5a352e4c3
|
||||
checksum: 10/783f8c4a0e419d1024446ae8593411c95443ea09b50c4a378986b48893998acda34429b2d1deebc065405a7ef40bb19e19c68fdeb93acd46ae98b156c42d5f39
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/runner@npm:4.1.4"
|
||||
"@vitest/runner@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/runner@npm:4.1.5"
|
||||
dependencies:
|
||||
"@vitest/utils": "npm:4.1.4"
|
||||
"@vitest/utils": "npm:4.1.5"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10/a852477adc6254e1d304bcba9b137f98f09a7001a557e8e4f4404518e3ade58a16ab459e83cf223e38cc37dc4b04d1248a14df56b056a0ae68fc54b19a1226fb
|
||||
checksum: 10/ba19d84a9f7bcc3102ae5304c23e5dae789aaf8fd283f826e3fd4aca87ea2687ed606cf89869773d15799666553fd265524f7d9a0869e2869e00ebd8fd53af5b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/snapshot@npm:4.1.4"
|
||||
"@vitest/snapshot@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/snapshot@npm:4.1.5"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:4.1.4"
|
||||
"@vitest/utils": "npm:4.1.4"
|
||||
"@vitest/pretty-format": "npm:4.1.5"
|
||||
"@vitest/utils": "npm:4.1.5"
|
||||
magic-string: "npm:^0.30.21"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10/e957cc95274a9663cd59e5b34c99b6e4e5cd989f04dadf9e3cec6c7bc64b4d167229644f31fd44c19c7acbbcb7cbbbb50e8084dbf1e0322ee411a697d80d490a
|
||||
checksum: 10/cf70530d8a7320c012bdf7f6ca4f3ddbbb47c9aeb9ff5d28319e552ce64db93423d0c4facff3e112c6d711ed4228369c8fa73c88350fe6c16cf04f9ac2558caf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/spy@npm:4.1.4"
|
||||
checksum: 10/516e465413fc6a22e0c7e99871f3b9703277c309e94e7247bbdb83a8e807e2da968cf7a30c61503afd6b565787e822786b8aad443210eba5488192a36730f3ab
|
||||
"@vitest/spy@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/spy@npm:4.1.5"
|
||||
checksum: 10/4db4bb3aea01cd737fdb06d8f498bcd2127b8c2afeaa78ff9df4147e1474aa26dd16f42dc0512c31385824e94dbb17b17fa0f4c60b7595b7b4ab946f098220ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "@vitest/utils@npm:4.1.4"
|
||||
"@vitest/utils@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "@vitest/utils@npm:4.1.5"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:4.1.4"
|
||||
"@vitest/pretty-format": "npm:4.1.5"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/f599ae744f0ff45edda90d0c52eea9809b7367adca39fc985f85880322236d089dfdf6625f04913f03a25a160eccbbc0b16dd3201ccc0ae48087992b1ea755d5
|
||||
checksum: 10/4f75a2df6f910578a361ae92eb92a2b6921f50cc748994f3b2e5900d0ae687b6683f33b090dedf9b96eaca23bac117817d9448a4a333c7a96b94ee767399f18c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8267,7 +8267,7 @@ __metadata:
|
||||
"@codemirror/lang-jinja": "npm:6.0.1"
|
||||
"@codemirror/lang-yaml": "npm:6.1.3"
|
||||
"@codemirror/language": "npm:6.12.3"
|
||||
"@codemirror/search": "npm:6.6.0"
|
||||
"@codemirror/search": "npm:6.7.0"
|
||||
"@codemirror/state": "npm:6.6.0"
|
||||
"@codemirror/view": "npm:6.41.1"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
@@ -8339,7 +8339,7 @@ __metadata:
|
||||
"@types/tar": "npm:7.0.87"
|
||||
"@types/webspeechapi": "npm:0.0.29"
|
||||
"@vibrant/color": "npm:4.0.4"
|
||||
"@vitest/coverage-v8": "npm:4.1.4"
|
||||
"@vitest/coverage-v8": "npm:4.1.5"
|
||||
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
|
||||
"@webcomponents/webcomponentsjs": "npm:2.8.0"
|
||||
babel-loader: "npm:10.1.1"
|
||||
@@ -8424,7 +8424,7 @@ __metadata:
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.59.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.4"
|
||||
vitest: "npm:4.1.5"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
webpackbar: "npm:7.0.0"
|
||||
weekstart: "npm:2.0.0"
|
||||
@@ -13299,17 +13299,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "vitest@npm:4.1.4"
|
||||
"vitest@npm:4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "vitest@npm:4.1.5"
|
||||
dependencies:
|
||||
"@vitest/expect": "npm:4.1.4"
|
||||
"@vitest/mocker": "npm:4.1.4"
|
||||
"@vitest/pretty-format": "npm:4.1.4"
|
||||
"@vitest/runner": "npm:4.1.4"
|
||||
"@vitest/snapshot": "npm:4.1.4"
|
||||
"@vitest/spy": "npm:4.1.4"
|
||||
"@vitest/utils": "npm:4.1.4"
|
||||
"@vitest/expect": "npm:4.1.5"
|
||||
"@vitest/mocker": "npm:4.1.5"
|
||||
"@vitest/pretty-format": "npm:4.1.5"
|
||||
"@vitest/runner": "npm:4.1.5"
|
||||
"@vitest/snapshot": "npm:4.1.5"
|
||||
"@vitest/spy": "npm:4.1.5"
|
||||
"@vitest/utils": "npm:4.1.5"
|
||||
es-module-lexer: "npm:^2.0.0"
|
||||
expect-type: "npm:^1.3.0"
|
||||
magic-string: "npm:^0.30.21"
|
||||
@@ -13327,12 +13327,12 @@ __metadata:
|
||||
"@edge-runtime/vm": "*"
|
||||
"@opentelemetry/api": ^1.9.0
|
||||
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
"@vitest/browser-playwright": 4.1.4
|
||||
"@vitest/browser-preview": 4.1.4
|
||||
"@vitest/browser-webdriverio": 4.1.4
|
||||
"@vitest/coverage-istanbul": 4.1.4
|
||||
"@vitest/coverage-v8": 4.1.4
|
||||
"@vitest/ui": 4.1.4
|
||||
"@vitest/browser-playwright": 4.1.5
|
||||
"@vitest/browser-preview": 4.1.5
|
||||
"@vitest/browser-webdriverio": 4.1.5
|
||||
"@vitest/coverage-istanbul": 4.1.5
|
||||
"@vitest/coverage-v8": 4.1.5
|
||||
"@vitest/ui": 4.1.5
|
||||
happy-dom: "*"
|
||||
jsdom: "*"
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -13363,7 +13363,7 @@ __metadata:
|
||||
optional: false
|
||||
bin:
|
||||
vitest: vitest.mjs
|
||||
checksum: 10/c5608c506ae9ab3d0baa7445290c240941ad54a93eb853a005b2fe518efb1b28282945e0565ca16a624cca5b23af0c33ee34fbc2c38e6664ea54b08b9a22a653
|
||||
checksum: 10/8b768514993d8908fc9b5f2d619943d23b81aaba9443132583bd58aeb441bf76d152961326de9ca328ff0efcddbf8a58f4568a7b66a4391202542ed772613d81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user