Skip to content

Commit

Permalink
Merge pull request #4 from reside-ic/mrc-4808
Browse files Browse the repository at this point in the history
Mrc 4808 Add Base Select
  • Loading branch information
M-Kusumgar authored Feb 22, 2024
2 parents 4ffb26f + 2dd7c08 commit 4c12880
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 13 deletions.
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"devDependencies": {
"@types/jest": "^29.5.8",
"@vitejs/plugin-vue": "^4.2.3",
"@vitest/coverage-istanbul": "^1.0.4",
"@vitest/coverage-istanbul": "^1.2.2",
"@vue/test-utils": "^2.4.1",
"feather-icons": "^4.29.1",
"jsdom": "^22.1.0",
Expand Down
123 changes: 123 additions & 0 deletions src/components/BaseSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>
<div>
<c-dropdown auto-close="outside"
class="vnm-dropdown"
:popper="false"
:visible="showDropdownMenu"
@hide="$emit('hide')">
<c-dropdown-toggle @click="$emit('toggle-click')">
<slot></slot>
</c-dropdown-toggle>
<c-dropdown-menu class="vnm-menu"
@click.prevent
@mousedown.prevent>
<template v-for="option in flatOptions">
<c-dropdown-item v-show="option.show" class="vnm-item">
<dropdown-item :option="option"
:checked="checkedObject ? checkedObject[option.path.join('/')] : undefined"
@expand="expand"
@collapse="collapse"
@select-item="$emit('select-item', $event)"></dropdown-item>
</c-dropdown-item>
</template>
</c-dropdown-menu>
</c-dropdown>
</div>
</template>

<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { CheckStatus, Option } from '../types';
import { CDropdown, CDropdownToggle, CDropdownItem, CDropdownMenu } from "@coreui/vue";
import useBaseSelect from '../mixins/useBaseSelect';
import DropdownItem from './DropdownItem.vue';
export default defineComponent({
emits: ["hide", "toggle-click", "select-item"],
components: {
CDropdown,
CDropdownToggle,
CDropdownMenu,
CDropdownItem,
DropdownItem
},
props: {
options: {
type: Array as PropType<Option[]>,
default: () => []
},
showDropdownMenu: {
type: Boolean
},
checkedObject: {
type: Object as PropType<Record<string, CheckStatus>>
}
},
setup(props) {
const { flatOptions, expand, collapse } = useBaseSelect(props.options);
return {
flatOptions,
expand,
collapse
}
},
});
</script>

<style scoped>
.vnm-dropdown {
width: 100%;
}
.vnm-menu {
width: 100%;
max-height: min(300px, 40vh);
overflow-y: auto;
}
.vnm-menu::-webkit-scrollbar {
width: 5px;
}
.vnm-menu::-webkit-scrollbar-track {
background: white;
border-radius: 5px;
}
.vnm-menu::-webkit-scrollbar-thumb {
background: lightgray;
border-radius: 5px;
}
.vnm-item {
padding: 0;
display: flex;
}
.item:hover {
cursor: pointer;
}
.dropdown-toggle {
max-width: 100%;
display: flex;
align-items: center;
}
.dropdown-toggle::after {
margin-left: auto !important;
}
.btn {
border: 1px solid #dddddd;
}
.btn:hover {
border: 1px solid #c7c7c7;
}
.btn:focus, .btn.focus {
box-shadow: none;
}
</style>
21 changes: 21 additions & 0 deletions src/mixins/useBaseSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ref } from "vue";
import { FlatOption, Option } from "../types";
import { collapseOptions, expandOptions, getFlattenedOptions } from "../utils.ts";

export default (options: Option[]) => {
const flatOptions = ref<FlatOption[]>(getFlattenedOptions(options));

const expand = (optionPath: string[]) => {
expandOptions(flatOptions.value, optionPath);
};

const collapse = (optionPath: string[]) => {
collapseOptions(flatOptions.value, optionPath);
};

return {
flatOptions,
expand,
collapse
}
}
73 changes: 73 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FlatOption, Option } from "./types";

export const getFlattenedOptions = (options: Option[]) => {
const flatOptions: FlatOption[] = [];
flattenOptions(flatOptions, options)
return flatOptions;
};

export const flattenOptions = (array: FlatOption[], options: Option[], path: string[] = [], show: boolean = true) => {
options.forEach(op => {
const hasChildren = !!(op.hasOwnProperty("children") && op.children && op.children.length > 0);
const newPath = [...path, op.id];

const baseOption = {
id: op.id,
label: op.label,
path: newPath,
show
};

if (hasChildren) {
array.push({...baseOption, hasChildren, open: false});
flattenOptions(array, op.children!, newPath, false);
} else {
array.push({...baseOption, hasChildren});
}
});
};

