diff --git a/biome.json b/biome.json index ca50e3c..ac48ebe 100644 --- a/biome.json +++ b/biome.json @@ -15,7 +15,9 @@ "enabled": true, "rules": { "recommended": true, - "style": {} + "style": { + "noNonNullAssertion": "off" + } } } } diff --git a/cypress.config.ts b/cypress.config.ts index 7bfdde4..1a6c85c 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,10 +1,12 @@ import { defineConfig } from "cypress"; export default defineConfig({ - component: { - devServer: { - framework: "react", - bundler: "vite", + component: { + devServer: { + // biome-ignore lint/suspicious/noExplicitAny: + framework: "cypress-ct-html" as any, + bundler: "vite", + viteConfig: {}, + }, }, - }, }); diff --git a/cypress/component/combobox.cy.ts b/cypress/component/combobox.cy.ts new file mode 100644 index 0000000..f9af547 --- /dev/null +++ b/cypress/component/combobox.cy.ts @@ -0,0 +1,252 @@ +import "../../src/combobox-framework"; + +describe("Component ", () => { + beforeEach(() => { + const items = [ + { value: 1, display: "one" }, + { value: 2, display: "two" }, + ]; + + cy.mount(` + + + `); + }); + + it("mounts", () => { + cy.get("input").should("exist"); + cy.get("[data-cy='listbox']").should("exist").should("not.be.visible"); + }); + + describe("Has working mouse navigation for", () => { + describe("Combobox ", () => { + it("opens the popup when clicked", () => { + cy.get("input").click(); + cy.get("[data-cy='listbox']").should("be.visible"); + }); + + it("closes the popup when clicked outside", () => { + cy.get("input").click(); + cy.get("body").click(); + cy.get("[data-cy='listbox']").should("not.be.visible"); + }); + }); + + describe("listbox", () => { + it("closes the popup and selects the item when clicked", () => { + cy.get("input").click(); + cy.get("[data-cy='listbox']").children().eq(0).click(); + cy.get("[data-cy='listbox']").should("not.be.visible"); + cy.get("input").should("have.value", "one"); + }); + }); + }); + + describe("Has working keyboard navigation for", () => { + describe("Combobox ", () => { + it("moves focus into the popup (first element) when downarrow is clicked", () => { + cy.get("input").should("exist").focus(); + cy.get("input").type("one"); + cy.pressKey("ArrowDown"); + cy.get("[data-cy='listbox']").should("be.visible"); + cy.focused().should("have.attr", "role", "option"); + }); + + it("moves focus into the popup (last element) when uparrow is clicked", () => { + cy.get("input").focus(); + cy.pressKey("ArrowUp"); + cy.get("[data-cy='listbox']").should("be.visible"); + cy.focused().should("have.attr", "role", "option"); + }); + + it("dismisses the popup if it is visible when escape is clicked", () => { + cy.get("input").focus(); + cy.pressKey("Escape"); + cy.get("[data-cy='listbox']").should("not.be.visible"); + }); + + it("clears the combobox if popoup is hidden and escape is clicked", () => { + cy.get("input").focus(); + cy.get("input").type("one"); + cy.pressKey("Escape"); + cy.pressKey("Escape"); + cy.get("input").should("have.value", ""); + }); + + it("displays the popup without moving focus when alt and downarrow are clicked", () => { + cy.get("input").focus(); + cy.pressKey("Escape"); // Close the popup so we acn open it + cy.pressKey("Alt"); + cy.pressKey("ArrowDown"); + cy.get("[data-cy='listbox']").should("be.visible"); + cy.focused().should("have.attr", "role", "combobox"); + }); + }); + + describe("listbox", () => { + beforeEach(() => { + cy.get("input").should("exist").focus(); + cy.get("[data-value='1']").focus(); + }); + + it("accepts the selected item when enter is clicked", () => { + cy.pressKey("Enter"); + cy.get("input").should("have.value", "one"); + }); + + it("closes the popup and returns focus to the combobox (also clears it) when escape is clicked", () => { + cy.pressKey("Escape"); + cy.get("[data-cy='listbox']").should("not.be.visible"); + cy.focused().should("have.attr", "role", "combobox"); + cy.get("input").should("have.value", ""); + }); + + it("moves focus to the next item in the list when arrowdown is clicked", () => { + cy.get("input").focus(); + cy.get("[data-value='1']").focus(); + cy.pressKey("ArrowDown"); + cy.focused().should("have.attr", "data-display", "two"); + }); + + it("focuses the first item in the list when arrowdown is clicked on the last item", () => { + cy.get("input").focus(); + cy.get("[data-value='2']").focus(); + cy.pressKey("ArrowDown"); + cy.focused().should("have.attr", "data-display", "one"); + }); + + it("moves focus to the previous item in the list when arrowup is clicked", () => { + cy.get("input").focus(); + cy.get("[data-value='2']").focus(); + cy.pressKey("ArrowUp"); + cy.focused().should("have.attr", "data-display", "one"); + }); + + it("focuses the first item in the list when arrowdown is clicked on the last item", () => { + cy.pressKey("ArrowUp"); + cy.focused().should("have.attr", "data-display", "two"); + }); + + it("returns focus to the combobox when home is clicked", () => { + cy.pressKey("Home"); + cy.focused().should("have.attr", "role", "combobox"); + }); + + it("returns focus to the combobox and places the cursor after the last character when end is clicked", () => { + cy.get("input").type("one"); + cy.pressKey("ArrowDown"); + cy.pressKey("End"); + cy.focused().should("have.attr", "role", "combobox"); + cy.get("input").should("satisfy", (input) => { + const el = input[0] as HTMLInputElement; + return el.selectionStart === el.value.length; + }); + }); + + it("returns focus to the combobox and deletes the character prior to the cursor when backspace is clicked", () => { + cy.get("input").type("one"); + cy.pressKey("ArrowDown"); + cy.pressKey("Backspace"); + cy.focused().should("have.attr", "role", "combobox"); + //cy.get("input").should("have.value", "on"); // Can't seem to mock this whith only js events + }); + + it("returns focus to the combobox when delete is clicked", () => { + cy.get("input").type("one"); + cy.pressKey("ArrowDown"); + cy.pressKey("Delete"); + cy.focused().should("have.attr", "role", "combobox"); + }); + + it("Returns focus to the combobox closing the popup when alt and uparrow are clicked", () => { + cy.pressKey("Alt"); + cy.pressKey("ArrowUp"); + cy.get("[data-cy='listbox']").should("not.be.visible"); + cy.focused().should("have.attr", "role", "combobox"); + }); + }); + }); + + describe("Handles options with special characters correctly", () => { + beforeEach(() => { + const items = [ + { value: 1, display: "[one\\|]" }, + { value: 2, display: "Something days 1-3" }, + { value: 3, display: "thre&" }, + ]; + + cy.mount(` + + + `); + }); + + it("searches for the item with special characters correctly", () => { + cy.get("input").type("[one\\|]"); + cy.get("[data-cy='listbox']").children().should("have.length", 1); + cy.get("[data-cy='listbox']").children().eq(0).should("have.text", "[one\\|]"); + }); + + it("selects the item with special characters correctly", () => { + cy.get("input").click(); + cy.get("[data-cy='listbox']").children().eq(0).click(); + cy.get("input").should("have.value", "[one\\|]"); + }); + }); + + describe("Handles listbox attribute correctly", () => { + it("does not force value when [data-cy='listbox'] is set to false", () => { + document + .getElementsByTagName("combobox-framework")[0] + .setAttribute("data-listbox", "false"); + cy.get("input").type("o"); + cy.get("input").blur(); + cy.get("input").should("have.value", "o"); + }); + + it("does not force value when listbox is missing", () => { + cy.get("input").type("o"); + cy.get("input").blur(); + cy.get("input").should("have.value", "o"); + }); + + it("does force value when listbox is set to true", () => { + document + .getElementsByTagName("combobox-framework")[0] + .setAttribute("data-listbox", "true"); + cy.get("input").type("o"); + cy.get("input").blur(); + cy.get("input").should("have.value", "one"); + }); + + it("does force value when [data-cy='listbox'] is set to something", () => { + document + .getElementsByTagName("combobox-framework")[0] + .setAttribute("data-listbox", "something"); + cy.get("input").type("o"); + cy.get("input").blur(); + cy.get("input").should("have.value", "one"); + }); + }); + + it("Handles attributes being set directly", () => { + cy.get("combobox-framework").should("have.attr", "data-value", ""); + cy.get("combobox-framework").invoke("attr", "data-value", "2"); + cy.get("combobox-framework").should("have.attr", "data-value", "2"); + cy.get("input").should("have.value", "two"); + }); +}); diff --git a/cypress/component/combobox.cy.tsx b/cypress/component/combobox.cy.tsx deleted file mode 100644 index 22c3fdb..0000000 --- a/cypress/component/combobox.cy.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React from "react"; -import { ComboboxWrapper } from "./react-wrapper/combobox-wrapper"; - -describe("Component ", () => { - beforeEach(() => { - cy.mount( - , - ); - }); - - it("mounts", () => { - cy.getByTestAttr("input").should("exist"); - cy.getByTestAttr("listbox").should("exist").should("not.be.visible"); - }); - - describe("Has working mouse navigation for", () => { - describe("Combobox ", () => { - it("opens the popup when clicked", () => { - cy.getByTestAttr("input").click(); - cy.getByTestAttr("listbox").should("be.visible"); - }); - - it("closes the popup when clicked outside", () => { - cy.getByTestAttr("input").click(); - cy.get("body").click(); - cy.getByTestAttr("listbox").should("not.be.visible"); - }); - }); - - describe("Listbox", () => { - it("closes the popup and selects the item when clicked", () => { - cy.getByTestAttr("input").click(); - cy.getByTestAttr("listbox").children().eq(0).click(); - cy.getByTestAttr("listbox").should("not.be.visible"); - cy.getByTestAttr("input").should("have.value", "one"); - }); - }); - }); - - describe("Has working keyboard navigation for", () => { - describe("Combobox ", () => { - it("moves focus into the popup (first element) when downarrow is clicked", () => { - cy.getByTestAttr("input").should("exist").focus(); - cy.getByTestAttr("input").type("one"); - cy.pressKey("ArrowDown"); - cy.getByTestAttr("listbox").should("be.visible"); - cy.focused().should("have.attr", "role", "option"); - }); - - it("moves focus into the popup (last element) when uparrow is clicked", () => { - cy.getByTestAttr("input").focus(); - cy.pressKey("ArrowUp"); - cy.getByTestAttr("listbox").should("be.visible"); - cy.focused().should("have.attr", "role", "option"); - }); - - it("dismisses the popup if it is visible when escape is clicked", () => { - cy.getByTestAttr("input").focus(); - cy.pressKey("Escape"); - cy.getByTestAttr("listbox").should("not.be.visible"); - }); - - it("clears the combobox if popoup is hidden and escape is clicked", () => { - cy.getByTestAttr("input").focus(); - cy.getByTestAttr("input").type("one"); - cy.pressKey("Escape"); - cy.pressKey("Escape"); - cy.getByTestAttr("input").should("have.value", ""); - }); - - it("displays the popup without moving focus when alt and downarrow are clicked", () => { - cy.getByTestAttr("input").focus(); - cy.pressKey("Escape"); // Close the popup so we acn open it - cy.pressKey("Alt"); - cy.pressKey("ArrowDown"); - cy.getByTestAttr("listbox").should("be.visible"); - cy.focused().should("have.attr", "role", "combobox"); - }); - }); - - describe("Listbox", () => { - beforeEach(() => { - cy.getByTestAttr("input").should("exist").focus(); - cy.getByTestAttr("1").focus(); - }); - - it("accepts the selected item when enter is clicked", () => { - cy.pressKey("Enter"); - cy.getByTestAttr("input").should("have.value", "one"); - }); - - it("closes the popup and returns focus to the combobox (also clears it) when escape is clicked", () => { - cy.pressKey("Escape"); - cy.getByTestAttr("listbox").should("not.be.visible"); - cy.focused().should("have.attr", "role", "combobox"); - cy.getByTestAttr("input").should("have.value", ""); - }); - - it("moves focus to the next item in the list when arrowdown is clicked", () => { - cy.pressKey("ArrowDown"); - cy.focused().should("have.text", "two"); - }); - - it("focuses the first item in the list when arrowdown is clicked on the last item", () => { - cy.pressKey("ArrowDown"); - cy.pressKey("ArrowDown"); - cy.focused().should("have.text", "one"); - }); - - it("moves focus to the previous item in the list when arrowup is clicked", () => { - cy.pressKey("ArrowDown"); - cy.pressKey("ArrowUp"); - cy.focused().should("have.text", "one"); - }); - - it("focuses the first item in the list when arrowdown is clicked on the last item", () => { - cy.pressKey("ArrowUp"); - cy.focused().should("have.text", "two"); - }); - - it("returns focus to the combobox when home is clicked", () => { - cy.pressKey("Home"); - cy.focused().should("have.attr", "role", "combobox"); - }); - - it("returns focus to the combobox and places the cursor after the last character when end is clicked", () => { - cy.getByTestAttr("input").type("one"); - cy.pressKey("ArrowDown"); - cy.pressKey("End"); - cy.focused().should("have.attr", "role", "combobox"); - cy.getByTestAttr("input").should("satisfy", (input) => { - const el = input[0] as HTMLInputElement; - return el.selectionStart === el.value.length; - }); - }); - - it("returns focus to the combobox and deletes the character prior to the cursor when backspace is clicked", () => { - cy.getByTestAttr("input").type("one"); - cy.pressKey("ArrowDown"); - cy.pressKey("Backspace"); - cy.focused().should("have.attr", "role", "combobox"); - //cy.getByTestAttr("input").should("have.value", "on"); // Can't seem to mock this whith only js events - }); - - it("returns focus to the combobox when delete is clicked", () => { - cy.getByTestAttr("input").type("one"); - cy.pressKey("ArrowDown"); - cy.pressKey("Delete"); - cy.focused().should("have.attr", "role", "combobox"); - }); - - it("Returns focus to the combobox closing the popup when alt and uparrow are clicked", () => { - cy.pressKey("Alt"); - cy.pressKey("ArrowUp"); - cy.getByTestAttr("listbox").should("not.be.visible"); - cy.focused().should("have.attr", "role", "combobox"); - }); - }); - }); - - describe("Handles options with special characters correctly", () => { - beforeEach(() => { - cy.mount( - test

" }, - ]} - />, - ); - }); - - it("searches for the item with special characters correctly", () => { - cy.getByTestAttr("input").type("[one\\|]"); - cy.getByTestAttr("listbox").children().should("have.length", 1); - cy.getByTestAttr("listbox").children().eq(0).should("have.text", "[one\\|]"); - }); - - it("selects the item with special characters correctly", () => { - cy.getByTestAttr("input").click(); - cy.getByTestAttr("listbox").children().eq(0).click(); - cy.getByTestAttr("input").should("have.value", "[one\\|]"); - }); - - it("does not htmlify the display text of options containing tags", () => { - cy.getByTestAttr("input").type("test"); - cy.getByTestAttr("listbox").children().eq(0).should("have.text", "

test

"); - }); - }); - - describe("Handles listbox attribute correctly", () => { - it("does not force value when listbox is set to false", () => { - document - .getElementsByTagName("combobox-framework")[0] - .setAttribute("data-listbox", "false"); - cy.getByTestAttr("input").type("o"); - cy.getByTestAttr("input").blur(); - cy.getByTestAttr("input").should("have.value", "o"); - }); - - it("does not force value when listbox is missing", () => { - cy.getByTestAttr("input").type("o"); - cy.getByTestAttr("input").blur(); - cy.getByTestAttr("input").should("have.value", "o"); - }); - - it("does force value when listbox is set to true", () => { - document - .getElementsByTagName("combobox-framework")[0] - .setAttribute("data-listbox", "true"); - cy.getByTestAttr("input").type("o"); - cy.getByTestAttr("input").blur(); - cy.getByTestAttr("input").should("have.value", "one"); - }); - - it("does force value when listbox is set to something", () => { - document - .getElementsByTagName("combobox-framework")[0] - .setAttribute("data-listbox", "something"); - cy.getByTestAttr("input").type("o"); - cy.getByTestAttr("input").blur(); - cy.getByTestAttr("input").should("have.value", "one"); - }); - }); -}); diff --git a/cypress/component/react-wrapper/combobox-wrapper.css b/cypress/component/react-wrapper/combobox-wrapper.css deleted file mode 100644 index 9c38bfb..0000000 --- a/cypress/component/react-wrapper/combobox-wrapper.css +++ /dev/null @@ -1,23 +0,0 @@ -combobox-framework [slot="list"] { - color: var(--background-color); - background-color: var(--text-color); - position: absolute; - top: anchor(bottom); - left: anchor(left); - right: anchor(right); - list-style: none; - margin: 0; - padding: 0; - border: 1px solid black; - border-radius: 0 0 0.2rem 0.2rem; -} - -combobox-framework [slot="list"] > * { - padding: 0.2rem; - cursor: pointer; -} - -combobox-framework [slot="list"] li:hover, -combobox-framework [slot="list"] tr:hover td { - background-color: var(--primary-color); -} \ No newline at end of file diff --git a/cypress/component/react-wrapper/combobox-wrapper.jsx b/cypress/component/react-wrapper/combobox-wrapper.jsx deleted file mode 100644 index 01749ab..0000000 --- a/cypress/component/react-wrapper/combobox-wrapper.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import "./combobox-wrapper.css"; -import "../../../src/combobox-framework"; - -export const ComboboxWrapper = ({ items, placeholder = "Choose a value" }) => { - return ( - - -
    - {items.map((item, index) => ( -
  • - {item.display} -
  • - ))} -
-
- ); -}; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 658dbf0..b5d9934 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -12,9 +12,6 @@ // // -- This is a parent command -- // Cypress.Commands.add('login', (email, password) => { ... }) -Cypress.Commands.add("getByTestAttr", (val) => { - return cy.get(`[data-cy="${val}"]`); -}); Cypress.Commands.add("pressKey", (val) => { const element = document.activeElement ?? document; diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html index ac6e79f..b2365de 100644 --- a/cypress/support/component-index.html +++ b/cypress/support/component-index.html @@ -5,6 +5,31 @@ Components App +
diff --git a/cypress/support/component.ts b/cypress/support/component.ts index 37f59ed..b561e2a 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -14,26 +14,23 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import "./commands"; // Alternatively you can use CommonJS syntax: // require('./commands') -import { mount } from 'cypress/react18' +import { mount } from "cypress-ct-html"; // Augment the Cypress namespace to include type definitions for // your custom command. // Alternatively, can be defined in cypress/support/component.d.ts // with a at the top of your spec. declare global { - namespace Cypress { - interface Chainable { - mount: typeof mount + namespace Cypress { + interface Chainable { + mount: typeof mount; + } } - } } -Cypress.Commands.add('mount', mount) - -// Example use: -// cy.mount() \ No newline at end of file +Cypress.Commands.add("mount", mount); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts new file mode 100644 index 0000000..f71c69b --- /dev/null +++ b/cypress/support/index.d.ts @@ -0,0 +1,9 @@ +declare namespace Cypress { + interface Chainable { + /** + * Custom command to press a key on the active element. + * @example cy.pressKey("Enter") + */ + pressKey: (val: string) => void; + } +} diff --git a/package.json b/package.json index 0be3765..5f4319a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.3.2", + "version": "1.4.0", "name": "combobox-framework", "description": "A framework for building comboboxes", "keywords": [ @@ -39,8 +39,6 @@ "@biomejs/biome": "^1.5.3", "@happy-dom/global-registrator": "^13.3.8", "bun-plugin-dts": "^0.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", "typescript": "^5.2.2", "vite": "^5.2.8", "cypress": "^13.7.2" diff --git a/src/combobox-framework.ts b/src/combobox-framework.ts index fc65fe6..fa4f25a 100644 --- a/src/combobox-framework.ts +++ b/src/combobox-framework.ts @@ -9,14 +9,14 @@ import { } from "./helpers"; export default class ComboboxFramework extends HTMLElement { - public _input: HTMLInputElement | null = null; - public _list: HTMLElement | null = null; - public _listContainer: HTMLElement | null = null; - public _originalList: HTMLElement | null = null; - public _isAltModifierPressed = false; - public _forceValue = false; - public _lastValue: string | undefined = undefined; - public _limit: number = Infinity; + public input: HTMLInputElement | null = null; + public list: HTMLElement | null = null; + public listContainer: HTMLElement | null = null; + public originalList: HTMLElement | null = null; + public isAltModifierPressed = false; + public shouldForceValue = false; + public lastValue: string | undefined = undefined; + public limit: number = Infinity; // #region Fuzzy search Fuse.js public _fuse: Fuse | null = null; @@ -27,108 +27,78 @@ export default class ComboboxFramework extends HTMLElement { // #endregion private abortController = new AbortController(); + private initFuseObj = () => { + const originalList = this.originalList ?? fetchOriginalList.call(this); + const elementArray = Array.from((originalList.cloneNode(true) as HTMLElement).children); + + return new Fuse(Array.from(elementArray), this._fuseOptions); + }; + + constructor() { + super(); + + this.abortController ??= new AbortController(); + this.attachShadow({ mode: "open" }); + } static get observedAttributes(): string[] { return ["data-value", "data-fuse-options", "data-listbox", "data-limit"]; } public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { - if (oldValue === newValue) return; // If the value is the same, do nothing + if (oldValue === newValue) return; - // #region Handle the attribute change switch (name) { case "data-value": this.selectItemByValue(newValue, false); break; - case "data-fuse-options": { - // #region If the fuse object is not created, save the options and return - if (!this._fuse) { - this._fuseOptions = JSON.parse(newValue); - return; - } - // #endregion - - // #region If the fuse object is created, recreate it and search the list - const originalList = fetchOriginalList.call(this); - + case "data-fuse-options": this._fuseOptions = JSON.parse(newValue); - this._fuse = new Fuse( - Array.from((originalList.cloneNode(true) as HTMLElement).children), - this._fuseOptions, - ); + this._fuse = this.initFuseObj(); this.searchList(); - // #endregion break; - } case "data-listbox": - if (newValue === "false") this._forceValue = false; - else this._forceValue = !!newValue; + if (newValue === "false") this.shouldForceValue = false; + else this.shouldForceValue = !!newValue; break; case "data-limit": - this._limit = parseInt(newValue); + this.limit = parseInt(newValue); break; } - // #endregion } public connectedCallback(): void { - // #region Create the shadow DOM - const shadow = this.attachShadow({ mode: "open" }); - shadow.innerHTML = ` - - - `; - // #endregion + const shadow = this.shadowRoot as ShadowRoot; + shadow.innerHTML = ``; - // #region Fetch the input and list elements fetchInput.call(this); fetchListContainer.call(this); fetchList.call(this); - // #endregion setBasicAttributes.call(this); - // #region Save the original list - // This is done to have a original copy of the list to later sort, filter, etc. - const originalList = fetchOriginalList.call(this); - // #endregion - - // #region Create the fuse object - this._fuse = new Fuse( - Array.from((originalList.cloneNode(true) as HTMLElement).children), - this._fuseOptions, - ); - // #endregion + this.originalList = fetchOriginalList.call(this); + this._fuse ??= this.initFuseObj.call(this); - // #region Do initial search the list this.searchList(); - // #endregion - - // #region Add event listeners this.addEventListeners(); - // #endregion - - // #region If forceValue is true, select the first item in the list this.forceValue(); - // #endregion } public disconnectedCallback(): void { - // #region Send signal to remove all event listeners this.abortController.abort(); - // #endregion } public toggleList( - newValue: boolean = this._input?.getAttribute("aria-expanded") !== true.toString(), + newValue: boolean = this.input?.getAttribute("aria-expanded") !== true.toString(), ): void { const input = fetchInput.call(this); input.setAttribute("aria-expanded", `${newValue}`); if (newValue) { - this._listContainer?.showPopover(); + this.listContainer?.showPopover(); } else { - this._listContainer?.hidePopover(); + this.listContainer?.hidePopover(); this.unfocusAllItems(); } } @@ -150,16 +120,13 @@ export default class ComboboxFramework extends HTMLElement { // #endregion // #region Add event listeners to the input element - if (!this._input) fetchInput.call(this); + if (!this.input) fetchInput.call(this); input.addEventListener("input", this.searchList.bind(this, true, true), { signal: this.abortController.signal, }); input.addEventListener("focus", this.toggleList.bind(this, true), { signal: this.abortController.signal, }); - // #endregion - - // #region Add event listeners to framework element input.addEventListener("keydown", handleComboBoxKeyPress.bind(this), { signal: this.abortController.signal, }); @@ -168,9 +135,7 @@ export default class ComboboxFramework extends HTMLElement { }); // #endregion - // #region Add event listeners to the list element this.addEventListenersToListItems(); - // #endregion } private addEventListenersToListItems(): void { @@ -194,7 +159,7 @@ export default class ComboboxFramework extends HTMLElement { private searchList(openList = true, clearValue = true): void { // #region Check if required variables are set - if (!this._fuse) throw new Error("Fuse object not found"); + this._fuse ??= this.initFuseObj(); const input = fetchInput.call(this); const list = fetchList.call(this); const originalList = fetchOriginalList.call(this); @@ -212,7 +177,7 @@ export default class ComboboxFramework extends HTMLElement { list.innerHTML = ""; list.append( ...Array.from((originalList.cloneNode(true) as HTMLElement).children) - .slice(0, this._limit) + .slice(0, this.limit) .sort( (a, b) => Number((b as HTMLElement).dataset.weight) - @@ -224,8 +189,7 @@ export default class ComboboxFramework extends HTMLElement { } // #endregion - // #region Search the list - let searchedList = this._fuse.search(input.value).slice(0, this._limit); + let searchedList = this._fuse.search(input.value).slice(0, this.limit); // #region Sort the list based on the weight of the items if they have a weight and a score searchedList = searchedList @@ -274,13 +238,8 @@ export default class ComboboxFramework extends HTMLElement { } // #endregion - // #region Add event listeners to the list item elements this.addEventListenersToListItems(); - // #endregion - - // #region Show the list after the search is complete this.toggleList(openList); - // #endregion } private highlightText(text: string, searchString: string): string { @@ -298,14 +257,10 @@ export default class ComboboxFramework extends HTMLElement { } private unfocusAllItems(): void { - // #region Check if required variables are set const list = fetchList.call(this); - // #endregion - // #region Unfocus all items in the list for (const item of list.querySelectorAll("[aria-selected]")) item.removeAttribute("aria-selected"); - // #endregion } public selectItem(item: HTMLElement, grabFocus = true): void { @@ -342,43 +297,36 @@ export default class ComboboxFramework extends HTMLElement { } public clearInput(grabFocus = true): void { - // #region Check if required variables are set const input = fetchInput.call(this); - // #endregion - // #region Clear the input element input.value = ""; if (grabFocus) input.focus(); this.toggleList(false); - // #endregion } public forceValue(): void { - // #region Check if required variables are set + if (!this.shouldForceValue) return; + if (this.dataset.value) return; + if (!this.input?.value) return; + const list = fetchList.call(this); - // #endregion - // #region If forceValue is true and we don't have a value selected, select the first item (best match) in the list or empty the input and value - if (this._forceValue && !!this._input?.value && !this.dataset.value) { - const bestMatch = list.children[0] as HTMLElement; - if (bestMatch) this.selectItem(bestMatch, false); - else { - this.clearInput(false); // Clear the input - this.dataset.value = ""; // Clear the value - this.sendChangeEvent(); // Send a change event - } + const bestMatch = list.children[0] as HTMLElement; + if (bestMatch) { + this.selectItem(bestMatch, false); + } else { + this.clearInput(false); // Clear the input + this.dataset.value = ""; // Clear the value + this.sendChangeEvent(); // Send a change event } - // #endregion } private sendChangeEvent(): void { - if (this.dataset.value === this._lastValue) return; + if (this.dataset.value === this.lastValue) return; const event = new Event("change"); this.dispatchEvent(event); - this._lastValue = this.dataset.value; + this.lastValue = this.dataset.value; } } -// #region Register the component customElements.define("combobox-framework", ComboboxFramework); -// #endregion diff --git a/src/handlers.ts b/src/handlers.ts index 9ed4f1a..f161ef2 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -15,7 +15,7 @@ export function handleComboBoxKeyPress(this: ComboboxFramework, event: KeyboardE // If the popup is available, moves focus into the popup: If the autocomplete behavior automatically selected a suggestion before Down Arrow was pressed, focus is placed on the suggestion following the automatically selected suggestion. Otherwise, places focus on the first focusable element in the popup. if (!isInputExpanded()) { this.toggleList(true); - if (!this._isAltModifierPressed) this.focusItem(list.children[0] as HTMLElement); + if (!this.isAltModifierPressed) this.focusItem(list.children[0] as HTMLElement); } else { this.focusItem(list.children[0] as HTMLElement); } @@ -44,7 +44,7 @@ export function handleComboBoxKeyPress(this: ComboboxFramework, event: KeyboardE } break; case KeyCode.Alt: - this._isAltModifierPressed = true; + this.isAltModifierPressed = true; break; } // #endregion @@ -77,7 +77,7 @@ export function handleListKeyPress(this: ComboboxFramework, event: KeyboardEvent } case KeyCode.ArrowUp: { // If alt is pressed, close the list and focus the input - if (this._isAltModifierPressed) { + if (this.isAltModifierPressed) { input.focus(); this.toggleList(false); event.preventDefault(); // prevent scrolling @@ -116,7 +116,7 @@ export function handleListKeyPress(this: ComboboxFramework, event: KeyboardEvent input.focus(); break; case KeyCode.Alt: - this._isAltModifierPressed = true; + this.isAltModifierPressed = true; break; default: // If the key is not handled, return focus to the input @@ -130,7 +130,7 @@ export function handleKeyUp(this: ComboboxFramework, event: KeyboardEvent): void // #region Handle the key press switch (event.key) { case "Alt": - this._isAltModifierPressed = false; + this.isAltModifierPressed = false; break; } // #endregion diff --git a/src/helpers.ts b/src/helpers.ts index 38ff0f0..407e53c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,36 +1,36 @@ import ComboboxFramework from "./combobox-framework"; export function fetchListContainer(this: ComboboxFramework): HTMLElement { - if (this._listContainer) return this._listContainer; - this._listContainer = this.querySelector('[slot="list"]') as HTMLElement; - return this._listContainer; + if (this.listContainer) return this.listContainer; + this.listContainer = this.querySelector('[slot="list"]') as HTMLElement; + return this.listContainer; } export function fetchList(this: ComboboxFramework): HTMLElement { - if (this._list) return this._list; - this._list = this.querySelector('[slot="list"] [data-list]') as HTMLElement; - if (!this._list) this._list = this.querySelector('[slot="list"]') as HTMLElement; - if (!this._list) throw new Error("List element not found"); + if (this.list) return this.list; + this.list = this.querySelector('[slot="list"] [data-list]') as HTMLElement; + if (!this.list) this.list = this.querySelector('[slot="list"]') as HTMLElement; + if (!this.list) throw new Error("List element not found"); - return this._list; + return this.list; } export function fetchInput(this: ComboboxFramework): HTMLInputElement { - if (this._input) return this._input; + if (this.input) return this.input; const input = this.querySelector('[slot="input"]') as HTMLInputElement; if (!input) throw new Error("Input element not found"); - this._input = input; + this.input = input; - return this._input; + return this.input; } export function fetchOriginalList(this: ComboboxFramework): HTMLElement { - if (this._originalList) return this._originalList; + if (this.originalList) return this.originalList; const list = fetchList.call(this); - this._originalList = list.cloneNode(true) as HTMLElement; + this.originalList = list.cloneNode(true) as HTMLElement; - return this._originalList; + return this.originalList; } export function setBasicAttributes(this: ComboboxFramework): void { @@ -53,7 +53,7 @@ export function setBasicAttributes(this: ComboboxFramework): void { // #endregion //#region set basic attributes for list element container - this._listContainer?.setAttribute("popover", "manual"); + this.listContainer?.setAttribute("popover", "manual"); //#endregion // #region Basic attributes for the list element diff --git a/test/handlers.test.ts b/test/handlers.test.ts index adb5947..15896c0 100644 --- a/test/handlers.test.ts +++ b/test/handlers.test.ts @@ -26,13 +26,13 @@ describe("handleComboBoxKeyPress", () => { fetchInput.call(combobox); fetchList.call(combobox); - combobox._input!.setAttribute("aria-expanded", "false"); + combobox.input!.setAttribute("aria-expanded", "false"); combobox.clearInput = mock(() => void 0); combobox.focusItem = mock(() => void 0); combobox.selectItem = mock(() => void 0); combobox.toggleList = mock((boolean) => - combobox._list!.setAttribute("aria-expanded", boolean.toString()), + combobox.list!.setAttribute("aria-expanded", boolean.toString()), ); document.body.appendChild(combobox); @@ -51,16 +51,16 @@ describe("handleComboBoxKeyPress", () => { // Assert setTimeout(() => { - expect(combobox._input!.getAttribute("aria-expanded")).toBe("true"); + expect(combobox.input!.getAttribute("aria-expanded")).toBe("true"); expect(combobox.focusItem).toHaveBeenCalledTimes(1); expect(combobox.toggleList).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(combobox._list!.children[3]); + expect(document.activeElement).toBe(combobox.list!.children[3]); }, 100); }); test("should not open the list if the list is already open", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); const event = new KeyboardEvent("keyDown", { key: "ArrowDown" }); // Act @@ -72,7 +72,7 @@ describe("handleComboBoxKeyPress", () => { test("if alt is pressed and list is closed, should only open the list", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "false"); + combobox.input!.setAttribute("aria-expanded", "false"); const event = { ...new KeyboardEvent("keyDown", { key: "ArrowDown" }), preventDefault: mock(() => void 0), @@ -93,7 +93,7 @@ describe("handleComboBoxKeyPress", () => { describe("ArrowUp", () => { test("if the list is open focus the last item in the list", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); const event = { ...new KeyboardEvent("keyDown", { key: "ArrowUp" }), preventDefault: mock(() => void 0), @@ -106,13 +106,13 @@ describe("handleComboBoxKeyPress", () => { setTimeout(() => { expect(combobox.focusItem).toHaveBeenCalledTimes(1); expect(event.preventDefault).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(combobox._list!.children[3]); + expect(document.activeElement).toBe(combobox.list!.children[3]); }, 100); }); test("if the list is closed only prevents default", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); const event = { ...new KeyboardEvent("keyDown", { key: "ArrowUp" }), preventDefault: mock(() => void 0), @@ -131,8 +131,8 @@ describe("handleComboBoxKeyPress", () => { describe("Escape", () => { test("only dismisses the popup if it is visible", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "true"); - combobox._input!.value = "test"; + combobox.input!.setAttribute("aria-expanded", "true"); + combobox.input!.value = "test"; const event = new KeyboardEvent("keyDown", { key: "Escape" }); // Act @@ -142,14 +142,14 @@ describe("handleComboBoxKeyPress", () => { setTimeout(() => { expect(combobox.toggleList).toHaveBeenCalledTimes(1); expect(combobox.clearInput).toHaveBeenCalledTimes(0); - expect(combobox._input!.value).toBe("test"); + expect(combobox.input!.value).toBe("test"); }, 100); }); test("clears the input if the popup is not visible", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "false"); - combobox._input!.value = "test"; + combobox.input!.setAttribute("aria-expanded", "false"); + combobox.input!.value = "test"; const event = new KeyboardEvent("keyDown", { key: "Escape" }); // Act @@ -159,7 +159,7 @@ describe("handleComboBoxKeyPress", () => { setTimeout(() => { expect(combobox.toggleList).toHaveBeenCalledTimes(0); expect(combobox.clearInput).toHaveBeenCalledTimes(1); - expect(combobox._input!.value).toBe(""); + expect(combobox.input!.value).toBe(""); }, 100); }); }); @@ -167,7 +167,7 @@ describe("handleComboBoxKeyPress", () => { describe("Enter", () => { test("autocompletes the combobox with the first suggestion if the list is open", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); const event = new KeyboardEvent("keyDown", { key: "Enter" }); // Act @@ -181,7 +181,7 @@ describe("handleComboBoxKeyPress", () => { test("does nothing if the list is closed", () => { // Arrange - combobox._input!.setAttribute("aria-expanded", "false"); + combobox.input!.setAttribute("aria-expanded", "false"); const event = new KeyboardEvent("keyDown", { key: "Enter" }); // Act @@ -201,7 +201,7 @@ describe("handleComboBoxKeyPress", () => { handleComboBoxKeyPress.call(combobox, event); // Assert - expect(combobox._isAltModifierPressed).toBe(true); + expect(combobox.isAltModifierPressed).toBe(true); }); }); }); @@ -224,7 +224,7 @@ describe("handleListKeyPress", () => { fetchInput.call(combobox); fetchList.call(combobox); - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); combobox.clearInput = mock(() => void 0); combobox.focusItem = mock(() => void 0); @@ -239,7 +239,7 @@ describe("handleListKeyPress", () => { // Arrange const event = { ...new KeyboardEvent("keyDown", { key: "Enter" }), - target: combobox._list!.children[0], + target: combobox.list!.children[0], } as KeyboardEvent; // Act @@ -247,7 +247,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(combobox._input!.getAttribute("aria-expanded")).toBe("false"); + expect(combobox.input!.getAttribute("aria-expanded")).toBe("false"); expect(combobox.selectItem).toHaveBeenCalledTimes(1); }, 100); }); @@ -258,7 +258,7 @@ describe("handleListKeyPress", () => { // Arrange const event = { ...new KeyboardEvent("keyDown", { key: "Escape" }), - target: combobox._list!.children[0], + target: combobox.list!.children[0], } as KeyboardEvent; // Act @@ -266,7 +266,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(combobox._input!.getAttribute("aria-expanded")).toBe("false"); + expect(combobox.input!.getAttribute("aria-expanded")).toBe("false"); expect(combobox.clearInput).toHaveBeenCalledTimes(1); }, 100); }); @@ -277,7 +277,7 @@ describe("handleListKeyPress", () => { // Arrange const event = { ...new KeyboardEvent("keyDown", { key: "ArrowDown" }), - target: combobox._list!.children[1], + target: combobox.list!.children[1], preventDefault: mock(() => void 0), } as KeyboardEvent; @@ -288,7 +288,7 @@ describe("handleListKeyPress", () => { setTimeout(() => { expect(combobox.focusItem).toHaveBeenCalledTimes(1); expect(event.preventDefault).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(combobox._list!.children[2]); // FIXME: This is borken, can be any item, so test does not really work + expect(document.activeElement).toBe(combobox.list!.children[2]); // FIXME: This is borken, can be any item, so test does not really work }, 100); }); @@ -296,7 +296,7 @@ describe("handleListKeyPress", () => { // Arrange const event = { ...new KeyboardEvent("keyDown", { key: "ArrowDown" }), - target: combobox._list!.children[3], + target: combobox.list!.children[3], preventDefault: mock(() => void 0), } as KeyboardEvent; @@ -307,7 +307,7 @@ describe("handleListKeyPress", () => { setTimeout(() => { expect(combobox.focusItem).toHaveBeenCalledTimes(1); expect(event.preventDefault).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(combobox._list!.children[0]); // FIXME: This is borken, can be any item, so test does not really work + expect(document.activeElement).toBe(combobox.list!.children[0]); // FIXME: This is borken, can be any item, so test does not really work }, 100); }); }); @@ -317,7 +317,7 @@ describe("handleListKeyPress", () => { // Arrange const event = { ...new KeyboardEvent("keyDown", { key: "ArrowUp" }), - target: combobox._list!.children[1], + target: combobox.list!.children[1], preventDefault: mock(() => void 0), } as KeyboardEvent; @@ -328,7 +328,7 @@ describe("handleListKeyPress", () => { setTimeout(() => { expect(combobox.focusItem).toHaveBeenCalledTimes(1); expect(event.preventDefault).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(combobox._list!.children[0]); // FIXME: This is borken, can be any item, so test does not really work + expect(document.activeElement).toBe(combobox.list!.children[0]); // FIXME: This is borken, can be any item, so test does not really work }, 100); }); @@ -336,7 +336,7 @@ describe("handleListKeyPress", () => { // Arrange const event = { ...new KeyboardEvent("keyDown", { key: "ArrowUp" }), - target: combobox._list!.children[3], + target: combobox.list!.children[3], preventDefault: mock(() => void 0), } as KeyboardEvent; @@ -347,16 +347,16 @@ describe("handleListKeyPress", () => { setTimeout(() => { expect(combobox.focusItem).toHaveBeenCalledTimes(1); expect(event.preventDefault).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(combobox._list!.children[0]); // FIXME: This is borken, can be any item, so test does not really work + expect(document.activeElement).toBe(combobox.list!.children[0]); // FIXME: This is borken, can be any item, so test does not really work }, 100); }); test("should close the list and focus the input if alt is pressed", () => { // Arrange - combobox._isAltModifierPressed = true; + combobox.isAltModifierPressed = true; const event = { ...new KeyboardEvent("keyDown", { key: "ArrowUp" }), - target: combobox._list!.children[1], + target: combobox.list!.children[1], preventDefault: mock(() => void 0), } as KeyboardEvent; @@ -365,7 +365,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(combobox._input).toBe(document.activeElement); + expect(combobox.input).toBe(document.activeElement); expect(combobox.toggleList).toHaveBeenCalledTimes(1); expect(event.preventDefault).toHaveBeenCalledTimes(1); }, 100); @@ -375,7 +375,7 @@ describe("handleListKeyPress", () => { describe("ArrowRight", () => { test("should return focus to the combobox without closing the list", () => { // Arrange - (combobox._list!.children[1] as HTMLElement).focus(); + (combobox.list!.children[1] as HTMLElement).focus(); const event = new KeyboardEvent("keyDown", { key: "ArrowRight" }); // Act @@ -383,7 +383,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(document.activeElement).toBe(combobox._input); + expect(document.activeElement).toBe(combobox.input); }, 100); }); }); @@ -391,7 +391,7 @@ describe("handleListKeyPress", () => { describe("ArrowLeft", () => { test("should return focus to the combobox without closing the list", () => { // Arrange - (combobox._list!.children[1] as HTMLElement).focus(); + (combobox.list!.children[1] as HTMLElement).focus(); const event = new KeyboardEvent("keyDown", { key: "ArrowLeft" }); // Act @@ -399,7 +399,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(document.activeElement).toBe(combobox._input); + expect(document.activeElement).toBe(combobox.input); }, 100); }); }); @@ -407,7 +407,7 @@ describe("handleListKeyPress", () => { describe("Home", () => { test("should return focus to the combobox without closing the list", () => { // Arrange - (combobox._list!.children[1] as HTMLElement).focus(); + (combobox.list!.children[1] as HTMLElement).focus(); const event = new KeyboardEvent("keyDown", { key: "Home" }); // Act @@ -415,7 +415,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(document.activeElement).toBe(combobox._input); + expect(document.activeElement).toBe(combobox.input); }, 100); }); }); @@ -423,7 +423,7 @@ describe("handleListKeyPress", () => { describe("End", () => { test("should return focus to the combobox without closing the list", () => { // Arrange - (combobox._list!.children[1] as HTMLElement).focus(); + (combobox.list!.children[1] as HTMLElement).focus(); const event = new KeyboardEvent("keyDown", { key: "End" }); // Act @@ -431,7 +431,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(document.activeElement).toBe(combobox._input); + expect(document.activeElement).toBe(combobox.input); }, 100); }); }); @@ -439,7 +439,7 @@ describe("handleListKeyPress", () => { describe("Backspace", () => { test("should return focus to the combobox without closing the list", () => { // Arrange - (combobox._list!.children[1] as HTMLElement).focus(); + (combobox.list!.children[1] as HTMLElement).focus(); const event = new KeyboardEvent("keyDown", { key: "Backspace" }); // Act @@ -447,7 +447,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(document.activeElement).toBe(combobox._input); + expect(document.activeElement).toBe(combobox.input); }, 100); }); }); @@ -455,7 +455,7 @@ describe("handleListKeyPress", () => { describe("Delete", () => { test("should return focus to the combobox without closing the list", () => { // Arrange - (combobox._list!.children[1] as HTMLElement).focus(); + (combobox.list!.children[1] as HTMLElement).focus(); const event = new KeyboardEvent("keyDown", { key: "Delete" }); // Act @@ -463,7 +463,7 @@ describe("handleListKeyPress", () => { // Assert setTimeout(() => { - expect(document.activeElement).toBe(combobox._input); + expect(document.activeElement).toBe(combobox.input); }, 100); }); }); @@ -477,7 +477,7 @@ describe("handleListKeyPress", () => { handleListKeyPress.call(combobox, event); // Assert - expect(combobox._isAltModifierPressed).toBe(true); + expect(combobox.isAltModifierPressed).toBe(true); }); }); }); @@ -495,14 +495,14 @@ describe("handleKeyUp", () => {
  • Item 4
  • `; - combobox._isAltModifierPressed = true; + combobox.isAltModifierPressed = true; const event = new KeyboardEvent("keyUp", { key: "Alt" }); // Act handleKeyUp.call(combobox, event); // Assert - expect(combobox._isAltModifierPressed).toBe(false); + expect(combobox.isAltModifierPressed).toBe(false); }); }); @@ -521,14 +521,14 @@ describe("hanldeBlur", () => { fetchInput.call(combobox); fetchList.call(combobox); - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); // Act handleBlur.call(combobox); // Assert setTimeout(() => { - expect(combobox._input!.getAttribute("aria-expanded")).toBe("false"); + expect(combobox.input!.getAttribute("aria-expanded")).toBe("false"); }, 100); }); @@ -546,15 +546,15 @@ describe("hanldeBlur", () => { fetchInput.call(combobox); fetchList.call(combobox); - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); // Act - combobox._list!.focus(); + combobox.list!.focus(); handleBlur.call(combobox); // Assert setTimeout(() => { - expect(combobox._input!.getAttribute("aria-expanded")).toBe("true"); + expect(combobox.input!.getAttribute("aria-expanded")).toBe("true"); }, 0); }); @@ -572,7 +572,7 @@ describe("hanldeBlur", () => { fetchInput.call(combobox); fetchList.call(combobox); - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); combobox.setAttribute("force-value", "true"); // Act @@ -580,7 +580,7 @@ describe("hanldeBlur", () => { // Assert setTimeout(() => { - expect(combobox._input!.value).toBe("Item 1"); + expect(combobox.input!.value).toBe("Item 1"); }, 100); }); @@ -598,7 +598,7 @@ describe("hanldeBlur", () => { fetchInput.call(combobox); fetchList.call(combobox); - combobox._input!.setAttribute("aria-expanded", "true"); + combobox.input!.setAttribute("aria-expanded", "true"); combobox.setAttribute("force-value", ""); // Act @@ -606,7 +606,7 @@ describe("hanldeBlur", () => { // Assert setTimeout(() => { - expect(combobox._input!.value).toBe(""); + expect(combobox.input!.value).toBe(""); }, 100); }); }); diff --git a/test/helpers.test.ts b/test/helpers.test.ts index 76bf2f8..217c76f 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -14,10 +14,10 @@ describe("fetchList", () => {
  • Item 4
  • `; - combobox._list = null; - expect(combobox._list).toEqual(null); + combobox.list = null; + expect(combobox.list).toEqual(null); fetchList.call(combobox); - expect(combobox._list).toEqual(combobox.querySelector("#list-element")); + expect(combobox.list).toEqual(combobox.querySelector("#list-element")); }); test("gets element marked with data-list if present", () => { @@ -33,10 +33,10 @@ describe("fetchList", () => { `; - combobox._list = null; - expect(combobox._list).toEqual(null); + combobox.list = null; + expect(combobox.list).toEqual(null); fetchList.call(combobox); - expect(combobox._list).toEqual(combobox.querySelector("#list-element")); + expect(combobox.list).toEqual(combobox.querySelector("#list-element")); }); test("does not get element marked with data-list if outside slot list", () => { @@ -51,10 +51,10 @@ describe("fetchList", () => {
  • Item 4
  • `; - combobox._list = null; - expect(combobox._list).toEqual(null); + combobox.list = null; + expect(combobox.list).toEqual(null); fetchList.call(combobox); - expect(combobox._list).toEqual(combobox.querySelector("#list-element")); + expect(combobox.list).toEqual(combobox.querySelector("#list-element")); }); test("throws error if no list element is found", () => { @@ -62,8 +62,8 @@ describe("fetchList", () => { combobox.innerHTML = ` `; - combobox._list = null; - expect(combobox._list).toEqual(null); + combobox.list = null; + expect(combobox.list).toEqual(null); expect(() => fetchList.call(combobox)).toThrowError("List element not found"); }); @@ -80,10 +80,10 @@ describe("fetchList", () => { const something = document.createElement("ul"); - combobox._list = something; - expect(combobox._list).toEqual(something); + combobox.list = something; + expect(combobox.list).toEqual(something); fetchList.call(combobox); - expect(combobox._list).toEqual(something); + expect(combobox.list).toEqual(something); }); }); @@ -101,10 +101,10 @@ describe("fetchOriginalList", () => { const something = document.createElement("ul"); - combobox._originalList = something; - expect(combobox._originalList).toEqual(something); + combobox.originalList = something; + expect(combobox.originalList).toEqual(something); fetchOriginalList.call(combobox); - expect(combobox._originalList).toEqual(something); + expect(combobox.originalList).toEqual(something); }); test("gets a clone of the list element if list element is already stored", () => { @@ -118,11 +118,11 @@ describe("fetchOriginalList", () => {
  • Item 4
  • `; - combobox._list = combobox.querySelector("#list-element"); - combobox._originalList = null; - expect(combobox._originalList).toEqual(null); + combobox.list = combobox.querySelector("#list-element"); + combobox.originalList = null; + expect(combobox.originalList).toEqual(null); fetchOriginalList.call(combobox); - expect(combobox._originalList).toEqual(combobox._list!.cloneNode(true)); + expect(combobox.originalList).toEqual(combobox.list!.cloneNode(true)); }); test("if no list is stored calls fetchList and sets original list to clone of list", () => { @@ -135,15 +135,14 @@ describe("fetchOriginalList", () => {
  • Item 3
  • Item 4
  • `; - combobox._list = null; - combobox._originalList = null; - expect(combobox._originalList).toEqual(null); + combobox.list = null; + combobox.originalList = null; + expect(combobox.originalList).toEqual(null); fetchOriginalList.call(combobox); - expect(combobox._originalList).toEqual(combobox._list!.cloneNode(true)); + expect(combobox.originalList).toEqual(combobox.list!.cloneNode(true)); }); }); - describe("fetchInput", () => { test("gets element with marked with slot input", () => { const combobox = document.createElement("combobox-framework") as ComboboxFramework; @@ -156,10 +155,10 @@ describe("fetchInput", () => {
  • Item 4
  • `; - combobox._input = null; - expect(combobox._input).toEqual(null); + combobox.input = null; + expect(combobox.input).toEqual(null); fetchInput.call(combobox); - expect(combobox._input).toEqual(combobox.querySelector("#input-element")); + expect(combobox.input).toEqual(combobox.querySelector("#input-element")); }); test("throws error if no input element is found", () => { @@ -172,8 +171,8 @@ describe("fetchInput", () => {
  • Item 4
  • `; - combobox._input = null; - expect(combobox._input).toEqual(null); + combobox.input = null; + expect(combobox.input).toEqual(null); expect(() => fetchInput.call(combobox)).toThrowError("Input element not found"); }); @@ -190,10 +189,10 @@ describe("fetchInput", () => { const something = document.createElement("input"); - combobox._input = something; - expect(combobox._input).toEqual(something); + combobox.input = something; + expect(combobox.input).toEqual(something); fetchInput.call(combobox); - expect(combobox._input).toEqual(something); + expect(combobox.input).toEqual(something); }); }); @@ -211,10 +210,10 @@ describe("fetchOriginalList", () => { const something = document.createElement("ul"); - combobox._originalList = something; - expect(combobox._originalList).toEqual(something); + combobox.originalList = something; + expect(combobox.originalList).toEqual(something); fetchOriginalList.call(combobox); - expect(combobox._originalList).toEqual(something); + expect(combobox.originalList).toEqual(something); }); test("gets a clone of the list element if list element is already stored", () => { @@ -228,11 +227,11 @@ describe("fetchOriginalList", () => {
  • Item 4
  • `; - combobox._list = combobox.querySelector("#list-element"); - combobox._originalList = null; - expect(combobox._originalList).toEqual(null); + combobox.list = combobox.querySelector("#list-element"); + combobox.originalList = null; + expect(combobox.originalList).toEqual(null); fetchOriginalList.call(combobox); - expect(combobox._originalList).toEqual(combobox._list!.cloneNode(true)); + expect(combobox.originalList).toEqual(combobox.list!.cloneNode(true)); }); test("if no list is stored calls fetchList and sets original list to clone of list", () => { @@ -246,11 +245,11 @@ describe("fetchOriginalList", () => {
  • Item 4
  • `; - combobox._list = null; - combobox._originalList = null; - expect(combobox._originalList).toEqual(null); + combobox.list = null; + combobox.originalList = null; + expect(combobox.originalList).toEqual(null); fetchOriginalList.call(combobox); - expect(combobox._originalList).toEqual(combobox._list!.cloneNode(true)); + expect(combobox.originalList).toEqual(combobox.list!.cloneNode(true)); }); }); @@ -272,11 +271,11 @@ describe("setBasicAttributes", () => { setBasicAttributes.call(combobox); - expect(combobox._input!.id).not.toEqual(""); - expect(combobox._input!.id.split("-")[0]).toEqual("input"); + expect(combobox.input!.id).not.toEqual(""); + expect(combobox.input!.id.split("-")[0]).toEqual("input"); - expect(combobox._list!.id).not.toEqual(""); - expect(combobox._list!.id.split("-")[0]).toEqual("list"); + expect(combobox.list!.id).not.toEqual(""); + expect(combobox.list!.id.split("-")[0]).toEqual("list"); }); test("if id is set on input, does not generate a new one", () => { @@ -295,8 +294,8 @@ describe("setBasicAttributes", () => { setBasicAttributes.call(combobox); - expect(combobox._input!.id).toEqual("input-element"); - expect(combobox._list!.id).toEqual("list-element"); + expect(combobox.input!.id).toEqual("input-element"); + expect(combobox.list!.id).toEqual("list-element"); }); }); @@ -316,17 +315,17 @@ describe("setBasicAttributes", () => { setBasicAttributes.call(combobox); - expect(combobox._input!.getAttribute("role")).toEqual("combobox"); - expect(combobox._input!.getAttribute("aria-controls")).toEqual("list-element"); - expect(combobox._input!.getAttribute("aria-expanded")).toEqual("false"); - expect(combobox._input!.getAttribute("aria-autocomplete")).toEqual("list"); - expect(combobox._input!.getAttribute("autocomplete")).toEqual("off"); + expect(combobox.input!.getAttribute("role")).toEqual("combobox"); + expect(combobox.input!.getAttribute("aria-controls")).toEqual("list-element"); + expect(combobox.input!.getAttribute("aria-expanded")).toEqual("false"); + expect(combobox.input!.getAttribute("aria-autocomplete")).toEqual("list"); + expect(combobox.input!.getAttribute("autocomplete")).toEqual("off"); - expect(combobox._list!.getAttribute("role")).toEqual("listbox"); - expect(combobox._list!.getAttribute("aria-multiselectable")).toEqual("false"); - expect(combobox._list!.tabIndex).toEqual(-1); + expect(combobox.list!.getAttribute("role")).toEqual("listbox"); + expect(combobox.list!.getAttribute("aria-multiselectable")).toEqual("false"); + expect(combobox.list!.tabIndex).toEqual(-1); - const listItems = combobox._list!.querySelectorAll("li"); + const listItems = combobox.list!.querySelectorAll("li"); for (const item of listItems) { expect(item.getAttribute("role")).toEqual("option"); expect(item.getAttribute("aria-selected")).toEqual("false"); diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index dec38b2..0000000 --- a/vite.config.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - root: "", - base: "./", - build: { - outDir: "docs", - emptyOutDir: true, - }, -};