Skip to content

fix: checkbox #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 32 additions & 34 deletions packages/components/checkbox/src/checkbox.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createProvideScope } from '@oku-ui/provide'
import type { PropType, Ref } from 'vue'
import { Transition, computed, defineComponent, h, onMounted, ref, watchEffect } from 'vue'
import { Transition, defineComponent, h, onMounted, ref, toRefs, watch, watchEffect } from 'vue'

import { composeEventHandlers } from '@oku-ui/utils'
import { useControllableRef, usePrevious, useSize } from '@oku-ui/use-composable'
import { useControllableRef, usePrevious, useRef, useSize } from '@oku-ui/use-composable'
import { Primitive } from '@oku-ui/primitive'

// import { useComposedRefs } from '@oku-ui/compose-refs'
import type { ElementType, MergeProps, PrimitiveProps, RefElement } from '@oku-ui/primitive'

import type { Scope } from '@oku-ui/provide'
Expand Down Expand Up @@ -121,26 +120,30 @@ const Checkbox = defineComponent({
props: {
checked: {
type: [Boolean, 'indeterminate'] as PropType<boolean | 'indeterminate'>,
default: false,
default: undefined,
},
defaultChecked: {
type: [Boolean, 'indeterminate'] as PropType<boolean | 'indeterminate'>,
default: false,
default: undefined,
},
required: {
type: Boolean,
default: undefined,
},
required: Boolean,
onCheckedChange: Function as PropType<(checked: CheckedState) => void>,
scopeCheckbox: {
type: Object as unknown as PropType<Scope>,
required: false,
default: undefined,
},
},
setup(props, { attrs, slots, expose }) {
const { checked: checkedProp, scopeCheckbox, defaultChecked, onCheckedChange, required } = props
const innerRef = ref()
const _innerRef = computed(() => innerRef.value?.$el)
const { checked: checkedProp, scopeCheckbox, defaultChecked, onCheckedChange, required } = toRefs(props)

const { _ref: buttonRef, refEl: buttonRefEl } = useRef<HTMLButtonElement>()

expose({
innerRef: _innerRef,
innerRef: buttonRefEl,
})

const {
Expand All @@ -150,60 +153,55 @@ const Checkbox = defineComponent({
...checkboxProps
} = attrs as CheckboxElement

const _button = computed<HTMLButtonElement>(() => _innerRef.value)
// const button = ref<HTMLButtonElement>()
// TODO: Change the useComposedRefs structure here if necessary (https://github.com/radix-ui/primitives/blob/c3f2189034e690e9fb564d484733144fdcbc02d7/packages/react/checkbox/src/Checkbox.tsx#L56)
// const composedRefs = useComposedRefs(innerRef, button)

const hasConsumerStoppedPropagationRef = ref(false)

const isFormControl = _button.value ? Boolean(_button.value.closest('form')) : true
const [checked, setChecked] = useControllableRef({
prop: checkedProp,
defaultProp: defaultChecked,
onChange: onCheckedChange,
const isFormControl = buttonRefEl.value ? Boolean(buttonRefEl.value.closest('form')) : true
const { state } = useControllableRef({
prop: checkedProp.value,
defaultProp: defaultChecked.value,
onChange: onCheckedChange.value,
})

const initialCheckedStateRef = ref()

onMounted(() => {
initialCheckedStateRef.value = checked.value
initialCheckedStateRef.value = state.value
})

watchEffect(() => {
const form = _button.value?.form
watch([buttonRefEl, state], () => {
const form = buttonRefEl.value?.form
if (form) {
const reset = () => setChecked(initialCheckedStateRef.value)
const reset = () => (state.value = initialCheckedStateRef.value)
form.addEventListener('reset', reset)
return () => form.removeEventListener('reset', reset)
}
})

CheckboxProvider({
scope: scopeCheckbox as Scope,
state: checked as Ref<CheckedState>,
scope: scopeCheckbox.value as Scope,
state: state as Ref<CheckedState>,
disabled: disabled as boolean,
})

const originalReturn = () =>
[h(Primitive.button, {
'type': 'button',
'role': 'checkbox',
'aria-checked': isIndeterminate(checked.value as any) ? 'mixed' : checked.value as any,
'aria-required': required,
'data-state': getState(checked.value as any),
'aria-checked': isIndeterminate(state.value as any) ? 'mixed' : state.value as any,
'aria-required': required.value,
'data-state': getState(state.value as any),
'data-disabled': disabled ? '' : undefined,
'disabled': disabled,
'value': value,
...checkboxProps,
'ref': innerRef,
'ref': buttonRef,
'onKeyDown': composeEventHandlers(checkboxProps.onKeydown, (event) => {
// According to WAI ARIA, Checkboxes don't activate on enter keypress
if (event.key === 'Enter')
event.preventDefault()
}),
'onClick': composeEventHandlers(checkboxProps.onClick, (event) => {
setChecked(prevChecked => (isIndeterminate(prevChecked) ? true : !prevChecked))
state.value = isIndeterminate(state.value as any) ? true : !(state.value as any)
if (isFormControl) {
// hasConsumerStoppedPropagationRef.value.current = event.isPropagationStopped()
// if checkbox is in a form, stop propagation from the button so that we only propagate
Expand All @@ -220,11 +218,11 @@ const Checkbox = defineComponent({
isFormControl && h(
BubbleInput,
{
control: _button.value,
control: buttonRefEl.value,
bubbles: !hasConsumerStoppedPropagationRef.value,
name,
value,
checked: checked.value,
checked: state.value,
required,
disabled,
// We transform because the input is absolutely positioned but we have
Expand Down Expand Up @@ -285,7 +283,7 @@ const CheckboxIndicator = defineComponent({
])

return originalReturn as unknown as {
innerRef: Ref<CheckboxIndicatorElement>
innerRef: Ref<HTMLButtonElement>
}
},
})
Expand Down
10 changes: 9 additions & 1 deletion packages/components/checkbox/src/stories/CheckboxDemo.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import type { CheckboxProps } from '@oku-ui/checkbox'
import type { CheckboxProps, CheckboxRef } from '@oku-ui/checkbox'
import { OkuCheckbox, OkuCheckboxIndicator } from '@oku-ui/checkbox'
import { OkuLabel } from '@oku-ui/label'
import { onMounted, ref } from 'vue'

export interface ICheckBoxProps extends CheckboxProps {
template?: '#1' | '#2' | '#3'
Expand All @@ -12,6 +13,12 @@ export interface ICheckBoxProps extends CheckboxProps {
withDefaults(defineProps<ICheckBoxProps>(), {

})

const refdd = ref<CheckboxRef>()

onMounted(() => {
console.log(refdd.value?.innerRef, 'tt')
})
</script>

<template>
Expand All @@ -24,6 +31,7 @@ withDefaults(defineProps<ICheckBoxProps>(), {
</h1>
<OkuCheckbox
id="checkbox"
ref="refdd"
class="w-6 h-6 flex bg-gray-300 rounded-md text-red-500 checked:text-red-600"
>
<OkuCheckboxIndicator class="w-6 h-6 flex items-center justify-center text-blue-500">
Expand Down
3 changes: 3 additions & 0 deletions packages/core/use-composable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"peerDependencies": {
"vue": "^3.3.1"
},
"dependencies": {
"@oku-ui/utils": "workspace:^"
},
"devDependencies": {
"@types/resize-observer-browser": "^0.1.7",
"tsconfig": "workspace:^"
Expand Down
4 changes: 3 additions & 1 deletion packages/core/use-composable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export { useControllableRef } from './useControllableRef'
export { useCallbackRef } from './useCallbackRef'
export { useSize } from './useSize'
export { usePrevious } from './usePrevious'
export { useComposedRefs, composeRefs } from './useComposedRefs'
export { useRef } from './useRef'
export { unrefElement } from './unrefElement'
export type { MaybeComputedElementRef } from './unrefElement'
20 changes: 20 additions & 0 deletions packages/core/use-composable/src/unrefElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ComponentPublicInstance } from 'vue'
import type { MaybeRef, MaybeRefOrGetter } from '@oku-ui/utils'
import { toValue } from '@oku-ui/utils'

export type VueInstance = ComponentPublicInstance
export type MaybeElementRef<T extends MaybeElement = MaybeElement> = MaybeRef<T>
export type MaybeComputedElementRef<T extends MaybeElement = MaybeElement> = MaybeRefOrGetter<T>
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null

export type UnRefElementReturn<T extends MaybeElement = MaybeElement> = T extends VueInstance ? Exclude<MaybeElement, VueInstance> : T | undefined

/**
* Get the dom element of a ref of element or Vue component instance
*
* @param elRef
*/
export function unrefElement<T extends MaybeElement>(elRef: MaybeComputedElementRef<T>): UnRefElementReturn<T> {
const plain = toValue(elRef)
return (plain as VueInstance)?.$el ?? plain
}
8 changes: 3 additions & 5 deletions packages/core/use-composable/src/useCallbackRef.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import type { Ref, UnwrapRef } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import type { Ref } from 'vue'

/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
* A custom function that converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
function useCallbackRef<T extends (...args: any[]) => any>(callback: T | undefined): T {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const callbackRef: Ref<T | undefined> = ref(callback)
const callbackRef: Ref<UnwrapRef<T> | undefined | T> = ref(callback)

watchEffect(() => {
callbackRef.value = callback
Expand Down
34 changes: 0 additions & 34 deletions packages/core/use-composable/src/useComposedRefs.ts

This file was deleted.

73 changes: 25 additions & 48 deletions packages/core/use-composable/src/useControllableRef.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Ref } from 'vue'
import { computed, ref, watch, watchEffect } from 'vue'
import { ref, watch, watchEffect } from 'vue'
import { useCallbackRef } from './useCallbackRef'

type UseControllableRefParams<T> = {
Expand All @@ -8,78 +8,55 @@ type UseControllableRefParams<T> = {
onChange?: (ref: T) => void
}

type SetRefFn<T> = (prevRef?: T) => T

function useControllableRef<T>({
prop,
defaultProp,
onChange = () => {},
}: UseControllableRefParams<T>) {
const uncontrolledProp = useUncontrolledRef({ defaultProp, onChange })
const uncontrolledRef = useUncontrolledRef({
defaultProp,
onChange,
})
const handleChange = useCallbackRef(onChange)
const isControlled = prop !== undefined
const state = computed(() => (isControlled ? prop : uncontrolledProp.value))
const handleChange = computed(() => onChange)

const setValue = (callback: (nextValue: T | undefined) => void | T): any => {
const refCallback = ref(callback)
const computedCallback = computed(() => refCallback.value)

watchEffect(() => {
refCallback.value = callback
})
const state = ref(isControlled ? prop : uncontrolledRef) as Ref<T | undefined>

// TODO: How to add handleChange watch. handleChange add watch auto run when prop change :/ not good
watch([state, uncontrolledRef, prop], () => {
if (isControlled) {
const setter = computedCallback.value as SetRefFn<T>
const value = typeof computedCallback.value === 'function' ? setter(prop) : computedCallback.value
const value = typeof state.value === 'function' ? state.value() : state.value
if (value !== prop)
handleChange.value(value as T)
handleChange(prop)
}
else {
const setter = callback as SetRefFn<T>
uncontrolledProp.value = typeof callback === 'function' ? setter(uncontrolledProp.value) : callback
uncontrolledRef.value = state.value
}
}
}, {
deep: true,
})

return [state, setValue] as const
return {
state,
}
}

function useUncontrolledRef<T>({
defaultProp,
onChange,
}: Omit<UseControllableRefParams<T>, 'prop'>) {
const uncontrolledRef = ref(defaultProp) as Ref<T | undefined>
const prevValueRef = ref(defaultProp) as Ref<T | undefined>
const state = ref(defaultProp) as Ref<T | undefined>
const prevValue = ref(defaultProp) as Ref<T | undefined>
const handleChange = useCallbackRef(onChange)

watch([uncontrolledRef, prevValueRef, handleChange], () => {
if (prevValueRef.value !== uncontrolledRef.value) {
handleChange(uncontrolledRef.value as T)
prevValueRef.value = uncontrolledRef.value
watchEffect(() => {
if (prevValue.value !== state.value) {
handleChange(state.value as T)
prevValue.value = state.value
}
})

return uncontrolledRef
return state
}

export { useControllableRef }

// type Callback<T extends any[]> = (...args: T) => void

// function useCallback<T extends any[]>(callback: Callback<T>, deps: any[]): (...args: T) => void {
// const refCallback = ref(callback)
// const computedCallback = computed(() => refCallback.value)

// const memoizedCallback = (...args: T) => computedCallback.value(...args)

// watchEffect(() => {
// refCallback.value = callback
// })

// if (deps.length > 0) {
// watch(deps, () => {
// refCallback.value = callback
// })
// }

// return memoizedCallback
// }
Loading