Skip to content

Commit

Permalink
feat(select)!: reuse AFloating & AListcomponents
Browse files Browse the repository at this point in the history
  • Loading branch information
jd-solanki committed Feb 9, 2023
1 parent e25f20c commit 21329e8
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 120 deletions.
12 changes: 6 additions & 6 deletions docs/components/demos/list/DemoListVModelSupport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const items = [
{ text: 'Cake gummi', disabled: true },
]
const itemsPropSelection = ref(0)
const slotSelection = ref(0)
const itemsPropSelection = ref<typeof items[number] | null>(null)
const slotSelection = ref<typeof items[number] | null>(null)
</script>

<template>
Expand All @@ -25,7 +25,7 @@ const slotSelection = ref(0)
<hr class="my-2">
<AList
class="mb-0"
:items="[{ subtitle: `Selected: ${itemsPropSelection}` }]"
:items="[{ subtitle: `Selected: ${itemsPropSelection && itemsPropSelection.text}` }]"
/>
</template>
</AList>
Expand All @@ -44,16 +44,16 @@ const slotSelection = ref(0)
:key="item.text"
:text="item.text"
:value="index"
:disable="item.disable"
:is-active="slotSelection === index"
:disabled="item.disabled"
:is-active="slotSelection?.text === item.text"
@click="handleListItemClick(item, index)"
/>
</template>
<template #after>
<hr class="my-2">
<AList
class="mb-0"
:items="[{ subtitle: `Selected: ${slotSelection}` }]"
:items="[{ subtitle: `Selected: ${slotSelection && slotSelection.text}` }]"
/>
</template>
</AList>
Expand Down
10 changes: 5 additions & 5 deletions docs/components/demos/select/DemoSelectBasic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ const fruits = ['banana', 'apple', 'watermelon', 'orange']
// Framework
const selectedFramework = ref()
const frameworks = [
{ label: 'VueJS', value: 'vue' },
{ label: 'ReactJS', value: 'react' },
{ label: 'AngularJS', value: 'angular' },
{ label: 'SolidJS', value: 'solid' },
{ label: 'AlpineJS', value: 'alpine' },
{ text: 'VueJS', value: 'vue' },
{ text: 'ReactJS', value: 'react' },
{ text: 'AngularJS', value: 'angular' },
{ text: 'SolidJS', value: 'solid' },
{ text: 'AlpineJS', value: 'alpine' },
]
</script>

Expand Down
7 changes: 7 additions & 0 deletions packages/anu-vue/src/components/floating/AFloating.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const emit = defineEmits<{
defineOptions({
name: 'AFloating',
inheritAttrs: false,
})
const { teleportTarget } = useTeleport()
Expand Down Expand Up @@ -106,6 +107,11 @@ if (props.modelValue === undefined) {
}
}
}
// Expose: https://vuejs.org/api/sfc-script-setup.html#defineexpose
defineExpose({
refFloating,
})
</script>

