-
-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(radio-group): ts to sfc (#548)
- Loading branch information
1 parent
08b2011
commit bfa7e67
Showing
26 changed files
with
1,610 additions
and
837 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.