Skip to content

Commit

Permalink
refactor(radio-group): ts to sfc (#548)
Browse files Browse the repository at this point in the history
  • Loading branch information
teleskop150750 committed Mar 25, 2024
1 parent 08b2011 commit bfa7e67
Show file tree
Hide file tree
Showing 26 changed files with 1,610 additions and 837 deletions.
80 changes: 2 additions & 78 deletions packages/vue/src/radio-group/BubbleInput.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,7 @@
import type { OkuElement } from '@oku-ui/primitive'
import { reactiveOmit, usePrevious, useSize } from '@oku-ui/use-composable'
import { defineComponent, h, mergeProps, reactive, ref, toRefs, watchEffect } from 'vue'
import type { PropType } from 'vue'

const BUBBLE_INPUT_NAME = 'OkuBubbleInput'

export type BubbleInputNaviteElement = OkuElement<'button'>
export type BubbleInputElement = Omit<HTMLButtonElement, 'checked'>
// Props

export interface BubbleInputProps {
checked: boolean
control: HTMLElement | null
control: HTMLElement | undefined
bubbles: boolean
}

const bubbleInputPropsObject = {
props: {
checked: {
type: Boolean as PropType<boolean>,
required: true,
},
control: {
type: Object as PropType<HTMLElement | null>,
default: null,
},
bubbles: {
type: Boolean as PropType<boolean | undefined>,
default: true,
},
},
}

const bubbleInput = defineComponent({
name: BUBBLE_INPUT_NAME,
inheritAttrs: false,
props: {
...bubbleInputPropsObject.props,
},
setup(props, { attrs }) {
const { control, checked, bubbles, ...inputProps } = toRefs(props)
const _reactive = reactive(inputProps)
const reactiveInputProps = reactiveOmit(_reactive, (key, _value) => key === undefined)

const inputRef = ref<HTMLInputElement | null>(null)
const prevChecked = usePrevious(checked)
const controlSize = useSize(control)

watchEffect(() => {
const input = inputRef.value!
const inputProto = window.HTMLInputElement.prototype
const descriptor = Object.getOwnPropertyDescriptor(inputProto, 'checked')
const setChecked = descriptor?.set
if (prevChecked.value !== checked.value && setChecked) {
const event = new Event('input', { bubbles: bubbles.value })
setChecked.call(input, checked.value)
input.dispatchEvent(event)
}
})

return () => h('input', {
'type': 'radio',
'aria-hidden': true,
'defaultChecked': checked.value,
...mergeProps(attrs, reactiveInputProps),
'tabindex': -1,
'ref': inputRef,
'style': {
...attrs.style as any,
...controlSize.value,
position: 'absolute',
pointerEvents: 'none',
opacity: 0,
margin: '0px',
},
})
},
})

export const OkuBubbleInput = bubbleInput as typeof bubbleInput &
(new () => {
$props: BubbleInputNaviteElement
})
52 changes: 52 additions & 0 deletions packages/vue/src/radio-group/BubbleInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { shallowRef, toRef, watch } from 'vue'
import type { BubbleInputProps } from './BubbleInput'
import { BUBBLE_INPUT_NAME } from './constants'
import { usePrevious, useSize } from '@oku-ui/use-composable'
defineOptions({
name: BUBBLE_INPUT_NAME,
})
const props = withDefaults(defineProps<BubbleInputProps>(), {
bubbles: true,
})
const inputRef = shallowRef<HTMLInputElement>()
const prevChecked = usePrevious(toRef(props, 'checked'))
const controlSize = useSize(() => props.control)
watch([prevChecked, () => props.checked, () => props.bubbles], () => {
if (!inputRef.value)
return
const input = inputRef.value
const inputProto = window.HTMLInputElement.prototype
const descriptor = Object.getOwnPropertyDescriptor(inputProto, 'checked')
const setChecked = descriptor?.set
if (prevChecked.value !== props.checked && setChecked) {
const event = new Event('change', { bubbles: props.bubbles })
setChecked.call(input, props.checked)
input.dispatchEvent(event)
}
})
</script>

<template>
<input
ref="inputRef"
type="radio"
aria-hidden="true"
:aria-checked="checked"
:checked="checked"
tabindex="-1"
:style="{
...controlSize,
position: 'absolute',
pointerEvents: 'none',
opacity: 0,
margin: '0px',
}"
>
</template>
169 changes: 25 additions & 144 deletions packages/vue/src/radio-group/Radio.ts
Original file line number Diff line number Diff line change
@@ -1,158 +1,39 @@
import { Primitive, primitiveProps } from '@oku-ui/primitive'
import type { OkuElement } from '@oku-ui/primitive'
import { createScope } from '@oku-ui/provide'
import { reactiveOmit, useComposedRefs, useForwardRef } from '@oku-ui/use-composable'
import { computed, defineComponent, h, mergeProps, reactive, ref, toRefs } from 'vue'
import type { PropType, Ref } from 'vue'
import { composeEventHandlers } from '@oku-ui/utils'
import { getState, scopeRadioProps } from './utils'
import { OkuBubbleInput } from './BubbleInput'
import type { PrimitiveProps } from '@oku-ui/primitive'
import { type Scope, createScope } from '@oku-ui/provide'
import type { Ref } from 'vue'
import { RADIO_NAME } from './constants'

