Skip to content

Commit

Permalink
refactor(slider): ts to sfc (oku-ui#505)
Browse files Browse the repository at this point in the history
* refactor(slider): ts to sfc

* fix: emits

* chore: revert comment

* chore: fix collection

* chore: update Slider.vue with new utility function

* chore: refactor collection and slider components

* fix slider component

* Update Slider component in Vue package
  • Loading branch information
productdevbook authored Jan 18, 2024
1 parent 00a9ea4 commit 23fbe11
Show file tree
Hide file tree
Showing 26 changed files with 931 additions and 1,213 deletions.
69 changes: 41 additions & 28 deletions packages/vue/src/collection/collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ComponentObjectPropsOptions, Ref, ShallowRef } from 'vue'
import { defineComponent, h, markRaw, reactive, ref, shallowRef, toRefs, watchEffect } from 'vue'
import { reactiveOmit, useComposedRefs, useForwardRef } from '@oku-ui/use-composable'
import { createProvideScope } from '@oku-ui/provide'
import type { ComponentObjectPropsOptions, Ref } from 'vue'
import { defineComponent, h, markRaw, ref, toRefs, watch, watchEffect } from 'vue'
import { useComponentRef } from '@oku-ui/use-composable'
import { createScope } from '@oku-ui/provide'
import { OkuSlot } from '@oku-ui/slot'

