mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 19:26:36 +00:00
Enable keyboard sorting for items-display-editor (#25546)
* Enable keyboard sorting for items-display-editor * Add alt + arrow and fix bugs * Fix alt bug * Improve selected drag highlight
This commit is contained in:
parent
412a0e9f6a
commit
c11d2c10df
@ -2,7 +2,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
|
|||||||
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
|
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
@ -64,92 +64,15 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
item: DisplayItem
|
item: DisplayItem
|
||||||
) => TemplateResult<1> | typeof nothing;
|
) => TemplateResult<1> | typeof nothing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to sort items by keyboard navigation.
|
||||||
|
*/
|
||||||
|
@state() private _dragIndex: number | null = null;
|
||||||
|
|
||||||
private _showIcon = new ResizeController(this, {
|
private _showIcon = new ResizeController(this, {
|
||||||
callback: (entries) => entries[0]?.contentRect.width > 450,
|
callback: (entries) => entries[0]?.contentRect.width > 450,
|
||||||
});
|
});
|
||||||
|
|
||||||
private _toggle(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const value = ev.currentTarget.value;
|
|
||||||
|
|
||||||
const hiddenItems = this._hiddenItems(this.items, this.value.hidden);
|
|
||||||
|
|
||||||
const newHidden = hiddenItems.map((item) => item.value);
|
|
||||||
|
|
||||||
if (newHidden.includes(value)) {
|
|
||||||
newHidden.splice(newHidden.indexOf(value), 1);
|
|
||||||
} else {
|
|
||||||
newHidden.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newVisibleItems = this._visibleItems(
|
|
||||||
this.items,
|
|
||||||
newHidden,
|
|
||||||
this.value.order
|
|
||||||
);
|
|
||||||
const newOrder = newVisibleItems.map((a) => a.value);
|
|
||||||
|
|
||||||
this.value = {
|
|
||||||
hidden: newHidden,
|
|
||||||
order: newOrder,
|
|
||||||
};
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _itemMoved(ev: CustomEvent): void {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
|
||||||
|
|
||||||
const visibleItems = this._visibleItems(
|
|
||||||
this.items,
|
|
||||||
this.value.hidden,
|
|
||||||
this.value.order
|
|
||||||
);
|
|
||||||
const newOrder = visibleItems.map((item) => item.value);
|
|
||||||
|
|
||||||
const movedItem = newOrder.splice(oldIndex, 1)[0];
|
|
||||||
newOrder.splice(newIndex, 0, movedItem);
|
|
||||||
|
|
||||||
this.value = {
|
|
||||||
...this.value,
|
|
||||||
order: newOrder,
|
|
||||||
};
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _navigate(ev) {
|
|
||||||
const value = ev.currentTarget.value;
|
|
||||||
fireEvent(this, "item-display-navigate-clicked", { value });
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _visibleItems = memoizeOne(
|
|
||||||
(items: DisplayItem[], hidden: string[], order: string[]) => {
|
|
||||||
const compare = orderCompare(order);
|
|
||||||
|
|
||||||
const visibleItems = items.filter((item) => !hidden.includes(item.value));
|
|
||||||
if (this.dontSortVisible) {
|
|
||||||
return visibleItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.sort((a, b) =>
|
|
||||||
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _allItems = memoizeOne(
|
|
||||||
(items: DisplayItem[], hidden: string[], order: string[]) => {
|
|
||||||
const visibleItems = this._visibleItems(items, hidden, order);
|
|
||||||
const hiddenItems = this._hiddenItems(items, hidden);
|
|
||||||
return [...visibleItems, ...hiddenItems];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
|
|
||||||
items.filter((item) => hidden.includes(item.value))
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const allItems = this._allItems(
|
const allItems = this._allItems(
|
||||||
this.items,
|
this.items,
|
||||||
@ -168,7 +91,7 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
${repeat(
|
${repeat(
|
||||||
allItems,
|
allItems,
|
||||||
(item) => item.value,
|
(item) => item.value,
|
||||||
(item: DisplayItem, _idx) => {
|
(item: DisplayItem, idx) => {
|
||||||
const isVisible = !this.value.hidden.includes(item.value);
|
const isVisible = !this.value.hidden.includes(item.value);
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
@ -180,9 +103,7 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
} = item;
|
} = item;
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
type=${ifDefined(
|
type="button"
|
||||||
this.showNavigationButton ? "button" : undefined
|
|
||||||
)}
|
|
||||||
@click=${this.showNavigationButton
|
@click=${this.showNavigationButton
|
||||||
? this._navigate
|
? this._navigate
|
||||||
: undefined}
|
: undefined}
|
||||||
@ -190,7 +111,12 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
class=${classMap({
|
class=${classMap({
|
||||||
hidden: !isVisible,
|
hidden: !isVisible,
|
||||||
draggable: isVisible && !disableSorting,
|
draggable: isVisible && !disableSorting,
|
||||||
|
"drag-selected": this._dragIndex === idx,
|
||||||
})}
|
})}
|
||||||
|
@keydown=${isVisible && !disableSorting
|
||||||
|
? this._listElementKeydown
|
||||||
|
: undefined}
|
||||||
|
.idx=${idx}
|
||||||
>
|
>
|
||||||
<span slot="headline">${label}</span>
|
<span slot="headline">${label}</span>
|
||||||
${description
|
${description
|
||||||
@ -199,6 +125,13 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
${isVisible && !disableSorting
|
${isVisible && !disableSorting
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
|
tabindex=${ifDefined(
|
||||||
|
this.showNavigationButton ? "0" : undefined
|
||||||
|
)}
|
||||||
|
.idx=${idx}
|
||||||
|
@keydown=${this.showNavigationButton
|
||||||
|
? this._dragHandleKeydown
|
||||||
|
: undefined}
|
||||||
class="handle"
|
class="handle"
|
||||||
.path=${mdiDrag}
|
.path=${mdiDrag}
|
||||||
slot="start"
|
slot="start"
|
||||||
@ -253,6 +186,180 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _toggle(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._dragIndex = null;
|
||||||
|
const value = ev.currentTarget.value;
|
||||||
|
|
||||||
|
const hiddenItems = this._hiddenItems(this.items, this.value.hidden);
|
||||||
|
|
||||||
|
const newHidden = hiddenItems.map((item) => item.value);
|
||||||
|
|
||||||
|
if (newHidden.includes(value)) {
|
||||||
|
newHidden.splice(newHidden.indexOf(value), 1);
|
||||||
|
} else {
|
||||||
|
newHidden.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVisibleItems = this._visibleItems(
|
||||||
|
this.items,
|
||||||
|
newHidden,
|
||||||
|
this.value.order
|
||||||
|
);
|
||||||
|
const newOrder = newVisibleItems.map((a) => a.value);
|
||||||
|
|
||||||
|
this.value = {
|
||||||
|
hidden: newHidden,
|
||||||
|
order: newOrder,
|
||||||
|
};
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
|
||||||
|
this._moveItem(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _moveItem(oldIndex, newIndex) {
|
||||||
|
if (oldIndex === newIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleItems = this._visibleItems(
|
||||||
|
this.items,
|
||||||
|
this.value.hidden,
|
||||||
|
this.value.order
|
||||||
|
);
|
||||||
|
const newOrder = visibleItems.map((item) => item.value);
|
||||||
|
|
||||||
|
const movedItem = newOrder.splice(oldIndex, 1)[0];
|
||||||
|
newOrder.splice(newIndex, 0, movedItem);
|
||||||
|
|
||||||
|
this.value = {
|
||||||
|
...this.value,
|
||||||
|
order: newOrder,
|
||||||
|
};
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _navigate(ev) {
|
||||||
|
const value = ev.currentTarget.value;
|
||||||
|
fireEvent(this, "item-display-navigate-clicked", { value });
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _visibleItems = memoizeOne(
|
||||||
|
(items: DisplayItem[], hidden: string[], order: string[]) => {
|
||||||
|
const compare = orderCompare(order);
|
||||||
|
|
||||||
|
const visibleItems = items.filter((item) => !hidden.includes(item.value));
|
||||||
|
if (this.dontSortVisible) {
|
||||||
|
return [
|
||||||
|
...visibleItems.filter((item) => !item.disableSorting),
|
||||||
|
...visibleItems.filter((item) => item.disableSorting),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.sort((a, b) =>
|
||||||
|
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _allItems = memoizeOne(
|
||||||
|
(items: DisplayItem[], hidden: string[], order: string[]) => {
|
||||||
|
const visibleItems = this._visibleItems(items, hidden, order);
|
||||||
|
const hiddenItems = this._hiddenItems(items, hidden);
|
||||||
|
return [...visibleItems, ...hiddenItems];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
|
||||||
|
items.filter((item) => hidden.includes(item.value))
|
||||||
|
);
|
||||||
|
|
||||||
|
private _maxSortableIndex = memoizeOne(
|
||||||
|
(items: DisplayItem[], hidden: string[]) =>
|
||||||
|
items.filter(
|
||||||
|
(item) => !item.disableSorting && !hidden.includes(item.value)
|
||||||
|
).length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
private _keyActivatedMove = (ev: KeyboardEvent, clearDragIndex = false) => {
|
||||||
|
const oldIndex = this._dragIndex;
|
||||||
|
|
||||||
|
if (ev.key === "ArrowUp") {
|
||||||
|
this._dragIndex = Math.max(0, this._dragIndex! - 1);
|
||||||
|
} else {
|
||||||
|
this._dragIndex = Math.min(
|
||||||
|
this._maxSortableIndex(this.items, this.value.hidden),
|
||||||
|
this._dragIndex! + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._moveItem(oldIndex, this._dragIndex);
|
||||||
|
|
||||||
|
// refocus the item after the sort
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.updateComplete;
|
||||||
|
const selectedElement = this.shadowRoot?.querySelector(
|
||||||
|
`ha-md-list-item:nth-child(${this._dragIndex! + 1})`
|
||||||
|
) as HTMLElement | null;
|
||||||
|
selectedElement?.focus();
|
||||||
|
if (clearDragIndex) {
|
||||||
|
this._dragIndex = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _sortKeydown = (ev: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
this._dragIndex !== null &&
|
||||||
|
(ev.key === "ArrowUp" || ev.key === "ArrowDown")
|
||||||
|
) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._keyActivatedMove(ev);
|
||||||
|
} else if (this._dragIndex !== null && ev.key === "Escape") {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._dragIndex = null;
|
||||||
|
this.removeEventListener("keydown", this._sortKeydown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private _listElementKeydown = (ev: KeyboardEvent) => {
|
||||||
|
if (ev.altKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._dragIndex = (ev.target as any).idx;
|
||||||
|
this._keyActivatedMove(ev, true);
|
||||||
|
} else if (
|
||||||
|
(!this.showNavigationButton && ev.key === "Enter") ||
|
||||||
|
ev.key === " "
|
||||||
|
) {
|
||||||
|
this._dragHandleKeydown(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private _dragHandleKeydown(ev: KeyboardEvent): void {
|
||||||
|
if (ev.key === "Enter" || ev.key === " ") {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (this._dragIndex === null) {
|
||||||
|
this._dragIndex = (ev.target as any).idx;
|
||||||
|
this.addEventListener("keydown", this._sortKeydown);
|
||||||
|
} else {
|
||||||
|
this.removeEventListener("keydown", this._sortKeydown);
|
||||||
|
this._dragIndex = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.removeEventListener("keydown", this._sortKeydown);
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
@ -273,6 +380,12 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
--md-list-item-two-line-container-height: 48px;
|
--md-list-item-two-line-container-height: 48px;
|
||||||
--md-list-item-one-line-container-height: 48px;
|
--md-list-item-one-line-container-height: 48px;
|
||||||
}
|
}
|
||||||
|
ha-md-list-item.drag-selected {
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 8px 4px rgba(var(--rgb-accent-color), 0.8),
|
||||||
|
inset 0px 2px 8px 4px rgba(var(--rgb-accent-color), 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
ha-md-list-item ha-icon-button {
|
ha-md-list-item ha-icon-button {
|
||||||
margin-left: -12px;
|
margin-left: -12px;
|
||||||
margin-right: -12px;
|
margin-right: -12px;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user