diff --git a/packages/oruga/src/components/autocomplete/Autocomplete.vue b/packages/oruga/src/components/autocomplete/Autocomplete.vue
index 3b54bce10..5bad84ab2 100644
--- a/packages/oruga/src/components/autocomplete/Autocomplete.vue
+++ b/packages/oruga/src/components/autocomplete/Autocomplete.vue
@@ -284,7 +284,8 @@ function setSelected(item: T | SpecialOption | undefined): void {
inputValue.value = props.clearOnSelect ? "" : option?.label || "";
checkHtml5Validity();
- setFocus();
+ if (props.keepOpen) setFocus();
+ else isActive.value = false;
}
// --- Event Handler ---
diff --git a/packages/oruga/src/components/autocomplete/examples/index.vue b/packages/oruga/src/components/autocomplete/examples/index.vue
index 11cffea1e..8ced950b0 100644
--- a/packages/oruga/src/components/autocomplete/examples/index.vue
+++ b/packages/oruga/src/components/autocomplete/examples/index.vue
@@ -5,6 +5,9 @@ import BaseCode from "./base.vue?raw";
import Options from "./options.vue";
import OptionsCode from "./options.vue?raw";
+import Slots from "./slots.vue";
+import SlotsCode from "./slots.vue?raw";
+
import Selection from "./selection.vue";
import SelectionCode from "./selection.vue?raw";
@@ -61,6 +64,17 @@ import ScrollCode from "./scroll.vue?raw";
+
+
+ A header and a footer can be added to the options list by using the
+ header
and footer
slots. The header and
+ footer can be made clickable by adding the
+ selectable-header
and
+ selectable-footer
props. Clicking them will clear the
+ input.
+
+
+
When check-scroll
prop is set, the component will emits
diff --git a/packages/oruga/src/components/autocomplete/examples/options.vue b/packages/oruga/src/components/autocomplete/examples/options.vue
index 06344a048..641f06cfc 100644
--- a/packages/oruga/src/components/autocomplete/examples/options.vue
+++ b/packages/oruga/src/components/autocomplete/examples/options.vue
@@ -241,12 +241,7 @@ const groupSelected = ref();
v-model="selected"
:options="options"
placeholder="e.g. Anne"
- open-on-focus
- selectable-header
- selectable-footer>
- No results found
- Header
- Footer
+ open-on-focus>
Selected: {{ selected }}
diff --git a/packages/oruga/src/components/autocomplete/examples/slots.vue b/packages/oruga/src/components/autocomplete/examples/slots.vue
new file mode 100644
index 000000000..bb7dda94e
--- /dev/null
+++ b/packages/oruga/src/components/autocomplete/examples/slots.vue
@@ -0,0 +1,36 @@
+
+
+
+
+ No results found
+ Header
+ Footer
+
+
+
+
diff --git a/packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts b/packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts
deleted file mode 100644
index abff1fedd..000000000
--- a/packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// import SimpleAutocomplete from "./SimpleAutocomplete.vue";
-
-// describe("", () => {
-// it("works", () => {
-// cy.mount(SimpleAutocomplete);
-// cy.get("input").click().type("j");
-// cy.get("input").type("{downarrow}{downarrow}{enter}");
-// cy.get("input").should("have.value", "Node.js");
-// });
-// });
diff --git a/packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue b/packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue
deleted file mode 100644
index 113e4dc0a..000000000
--- a/packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
- Selected: {{ selected }}
-
-
- No results found
-
-
-
diff --git a/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap b/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap
index 2dbf05fab..6906432b0 100644
--- a/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap
+++ b/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap
@@ -25,13 +25,142 @@ exports[`OAutocomplete tests > render correctly 1`] = `
@binding {number} focusedIndex - index of the focused element
@binding {() => void} toggle - toggle dropdown active state
-->
-
-
+
+
+
+ Angular
+
+
+
+ Angular 2
+
+
+
+ Aurelia
+
+
+
+ Backbone
+
+
+
+ Ember
+
+
+
+ jQuery
+
+
+
+ Meteor
+
+
+
+ Node.js
+
+
+
+ Polymer
+
+
+
+ React
+
+
+
+ RxJS
+
+
+
+ Vue.js
+
+
+
diff --git a/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts b/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts
index 2522a5ea1..e43e52adf 100644
--- a/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts
+++ b/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts
@@ -1,21 +1,348 @@
-import { describe, test, expect, afterEach } from "vitest";
+import { describe, test, expect, afterEach, vi } from "vitest";
import { enableAutoUnmount, mount } from "@vue/test-utils";
-
-import OAutocomplete from "@/components/autocomplete/Autocomplete.vue";
+import { nextTick } from "vue";
+import { setTimeout } from "timers/promises";
import type { OptionsGroupProp, OptionsItem, OptionsProp } from "@/composables";
+import OAutocomplete from "@/components/autocomplete/Autocomplete.vue";
+
describe("OAutocomplete tests", () => {
enableAutoUnmount(afterEach);
+ const OPTIONS = [
+ "Angular",
+ "Angular 2",
+ "Aurelia",
+ "Backbone",
+ "Ember",
+ "jQuery",
+ "Meteor",
+ "Node.js",
+ "Polymer",
+ "React",
+ "RxJS",
+ "Vue.js",
+ ];
+
test("render correctly", () => {
- const wrapper = mount(OAutocomplete);
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS },
+ });
expect(!!wrapper.vm).toBeTruthy();
expect(wrapper.exists()).toBeTruthy();
expect(wrapper.attributes("data-oruga")).toBe("autocomplete");
expect(wrapper.html()).toMatchSnapshot();
});
+ test("has a dropdown menu hidden by default", () => {
+ const wrapper = mount(OAutocomplete, { attachTo: document.body });
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeFalsy();
+ });
+
+ test("can emit input, focus and blur events", async () => {
+ const VALUE_TYPED = "test";
+
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS },
+ });
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ // open menu
+ await input.trigger("focus");
+ expect(wrapper.emitted("focus")).toHaveLength(1);
+
+ await input.setValue(VALUE_TYPED);
+ await input.trigger("input");
+
+ expect(wrapper.emitted("update:input")).toHaveLength(1);
+ expect(wrapper.emitted("update:input")![0]).toContain(VALUE_TYPED);
+
+ await input.trigger("blur");
+ expect(wrapper.emitted("blur")).toBeDefined();
+ });
+
+ test("can autocomplete with keydown", async () => {
+ const VALUE_TYPED = "Ang";
+
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, openOnFocus: true, keepOpen: false },
+ attachTo: document.body,
+ });
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ // open menu
+ await input.trigger("focus");
+ await input.setValue(VALUE_TYPED);
+
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeTruthy();
+
+ await input.trigger("keydown", { key: "Down" });
+ await input.trigger("keydown", { key: "Enter" });
+
+ expect(input.element.value).toBe(OPTIONS[0]);
+ expect(wrapper.emitted("select")).toStrictEqual([[OPTIONS[0]]]);
+ expect(wrapper.emitted("update:modelValue")).toStrictEqual([
+ [OPTIONS[0]],
+ ]);
+
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeFalsy();
+ });
+
+ test("close dropdown on esc", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, openOnFocus: true },
+ attachTo: document.body,
+ });
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ // open menu
+ await input.trigger("focus");
+
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeTruthy();
+
+ await input.trigger("keydown", { key: "Escape" });
+
+ expect(dropdown.isVisible()).toBeFalsy();
+ });
+
+ test("close dropdown on click outside", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, openOnFocus: true },
+ attachTo: document.body,
+ });
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ // open menu
+ await input.trigger("focus");
+ await setTimeout(); // await event handler get set
+
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeTruthy();
+
+ // click outside
+ window.dispatchEvent(new Event("click"));
+ await nextTick(); // await dom update
+
+ expect(dropdown.isVisible()).toBeFalsy();
+ });
+
+ test("open dropdown on down key click", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS },
+ attachTo: document.body,
+ });
+
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeFalsy();
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ await input.trigger("focus");
+ await input.trigger("keydown", { key: "Down" });
+
+ expect(dropdown.isVisible()).toBeTruthy();
+ });
+
+ test("manages tab pressed as expected", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, openOnFocus: true, keepFirst: true },
+ attachTo: document.body,
+ });
+
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeFalsy();
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ await input.trigger("keydown", { key: "Tab" });
+ expect(dropdown.isVisible()).toBeFalsy();
+
+ await input.trigger("focus");
+
+ await input.trigger("keydown", { key: "Tab" });
+ expect(input.element.value).toBe("");
+ });
+
+ test("can openOnFocus and keepFirst", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, openOnFocus: true, keepFirst: true },
+ attachTo: document.body,
+ });
+
+ const dropdown = wrapper.find(".o-drop__menu");
+ expect(dropdown.exists()).toBeTruthy();
+ expect(dropdown.isVisible()).toBeFalsy();
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ await input.trigger("focus");
+
+ expect(dropdown.isVisible()).toBeTruthy();
+ });
+
+ test("reset events before destroy", async () => {
+ document.removeEventListener = vi.fn();
+ window.removeEventListener = vi.fn();
+
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS },
+ });
+ await setTimeout(); // await event handler get set
+
+ wrapper.unmount();
+
+ expect(document.removeEventListener).toBeCalledTimes(2);
+ // remove scroll listener
+ expect(document.removeEventListener).toBeCalledWith(
+ "scroll",
+ expect.any(Function),
+ );
+
+ expect(window.removeEventListener).toBeCalledTimes(2);
+ // remove position listener
+ expect(window.removeEventListener).toBeCalledWith(
+ "resize",
+ expect.any(Function),
+ );
+ });
+
+ test("clear button does not exist when the search input is empty", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: {
+ options: OPTIONS,
+ modelValue: "",
+ clearable: true,
+ },
+ });
+
+ const subject = wrapper.find(".o-icon");
+ expect(subject.exists()).toBeFalsy();
+ });
+
+ test("clears search input text when clear button gets clicked", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: {
+ options: OPTIONS,
+ modelValue: OPTIONS[5],
+ clearable: true,
+ },
+ });
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+ expect(input.element.value).toBe(OPTIONS[5]);
+
+ const icon = wrapper.find(".o-icon");
+ expect(icon.exists()).toBeTruthy();
+ await icon.trigger("click");
+
+ expect(input.element.value).toEqual("");
+ });
+
+ test("clear button does not appear when clearable property is not set to true", () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, modelValue: OPTIONS[5] },
+ });
+ const subject = wrapper.find(".o-icon").exists();
+
+ expect(subject).toBeFalsy();
+ });
+
+ test("can emit select-header by keyboard and click", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: {
+ openOnFocus: true,
+ keepOpen: true,
+ selectableHeader: true,
+ selectableFooter: true,
+ },
+ slots: {
+ header: "SLOT HEADER
",
+ footer: "SLOT FOOTER
",
+ },
+ });
+
+ const input = wrapper.find("input");
+ expect(input.exists()).toBeTruthy();
+
+ // open menu
+ await input.trigger("focus");
+
+ // move to header and select by enter
+ await input.trigger("keydown", { key: "Down" });
+ await input.trigger("keydown", { key: "Enter" });
+
+ expect(wrapper.emitted("select-header")).toHaveLength(1);
+
+ const header = wrapper.find(".o-acp__item-header");
+ expect(header.exists()).toBeTruthy();
+ await header.trigger("click");
+
+ expect(wrapper.emitted("select-header")).toHaveLength(2);
+ });
+
+ test("can emit select-footer by keyboard and click", async () => {
+ const wrapper = mount(OAutocomplete, {
+ props: {
+ openOnFocus: true,
+ keepOpen: true,
+ selectableHeader: true,
+ selectableFooter: true,
+ },
+ slots: {
+ header: "SLOT HEADER
",
+ footer: "SLOT FOOTER
",
+ },
+ });
+ const input = wrapper.find("input");
+
+ // open menu
+ await input.trigger("focus");
+
+ // move to footer and select by enter
+ await input.trigger("keydown", { key: "Down" });
+ await input.trigger("keydown", { key: "Down" });
+ await input.trigger("keydown", { key: "Enter" });
+
+ expect(wrapper.emitted("select-footer")).toHaveLength(1);
+
+ const footer = wrapper.find(".o-acp__item-footer");
+ expect(footer.exists()).toBeTruthy();
+ await footer.trigger("click");
+
+ expect(wrapper.emitted("select-footer")).toHaveLength(2);
+ });
+
+ test("has configurable menu and item tags", () => {
+ const wrapper = mount(OAutocomplete, {
+ props: { options: OPTIONS, menuTag: "ul", itemTag: "li" },
+ });
+ expect(wrapper.find("ul.o-drop__menu").exists()).toBeTruthy();
+ expect(wrapper.find("li.o-acp__item").exists()).toBeTruthy();
+ });
+
describe("render options props correctly", () => {
test("handle options as primitves correctly", () => {
const options: OptionsProp = ["Flint", "Silver", "Vane", 0, 1, 2];
diff --git a/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap b/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap
index 25ec22b37..2a11ddb93 100644
--- a/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap
+++ b/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`Dropdown tests > render correctly with items 1`] = `
+exports[`ODropdown tests > render correctly with items 1`] = `
"