export const collectionProps = {
Expand All @@ -19,24 +19,27 @@ export type CollectionElement = HTMLElement
// This is because we encountered issues with generic types that cannot be statically analysed
// due to creating them dynamically via createCollection.

export function createCollection<ItemElement extends HTMLElement, T = object>(name: string, ItemData?: ComponentObjectPropsOptions) {
export function createCollection<ItemElement extends {
$el: CollectionElement
[key: string]: any
}, T = object>(name: string, ItemData?: ComponentObjectPropsOptions) {
/* -----------------------------------------------------------------------------------------------
* CollectionProvider
* --------------------------------------------------------------------------------------------- */

const PROVIDER_NAME = `${name}CollectionProvider`
const [createCollectionProvide, createCollectionScope] = createProvideScope(PROVIDER_NAME)
const [createCollectionProvide, createCollectionScope] = createScope(PROVIDER_NAME)

type ContextValue = {
collectionRef: Ref<ItemElement | undefined>
itemMap: ShallowRef<Map<Ref<ItemElement | null | undefined>, {
itemMap: Ref<Map<ItemElement, {
ref: Ref<ItemElement>
} & T>>
}

const [CollectionProviderImpl, useCollectionInject] = createCollectionProvide<ContextValue>(
const [useCollectionProvide, useCollectionInject] = createCollectionProvide<ContextValue>(
PROVIDER_NAME,
{ collectionRef: ref(undefined), itemMap: shallowRef(new Map()) },
{ collectionRef: ref(undefined), itemMap: ref(new Map()) },
)

const CollectionProvider = defineComponent({
Expand All @@ -47,8 +50,8 @@ export function createCollection<ItemElement extends HTMLElement, T = object>(na
},
setup(props, { slots }) {
const collectionRef = ref<ItemElement>()
const itemMap = shallowRef(new Map())
CollectionProviderImpl({
const itemMap = ref(new Map())
useCollectionProvide({
collectionRef,
itemMap,
scope: props.scope,
Expand Down Expand Up @@ -76,10 +79,14 @@ export function createCollection<ItemElement extends HTMLElement, T = object>(na
},
setup(props, { slots }) {
const inject = useCollectionInject(COLLECTION_SLOT_NAME, props.scope)
const forwardedRef = useForwardRef()
const composedRefs = useComposedRefs(forwardedRef, inject.collectionRef)

return () => h(OkuSlot, { ref: composedRefs }, () => slots.default?.())
const { componentRef } = useComponentRef<ItemElement | null>()

watch(componentRef, () => {
inject.collectionRef.value = componentRef.value as any
})

return () => h(OkuSlot, { ref: componentRef }, slots)
},
})

Expand All @@ -103,24 +110,27 @@ export function createCollection<ItemElement extends HTMLElement, T = object>(na
setup(props, { attrs, slots }) {
const { scope, ...itemData } = toRefs(props)

const _reactive = reactive(itemData)
const reactiveItemData = reactiveOmit(_reactive, (key, _value) => key === undefined)

const refValue = ref<ItemElement | null>()
const forwardedRef = useForwardRef()
const { componentRef, currentElement } = useComponentRef<ItemElement | null>()

const inject = useCollectionInject(ITEM_SLOT_NAME, scope.value)
const composedRefs = useComposedRefs(refValue, forwardedRef)

watchEffect((onClean) => {
inject.itemMap.value.set(markRaw(refValue), { ref: markRaw(refValue), ...(reactiveItemData as any), ...attrs })

onClean(() => {
inject.itemMap.value.delete(refValue)
})
if (currentElement.value && currentElement) {
inject.itemMap.value.set(markRaw(currentElement.value), {
ref: {
value: markRaw(componentRef),
},
...(itemData as any),
...attrs,
})

onClean(() => {
inject.itemMap.value.delete(currentElement.value!)
})
}
})

return () => h(OkuSlot, { ref: composedRefs, ...{ [ITEM_DATA_ATTR]: '' } }, () => slots.default?.())
return () => h(OkuSlot, { ref: componentRef, ...{ [ITEM_DATA_ATTR]: '' } }, slots)
},
})

Expand All @@ -137,13 +147,16 @@ export function createCollection<ItemElement extends HTMLElement, T = object>(na
if (!collectionNode)
return []

const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`))
const orderedNodes = Array.from(collectionNode.$el.querySelectorAll(`[${ITEM_DATA_ATTR}]`))

const items = Array.from(inject.itemMap.value.values())

const orderedItems = items.sort(
(a, b) => {
return orderedNodes.indexOf(a.ref.value) - orderedNodes.indexOf(b.ref.value)
return orderedNodes.indexOf(a.ref.value.$el) - orderedNodes.indexOf(b.ref.value.$el)
},
)

return orderedItems
}
return getItems
Expand Down
18 changes: 9 additions & 9 deletions packages/vue/src/provide/createProvide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface CreateScope {
(): ScopeHook
}

function createProvideScope<T extends string>(scopeName: T, createProvideScopeDeps: CreateScope[] = []) {
function createScope<T extends string>(scopeName: T, createScopeDeps: CreateScope[] = []) {
let defaultProviders: any[] = []
/* -----------------------------------------------------------------------------------------------
* createProvide
Expand Down Expand Up @@ -70,10 +70,10 @@ function createProvideScope<T extends string>(scopeName: T, createProvideScopeDe
}

// return [Provider, useInject] as const
return {
return [
useProvider,
useInject,
}
] as const
}

/* -----------------------------------------------------------------------------------------------
Expand All @@ -97,15 +97,15 @@ function createProvideScope<T extends string>(scopeName: T, createProvideScopeDe
}

createScope.scopeName = scopeName
// return [createProvide, composeProvderScopes(createScope, ...createProvideScopeDeps)] as const
// return [createProvide, composeProvderScopes(createScope, ...createScopeDeps)] as const

return {
return [
createProvide,
composeProviderScopes: composeProviderScopes(createScope, ...createProvideScopeDeps),
}
composeScopes(createScope, ...createScopeDeps),
] as const
}

function composeProviderScopes(...scopes: CreateScope[]) {
function composeScopes(...scopes: CreateScope[]) {
const baseScope = scopes[0]
if (scopes.length === 1)
return baseScope
Expand Down Expand Up @@ -140,5 +140,5 @@ export const ScopePropObject = {
required: false,
}

export { createProvide, createProvideScope }
export { createProvide, createScope }
export type { CreateScope, Scope }
2 changes: 1 addition & 1 deletion packages/vue/src/provide/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { createProvide, createProvideScope, ScopePropObject } from './createProvide'
export { createProvide, createScope, ScopePropObject } from './createProvide'
export type { CreateScope, Scope } from './createProvide'
201 changes: 201 additions & 0 deletions packages/vue/src/slider/Slider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<script lang="ts">
import type { AriaAttributes, Ref } from 'vue'
import { computed, ref, toRefs } from 'vue'
import type { PrimitiveProps } from '@oku-ui/primitive'
import { useComponentRef, useVModel } from '@oku-ui/use-composable'
import { clamp, composeEventHandlers } from '@oku-ui/utils'
import { ARROW_KEYS, CollectionProvider, CollectionSlot, PAGE_KEYS, getClosestValueIndex, getDecimalCount, getNextSortedValues, hasMinStepsBetweenValues, roundValue, sliderProvider } from './utils'
import type { Direction } from './utils'
import type { SliderThumbElement } from './SliderThumb.vue'
import type { Scope } from '@oku-ui/provide'
export interface SliderProps extends PrimitiveProps {
scopeOkuSlider?: Scope
name?: string
disabled?: boolean
orientation?: AriaAttributes['aria-orientation']
dir?: Direction
min?: number
max?: number
step?: number
minStepsBetweenThumbs?: number
value?: number[]
defaultValue?: number[]
inverted?: boolean
}
export type SliderEmits = {
'valueChange': [value: number[]]
'valueCommit': [value: number[]]
'slideStart': [event: number]
'slideMove': [event: number]
'slideEnd': []
'homeKeyDown': [event: KeyboardEvent]
'endKeyDown': [event: KeyboardEvent]
'stepKeyDown': [event: KeyboardEvent]
'keydown': [event: KeyboardEvent]
'pointerdown': [event: PointerEvent]
'pointermove': [event: PointerEvent]
'pointerup': [event: PointerEvent]
'update:value': [value: number[]]
}
</script>

<script setup lang="ts">
import OkuSliderHorizontal from './SliderHorizontal.vue'
import OkuSliderVertical from './SliderVertical.vue'
import OkuSliderBubbleInput from './SliderBubbleInput.vue'
defineOptions({
name: 'OkuSlider',
inheritAttrs: false,
})
const props = withDefaults(defineProps<SliderProps>(), {
disabled: false,
name: undefined,
orientation: 'horizontal',
dir: undefined,
min: 0,
max: 100,
step: 1,
minStepsBetweenThumbs: 0,
value: undefined,
defaultValue: () => [100],
inverted: false,
})
const emits = defineEmits<SliderEmits>()
const propsRef = toRefs(props)
const { componentRef, currentElement } = useComponentRef<HTMLSpanElement | null>()
// new Set()
const initialThumbSet = new Set<SliderThumbElement>()
const thumbRefs = ref(initialThumbSet)
const valueIndexToChangeRef = ref<number>(0)
const isHorizontal = props.orientation === 'horizontal'
const values = useVModel(props, 'value', emits, {
defaultValue: props.defaultValue,
passive: (props.value === undefined) as false,
shouldEmit(v: any) {
emits('valueChange', v)
return true
},
}) as Ref<number[]>
// We set this to true by default so that events bubble to forms without JS (SSR)
const isFormControl = computed(() => {
return false
})
const valuesBeforeSlideStartRef = ref<number[]>(values.value as number[])
function handleSlideStart(value: number) {
const closestIndex = getClosestValueIndex(values.value, value)
updateValues(value, closestIndex)
}
function handleSlideMove(value: number) {
updateValues(value, valueIndexToChangeRef.value)
}
function handleSlideEnd() {
const prevValue = valuesBeforeSlideStartRef.value[valueIndexToChangeRef.value]
const nextValue = values.value?.[valueIndexToChangeRef.value]
const hasChanged = nextValue !== prevValue
if (hasChanged)
emits('valueCommit', values.value)
}
function updateValues(value: number, atIndex: number, { commit } = { commit: false }) {
const decimalCount = getDecimalCount(props.step)
const snapToStep = roundValue(Math.round((value - props.min) / props.step) * props.step + props.min, decimalCount)
const nextValue = clamp(snapToStep, [props.min, props.max])
const prevValues = values.value
const newData = () => {
const nextValues = getNextSortedValues(prevValues, nextValue, atIndex)
if (hasMinStepsBetweenValues(nextValues, props.minStepsBetweenThumbs * props.step)) {
valueIndexToChangeRef.value = nextValues.indexOf(nextValue)
const hasChanged = String(nextValues) !== String(prevValues)
if (hasChanged && commit)
emits('valueCommit', nextValues)
return hasChanged ? nextValues : prevValues
}
else {
return prevValues
}
}
values.value = newData()
}
sliderProvider({
scope: props.scopeOkuSlider,
disabled: propsRef.disabled,
min: propsRef.min,
max: propsRef.max,
valueIndexToChangeRef,
thumbs: thumbRefs,
values,
orientation: propsRef.orientation,
})
defineExpose({
$el: currentElement,
})
</script>

<template>
<CollectionProvider
:scope="props.scopeOkuSlider"
>
<CollectionSlot
:scope="props.scopeOkuSlider"
>
<component
:is="isHorizontal ? OkuSliderHorizontal : OkuSliderVertical"
ref="componentRef"
:as-child="props.asChild"
:min="props.min"
:max="props.max"
:inverted="inverted"
v-bind="$attrs"
@pointerdown="composeEventHandlers((event: any) => {
emits('pointerdown', event)
}, () => {
if (!disabled) valuesBeforeSlideStartRef = values;
})($event)"
@slide-start="props.disabled ? undefined : handleSlideStart($event)"
@slide-move="props.disabled ? undefined : handleSlideMove($event)"
@slide-end="props.disabled ? undefined : handleSlideEnd()"
@home-key-down="() => !props.disabled && updateValues(min, 0, { commit: true })"
@end-key-down="() =>
!props.disabled && updateValues(max, (values?.length || 0) - 1, { commit: true })"
@step-key-down="({ direction: stepDirection, event }) => {
if (!props.disabled) {
const isPageKey = PAGE_KEYS.includes(event.key)
const isSkipKey = isPageKey || (event.shiftKey && ARROW_KEYS.includes(event.key))
const multiplier = isSkipKey ? 10 : 1
const atIndex = valueIndexToChangeRef
const value = values?.[atIndex]
const stepInDirection = props.step * multiplier * stepDirection
updateValues((value || 0) + stepInDirection, atIndex, { commit: true })
}
}"
>
<slot />
</component>
</CollectionSlot>
<template v-if="isFormControl">
<OkuSliderBubbleInput
v-for="(value, index) in values"
:key="index"
:value="value"
:name="props.name ? props.name + (values?.length > 1 ? '[]' : '') : undefined"
/>
</template>
</CollectionProvider>
</template>
Loading

0 comments on commit 23fbe11

Please sign in to comment.