Archived
Extract selected item scrolling directive
This commit is contained in:
@@ -2,6 +2,7 @@ import { LitElement, html, type PropertyValues } from "lit";
|
|||||||
import { customElement, property, query, state } from "lit/decorators.js";
|
import { customElement, property, query, state } from "lit/decorators.js";
|
||||||
import type { AppAction } from "../actions";
|
import type { AppAction } from "../actions";
|
||||||
import { formatShortcut } from "../keyboardShortcuts";
|
import { formatShortcut } from "../keyboardShortcuts";
|
||||||
|
import { scrollWhenSelected } from "./scrollWhenSelected";
|
||||||
import { actionPaletteStyles } from "./shared";
|
import { actionPaletteStyles } from "./shared";
|
||||||
|
|
||||||
@customElement("action-palette")
|
@customElement("action-palette")
|
||||||
@@ -33,7 +34,7 @@ export class ActionPalette extends LitElement {
|
|||||||
</header>
|
</header>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
${actions.length === 0 ? html`<div class="empty">No actions found.</div>` : actions.map((action, index) => html`
|
${actions.length === 0 ? html`<div class="empty">No actions found.</div>` : actions.map((action, index) => html`
|
||||||
<button class=${index === this.selectedIndex ? "selected" : ""} @click=${() => { this.run(action); }}>
|
<button class=${index === this.selectedIndex ? "selected" : ""} ${scrollWhenSelected(index === this.selectedIndex, action.id)} @click=${() => { this.run(action); }}>
|
||||||
<span class="main">
|
<span class="main">
|
||||||
<strong>${action.title}</strong>
|
<strong>${action.title}</strong>
|
||||||
${action.description !== undefined && action.description !== "" ? html`<small>${action.description}</small>` : null}
|
${action.description !== undefined && action.description !== "" ? html`<small>${action.description}</small>` : null}
|
||||||
@@ -53,16 +54,10 @@ export class ActionPalette extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(changed: PropertyValues) {
|
protected override updated(changed: PropertyValues) {
|
||||||
if (changed.has("actions") || changed.has("queryText")) {
|
if (!changed.has("actions") && !changed.has("queryText")) return;
|
||||||
const maxIndex = Math.max(0, this.filteredActions().length - 1);
|
const maxIndex = Math.max(0, this.filteredActions().length - 1);
|
||||||
if (this.selectedIndex > maxIndex) this.selectedIndex = maxIndex;
|
if (this.selectedIndex > maxIndex) this.selectedIndex = maxIndex;
|
||||||
}
|
}
|
||||||
if (changed.has("selectedIndex") || changed.has("actions") || changed.has("queryText")) this.scrollSelectedIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private scrollSelectedIntoView() {
|
|
||||||
this.renderRoot.querySelector<HTMLElement>(".options button.selected")?.scrollIntoView({ block: "nearest" });
|
|
||||||
}
|
|
||||||
|
|
||||||
private filteredActions(): AppAction[] {
|
private filteredActions(): AppAction[] {
|
||||||
const query = this.queryText.trim().toLowerCase();
|
const query = this.queryText.trim().toLowerCase();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LitElement, html, type PropertyValues } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { scrollWhenSelected } from "./scrollWhenSelected";
|
||||||
import { autocompleteStyles, type CompletionItem } from "./shared";
|
import { autocompleteStyles, type CompletionItem } from "./shared";
|
||||||
|
|
||||||
@customElement("autocomplete-menu")
|
@customElement("autocomplete-menu")
|
||||||
@@ -13,7 +14,7 @@ export class AutocompleteMenu extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
${this.items.map((item, index) => html`
|
${this.items.map((item, index) => html`
|
||||||
<button class=${index === this.selectedIndex ? "selected" : ""} @mousedown=${(event: MouseEvent) => { event.preventDefault(); this.onPick?.(item); }}>
|
<button class=${index === this.selectedIndex ? "selected" : ""} ${scrollWhenSelected(index === this.selectedIndex, item)} @mousedown=${(event: MouseEvent) => { event.preventDefault(); this.onPick?.(item); }}>
|
||||||
<strong>${item.insertText}</strong>
|
<strong>${item.insertText}</strong>
|
||||||
<span>${item.detail}</span>
|
<span>${item.detail}</span>
|
||||||
${item.description !== undefined && item.description !== "" ? html`<small>${item.description}</small>` : null}
|
${item.description !== undefined && item.description !== "" ? html`<small>${item.description}</small>` : null}
|
||||||
@@ -23,13 +24,5 @@ export class AutocompleteMenu extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(changed: PropertyValues) {
|
|
||||||
if (changed.has("selectedIndex") || changed.has("items")) this.scrollSelectedIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private scrollSelectedIntoView() {
|
|
||||||
this.renderRoot.querySelector<HTMLElement>("button.selected")?.scrollIntoView({ block: "nearest" });
|
|
||||||
}
|
|
||||||
|
|
||||||
static override styles = autocompleteStyles;
|
static override styles = autocompleteStyles;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LitElement, html, type PropertyValues } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import type { CommandOption } from "../api";
|
import type { CommandOption } from "../api";
|
||||||
|
import { scrollWhenSelected } from "./scrollWhenSelected";
|
||||||
import { commandPickerStyles } from "./shared";
|
import { commandPickerStyles } from "./shared";
|
||||||
|
|
||||||
@customElement("command-picker")
|
@customElement("command-picker")
|
||||||
@@ -21,7 +22,7 @@ export class CommandPicker extends LitElement {
|
|||||||
</header>
|
</header>
|
||||||
<div class="options" @keydown=${(event: KeyboardEvent) => { this.handleKeyDown(event); }} tabindex="0">
|
<div class="options" @keydown=${(event: KeyboardEvent) => { this.handleKeyDown(event); }} tabindex="0">
|
||||||
${this.options.map((option, index) => html`
|
${this.options.map((option, index) => html`
|
||||||
<button class=${index === this.selectedIndex ? "selected" : ""} @click=${() => this.onPick?.(option.value)}>
|
<button class=${index === this.selectedIndex ? "selected" : ""} ${scrollWhenSelected(index === this.selectedIndex, option.value)} @click=${() => this.onPick?.(option.value)}>
|
||||||
<span>${option.label}</span>
|
<span>${option.label}</span>
|
||||||
${option.description !== undefined && option.description !== "" ? html`<small>${option.description}</small>` : null}
|
${option.description !== undefined && option.description !== "" ? html`<small>${option.description}</small>` : null}
|
||||||
</button>
|
</button>
|
||||||
@@ -36,14 +37,6 @@ export class CommandPicker extends LitElement {
|
|||||||
this.renderRoot.querySelector<HTMLElement>(".options")?.focus();
|
this.renderRoot.querySelector<HTMLElement>(".options")?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(changed: PropertyValues) {
|
|
||||||
if (changed.has("selectedIndex") || changed.has("options")) this.scrollSelectedIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private scrollSelectedIntoView() {
|
|
||||||
this.renderRoot.querySelector<HTMLElement>(".options button.selected")?.scrollIntoView({ block: "nearest" });
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleKeyDown(event: KeyboardEvent) {
|
private handleKeyDown(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Directive, PartType, directive, type Part, type PartInfo } from "lit/directive.js";
|
||||||
|
|
||||||
|
class ScrollWhenSelectedDirective extends Directive {
|
||||||
|
private readonly isElementPart: boolean;
|
||||||
|
private wasSelected = false;
|
||||||
|
private previousKey: unknown;
|
||||||
|
|
||||||
|
constructor(partInfo: PartInfo) {
|
||||||
|
super(partInfo);
|
||||||
|
this.isElementPart = partInfo.type === PartType.ELEMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
override update(part: Part, [selected, key]: [boolean, unknown?]) {
|
||||||
|
if (!this.isElementPart) throw new Error("scrollWhenSelected must be used on an element");
|
||||||
|
if (selected && (!this.wasSelected || key !== this.previousKey)) {
|
||||||
|
const element = (part as Part & { element?: Element }).element;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (element instanceof HTMLElement) element.scrollIntoView({ block: "nearest" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.wasSelected = selected;
|
||||||
|
this.previousKey = key;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
override render(_selected: boolean, _key?: unknown) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scrollWhenSelected = directive(ScrollWhenSelectedDirective);
|
||||||
Reference in New Issue
Block a user