const RADIO_NAME = 'OkuRadio'
// Props

export const [createRadioProvide, createRadioScope] = createScope(RADIO_NAME)

type RadioProvideValue = {
checked: Ref<boolean>
disabled?: Ref<boolean | undefined>
}

export const useRadioScope = createRadioScope()

export const [radioProvider, useRadioInject] = createRadioProvide<RadioProvideValue>(RADIO_NAME)

export type RadioIntrinsicNaviteElement = OkuElement<'button'>
export type RadioElement = HTMLButtonElement

export interface RadioProps {
checked?: boolean
required?: boolean
disabled?: boolean
export interface RadioProps extends PrimitiveProps {
/** The value given as data when submitted with a `name`. */
value?: string
/** When `true`, prevents the user from interacting with the radio item. */
disabled?: boolean
/** When `true`, indicates that the user must check the radio item before the owning form can be submitted. */
required?: boolean
checked?: boolean
name?: string

scopeOkuRadio?: Scope
}

// Emits

export type RadioEmits = {
click: [event: MouseEvent]
check: []
click: (event: MouseEvent) => void
}

export const radioProps = {
props: {
checked: {
type: Boolean as PropType<boolean | undefined>,
default: undefined,
},
required: {
type: Boolean as PropType<boolean | undefined>,
default: undefined,
},
disabled: {
type: Boolean as PropType<boolean | undefined>,
default: undefined,
},
name: {
type: String as PropType<string | undefined>,
default: undefined,
},
value: {
type: String as PropType<string>,
default: 'on',
},
...primitiveProps,
},
emits: {
check: () => true,
// eslint-disable-next-line unused-imports/no-unused-vars
click: (event: MouseEvent) => true,
},
}

const radio = defineComponent({
name: RADIO_NAME,
inheritAttrs: false,
props: {
...radioProps.props,
...scopeRadioProps,
},
emits: radioProps.emits,
setup(props, { attrs, slots, emit }) {
const {
scopeOkuRadio,
name,
checked: checkedProp,
required,
disabled,
value,
...groupProps
} = toRefs(props)
const checked = computed(() => checkedProp.value || false)
const _reactive = reactive(groupProps)
const reactiveGroupProps = reactiveOmit(_reactive, (key, _value) => key === undefined)
// Context

const hasConsumerStoppedPropagationRef = ref(false)
const buttonRef = ref<HTMLButtonElement | null>(null)
const forwardedRef = useForwardRef()
const composedRefs = useComposedRefs(buttonRef, forwardedRef)

const isFormControl = computed(() => buttonRef.value ? Boolean(buttonRef.value.closest('form')) : false)
export const [createRadioProvide, createRadioScope] = createScope(RADIO_NAME)

radioProvider({
checked,
disabled,
scope: scopeOkuRadio.value,
})
type RadioContextValue = {
checked: Ref<boolean>
disabled?: Ref<boolean | undefined>
}

return () => [
h(Primitive.button, {
'type': 'button',
'role': 'radio',
'aria-checked': checked.value,
'data-state': getState(checked.value || false),
'data-disabled': disabled.value ? '' : undefined,
'disabled': disabled.value,
'value': value.value,
...mergeProps(attrs, reactiveGroupProps),
'ref': composedRefs,
'onClick': composeEventHandlers((e: MouseEvent) => {
emit('click', e)
}, (event: MouseEvent) => {
// radios cannot be unchecked so we only communicate a checked state
if (!checked.value)
emit('check')
if (isFormControl.value) {
// TODO: check `isPropagationStopped`
// hasConsumerStoppedPropagationRef.value = event.isPropagationStopped()
// if radio is in a form, stop propagation from the button so that we only propagate
// one click event (from the input). We propagate changes from an input so that native
// form validation works and form events reflect radio updates.
if (!hasConsumerStoppedPropagationRef.value)
event.stopPropagation()
}
}),
}, {
default: () => slots.default?.(),
}),
isFormControl.value && h(OkuBubbleInput, {
control: buttonRef.value,
bubbles: !hasConsumerStoppedPropagationRef.value,
name: name.value,
value: value.value,
checked: checked.value || false,
required: required.value,
disabled: disabled.value,
// We transform because the input is absolutely positioned but we have
// rendered it **after** the button. This pulls it back to sit on top
// of the button.
style: {
transform: 'translateX(-100%)',
},
}),
]
},
})
export const useRadioScope = createRadioScope()

export const OkuRadio = radio as typeof radio &
(new () => {
$props: RadioIntrinsicNaviteElement
})
export const [provideRadioContext, useRadioContext] = createRadioProvide<RadioContextValue>(RADIO_NAME)
Loading

0 comments on commit bfa7e67

Please sign in to comment.