const arraysAreEqual = (array1: string[], array2: string[]) => {
return JSON.stringify(array1) === JSON.stringify(array2);
};

export const expandOptions = (array: FlatOption[], optionPath: string[]) => {
const openOptionsPaths: string[][] = [optionPath];
let index = array.findIndex(op => arraysAreEqual(op.path, optionPath));

const currentOption = array[index];
if (currentOption.hasChildren) {
currentOption.open = true;
}
index++;

while (index < array.length && array[index].path.length > optionPath.length) {
const currentOption = array[index];
const show = openOptionsPaths.some(openOpPath => {
return openOpPath.length === array[index].path.length - 1 &&
arraysAreEqual(openOpPath, array[index].path.slice(0, -1));
});
currentOption.show = show;

if (currentOption.hasChildren && currentOption.open) {
openOptionsPaths.push(currentOption.path);
}

index++;
}
};

export const collapseOptions = (array: FlatOption[], optionPath: string[]) => {
let index = array.findIndex(op => arraysAreEqual(op.path, optionPath));

const currentOption = array[index];
if (currentOption.hasChildren) {
currentOption.open = false;
}
index++;

while (index < array.length && array[index].path.length > optionPath.length) {
array[index].show = false;
index++;
};
};
114 changes: 114 additions & 0 deletions tests/components/baseSelect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { mount } from "@vue/test-utils";
import BaseSelect from "../../src/components/BaseSelect.vue";
import { CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from "@coreui/vue";
import DropdownItem from "../../src/components/DropdownItem.vue";

const options = [
{
id: "id1",
label: "parent1",
children: [
{
id: "id1_1",
label: "child1"
}
]
},
{
id: "id2",
label: "parent2"
}
];

const flatOptions = [
{
id: 'id1',
label: 'parent1',
path: ['id1'],
show: true,
hasChildren: true,
open: false
},
{
id: 'id1_1',
label: 'child1',
path: ['id1', 'id1_1'],
show: false,
hasChildren: false
},
{
id: 'id2',
label: 'parent2',
path: ['id2'],
show: true,
hasChildren: false
},
];

describe("base select tests", () => {
const getWrapper = (emptyOptions: boolean = false) => {
return mount(BaseSelect, {
props: {
options: emptyOptions ? undefined : options
},
slots: {
default: '<div id="slot">Random</div>'
}
});
};

test("renders as expected for single select", () => {
const wrapper = getWrapper();

const cDropdown = wrapper.findComponent(CDropdown);
expect(cDropdown.props("autoClose")).toBe("outside");
expect(cDropdown.props("popper")).toBe(false);
expect(cDropdown.classes()).toContain("vnm-dropdown");

const cDropdownToggle = wrapper.findComponent(CDropdownToggle);
expect(cDropdownToggle.find("#slot").text()).toBe("Random");

const cDropdownMenu = wrapper.findComponent(CDropdownMenu);
expect(cDropdownMenu.exists()).toBe(true);
expect(cDropdownMenu.classes()).toContain("vnm-menu");

const cDropdownItems = wrapper.findAllComponents(CDropdownItem);
// middle item is a child so not visible right now
expect(cDropdownItems.every((item, index) => index === 1 ? !item.isVisible() : item.isVisible()))
.toBe(true);
expect(cDropdownItems.every(item => expect(item.classes()).toContain("vnm-item"))).toBe(true);

const dropdownItem = wrapper.findAllComponents(DropdownItem);
dropdownItem.forEach((item, index) => {
expect(item.props("option")).toStrictEqual(flatOptions[index]);
expect(item.props("checked")).toBe(undefined);
});
});

test("hide event in cDropdown emits hide", () => {
const wrapper = getWrapper();
const cDropdown = wrapper.findComponent(CDropdown);
cDropdown.vm.$emit("hide");
expect(wrapper.emitted("hide")![0]).toStrictEqual([]);
});

test("click event in cDropdownToggle emits toggle-click", () => {
const wrapper = getWrapper();
const cDropdownToggle = wrapper.findComponent(CDropdownToggle);
cDropdownToggle.vm.$emit("click");
expect(wrapper.emitted("toggle-click")![0]).toStrictEqual([]);
});

test("select item emit works as expected", () => {
const wrapper = getWrapper();
const dropdownItem1 = wrapper.findAllComponents(DropdownItem)[0];
dropdownItem1.vm.$emit("select-item", "test");
expect(wrapper.emitted("select-item")![0][0]).toBe("test");
});

test("empty options render nothing", () => {
const wrapper = getWrapper(true);
const dropdownItems = wrapper.findAllComponents(DropdownItem);
expect(dropdownItems.length).toBe(0);
});
});
Loading

0 comments on commit 4c12880

Please sign in to comment.