Skip to content

Commit

Permalink
chore: checkbox separate each component (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
productdevbook authored Jul 20, 2023
1 parent 8e68694 commit 261f4f1
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 141 deletions.
84 changes: 84 additions & 0 deletions packages/components/checkbox/src/bubbleInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { PropType, Ref } from 'vue'
import { defineComponent, h, ref, toRefs, watchEffect } from 'vue'

import { usePrevious, useSize } from '@oku-ui/use-composable'

import type { ElementType, PrimitiveProps } from '@oku-ui/primitive'

import { type CheckedState, isIndeterminate } from './utils'

type BubbleInputElement = ElementType<'input'>

interface BubbleInputProps extends PrimitiveProps {
checked: Ref<CheckedState>
control: HTMLElement | null
bubbles: boolean
}

const OkuBubbleInput = defineComponent({
name: 'OkuBubbleInput',
inheritAttrs: false,
props: {
checked: {
type: [Boolean, String, Number] as PropType<
boolean | string | number | undefined | 'indeterminate'
>,
default: false,
},
control: {
type: Object as PropType<Ref<HTMLElement | null>>,
default: null,
},
bubbles: {
type: Boolean,
default: true,
},
},
setup(props, { attrs }) {
const { ...inputAttrs } = attrs as BubbleInputElement
const { checked, bubbles, control } = toRefs(props)
const _ref = ref<HTMLInputElement>()

const prevChecked = usePrevious(checked)
const controlSize = useSize(control)

watchEffect(() => {
const input = _ref.value!
const inputProto = window.HTMLInputElement.prototype
const descriptor = Object.getOwnPropertyDescriptor(inputProto, 'checked') as PropertyDescriptor
const setChecked = descriptor.set

if (prevChecked !== checked.value && setChecked) {
const event = new Event('click', { bubbles: bubbles.value })
input.indeterminate = isIndeterminate(checked.value)
setChecked.call(input, isIndeterminate(checked.value) ? false : checked)
input.dispatchEvent(event)
}
})

return () =>
h('input', {
'type': 'checkbox',
'aria-hidden': true,
'defaultChecked': isIndeterminate(checked.value) ? false : checked,
...inputAttrs,
'tabIndex': -1,
'ref': _ref,
'style': {
...inputAttrs.style as any,
...controlSize,
position: 'absolute',
pointerEvents: 'none',
opacity: 0,
margin: 0,
},
})
},
})

export type {
BubbleInputProps,
}
export {
OkuBubbleInput,
}
147 changes: 8 additions & 139 deletions packages/components/checkbox/src/checkbox.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,27 @@
import { createProvideScope } from '@oku-ui/provide'
import type { PropType, Ref } from 'vue'
import { Transition, computed, defineComponent, h, onMounted, ref, toRefs, watchEffect } from 'vue'
import { computed, defineComponent, h, onMounted, ref, toRefs, watchEffect } from 'vue'

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

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

import type { Scope } from '@oku-ui/provide'
import { type CheckedState, getState, isIndeterminate } from './utils'
import { OkuBubbleInput } from './bubbleInput'

function isIndeterminate(checked?: CheckedState): checked is 'indeterminate' {
return checked === 'indeterminate'
}

function getState(checked: CheckedState) {
return isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked'
}

/* -------------------------------------------------------------------------------------------------
* Checkbox
* ----------------------------------------------------------------------------------------------- */

type BubbleInputElement = ElementType<'input'>

interface BubbleInputProps extends PrimitiveProps {
checked: Ref<CheckedState>
control: HTMLElement | null
bubbles: boolean
}

const BubbleInput = defineComponent({
name: 'BubbleInput',
inheritAttrs: false,
props: {
checked: {
type: [Boolean, String, Number] as PropType<
boolean | string | number | undefined | 'indeterminate'
>,
default: false,
},
control: {
type: Object as PropType<Ref<HTMLElement | null>>,
default: null,
},
bubbles: {
type: Boolean,
default: true,
},
},
setup(props, { attrs }) {
const { ...inputAttrs } = attrs as BubbleInputElement
const { checked, bubbles, control } = toRefs(props)
const _ref = ref<HTMLInputElement>()

const prevChecked = usePrevious(checked)
const controlSize = useSize(control)

watchEffect(() => {
const input = _ref.value!
const inputProto = window.HTMLInputElement.prototype
const descriptor = Object.getOwnPropertyDescriptor(inputProto, 'checked') as PropertyDescriptor
const setChecked = descriptor.set

if (prevChecked !== checked.value && setChecked) {
const event = new Event('click', { bubbles: bubbles.value })
input.indeterminate = isIndeterminate(checked.value)
setChecked.call(input, isIndeterminate(checked.value) ? false : checked)
input.dispatchEvent(event)
}
})

return () =>
h('input', {
'type': 'checkbox',
'aria-hidden': true,
'defaultChecked': isIndeterminate(checked.value) ? false : checked,
...inputAttrs,
'tabIndex': -1,
'ref': _ref,
'style': {
...inputAttrs.style as any,
...controlSize,
position: 'absolute',
pointerEvents: 'none',
opacity: 0,
margin: 0,
},
})
},
})

const CHECKBOX_NAME = 'Checkbox'
const CHECKBOX_NAME = 'OkuCheckbox'

const [createCheckboxProvider, _createCheckboxScope] = createProvideScope(CHECKBOX_NAME)

type CheckedState = boolean | string | number | undefined | 'indeterminate'

type CheckboxInjectValue = {
state: Ref<CheckedState>
disabled?: boolean
}

const [CheckboxProvider, useCheckboxInject]
export const [CheckboxProvider, useCheckboxInject]
= createCheckboxProvider<CheckboxInjectValue>(CHECKBOX_NAME)

type CheckboxElement = ElementType<'button'>
Expand All @@ -118,7 +37,7 @@ interface CheckboxProps extends PrimitiveProps {
const checkboxDisplayName = 'OkuCheckbox'
const Checkbox = defineComponent({
name: checkboxDisplayName,
components: { BubbleInput },
components: { OkuBubbleInput },
inheritAttrs: false,
props: {
modelValue: {
Expand Down Expand Up @@ -239,7 +158,7 @@ const Checkbox = defineComponent({
default: () => slots.default?.(),
}),
isFormControl.value && h(
BubbleInput,
OkuBubbleInput,
{
bubbles: !hasConsumerStoppedPropagationRef.value,
name,
Expand Down Expand Up @@ -267,71 +186,21 @@ interface CheckboxIndicatorProps extends PrimitiveProps {
forceMount?: true
}

const INDICATOR_NAME = 'CheckboxIndicator'

const CheckboxIndicator = defineComponent({
name: 'CheckboxIndicator',
components: { Transition },
props: {
scopeCheckbox: {
type: Object as unknown as PropType<Scope>,
required: false,
},
forceMount: Boolean,
},
setup(props, { attrs, expose, slots }) {
const { scopeCheckbox, forceMount } = toRefs(props)
const { ...indicatorProps } = attrs as CheckboxIndicatorElement
const { $el, newRef } = useRef<CheckboxIndicatorElement>()
expose({
innerRef: $el,
})

const context = useCheckboxInject(INDICATOR_NAME, scopeCheckbox.value)

const originalReturn = () => h(Transition, {}, {
default: () => (forceMount.value || isIndeterminate(context.value.state.value) || context.value.state.value === true)
? h(Primitive.span, {
'ref': newRef,
'data-state': getState(context.value.state.value),
'data-disabled': context.value.disabled ? '' : undefined,
...indicatorProps,
'style': { pointerEvents: 'none', ...attrs.style as any },
},
{
default: () => slots.default?.(),
},
)
: null,
})

return originalReturn as unknown as {
innerRef: Ref<HTMLButtonElement>
}
},
})

// TODO: https://github.com/vuejs/core/pull/7444 after delete
type _OkuCheckboxProps = MergeProps<CheckboxProps, CheckboxElement>
type _OkuCheckboxIndicatorProps = MergeProps<CheckboxIndicatorProps, CheckboxIndicatorElement>

type CheckboxRef = RefElement<typeof OkuCheckbox>
type CheckboxIndicatorRef = RefElement<typeof OkuCheckboxIndicator>

const OkuCheckbox = Checkbox as typeof Checkbox & (new () => { $props: _OkuCheckboxProps })
const OkuCheckboxIndicator = CheckboxIndicator as typeof CheckboxIndicator & (new () => { $props: _OkuCheckboxIndicatorProps })

export {
OkuCheckbox,
OkuCheckboxIndicator,
}

export type {
CheckboxProps,
CheckboxIndicatorProps,
CheckboxElement,
CheckboxIndicatorElement,
BubbleInputProps,
CheckboxRef,
CheckboxIndicatorRef,
}
78 changes: 78 additions & 0 deletions packages/components/checkbox/src/checkboxIndicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { PropType, Ref } from 'vue'
import { Transition, defineComponent, h, toRefs } from 'vue'

import { useRef } from '@oku-ui/use-composable'
import { Primitive } from '@oku-ui/primitive'

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

import type { Scope } from '@oku-ui/provide'
import { getState, isIndeterminate } from './utils'
import { useCheckboxInject } from './checkbox'

type CheckboxIndicatorElement = ElementType<'span'>

interface CheckboxIndicatorProps extends PrimitiveProps {
forceMount?: true
}

const INDICATOR_NAME = 'OkuCheckboxIndicator'

const CheckboxIndicator = defineComponent({
name: INDICATOR_NAME,
components: { Transition },
props: {
scopeCheckbox: {
type: Object as unknown as PropType<Scope>,
required: false,
},
forceMount: Boolean,
},
setup(props, { attrs, expose, slots }) {
const { scopeCheckbox, forceMount } = toRefs(props)
const { ...indicatorProps } = attrs as CheckboxIndicatorElement
const { $el, newRef } = useRef<CheckboxIndicatorElement>()
expose({
innerRef: $el,
})

const context = useCheckboxInject(INDICATOR_NAME, scopeCheckbox.value)

const originalReturn = () => h(Transition, {}, {
default: () => (forceMount.value || isIndeterminate(context.value.state.value) || context.value.state.value === true)
? h(Primitive.span, {
'ref': newRef,
'data-state': getState(context.value.state.value),
'data-disabled': context.value.disabled ? '' : undefined,
...indicatorProps,
'style': { pointerEvents: 'none', ...attrs.style as any },
},
{
default: () => slots.default?.(),
},
)
: null,
})

return originalReturn as unknown as {
innerRef: Ref<HTMLButtonElement>
}
},
})

// TODO: https://github.com/vuejs/core/pull/7444 after delete
type _OkuCheckboxIndicatorProps = MergeProps<CheckboxIndicatorProps, CheckboxIndicatorElement>

type CheckboxIndicatorRef = RefElement<typeof OkuCheckboxIndicator>

const OkuCheckboxIndicator = CheckboxIndicator as typeof CheckboxIndicator & (new () => { $props: _OkuCheckboxIndicatorProps })

export {
OkuCheckboxIndicator,
}

export type {
CheckboxIndicatorProps,
CheckboxIndicatorElement,
CheckboxIndicatorRef,
}
6 changes: 4 additions & 2 deletions packages/components/checkbox/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export { OkuCheckbox, OkuCheckboxIndicator } from './checkbox'
export { OkuCheckbox } from './checkbox'

export type {
CheckboxProps,
CheckboxIndicatorProps,
CheckboxElement,
CheckboxIndicatorElement,
CheckboxRef,
CheckboxIndicatorRef,
} from './checkbox'

export { OkuCheckboxIndicator } from './checkboxIndicator'
export type { CheckboxIndicatorRef } from './checkboxIndicator'
14 changes: 14 additions & 0 deletions packages/components/checkbox/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type CheckedState = boolean | string | number | undefined | 'indeterminate'

function isIndeterminate(checked?: CheckedState): checked is 'indeterminate' {
return checked === 'indeterminate'
}

function getState(checked: CheckedState) {
return isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked'
}

export {
isIndeterminate,
getState,
}

0 comments on commit 261f4f1

Please sign in to comment.