Skip to content

Commit

Permalink
refactor(checkbox): ts to sfc (oku-ui#495)
Browse files Browse the repository at this point in the history
  • Loading branch information
productdevbook authored Jan 14, 2024
1 parent 1041889 commit 29f5d40
Show file tree
Hide file tree
Showing 15 changed files with 366 additions and 487 deletions.
64 changes: 64 additions & 0 deletions packages/vue/src/checkbox/BubbleInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import type { CheckedState } from './Checkbox.vue'
export interface BubbleInputProps {
checked: CheckedState
control: HTMLElement | null
bubbles: boolean
}
</script>

<script setup lang="ts">
import { computed, defineOptions, defineProps, toRefs, watchEffect, withDefaults } from 'vue'
import { useComponentRef, usePrevious, useSize } from '@oku-ui/use-composable'
import { isIndeterminate } from './utils'
defineOptions({
name: 'OkuBubbleInput',
})
const props = withDefaults(defineProps<BubbleInputProps>(), {
})
const { componentRef, currentElement } = useComponentRef<HTMLInputElement | null>()
const { checked, control } = toRefs(props)
const prevChecked = usePrevious(checked)
const controlSize = computed(() => useSize(control))
// Bubble checked change to parents (e.g form change event)
watchEffect(() => {
const input = currentElement.value!
const inputProto = window.HTMLInputElement.prototype
const descriptor = Object.getOwnPropertyDescriptor(inputProto, 'checked') as PropertyDescriptor
const setChecked = descriptor.set
if (prevChecked.value !== props.checked && setChecked) {
const event = new Event('click', { bubbles: props.bubbles })
input.indeterminate = isIndeterminate(props.checked)
setChecked.call(input, isIndeterminate(props.checked) ? false : props.checked)
input.dispatchEvent(event)
}
})
</script>

<template>
<input
ref="componentRef"
type="checkbox"
:defaultChecked="isIndeterminate(checked) ? false : checked"
tabindex="-1"
:style=" {
...$attrs.style as any,
...controlSize,
position: 'absolute',
pointerEvents: 'none',
opacity: 0,
margin: '0px',
}"
>
</template>
168 changes: 168 additions & 0 deletions packages/vue/src/checkbox/Checkbox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<script lang="ts">
import { createProvideScope } from '@oku-ui/provide'
import type { PrimitiveProps } from '@oku-ui/primitive'
export interface ScopeCheckbox {
scopeOkuCheckbox?: any
}
export type CheckedState = boolean | 'indeterminate'
export type CheckboxProvide = {
_names: 'OkuCheckbox'
state: Ref<CheckedState>
disabled?: Ref<boolean | undefined>
}
export interface CheckboxProps extends PrimitiveProps {
scopeOkuCheckbox?: any
checked?: CheckedState
defaultChecked?: CheckedState
required?: boolean
name?: string
disabled?: boolean
value?: string
}
export type CheckboxEmits = {
'update:modelValue': [checked: CheckedState]
'checkedChange': [checked: CheckedState]
'keydown': [event: KeyboardEvent]
'click': [event: MouseEvent]
}
export const { composeProviderScopes, createProvide } = createProvideScope<CheckboxProvide['_names']>('OkuCheckbox')
export const { useInject, useProvider }
= createProvide<Omit<CheckboxProvide, '_names'>>('OkuCheckbox')
</script>

<script setup lang="ts">
import type { Ref } from 'vue'
import { computed, defineOptions, defineProps, onMounted, ref, toRef, watchEffect, withDefaults } from 'vue'
import { useComponentRef, useControllable, useVModel } from '@oku-ui/use-composable'
import { Primitive } from '@oku-ui/primitive'
import { composeEventHandlers } from '@oku-ui/utils'
import OkuBubbleInput from './BubbleInput.vue'
import { getState, isIndeterminate } from './utils'
defineOptions({
name: 'OkuCheckbox',
inheritAttrs: false,
})
const props = withDefaults(defineProps<CheckboxProps>(), {
is: 'button',
value: 'on',
checked: undefined,
defaultChecked: undefined,
})
const emits = defineEmits<CheckboxEmits>()
const { componentRef, currentElement } = useComponentRef<HTMLButtonElement | null>()
const hasConsumerStoppedPropagationRef = ref(false)
// We set this to true by default so that events bubble to forms without JS (SSR)
const isFormControl = ref<boolean>(false)
onMounted(() => {
isFormControl.value = currentElement.value
? typeof currentElement.value.closest === 'function'
&& Boolean(currentElement.value.closest('form'))
: true
})
const modelValue = useVModel(props, 'checked', emits, {
defaultValue: props.defaultChecked,
passive: (props.checked === undefined) as false,
})
const [checked, setChecked] = useControllable({
prop: computed(() => modelValue.value),
defaultProp: computed(() => props.defaultChecked),
onChange: (result: any) => {
emits('checkedChange', result)
emits('update:modelValue', result)
},
initialValue: false,
})
const initialCheckedStateRef = ref(checked.value)
watchEffect((onInvalidate) => {
const form = currentElement.value?.form
if (form) {
const reset = () => setChecked(initialCheckedStateRef.value)
form.addEventListener('reset', reset)
onInvalidate(() => form.removeEventListener('reset', reset))
}
})
useProvider({
scope: props.scopeOkuCheckbox,
state: checked,
disabled: toRef(props, 'disabled'),
})
</script>

<template>
<Primitive
:is="props.is"
v-bind="$attrs"
ref="componentRef"
type="button"
:as-child="props.asChild"
role="checkbox"
:aria-checked="isIndeterminate(checked) ? 'mixed' : checked"
:aria-required="props.required"
:data-state="getState(checked)"
:data-disabled="props.disabled ? '' : undefined"
:disabled="props.disabled"
:value="props.value"
@keydown="composeEventHandlers<CheckboxEmits['keydown'][0]>((event) => {
emits('keydown', event)
}, (event) => {
// According to WAI ARIA, Checkboxes don't activate on enter keypress
if (event.key === 'Enter')
event.preventDefault()
})($event)"
@click="composeEventHandlers<CheckboxEmits['click'][0]>(
(event) => {
emits('click', event)
}, (event) => {
setChecked(isIndeterminate(checked) ? true : !checked)
if (isFormControl) {
// TODO: isPropagationStopped() is not supported in vue
// hasConsumerStoppedPropagationRef.value = event.isPropagationStopped()
// if checkbox 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 checkbox updates.
if (!hasConsumerStoppedPropagationRef)
event.stopPropagation()
}
})($event)"
>
<slot />
</Primitive>

<OkuBubbleInput
v-if="isFormControl"
:control="currentElement"
:bubbles="!hasConsumerStoppedPropagationRef"
:name="props.name"
:checked="!!checked"
:value="props.value"
:required="props.required"
:disabled="props.disabled"
:style="{
// 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.
transform: 'translateX(-100%)',
}"
/>
</template>
55 changes: 55 additions & 0 deletions packages/vue/src/checkbox/CheckboxIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script lang="ts">
// import type { CheckedState, ScopeCheckbox } from './Checkbox.vue'
import { Primitive } from '@oku-ui/primitive'
import type { PrimitiveProps } from '@oku-ui/primitive'
import { useInject } from './Checkbox.vue'
export interface CheckboxIndicatorProps extends PrimitiveProps {
scopeOkuCheckbox?: any
forceMount?: true
}
</script>

<script setup lang="ts">
import { defineOptions, defineProps, withDefaults } from 'vue'
import { useComponentRef } from '@oku-ui/use-composable'
import { getState, isIndeterminate } from './utils'
import { OkuPresence } from '@oku-ui/presence'
defineOptions({
name: 'OkuCheckboxIndicator',
inheritAttrs: false,
})
const props = withDefaults(defineProps<CheckboxIndicatorProps>(), {
is: 'span',
forceMount: undefined,
})
const { componentRef } = useComponentRef<HTMLInputElement | null>()
const inject = useInject('OkuCheckbox', props.scopeOkuCheckbox)
</script>

<template>
<OkuPresence
:present="props.forceMount || isIndeterminate(inject.state.value) || inject.state.value === true"
>
<Primitive
:is="props.is"
ref="componentRef"
:as-child="props.asChild"
:data-state="getState(inject.state.value)"
:data-disabled="inject.disabled"
:style="{
pointerEvents: 'none', ...$attrs.style as any,
}"
v-bind="$attrs"
>
<slot />
</Primitive>
</OkuPresence>
</template>
67 changes: 0 additions & 67 deletions packages/vue/src/checkbox/bubble-input.ts

This file was deleted.

Loading

0 comments on commit 29f5d40

Please sign in to comment.