Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
carsoli committed May 20, 2022
1 parent dd6fbfb commit 05abb5b
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
:getEmptyValue="() => (isMultiple ? [] : null)"
:helpTextSlot="helpTextSlot"
>
<div
class="kt-field-select__input-and-tags"
@click.stop="handleInputClick"
>
<div class="kt-field-select__input-and-tags">
<div
v-if="isMultiple && !field.isEmpty"
class="kt-field-select__tags"
Expand Down Expand Up @@ -41,14 +38,13 @@
/>
</div>
</div>
<!-- @blur="setIsDropdownOpen(false)" -->
<input
v-if="isInputVisible"
ref="inputRef"
v-bind="inputProps"
@blur="handleInputBlur"
@click.stop="handleInputClick"
@focus="handleInputFocus"
@input="updateQuery"
@keydown.enter="setIsDropdownOpen(!isDropdownOpen)"
/>
</div>
<template v-slot:actionIcon="{ classes, handleClear, showClear }">
Expand Down Expand Up @@ -104,25 +100,6 @@ import FieldSelectOptions from './Options.vue'
const UPDATE_QUERY = 'update:query'
const isTippyOrInTippy = (element: Element, tippy: Element | null): boolean => {
if (tippy === null) return false
let currentElement: Element | null = element
while (currentElement) {
if (currentElement.id.startsWith('tippy-')) return true // TODO: is this sustainable?
if (currentElement.isSameNode(tippy)) return true // TODO: this one is broken here
currentElement = currentElement?.parentElement
}
return false
}
// const isLogicallyEqualArray = (
// currentValue: MultiValue,
// newValue: MultiValue,
// ) => isEqual([...currentValue].sort(), [...newValue].sort())
const propsSchema = Shared.propsSchema
.merge(Shared.isMultipleSchema)
.merge(Shared.isRemoteSchema)
Expand Down Expand Up @@ -174,9 +151,9 @@ export default defineComponent<
const localQuery = ref<string | null>(null)
const selectTippy = useSelectTippy()
const { isDropdownOpen, isDropdownMounted, ...selectTippy } =
useSelectTippy()
const isInputFocused = ref(false)
const selectedLabel = computed((): string | null => {
if (field.currentValue === null) return null
return (
Expand All @@ -187,13 +164,7 @@ export default defineComponent<
const { forceUpdateKey, forceUpdate } = useForceUpdate()
const isUserInteracting = computed(
() => isInputFocused.value || selectTippy.isDropdownOpen.value,
)
watch(isUserInteracting, (newValue) => {
console.info('isUserInteracting watcher')
watch(isDropdownMounted, (newValue) => {
if (props.isRemote) {
if (!newValue && props.query !== null) emit(UPDATE_QUERY, null)
} else localQuery.value = null
Expand All @@ -215,49 +186,15 @@ export default defineComponent<
o.label.toLowerCase().includes(localQuery.value.toLowerCase()),
),
),
handleInputBlur: async (event: { relatedTarget: HTMLElement }) => {
setTimeout(async () => {
console.info('handleInputBlur')
const blurToElement = event.relatedTarget
if (
!isTippyOrInTippy(blurToElement, selectTippy.tippyContentRef.value)
) {
await Vue.nextTick()
console.info('handleInputBlur close')
selectTippy.setIsDropdownOpen(false)
}
console.info('handleInputBlur remove isInputFocused')
isInputFocused.value = false
}, 500) // 100 isn’t enough...
},
handleInputClick: async () => {
console.info('handleInputClick')
await Vue.nextTick()
console.info('handleInputClick open')
// always show the dropdown if the input was clicked
selectTippy.setIsDropdownOpen(true)
await Vue.nextTick()
console.info('handleInputClick focus')
inputRef.value?.focus()
},
handleInputFocus: () => {
console.info('handleInputFocus')
// always show the dropdown if the input was focused
selectTippy.setIsDropdownOpen(true)
isInputFocused.value = true
},
inputProps: computed(() => ({
class: ['kt-field-select__wrapper'],
forceUpdateKey: forceUpdateKey.value,
placeholder: props.placeholder ?? undefined,
size: 1,
style: !isUserInteracting.value ? 'flex: 1' : undefined,
style: !isDropdownOpen.value ? 'flex: 1' : undefined,
type: 'text',
value: (() => {
if (isUserInteracting.value)
if (isDropdownOpen.value)
return (
(props.isRemote ? props.query : localQuery.value) ?? undefined
)
Expand All @@ -266,21 +203,19 @@ export default defineComponent<
})(),
})),
inputRef,
isDropdownOpen: selectTippy.isDropdownOpen,
isDropdownOpen,
isInputVisible: computed(
// save some space in multi select when tags are shown
() => !props.isMultiple || isUserInteracting.value || field.isEmpty,
() => !props.isMultiple || isDropdownOpen.value || field.isEmpty,
),
tippyContentRef: selectTippy.tippyContentRef,
tippyTriggerRef: selectTippy.tippyTriggerRef,
onOptionsInput: (value: MultiValue) => {
// optimization: no need to change the value if it’s already set
// if (!isLogicallyEqualArray(castArray(field.currentValue), value))
field.setValue(props.isMultiple ? value : value?.[0] ?? null)
// single select: close the tippy instance whenever a selection is made.
// This also intentionally resets the query so that the api-call (for example)
// can already trigger and load the non-filtered options
// This (watcher on isDropdownMounted) also intentionally resets the query
// so that the api-call (for example) can already trigger and load the non-filtered options
if (!props.isMultiple) selectTippy.setIsDropdownOpen(false)
},
optionsValue: computed(() => {
Expand All @@ -291,8 +226,6 @@ export default defineComponent<
: [field.currentValue as SingleValue]
}),
removeTag: (value: Shared.Option['value']) => {
console.info('removeTag', value)
if (!props.isMultiple)
throw new Error(
'GenericSelectField: Unexpected multi function on single select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const useSelectTippy = () => {

// track in a ref because the `tippy.state.isShown` doesn’t immediately update
const isDropdownOpen = ref(false)
const isDropdownMounted = ref(true)

const { tippy } = useTippy(
tippyTriggerRef,
Expand All @@ -28,17 +29,23 @@ export const useSelectTippy = () => {
maxWidth: 'none',
offset: [0, ARROW_HEIGHT],
onShow: () => {
// More correct here, don't move to `onShown()`
isDropdownMounted.value = true

isDropdownOpen.value = true
},
onHide: () => {
isDropdownOpen.value = false
},
onHidden: () => {
isDropdownMounted.value = false
},
placement: 'bottom',
popperOptions: {
modifiers: [sameWidth],
},
theme: 'light-border',
trigger: 'manual',
trigger: 'click',
})),
)

Expand All @@ -54,6 +61,7 @@ export const useSelectTippy = () => {
}

return {
isDropdownMounted,
isDropdownOpen,
setIsDropdownOpen,
tippyContentRef,
Expand Down
16 changes: 8 additions & 8 deletions packages/kotti-ui/source/kotti-field-select/todo.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
- [x] `:helpTextSlot="$slots.helpText"`
- [ ] clicking an option counts as blur haha

- [ ] FIX: if field is multi and not empty and not interacting, hide input

- [ ] restore arrow-key navigation for options (isHovered)
- [ ] (optional) add slot support for options
- [ ] FIX: if field is multi and not empty and not interacting, hide input
- [ ] check that `enter` from `input` doesn't trigger the submit (of a form it's used within)

## 2022-04-20

- 500ms delay on blur works but 100ms doesn’t...
- Consider <https://stackoverflow.com/a/38317768> and setting tippy to manual mode...
- Honestly, the most realistic and sustainable solution is to either move the search query inside the dropdown or to not show the tags
- Consider making `isInputVisible` delayed when closing so that it only updates after 500ms

## V2

1. Up & down arrows
2. Clickoutside <https://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element/38317768#38317768>
3. support `tabIndex`
4. check if data-test is implemented sufficiently
1. Clickoutside <https://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element/38317768#38317768>
2. support `tabIndex`
3. check if data-test is implemented sufficiently

0 comments on commit 05abb5b

Please sign in to comment.