From bfa7e67a80818c88b3fea4fdb9382e5eaeddd35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=9A=D1=83=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2?= <62594983+teleskop150750@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:24:46 +0300 Subject: [PATCH] refactor(radio-group): ts to sfc (#548) --- packages/vue/src/radio-group/BubbleInput.ts | 80 +---- packages/vue/src/radio-group/BubbleInput.vue | 52 +++ packages/vue/src/radio-group/Radio.ts | 169 ++------- packages/vue/src/radio-group/Radio.vue | 93 +++++ packages/vue/src/radio-group/RadioGroup.ts | 196 ++-------- packages/vue/src/radio-group/RadioGroup.vue | 67 ++++ .../src/radio-group/RadioGroupIndicator.ts | 54 +-- .../src/radio-group/RadioGroupIndicator.vue | 22 ++ .../vue/src/radio-group/RadioGroupItem.ts | 134 +------ .../vue/src/radio-group/RadioGroupItem.vue | 110 ++++++ .../vue/src/radio-group/RadioIndicator.ts | 60 +--- .../vue/src/radio-group/RadioIndicator.vue | 40 +++ packages/vue/src/radio-group/constants.ts | 6 + packages/vue/src/radio-group/index.ts | 50 +-- .../stories/RadioGroupAnimated.vue | 143 ++++++++ .../stories/RadioGroupChromatic.vue | 338 ++++++++++++++++++ .../stories/RadioGroupControlled.vue | 117 ++++++ .../radio-group/stories/RadioGroupDemo.vue | 165 ++------- .../radio-group/stories/RadioGroupStyles.vue | 116 ++++++ .../radio-group/stories/RadioGroupUnset.vue | 110 ++++++ .../stories/RadioGroupWithinForm.vue | 154 ++++++++ .../src/radio-group/stories/radio.stories.ts | 109 ++++++ .../src/radio-group/stories/tabs.stories.ts | 36 -- packages/vue/src/radio-group/types.ts | 1 + .../vue/src/use-composable/useComponentRef.ts | 2 +- packages/vue/src/use-composable/useSize.ts | 23 +- 26 files changed, 1610 insertions(+), 837 deletions(-) create mode 100644 packages/vue/src/radio-group/BubbleInput.vue create mode 100644 packages/vue/src/radio-group/Radio.vue create mode 100644 packages/vue/src/radio-group/RadioGroup.vue create mode 100644 packages/vue/src/radio-group/RadioGroupIndicator.vue create mode 100644 packages/vue/src/radio-group/RadioGroupItem.vue create mode 100644 packages/vue/src/radio-group/RadioIndicator.vue create mode 100644 packages/vue/src/radio-group/constants.ts create mode 100644 packages/vue/src/radio-group/stories/RadioGroupAnimated.vue create mode 100644 packages/vue/src/radio-group/stories/RadioGroupChromatic.vue create mode 100644 packages/vue/src/radio-group/stories/RadioGroupControlled.vue create mode 100644 packages/vue/src/radio-group/stories/RadioGroupStyles.vue create mode 100644 packages/vue/src/radio-group/stories/RadioGroupUnset.vue create mode 100644 packages/vue/src/radio-group/stories/RadioGroupWithinForm.vue create mode 100644 packages/vue/src/radio-group/stories/radio.stories.ts delete mode 100644 packages/vue/src/radio-group/stories/tabs.stories.ts create mode 100644 packages/vue/src/radio-group/types.ts diff --git a/packages/vue/src/radio-group/BubbleInput.ts b/packages/vue/src/radio-group/BubbleInput.ts index ba77cfd08..98951da98 100644 --- a/packages/vue/src/radio-group/BubbleInput.ts +++ b/packages/vue/src/radio-group/BubbleInput.ts @@ -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 +// Props export interface BubbleInputProps { checked: boolean - control: HTMLElement | null + control: HTMLElement | undefined bubbles: boolean } - -const bubbleInputPropsObject = { - props: { - checked: { - type: Boolean as PropType, - required: true, - }, - control: { - type: Object as PropType, - default: null, - }, - bubbles: { - type: Boolean as PropType, - 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(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 - }) diff --git a/packages/vue/src/radio-group/BubbleInput.vue b/packages/vue/src/radio-group/BubbleInput.vue new file mode 100644 index 000000000..500329f77 --- /dev/null +++ b/packages/vue/src/radio-group/BubbleInput.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/vue/src/radio-group/Radio.ts b/packages/vue/src/radio-group/Radio.ts index 9a3fd89b7..c80f68b68 100644 --- a/packages/vue/src/radio-group/Radio.ts +++ b/packages/vue/src/radio-group/Radio.ts @@ -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 - disabled?: Ref -} - -export const useRadioScope = createRadioScope() - -export const [radioProvider, useRadioInject] = createRadioProvide(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, - default: undefined, - }, - required: { - type: Boolean as PropType, - default: undefined, - }, - disabled: { - type: Boolean as PropType, - default: undefined, - }, - name: { - type: String as PropType, - default: undefined, - }, - value: { - type: String as PropType, - 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(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 + disabled?: Ref +} - 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(RADIO_NAME) diff --git a/packages/vue/src/radio-group/Radio.vue b/packages/vue/src/radio-group/Radio.vue new file mode 100644 index 000000000..3f864a7ad --- /dev/null +++ b/packages/vue/src/radio-group/Radio.vue @@ -0,0 +1,93 @@ + + + diff --git a/packages/vue/src/radio-group/RadioGroup.ts b/packages/vue/src/radio-group/RadioGroup.ts index 24a78ce92..6247f18c1 100644 --- a/packages/vue/src/radio-group/RadioGroup.ts +++ b/packages/vue/src/radio-group/RadioGroup.ts @@ -1,187 +1,47 @@ -import type { OkuElement, PrimitiveProps } from '@oku-ui/primitive' -import { Primitive, primitiveProps } from '@oku-ui/primitive' -import { computed, defineComponent, h, mergeProps, reactive, toRefs, useModel } from 'vue' -import type { PropType, Ref } from 'vue' +import type { Scope } from '@oku-ui/provide' import { createScope } from '@oku-ui/provide' -import { OkuRovingFocusGroup, createRovingFocusGroupScope } from '@oku-ui/roving-focus' -import { reactiveOmit, useControllable, useForwardRef } from '@oku-ui/use-composable' -import { useDirection } from '@oku-ui/direction' +import type { Ref } from 'vue' import type { RovingFocusGroupProps } from '@oku-ui/roving-focus' - -import { type RadioProps, createRadioScope } from './Radio' -import { scopeRadioGroupProps } from './utils' - -const RADIO_GROUP_NAME = 'OkuRadioGroup' - -export const [createRadioGroupProvider, createRadioGroupScope] = createScope(RADIO_GROUP_NAME, [ - createRovingFocusGroupScope, - createRadioScope, -]) - -export const [RadioGroupProvider, useRadioGroupInject] - = createRadioGroupProvider(RADIO_GROUP_NAME) - -export const useRovingFocusGroupScope = createRovingFocusGroupScope() - -export type RadioGroupNaviteElement = OkuElement<'div'> -export type RadioElement = HTMLDivElement - -export interface RadioGroupProvideValue { - name?: Ref - required: Ref - disabled: Ref - value?: Ref - onValueChange: (value: string) => void -} - -export type RadioGroupEmits = { - 'update:modelValue': [value: string | undefined] - 'valueChange': [value: string | undefined] -} +import { createRovingFocusGroupScope } from '@oku-ui/roving-focus' +import type { PrimitiveProps } from '@oku-ui/primitive' +import { RADIO_GROUP_NAME } from './constants' +import type { RadioProps } from './Radio' +import { createRadioScope } from './Radio' export interface RadioGroupProps extends PrimitiveProps { - modelValue?: string | undefined - name?: RadioGroupProvideValue['name'] + name?: string | undefined required?: RadioProps['required'] disabled?: RadioProps['disabled'] dir?: RovingFocusGroupProps['dir'] orientation?: RovingFocusGroupProps['orientation'] loop?: RovingFocusGroupProps['loop'] defaultValue?: string - value?: RadioGroupProvideValue['value'] -} + value?: string | undefined -export const radioGroupProps = { - props: { - modelValue: { - type: [String] as PropType< - string | undefined - >, - default: undefined, - }, - name: { - type: String as PropType, - default: undefined, - }, - required: { - type: Boolean as PropType, - default: false, - }, - disabled: { - type: Boolean as PropType, - default: false, - }, - dir: { - type: String as PropType, - default: undefined, - }, - orientation: { - type: String as PropType, - default: undefined, - }, - loop: { - type: Boolean as PropType, - default: true, - }, - defaultValue: { - type: String as PropType, - default: undefined, - }, - value: { - type: String as PropType, - default: undefined, - }, - ...primitiveProps, - }, - emits: { - // eslint-disable-next-line unused-imports/no-unused-vars - 'update:modelValue': (value: string) => true, - // eslint-disable-next-line unused-imports/no-unused-vars - 'valueChange': (value: string) => true, - }, + scopeOkuRadioGroup?: Scope } -const radioGroup = defineComponent({ - name: RADIO_GROUP_NAME, - inheritAttrs: false, - props: { - ...radioGroupProps.props, - ...scopeRadioGroupProps, - }, - emits: radioGroupProps.emits, - setup(props, { slots, emit, attrs }) { - const { - scopeOkuRadioGroup, - name, - defaultValue, - value: valueProp, - required, - disabled, - orientation, - dir, - loop, - ...groupProps - } = toRefs(props) - const _reactive = reactive(groupProps) - const reactiveGroupProps = reactiveOmit(_reactive, (key, _value) => key === undefined) +// Emits - const rovingFocusGroupScope = useRovingFocusGroupScope(scopeOkuRadioGroup.value) - const direction = useDirection(dir) +export type RadioGroupEmits = { + 'update:value': [value: string | undefined] +} - const forwardedRef = useForwardRef() - const modelValue = useModel(props, 'modelValue') - const proxyValue = computed(() => { - if (modelValue.value !== undefined) - return modelValue.value - if (valueProp.value !== undefined) - return valueProp.value - return undefined - }) +// Context - const { state, updateValue } = useControllable({ - prop: computed(() => proxyValue.value), - defaultProp: computed(() => defaultValue.value), - onChange: (result) => { - emit('valueChange', result) - modelValue.value = result - }, - }) +export interface RadioGroupContextValue { + name: Ref + required: Ref + disabled: Ref + value: Ref + onValueChange: (value: string) => void +} - RadioGroupProvider({ - scope: props.scopeOkuRadioGroup, - name, - required, - disabled, - value: state, - onValueChange(value: string) { - updateValue(value) - }, - }) +export const [createRadioGroupProvider, createRadioGroupScope] = createScope(RADIO_GROUP_NAME, [ + createRovingFocusGroupScope, + createRadioScope, +]) - return () => - h(OkuRovingFocusGroup, { - asChild: true, - ...rovingFocusGroupScope, - orientation: orientation.value, - dir: direction.value, - loop: loop.value, - }, { - default: () => h(Primitive.div, { - 'role': 'radiogroup', - 'aria-required': required.value, - 'aria-oriented': orientation.value, - 'data-disabled': disabled.value ? '' : undefined, - 'dir': direction.value, - ...mergeProps(attrs, reactiveGroupProps), - 'ref': forwardedRef, - }, { - default: () => slots.default?.(), - }), - }) - }, -}) +export const [provideRadioGroupContext, useRadioGroupContext] = createRadioGroupProvider(RADIO_GROUP_NAME) -export const OkuRadioGroup = radioGroup as typeof radioGroup & - (new () => { - $props: RadioGroupNaviteElement - }) +export const useRovingFocusGroupScope = createRovingFocusGroupScope() diff --git a/packages/vue/src/radio-group/RadioGroup.vue b/packages/vue/src/radio-group/RadioGroup.vue new file mode 100644 index 000000000..7ef1cd375 --- /dev/null +++ b/packages/vue/src/radio-group/RadioGroup.vue @@ -0,0 +1,67 @@ + + + diff --git a/packages/vue/src/radio-group/RadioGroupIndicator.ts b/packages/vue/src/radio-group/RadioGroupIndicator.ts index 00bd9f7ba..f473a4a81 100644 --- a/packages/vue/src/radio-group/RadioGroupIndicator.ts +++ b/packages/vue/src/radio-group/RadioGroupIndicator.ts @@ -1,48 +1,14 @@ -import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' -import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' -import type { RadioElement } from './Radio' -import { useRadioScope } from './Radio' -import { OkuRadioIndicator } from './RadioIndicator' -import type { RadioIndicatorNaviteElement, RadioIndicatorProps } from './RadioIndicator' -import { scopeRadioGroupProps } from './utils' -import { radioGroupProps } from './RadioGroup' +import type { PrimitiveProps } from '@oku-ui/primitive' +import type { Scope } from '@oku-ui/provide' -const INDICATOR_NAME = 'OkuRadioGroupIndicator' +// Props -export type RadioGroupIndicatorNaviteElement = RadioIndicatorNaviteElement -export type RadioGroupIndicatorElement = RadioElement +export interface RadioGroupIndicatorProps extends PrimitiveProps { + /** + * Used to force mounting when more control is needed. Useful when + * controlling animation with React animation libraries. + */ + forceMount?: true -export interface RadioGroupIndicatorProps extends RadioIndicatorProps { } - -export const radioGroupIndicatorProps = { - props: { - ...radioGroupProps.props, - }, + scopeOkuRadioGroup?: Scope } - -const RadioGroupIndicator = defineComponent({ - name: INDICATOR_NAME, - inheritAttrs: false, - props: { - ...radioGroupIndicatorProps.props, - ...scopeRadioGroupProps, - }, - setup(props, { attrs }) { - const { scopeOkuRadioGroup, ...indicatorProps } = toRefs(props) - const _reactive = reactive(indicatorProps) - const reactiveIndicatorProps = reactiveOmit(_reactive, (key, _value) => key === undefined) - const radioScope = useRadioScope(scopeOkuRadioGroup.value) - const forwardedRef = useForwardRef() - - return () => h(OkuRadioIndicator, { - ...radioScope, - ...mergeProps(attrs, reactiveIndicatorProps), - ref: forwardedRef, - }) - }, -}) - -export const OkuRadioGroupIndicator = RadioGroupIndicator as typeof RadioGroupIndicator & - (new () => { - $props: RadioGroupIndicatorNaviteElement - }) diff --git a/packages/vue/src/radio-group/RadioGroupIndicator.vue b/packages/vue/src/radio-group/RadioGroupIndicator.vue new file mode 100644 index 000000000..245c49e83 --- /dev/null +++ b/packages/vue/src/radio-group/RadioGroupIndicator.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/vue/src/radio-group/RadioGroupItem.ts b/packages/vue/src/radio-group/RadioGroupItem.ts index 9e88defe5..df3cc9870 100644 --- a/packages/vue/src/radio-group/RadioGroupItem.ts +++ b/packages/vue/src/radio-group/RadioGroupItem.ts @@ -1,131 +1,17 @@ -import { computed, defineComponent, h, mergeProps, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue' -import { reactiveOmit, useComposedRefs, useForwardRef } from '@oku-ui/use-composable' +import type { PrimitiveProps } from '@oku-ui/primitive' +import type { Scope } from '@oku-ui/provide' -import { OkuRovingFocusGroupItem } from '@oku-ui/roving-focus' -import { composeEventHandlers } from '@oku-ui/utils' -import { propsOmit } from '@oku-ui/primitive' -import { useRadioGroupInject, useRovingFocusGroupScope } from './RadioGroup' -import type { RadioGroupNaviteElement } from './RadioGroup' -import type { RadioElement, RadioEmits, RadioProps } from './Radio' -import { OkuRadio, radioProps, useRadioScope } from './Radio' -import { ARROW_KEYS, scopeRadioGroupProps } from './utils' +// Props -const ITEM_NAME = 'OkuRadioGroupItem' - -export type RadioGroupItemNaviteElement = RadioGroupNaviteElement -export type RadioGroupItemElement = HTMLDivElement - -export interface RadioGroupItemProps extends Omit { +export interface RadioGroupItemProps extends PrimitiveProps { + disabled?: boolean value: string -} -export type RadioGroupItemEmits = Omit & { - focus: [event: FocusEvent] + scopeOkuRadioGroup?: Scope } -export const radioGroupItemProps = { - props: { - ...propsOmit(radioProps.props, ['name']), - }, - emits: { - ...propsOmit(radioProps.emits, ['check']), - // eslint-disable-next-line unused-imports/no-unused-vars - focus: (event: FocusEvent) => true, - }, -} - -const RadioGroupItem = defineComponent({ - name: ITEM_NAME, - inheritAttrs: false, - props: { - ...radioGroupItemProps.props, - ...scopeRadioGroupProps, - }, - emits: radioGroupItemProps.emits, - setup(props, { slots, emit, attrs }) { - const { - scopeOkuRadioGroup, - disabled, - ...itemProps - } = toRefs(props) - const _reactive = reactive(itemProps) - const reactiveItemProps = reactiveOmit(_reactive, (key, _value) => key === undefined) - - const inject = useRadioGroupInject(ITEM_NAME, scopeOkuRadioGroup.value) - - const isDisabled = computed(() => inject.disabled.value || disabled.value) - const rovingFocusGroupScope = useRovingFocusGroupScope(scopeOkuRadioGroup.value) - const radioScope = useRadioScope(scopeOkuRadioGroup.value) - - const rootRef = ref(null) - const forwardedRef = useForwardRef() - const composedRefs = useComposedRefs(rootRef, forwardedRef) +// Emits - const checked = computed(() => inject.value?.value === reactiveItemProps.value) - const isArrowKeyPressedRef = ref(false) - - const handleKeyDown = (event: KeyboardEvent) => { - if (ARROW_KEYS.includes(event.key)) - isArrowKeyPressedRef.value = true - } - const handleKeyUp = () => { - isArrowKeyPressedRef.value = false - } - - onMounted(() => { - document.addEventListener('keydown', handleKeyDown) - document.addEventListener('keyup', handleKeyUp) - }) - - onBeforeUnmount(() => { - document.removeEventListener('keydown', handleKeyDown) - document.removeEventListener('keyup', handleKeyUp) - }) - - return () => h(OkuRovingFocusGroupItem, { - asChild: true, - ...rovingFocusGroupScope, - focusable: !isDisabled.value, - active: checked.value, - }, { - default: () => h(OkuRadio, { - disabled: isDisabled.value, - required: inject.required.value || reactiveItemProps.required, - checked: checked.value, - ...radioScope, - ...mergeProps(attrs, reactiveItemProps), - ref: composedRefs, - onCheck: () => { - return inject.onValueChange(props.value) - }, - onKeydown: composeEventHandlers((event: any) => { - // According to WAI ARIA, radio groups don't activate items on enter keypress - if (event.key === 'Enter') - event.preventDefault() - }), - onFocus: composeEventHandlers((e) => { - emit('focus', e) - }, () => { - /** - * Our `RovingFocusGroup` will focus the radio when navigating with arrow keys - * and we need to "check" it in that case. We click it to "check" it (instead - * of updating `context.value`) so that the radio change event fires. - */ - - setTimeout(() => { - if (isArrowKeyPressedRef.value) - rootRef.value?.click() - }, 0) - }), - - }, { - default: () => slots.default?.(), - }), - }) - }, -}) - -export const OkuRadioGroupItem = RadioGroupItem as typeof RadioGroupItem & - (new () => { - $props: RadioGroupItemNaviteElement - }) +export type RadioGroupItemEmits = { + focus: [event: FocusEvent] +} diff --git a/packages/vue/src/radio-group/RadioGroupItem.vue b/packages/vue/src/radio-group/RadioGroupItem.vue new file mode 100644 index 000000000..36bcff691 --- /dev/null +++ b/packages/vue/src/radio-group/RadioGroupItem.vue @@ -0,0 +1,110 @@ + + + diff --git a/packages/vue/src/radio-group/RadioIndicator.ts b/packages/vue/src/radio-group/RadioIndicator.ts index 48d0570c2..bf6dedf89 100644 --- a/packages/vue/src/radio-group/RadioIndicator.ts +++ b/packages/vue/src/radio-group/RadioIndicator.ts @@ -1,16 +1,7 @@ -import { Primitive, primitiveProps } from '@oku-ui/primitive' -import type { OkuElement, PrimitiveProps } from '@oku-ui/primitive' -import { computed, defineComponent, h, mergeProps, reactive, toRefs } from 'vue' -import type { PropType } from 'vue' -import { OkuPresence } from '@oku-ui/presence' -import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' -import { useRadioInject } from './Radio' -import { getState, scopeRadioProps } from './utils' +import type { PrimitiveProps } from '@oku-ui/primitive' +import type { Scope } from '@oku-ui/provide' -const INDICATOR_NAME = 'OkuRadioIndicator' - -export type RadioIndicatorNaviteElement = OkuElement<'span'> -export type RadioIndicatorElement = HTMLSpanElement +// Props export interface RadioIndicatorProps extends PrimitiveProps { /** @@ -18,49 +9,6 @@ export interface RadioIndicatorProps extends PrimitiveProps { * controlling animation with React animation libraries. */ forceMount?: true -} -export const radioIndicatorProps = { - props: { - forceMount: { - type: Boolean as PropType, - default: undefined, - }, - ...primitiveProps, - }, + scopeOkuRadio?: Scope } - -const RadioIndicator = defineComponent({ - name: INDICATOR_NAME, - inheritAttrs: false, - props: { - ...radioIndicatorProps.props, - ...scopeRadioProps, - }, - setup(props, { attrs }) { - const { forceMount, scopeOkuRadio, ...indicatorProps } = toRefs(props) - const _reactive = reactive(indicatorProps) - const reactiveIndicatorProps = reactiveOmit(_reactive, (key, _value) => key === undefined) - const inject = useRadioInject(INDICATOR_NAME, scopeOkuRadio.value) - const forwardedRef = useForwardRef() - - return () => { - return h(OkuPresence, { - present: computed(() => forceMount.value || inject.checked.value).value, - }, { - default: () => - h(Primitive.span, { - 'data-state': getState(inject.checked.value), - 'data-disabled': inject.disabled?.value ? '' : undefined, - ...mergeProps(attrs, reactiveIndicatorProps), - 'ref': forwardedRef, - }), - }) - } - }, -}) - -export const OkuRadioIndicator = RadioIndicator as typeof RadioIndicator & - (new () => { - $props: RadioIndicatorNaviteElement - }) diff --git a/packages/vue/src/radio-group/RadioIndicator.vue b/packages/vue/src/radio-group/RadioIndicator.vue new file mode 100644 index 000000000..49277a1d6 --- /dev/null +++ b/packages/vue/src/radio-group/RadioIndicator.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/vue/src/radio-group/constants.ts b/packages/vue/src/radio-group/constants.ts new file mode 100644 index 000000000..bfaded72c --- /dev/null +++ b/packages/vue/src/radio-group/constants.ts @@ -0,0 +1,6 @@ +export const RADIO_NAME = 'OkuRadio' +export const BUBBLE_INPUT_NAME = 'OkuBubbleInput' +export const RADIO_INDICATOR_NAME = 'OkuRadioIndicator' +export const RADIO_GROUP_NAME = 'OkuRadioGroup' +export const RADIO_GROUP_ITEM_NAME = 'OkuRadioGroupItem' +export const RADIO_GROUP_INDICATOR_NAME = 'OkuRadioGroupIndicator' diff --git a/packages/vue/src/radio-group/index.ts b/packages/vue/src/radio-group/index.ts index 524fe119f..479e4dcd1 100644 --- a/packages/vue/src/radio-group/index.ts +++ b/packages/vue/src/radio-group/index.ts @@ -1,44 +1,14 @@ -export { - OkuRadioGroup, - createRadioGroupScope, - radioGroupProps, -} from './RadioGroup' +export { default as OkuRadio } from './Radio.vue' +export { default as OkuRadioGroup } from './RadioGroup.vue' +export { default as OkuRadioGroupItem } from './RadioGroupItem.vue' +export { default as OkuRadioGroupIndicator } from './RadioGroupIndicator.vue' -export type { - RadioElement, - RadioGroupNaviteElement, - RadioGroupProps, - RadioGroupEmits, -} from './RadioGroup' +export { createRadioGroupScope, type RadioGroupProps, type RadioGroupEmits } from './RadioGroup' +export type { RadioGroupItemProps, RadioGroupItemEmits } from './RadioGroupItem' +export type { RadioGroupIndicatorProps } from './RadioGroupIndicator' -export { - OkuRadioGroupItem, - radioGroupItemProps, -} from './RadioGroupItem' +export type { RadioElement } from './types' -export type { - RadioGroupItemProps, - RadioGroupItemElement, - RadioGroupItemEmits, -} from './RadioGroupItem' +export { scopeRadioGroupProps, scopeRadioProps } from './utils' -export { - OkuRadioGroupIndicator, - radioGroupIndicatorProps, -} from './RadioGroupIndicator' - -export type { - RadioGroupIndicatorElement, - RadioGroupIndicatorProps, - RadioGroupIndicatorNaviteElement, -} from './RadioGroupIndicator' - -export { - scopeRadioGroupProps, - scopeRadioProps, -} from './utils' - -export type { - ScopeRadio, - ScopeRadioGroup, -} from './utils' +export type { ScopeRadio, ScopeRadioGroup } from './utils' diff --git a/packages/vue/src/radio-group/stories/RadioGroupAnimated.vue b/packages/vue/src/radio-group/stories/RadioGroupAnimated.vue new file mode 100644 index 000000000..428b04726 --- /dev/null +++ b/packages/vue/src/radio-group/stories/RadioGroupAnimated.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/packages/vue/src/radio-group/stories/RadioGroupChromatic.vue b/packages/vue/src/radio-group/stories/RadioGroupChromatic.vue new file mode 100644 index 000000000..8b7ac3d83 --- /dev/null +++ b/packages/vue/src/radio-group/stories/RadioGroupChromatic.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/packages/vue/src/radio-group/stories/RadioGroupControlled.vue b/packages/vue/src/radio-group/stories/RadioGroupControlled.vue new file mode 100644 index 000000000..837fafaac --- /dev/null +++ b/packages/vue/src/radio-group/stories/RadioGroupControlled.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/packages/vue/src/radio-group/stories/RadioGroupDemo.vue b/packages/vue/src/radio-group/stories/RadioGroupDemo.vue index 37e2fd8ec..c0a7d37d8 100644 --- a/packages/vue/src/radio-group/stories/RadioGroupDemo.vue +++ b/packages/vue/src/radio-group/stories/RadioGroupDemo.vue @@ -1,149 +1,42 @@ diff --git a/packages/vue/src/radio-group/stories/RadioGroupStyles.vue b/packages/vue/src/radio-group/stories/RadioGroupStyles.vue new file mode 100644 index 000000000..7ed0405d4 --- /dev/null +++ b/packages/vue/src/radio-group/stories/RadioGroupStyles.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/packages/vue/src/radio-group/stories/RadioGroupUnset.vue b/packages/vue/src/radio-group/stories/RadioGroupUnset.vue new file mode 100644 index 000000000..a4d732f49 --- /dev/null +++ b/packages/vue/src/radio-group/stories/RadioGroupUnset.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/packages/vue/src/radio-group/stories/RadioGroupWithinForm.vue b/packages/vue/src/radio-group/stories/RadioGroupWithinForm.vue new file mode 100644 index 000000000..afb48178f --- /dev/null +++ b/packages/vue/src/radio-group/stories/RadioGroupWithinForm.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/packages/vue/src/radio-group/stories/radio.stories.ts b/packages/vue/src/radio-group/stories/radio.stories.ts new file mode 100644 index 000000000..5e0ef3cb3 --- /dev/null +++ b/packages/vue/src/radio-group/stories/radio.stories.ts @@ -0,0 +1,109 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import OkuTabsComponent from './RadioGroupDemo.vue' + +const meta = { + title: 'Components/RadioGroup', + component: OkuTabsComponent, + args: { + template: 'styled', + }, + +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Styled: Story = { + args: { + template: 'styled', + // allshow: true, + }, + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} +export const Controlled: Story = { + args: { + template: 'controlled', + // allshow: true, + }, + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Unset: Story = { + args: { + template: 'unset', + // allshow: true, + }, + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const WithinForm: Story = { + args: { + template: 'within-form', + // allshow: true, + }, + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Animated: Story = { + args: { + template: 'animated', + // allshow: true, + }, + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Chromatic: Story = { + args: { + template: 'chromatic', + // allshow: true, + }, + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} diff --git a/packages/vue/src/radio-group/stories/tabs.stories.ts b/packages/vue/src/radio-group/stories/tabs.stories.ts deleted file mode 100644 index 0e07cded6..000000000 --- a/packages/vue/src/radio-group/stories/tabs.stories.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3' - -import type { ITabsProps } from './RadioGroupDemo.vue' -import OkuTabsComponent from './RadioGroupDemo.vue' - -interface StoryProps extends ITabsProps { } - -const meta = { - title: 'Components/RadioGroup', - component: OkuTabsComponent, - args: { - template: '#1', - }, - -} satisfies Meta & { - args: StoryProps -} - -export default meta -type Story = StoryObj - -export const Styled: Story = { - args: { - template: '#1', - // allshow: true, - }, - render: (args: any) => ({ - components: { OkuTabsComponent }, - setup() { - return { args } - }, - template: ` - - `, - }), -} diff --git a/packages/vue/src/radio-group/types.ts b/packages/vue/src/radio-group/types.ts new file mode 100644 index 000000000..e317cb810 --- /dev/null +++ b/packages/vue/src/radio-group/types.ts @@ -0,0 +1 @@ +export type RadioElement = HTMLButtonElement diff --git a/packages/vue/src/use-composable/useComponentRef.ts b/packages/vue/src/use-composable/useComponentRef.ts index 7cbbbfecd..ce3908f9d 100644 --- a/packages/vue/src/use-composable/useComponentRef.ts +++ b/packages/vue/src/use-composable/useComponentRef.ts @@ -10,7 +10,7 @@ export function useComponentRef() { const currentElement = computed(() => { // $el could be text/comment for non-single root normal or text root, thus we retrieve the nextElementSibling const el = ['#text', '#comment'].includes(componentRef.value?.$el?.nodeName) - ? unwrapEl(componentRef.value) + ? componentRef.value?.$el.nextElementSibling : unwrapEl(componentRef) return el as T | undefined diff --git a/packages/vue/src/use-composable/useSize.ts b/packages/vue/src/use-composable/useSize.ts index 88faeb620..d859a2dd4 100644 --- a/packages/vue/src/use-composable/useSize.ts +++ b/packages/vue/src/use-composable/useSize.ts @@ -1,19 +1,22 @@ /// -import { ref, watchEffect } from 'vue' -import type { Ref } from 'vue' +import { toValue } from '@oku-ui/utils' +import { shallowRef, watchEffect } from 'vue' +import type { MaybeRefOrGetter } from 'vue' interface Size { width: number height: number } -function useSize(element: Ref) { - const size = ref(undefined) +function useSize(element: MaybeRefOrGetter) { + const size = shallowRef(undefined) watchEffect((onInvalidate) => { - if (element.value) { - size.value = { width: element.value.offsetWidth, height: element.value.offsetHeight } + const elementValue = toValue(element) + + if (elementValue) { + size.value = { width: elementValue.offsetWidth, height: elementValue.offsetHeight } const resizeObserver = new ResizeObserver((entries) => { if (!Array.isArray(entries)) @@ -38,16 +41,16 @@ function useSize(element: Ref) { else { // for browsers that don't support `borderBoxSize` // we calculate it ourselves to get the correct border box. - width = element.value!.offsetWidth - height = element.value!.offsetHeight + width = elementValue!.offsetWidth + height = elementValue!.offsetHeight } size.value = { width, height } }) - resizeObserver.observe(element.value, { box: 'border-box' }) + resizeObserver.observe(elementValue, { box: 'border-box' }) - onInvalidate(() => resizeObserver.unobserve(element.value!)) + onInvalidate(() => resizeObserver.unobserve(elementValue!)) } else { // We only want to reset to `undefined` when the element becomes `null`,