mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
feat: improvements to how name input keyboard keys are handled and don't
focus suggestions by default
This commit is contained in:
parent
490e9102b8
commit
52b3c31dbd
3 changed files with 117 additions and 16 deletions
|
|
@ -94,8 +94,8 @@ describe("TimesheetNameInput", () => {
|
|||
expect(onDebouncedChange).toHaveBeenCalledOnce();
|
||||
expect(getFilteredSuggestions).toHaveBeenCalledOnce();
|
||||
|
||||
// Selection focus should reset to the first suggestion
|
||||
expect(setSuggestionFocus).toHaveBeenLastCalledWith(0);
|
||||
// Selection focus should reset to nothing
|
||||
expect(setSuggestionFocus).toHaveBeenLastCalledWith(-1);
|
||||
|
||||
// Suggestions should contain both items
|
||||
const suggestions = containerEl.querySelectorAll(".timekeep-suggestion");
|
||||
|
|
@ -122,8 +122,8 @@ describe("TimesheetNameInput", () => {
|
|||
expect(onDebouncedChange).toHaveBeenCalledOnce();
|
||||
expect(getFilteredSuggestions).toHaveBeenCalledOnce();
|
||||
|
||||
// Selection focus should reset to the first suggestion
|
||||
expect(setSuggestionFocus).toHaveBeenLastCalledWith(0);
|
||||
// Selection focus should reset to nothing
|
||||
expect(setSuggestionFocus).toHaveBeenLastCalledWith(-1);
|
||||
|
||||
// Suggestions should contain just the matching item
|
||||
const suggestions = containerEl.querySelectorAll(".timekeep-suggestion");
|
||||
|
|
@ -282,14 +282,52 @@ describe("TimesheetNameInput", () => {
|
|||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
|
||||
);
|
||||
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(2);
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(3);
|
||||
expect(onSelectSuggestion).toHaveBeenLastCalledWith("Test 1");
|
||||
});
|
||||
|
||||
it("down arrow should open the suggestions when they are closed", () => {
|
||||
autocomplete.names.setState(["Test", "Test 1"]);
|
||||
|
||||
const onKeyDown = vi.spyOn(component, "onKeyDown");
|
||||
const onSelectSuggestion = vi.spyOn(component, "onSelectSuggestion");
|
||||
const setSuggestionsOpen = vi.spyOn(component, "setSuggestionsOpen");
|
||||
|
||||
component.load();
|
||||
|
||||
const inputEl = containerEl.querySelector(".timekeep-name")! as HTMLInputElement;
|
||||
expect(inputEl).not.toBeNull();
|
||||
|
||||
inputEl.value = "Test";
|
||||
inputEl.focus();
|
||||
inputEl.dispatchEvent(new Event("focus", { bubbles: true }));
|
||||
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Escape" })
|
||||
);
|
||||
|
||||
//
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
|
||||
expect(setSuggestionsOpen).toHaveBeenLastCalledWith(true);
|
||||
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
|
||||
);
|
||||
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(3);
|
||||
expect(onSelectSuggestion).toHaveBeenLastCalledWith("Test");
|
||||
});
|
||||
|
||||
it("up arrow should be able to move the focused suggestion up", () => {
|
||||
autocomplete.names.setState(["Test", "Test 1"]);
|
||||
|
||||
|
|
@ -307,6 +345,9 @@ describe("TimesheetNameInput", () => {
|
|||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowUp" })
|
||||
);
|
||||
|
|
@ -314,7 +355,7 @@ describe("TimesheetNameInput", () => {
|
|||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
|
||||
);
|
||||
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(3);
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(4);
|
||||
expect(onSelectSuggestion).toHaveBeenLastCalledWith("Test");
|
||||
});
|
||||
|
||||
|
|
@ -332,14 +373,74 @@ describe("TimesheetNameInput", () => {
|
|||
inputEl.value = "Test";
|
||||
inputEl.focus();
|
||||
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
|
||||
);
|
||||
|
||||
expect(onKeyDown).toHaveBeenCalledOnce();
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(2);
|
||||
expect(onSelectSuggestion).toHaveBeenLastCalledWith("Test");
|
||||
});
|
||||
|
||||
it("enter should do nothing without a focused suggestion", () => {
|
||||
autocomplete.names.setState(["Test", "Other"]);
|
||||
|
||||
const onKeyDown = vi.spyOn(component, "onKeyDown");
|
||||
const onSelectSuggestion = vi.spyOn(component, "onSelectSuggestion");
|
||||
|
||||
component.load();
|
||||
|
||||
const inputEl = containerEl.querySelector(".timekeep-name")! as HTMLInputElement;
|
||||
expect(inputEl).not.toBeNull();
|
||||
|
||||
inputEl.value = "Test";
|
||||
inputEl.focus();
|
||||
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
|
||||
);
|
||||
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(1);
|
||||
expect(onSelectSuggestion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("enter should do nothing when the suggestions box is closed", () => {
|
||||
autocomplete.names.setState(["Test", "Test 1", "Other"]);
|
||||
|
||||
const onKeyDown = vi.spyOn(component, "onKeyDown");
|
||||
const onSelectSuggestion = vi.spyOn(component, "onSelectSuggestion");
|
||||
const setSuggestionsOpen = vi.spyOn(component, "setSuggestionsOpen");
|
||||
const onClickOutside = vi.spyOn(component, "onClickOutside");
|
||||
|
||||
component.load();
|
||||
|
||||
const inputEl = containerEl.querySelector(".timekeep-name")! as HTMLInputElement;
|
||||
expect(inputEl).not.toBeNull();
|
||||
|
||||
inputEl.value = "Test";
|
||||
inputEl.focus();
|
||||
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowDown" })
|
||||
);
|
||||
|
||||
document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
expect(onClickOutside).toHaveBeenCalledOnce();
|
||||
|
||||
expect(setSuggestionsOpen).toHaveBeenLastCalledWith(false);
|
||||
|
||||
inputEl.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
|
||||
);
|
||||
|
||||
expect(onKeyDown).toHaveBeenCalledTimes(2);
|
||||
expect(onSelectSuggestion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("enter should do nothing without any suggestions", () => {
|
||||
autocomplete.names.setState(["Test", "Other"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -158,12 +158,8 @@ export class TimesheetNameInput extends DomComponent {
|
|||
const suggestions = this.getFilteredSuggestions();
|
||||
this.#suggestions = suggestions;
|
||||
|
||||
this.updateSuggestionsOpen();
|
||||
this.setSuggestionsOpen(true);
|
||||
this.renderSuggestions();
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
this.setSuggestionFocus(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,14 +229,17 @@ export class TimesheetNameInput extends DomComponent {
|
|||
}
|
||||
|
||||
case "Enter": {
|
||||
event.preventDefault();
|
||||
|
||||
// Clamp focus within suggestion bounds before making decisions
|
||||
this.clampSuggestionFocus();
|
||||
|
||||
// Don't act if the user hasn't selected an item
|
||||
const focusIndex = this.#suggestionFocusIndex;
|
||||
if (focusIndex < 0) return;
|
||||
|
||||
// Don't act if the suggestions aren't open
|
||||
if (!this.#suggestionsOpen) return;
|
||||
|
||||
event.preventDefault();
|
||||
const suggestion = this.#suggestions[focusIndex];
|
||||
assert(suggestion, "Suggestion should always be within defined bounds");
|
||||
|
||||
|
|
@ -250,6 +249,7 @@ export class TimesheetNameInput extends DomComponent {
|
|||
|
||||
case "Escape": {
|
||||
this.setSuggestionsOpen(false);
|
||||
this.setSuggestionFocus(-1);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ export class TimesheetNameInput extends DomComponent {
|
|||
if (this.#suggestions.length > 0) {
|
||||
const focusIndex = this.#suggestionFocusIndex;
|
||||
const newFocusIndex =
|
||||
focusIndex === -1 ? 0 : Math.min(focusIndex, this.#suggestions.length - 1);
|
||||
focusIndex === -1 ? -1 : Math.min(focusIndex, this.#suggestions.length - 1);
|
||||
|
||||
this.setSuggestionFocus(newFocusIndex);
|
||||
} else {
|
||||
|
|
@ -305,6 +305,7 @@ export class TimesheetNameInput extends DomComponent {
|
|||
// Update the input aria-activedescendant for screen readers
|
||||
if (index === -1) {
|
||||
inputEl.removeAttribute("aria-activedescendant");
|
||||
suggestionsEl.scrollTo({ top: 0 });
|
||||
} else {
|
||||
inputEl.setAttribute(
|
||||
"aria-activedescendant",
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ export class TimesheetTable extends DomComponent {
|
|||
clearRows() {
|
||||
// Unload existing children and reset the rows list
|
||||
for (const row of this.#rows) {
|
||||
console.log(row);
|
||||
this.removeChild(row);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue