Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/development' into feat-nuxt-st…
Browse files Browse the repository at this point in the history
…onecrop
  • Loading branch information
Rohan Bansal committed Jan 20, 2025
2 parents b482717 + caa821f commit 8979acd
Show file tree
Hide file tree
Showing 46 changed files with 614 additions and 191 deletions.
24 changes: 24 additions & 0 deletions aform/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
{
"name": "@stonecrop/aform",
"entries": [
{
"version": "0.3.11",
"tag": "@stonecrop/aform_v0.3.11",
"date": "Mon, 20 Jan 2025 07:32:03 GMT",
"comments": {
"none": [
{
"comment": "allow external clicks, fix on result selection and clear search when necesary\""
}
]
}
},
{
"version": "0.3.10",
"tag": "@stonecrop/aform_v0.3.10",
"date": "Wed, 15 Jan 2025 14:30:10 GMT",
"comments": {}
},
{
"version": "0.3.9",
"tag": "@stonecrop/aform_v0.3.9",
"date": "Mon, 13 Jan 2025 11:31:50 GMT",
"comments": {}
},
{
"version": "0.3.8",
"tag": "@stonecrop/aform_v0.3.8",
Expand Down
19 changes: 18 additions & 1 deletion aform/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
# Change Log - @stonecrop/aform

This log was last generated on Thu, 09 Jan 2025 11:21:58 GMT and should not be manually modified.
This log was last generated on Mon, 20 Jan 2025 07:32:03 GMT and should not be manually modified.

## 0.3.11
Mon, 20 Jan 2025 07:32:03 GMT

### Updates

- allow external clicks, fix on result selection and clear search when necesary"

## 0.3.10
Wed, 15 Jan 2025 14:30:10 GMT

_Version update only_

## 0.3.9
Mon, 13 Jan 2025 11:31:50 GMT

_Version update only_

## 0.3.8
Thu, 09 Jan 2025 11:21:58 GMT
Expand Down
3 changes: 2 additions & 1 deletion aform/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stonecrop/aform",
"version": "0.3.8",
"version": "0.3.11",
"license": "MIT",
"type": "module",
"author": {
Expand Down Expand Up @@ -45,6 +45,7 @@
"@stonecrop/themes": "workspace:*",
"@stonecrop/utilities": "workspace:*",
"@vueuse/core": "^12.0.0",
"@vueuse/components": "^12.0.0",
"vue": "^3.5.11"
},
"devDependencies": {
Expand Down
163 changes: 85 additions & 78 deletions aform/src/components/form/ADropdown.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
<template>
<div class="autocomplete" :class="{ isOpen: isOpen }">
<div class="autocomplete" :class="{ isOpen: dropdown.open }" v-on-click-outside="onClickOutside">
<div class="input-wrapper">
<input
ref="mopInput"
type="text"
@input="onChange"
@focus="onChange"
v-model="search"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter="onEnter" />

<ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results">
<li class="loading autocomplete-result" v-if="isLoading">Loading results...</li>
type="text"
@input="filter"
@focus="openDropdown"
@keydown.down="selectNextResult"
@keydown.up="selectPrevResult"
@keydown.enter="setCurrentResult"
@keydown.esc="onClickOutside"
@keydown.tab="onClickOutside" />

<ul id="autocomplete-results" v-show="dropdown.open" class="autocomplete-results">
<li class="loading autocomplete-result" v-if="dropdown.loading">Loading results...</li>
<li
v-else
v-for="(result, i) in results"
:key="i"
@click="setResult(result)"
v-for="(result, i) in dropdown.results"
:key="result"
@click.stop="setResult(result)"
class="autocomplete-result"
:class="{ 'is-active': i === arrowCounter }">
:class="{ 'is-active': i === dropdown.activeItemIndex }">
{{ result }}
</li>
</ul>
Expand All @@ -29,94 +30,99 @@
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref /* useTemplateRef */ } from 'vue'
import { vOnClickOutside } from '@vueuse/components'
import { reactive } from 'vue'
const { label, items, isAsync } = defineProps<{
const { label, items, isAsync, filterFunction } = defineProps<{
label: string
items?: string[]
isAsync?: boolean
filterFunction?: (search: string) => string[] | Promise<string[]>
}>()
const emit = defineEmits(['filterChanged'])
const results = ref(items)
const search = defineModel<string>()
const isLoading = ref(false)
const arrowCounter = ref(0)
const isOpen = ref(false)
// const mopInput = useTemplateRef<HTMLInputElement>('mopInput')
onMounted(() => {
document.addEventListener('click', handleClickOutside)
filterResults()
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
const dropdown = reactive({
activeItemIndex: null as number | null,
open: false,
loading: false,
results: items,
})
const setResult = result => {
search.value = result
closeResults()
}
const filterResults = () => {
if (!search.value) {
results.value = items
} else {
results.value = items.filter(item => {
return item.toLowerCase().indexOf(search.value.toLowerCase()) > -1
})
}
}
const onChange = () => {
isOpen.value = true
if (isAsync) {
isLoading.value = true
emit('filterChanged', search.value)
const onClickOutside = () => closeDropdown()
const filter = async () => {
dropdown.open = true
if (filterFunction) {
if (isAsync) dropdown.loading = true
try {
const filteredResults = await filterFunction(search.value || '')
dropdown.results = filteredResults
} catch {
dropdown.results = []
} finally {
if (isAsync) dropdown.loading = false
}
} else {
filterResults()
}
}
const handleClickOutside = () => {
closeResults()
arrowCounter.value = 0
const setResult = (result: string) => {
search.value = result
closeDropdown(result)
}
const closeResults = () => {
isOpen.value = false
const openDropdown = () => {
dropdown.activeItemIndex = isAsync ? null : search.value ? items?.indexOf(search.value) || null : null
dropdown.open = true
// TODO: this should probably call the async function if it's async
dropdown.results = isAsync ? [] : items
}
// TODO: (test) when would this occur? how should this be tested?
if (!items.includes(search.value)) {
const closeDropdown = (result?: string) => {
dropdown.activeItemIndex = null
dropdown.open = false
if (!items?.includes(result || search.value || '')) {
search.value = ''
}
}
const onArrowDown = () => {
if (arrowCounter.value < results.value.length) {
arrowCounter.value = arrowCounter.value + 1
const filterResults = () => {
if (!search.value) {
dropdown.results = items
} else {
dropdown.results = items?.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
}
}
const onArrowUp = () => {
if (arrowCounter.value > 0) {
arrowCounter.value = arrowCounter.value - 1
const selectNextResult = () => {
const resultsLength = dropdown.results?.length || 0
if (dropdown.activeItemIndex != null) {
const currentIndex = isNaN(dropdown.activeItemIndex) ? 0 : dropdown.activeItemIndex
dropdown.activeItemIndex = (currentIndex + 1) % resultsLength
} else {
dropdown.activeItemIndex = 0
}
}
const onEnter = () => {
search.value = results.value[arrowCounter.value]
closeResults()
arrowCounter.value = 0
const selectPrevResult = () => {
const resultsLength = dropdown.results?.length || 0
if (dropdown.activeItemIndex != null) {
const currentIndex = isNaN(dropdown.activeItemIndex) ? 0 : dropdown.activeItemIndex
dropdown.activeItemIndex = (currentIndex - 1 + resultsLength) % resultsLength
} else {
dropdown.activeItemIndex = resultsLength - 1
}
}
// const openWithSearch = () => {
// search.value = ''
// onChange()
// mopInput.value.focus()
// }
const setCurrentResult = () => {
if (dropdown.results) {
const currentIndex = dropdown.activeItemIndex || 0
const result = dropdown.results[currentIndex]
setResult(result)
}
dropdown.activeItemIndex = 0
}
</script>

<style scoped>
Expand All @@ -126,7 +132,6 @@ const onEnter = () => {
}
.input-wrapper {
min-width: 40ch;
border: 1px solid transparent;
padding: 0rem;
margin: 0rem;
Expand Down Expand Up @@ -167,25 +172,27 @@ label {
.autocomplete-results {
position: absolute;
width: calc(100% - 1ch + 1.5px);
z-index: 1;
z-index: 999;
padding: 0;
margin: 0;
color: #000000;
color: var(--sc-input-active-border-color);
border: 1px solid var(--sc-input-active-border-color);
border-radius: 0 0 0.25rem 0.25rem;
border-top: none;
background-color: #fff;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 6px;
cursor: pointer;
border-bottom: 0.5px solid lightgray;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: var(--sc-row-color-zebra-light);
color: #000000;
color: var(--sc-input-active-border-color);
}
</style>
45 changes: 37 additions & 8 deletions aform/tests/dropdown.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'

import ADropdown from '../src/components/form/ADropdown.vue'

Expand Down Expand Up @@ -56,6 +56,7 @@ describe('dropdown input component', () => {

// arrow down to select the second item
await input.trigger('keydown', { key: 'ArrowDown' })
await input.trigger('keydown', { key: 'ArrowDown' })
await input.trigger('keydown', { key: 'Enter' })

updateEvents = wrapper.emitted('update:modelValue')
Expand All @@ -78,9 +79,9 @@ describe('dropdown input component', () => {
expect(updateEvents![3]).toEqual(['Apple'])
})

it('emits filter change event when dropdown item is selected using mouse in async', async () => {
it('emits filter change event when dropdown item is selected using mouse in sync', async () => {
const wrapper = mount(ADropdown, {
props: { modelValue: dropdownData.value, label: dropdownData.label, items: dropdownData.items, isAsync: true },
props: { modelValue: dropdownData.value, label: dropdownData.label, items: dropdownData.items, isAsync: false },
})

await wrapper.find('input').setValue('')
Expand All @@ -94,11 +95,39 @@ describe('dropdown input component', () => {
await wrapper.vm.$nextTick()

valueUpdateEvents = wrapper.emitted('update:modelValue')
expect(valueUpdateEvents).toHaveLength(1)
expect(valueUpdateEvents![0]).toEqual([''])
expect(valueUpdateEvents).toHaveLength(2)
expect(valueUpdateEvents![1]).toEqual(['Apple'])
})

it('emits filter change event when dropdown item is selected using mouse in async', async () => {
const mockFilterFunction = vi.fn(search => {
if (search === 'a') {
return ['Apple', 'Orange', 'Pear']
}
return []
})

const filterChangedEvents = wrapper.emitted('filterChanged')
expect(filterChangedEvents).toHaveLength(1)
expect(filterChangedEvents![0]).toEqual([''])
const wrapper = mount(ADropdown, {
props: {
modelValue: dropdownData.value,
label: dropdownData.label,
items: dropdownData.items,
isAsync: true,
filterFunction: mockFilterFunction,
},
})

const input = wrapper.find('input')
await input.setValue('a')
await wrapper.vm.$nextTick()

expect(mockFilterFunction).toHaveBeenCalledWith('a')
expect(mockFilterFunction).toHaveBeenCalledTimes(1)

const liElements = wrapper.findAll('li')
expect(liElements).toHaveLength(3)
expect(liElements.at(0)?.text()).toBe('Apple')
expect(liElements.at(1)?.text()).toBe('Orange')
expect(liElements.at(2)?.text()).toBe('Pear')
})
})
Loading

0 comments on commit 8979acd

Please sign in to comment.