<template>
Expand All @@ -117,6 +123,7 @@ if (props.modelValue === undefined) {
<Transition :name="props.transition || undefined">
<div
v-show="props.modelValue ?? isFloatingElVisible"
v-bind="$attrs"
ref="refFloating"
class="a-floating"
:style="{
Expand Down
18 changes: 8 additions & 10 deletions packages/anu-vue/src/components/floating/middlewares.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import type { ElementRects } from '@floating-ui/vue'
import type { Ref } from 'vue'

export const sameWidth = (floatingEl: Ref<HTMLElement>) => {
return {
name: 'sameWidth',
fn: ({ rects, x, y }: { rects: ElementRects; x: number; y: number }) => {
// Set width of reference to floating
floatingEl.value.style.minWidth = `${rects.reference.width}px`
export const sameWidth = (floatingEl: Ref<HTMLElement>) => ({
name: 'sameWidth',
fn: ({ rects, x, y }: { rects: ElementRects; x: number; y: number }) => {
// Set width of reference to floating
unrefElement(floatingEl).style.minWidth = `${rects.reference.width}px`

return { x, y }
},
}
}
return { x, y }
},
})
52 changes: 37 additions & 15 deletions packages/anu-vue/src/components/list/AList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,83 @@ import type { ExtractPropTypes, PropType } from 'vue'
import type { ListItemProps } from '@/components/list-item'
import { AListItem } from '@/components/list-item'
import { useGroupModel } from '@/composables'
import { isEmptyArray } from '@/utils/helpers'
import { spacing as spacingProp } from '@/composables/useProps'
import { useSpacing } from '@/composables/useSpacing'
type ListItem = ListItemProps | string
const props = defineProps({
/**
* Items to render in list
*/
items: {
type: Array as PropType<ListItemProps[]>,
'items': {
type: Array as PropType<ListItem[]>,
default: () => [],
},
/**
* Enable selecting multiple list items
*/
multi: Boolean,
'multi': Boolean,
/**
* Bind v-model value to selected list item
*/
modelValue: null,
'modelValue': null,
/**
* By default when icon props are used icon rendered at start. Use `iconAppend` to render icon at end.
*/
iconAppend: Boolean,
'iconAppend': Boolean,
/**
* By default when avatar props are used avatar is added at start. Use `avatarAppend` to render avatar at end.
*/
avatarAppend: Boolean,
'avatarAppend': Boolean,
// 鈩癸笍 Workaround for checking if event is present on component instance: https://github.com/vuejs/core/issues/5220#issuecomment-1007488240
'onClick:item': Function,
'spacing': spacingProp,
})
const emit = defineEmits<{
(e: 'update:modelValue', value: (ExtractPropTypes<typeof props>)['modelValue']): void
// 鈩癸笍 Fix type => (e: 'click:item', value: (ExtractPropTypes<typeof props>)['items'][number]): void
(e: 'click:item', value: { index: number; item: ListItem; value: any }): void
}>()
defineOptions({
name: 'AList',
})
const spacing = useSpacing(toRef(props, 'spacing'))
const extractItemValueFromItemOption = (item: ListItem) => typeof item === 'string' ? item : (item.value || item)
const { options, select: selectListItem, value } = useGroupModel({
options: !isEmptyArray(props.items) && props.items[0].value
? props.items.map(i => i.value)
: props.items.length,
options: props.items.map(i => extractItemValueFromItemOption(i)),
multi: props.multi,
})
// const isActive = computed(() => options.value[itemIndex].isSelected)
const handleListItemClick = (item: ListItemProps, index: number) => {
selectListItem(item.value || index)
const handleListItemClick = (item: ListItem, index: number) => {
selectListItem(extractItemValueFromItemOption(item) || index)
emit('update:modelValue', value.value)
emit('click:item', {
index,
item,
value: value.value,
})
}
</script>

<template>
<ul class="a-list grid gap-$a-list-gap">
<ul
class="a-list grid gap-$a-list-gap"
:style="[{ '--a-spacing': spacing / 100 }]"
>
<!-- 馃憠 Slot: before -->
<li v-if="$slots.before">
<slot name="before" />
Expand All @@ -69,13 +90,14 @@ const handleListItemClick = (item: ListItemProps, index: number) => {
<AListItem
v-for="(item, index) in props.items"
:key="index"
v-bind="item"
:text="typeof item === 'string' ? item : undefined"
v-bind="typeof item === 'string' ? {} : item"
:avatar-append="props.avatarAppend"
:icon-append="props.iconAppend"
:is-active="options[index].isSelected as unknown as boolean"
:value="props.modelValue !== undefined ? options[index] : undefined"
v-on="{
click: item.value || (props.modelValue !== undefined)
click: props['onClick:item'] || (props.modelValue !== undefined)
? () => handleListItemClick(item, index)
: null,
}"
Expand Down
113 changes: 36 additions & 77 deletions packages/anu-vue/src/components/select/ASelect.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
<script lang="ts" setup>
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
import { flip, offset, shift } from '@floating-ui/vue'
import { defu } from 'defu'
import type { ExtractPropTypes, PropType } from 'vue'
import { ACard, AList } from '@/components'
import { ABaseInput, baseInputProps } from '@/components/base-input'
import { useTeleport } from '@/composables/useTeleport'
import { AFloating, sameWidthFloatingUIMiddleware } from '@/components/floating'
import type { ListItemProps } from '@/components/list-item'
import { isObject } from '@/utils/helpers'
export interface ObjectOption { label: string; value: string | number }
type SelectOption = string | number | ObjectOption
const props = defineProps(defu({
// 鈩癸笍 If we want any type need to set `propName: { type: null }`. Using `propName: null` will omit (disable) the prop.
modelValue: { type: null },
options: {
type: [String, Number, Object] as PropType<SelectOption[]>,
type: Array as PropType<ListItemProps[]>,
default: () => [],
},
emitObject: Boolean,
// 鈩癸笍 If we want any type need to set `propName: { type: null }`. Using `propName: null` will omit (disable) the prop.
optionsWrapperClasses: { type: null },
listClasses: { type: null },
}, baseInputProps))
const emit = defineEmits<{
Expand All @@ -36,52 +37,14 @@ defineOptions({
const _baseInputProps = reactivePick(props, Object.keys(baseInputProps) as Array<keyof typeof baseInputProps>)
const { teleportTarget } = useTeleport()
const isMounted = useMounted()
// SECTION Floating
// Template refs
const refReference = ref()
const selectRef = ref<HTMLSelectElement>()
const refFloating = ref()
const selectRef = ref<HTMLSelectElement>()
const isObjectOption = (option: SelectOption) => isObject(option) && 'label' in option && 'value' in option
const isOptionsVisible = ref(false)
const calculateFloatingPosition = async () => {
const { x, y } = await computePosition(refReference.value.refInputContainer, refFloating.value, {
placement: 'bottom-start',
middleware: [
offset(6),
{
name: 'sameWidth',
fn: ({ rects, x, y }) => {
// Set width of reference to floating
refFloating.value.style.width = `${rects.reference.width}px`
return { x, y }
},
},
flip(),
shift({ padding: 10 }),
],
})
Object.assign(refFloating.value.style, {
left: `${x}px`,
top: `${y}px`,
})
}
let floatingUiCleanup: Function = () => { }
onMounted(() => {
nextTick(() => {
floatingUiCleanup = autoUpdate(refReference.value.refInputContainer, refFloating.value, calculateFloatingPosition)
})
})
onBeforeUnmount(() => floatingUiCleanup())
onClickOutside(
refFloating,
_event => {
Expand All @@ -104,27 +67,24 @@ const handleInputClick = () => {
}
// 馃憠 Options
const optionClasses = 'a-select-option states before:transition-none cursor-pointer text-ellipsis overflow-hidden'
const handleOptionClick = (option: SelectOption) => {
const value = isObjectOption(option) && !props.emitObject ? (option as ObjectOption).value : option
emit('change', value)
emit('input', value)
emit('update:modelValue', value)
const handleOptionClick = (item: ListItemProps, value: any) => {
const valueToEmit = props.emitObject ? item : value
emit('change', valueToEmit)
emit('input', valueToEmit)
emit('update:modelValue', valueToEmit)
}
const closeOptions = (event: MouseEvent) => {
if (event.target !== refFloating.value)
isOptionsVisible.value = false
}
// 馃憠 Value
const selectedValue = computed(() => {
const option = props.options.find(option => isObjectOption(option)
? (option as ObjectOption).value === (!props.emitObject ? props.modelValue : (props.modelValue as ObjectOption).value)
: option === props.modelValue)
return option ? isObjectOption(option) ? (option as ObjectOption).label : option : (props.modelValue as ObjectOption | undefined)?.label || ''
})
// 馃憠 Middleware
const middleware = () => [
offset(6),
sameWidthFloatingUIMiddleware(refFloating),
flip(),
shift({ padding: 10 }),
]
</script>

<template>
Expand Down Expand Up @@ -155,33 +115,32 @@ const selectedValue = computed(() => {
ref="selectRef"
readonly
class="a-select-input"
:value="selectedValue"
:value="isObject(modelValue) ? modelValue.text : modelValue"
>
</template>
</ABaseInput>

<!-- 馃憠 Select options -->
<Teleport
v-if="isMounted"
:to="teleportTarget"
<AFloating
:reference-el="refReference && refReference.refInputContainer"
:middleware="middleware"
class="a-select-floating"
>
<ul
<ACard
v-show="isOptionsVisible"
ref="refFloating"
class="a-select-options-container absolute bg-[hsl(var(--a-surface-c))]"
:data-slots="Object.keys($slots)"
class="a-select-options-container bg-[hsl(var(--a-surface-c))]"
:class="props.optionsWrapperClasses"
@click="closeOptions"
>
<slot :attrs="{ class: optionClasses }">
<li
v-for="(option, index) in props.options"
:key="index"
:class="optionClasses"
@click="handleOptionClick(option)"
>
{{ isObjectOption(option) ? (option as ObjectOption).label : option }}
</li>
</slot>
</ul>
</Teleport>
<AList
:items="options"
:value="props.modelValue"
class="a-select-options-list"
:class="props.listClasses"
@click:item="({ item, value }) => handleOptionClick(item, value)"
/>
</ACard>
</AFloating>
</template>
Loading

0 comments on commit 21329e8

Please sign in to comment.