Skip to content

Commit

Permalink
refactor(collapsible): add presence and separate each component (#221)
Browse files Browse the repository at this point in the history
* refactor(collapsible): separate each component

* feat: add live demo and fix tailwindcss

* refaktor: component name

* fix: collapsible

* fix: test

* fix: lock
  • Loading branch information
productdevbook authored Jul 19, 2023
1 parent 597113f commit 6565829
Show file tree
Hide file tree
Showing 14 changed files with 485 additions and 219 deletions.
1 change: 1 addition & 0 deletions packages/components/collapsible/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"vue": "^3.3.2"
},
"dependencies": {
"@oku-ui/presence": "latest",
"@oku-ui/primitive": "latest",
"@oku-ui/provide": "latest",
"@oku-ui/use-composable": "latest",
Expand Down
195 changes: 26 additions & 169 deletions packages/components/collapsible/src/collapsible.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
import type { PropType, Ref } from 'vue'
import { Transition, computed, defineComponent, h, ref, toRefs } from 'vue'
import type { ElementType } from '@oku-ui/primitive'
import { computed, defineComponent, h, toRefs, useModel } from 'vue'
import type { ElementType, MergeProps, PrimitiveProps, RefElement } from '@oku-ui/primitive'
import type { Scope } from '@oku-ui/provide'
import { createProvideScope } from '@oku-ui/provide'
import { Primitive } from '@oku-ui/primitive'
import { composeEventHandlers } from '@oku-ui/utils'

import { useControllable, useId, useRef } from '@oku-ui/use-composable'
import { getState } from './utils'

// ---Collapsible---
interface CollapsibleProps { }
interface CollapsibleProps extends PrimitiveProps {
}
type CollapsibleElement = ElementType<'div'>
type CollapsibleTriggerElement = ElementType<'button'>
type CollapsibleContextValue = {

type CollapsibleProvideValue = {
contentId: string
disabled?: Ref<boolean>
open: Ref<boolean>
onOpenToggle(): void
}

const COLLAPSIBLE_NAME = 'Collapsible'
const [createCollapsibleProvide, _createCollapsibleScope] = createProvideScope(COLLAPSIBLE_NAME)
const [collapsibleProvider, useCollapsibleInject]
= createCollapsibleProvide<CollapsibleContextValue>(COLLAPSIBLE_NAME)
export const [createCollapsibleProvide, _createCollapsibleScope] = createProvideScope(COLLAPSIBLE_NAME)
export const [collapsibleProvider, useCollapsibleInject]
= createCollapsibleProvide<CollapsibleProvideValue>(COLLAPSIBLE_NAME)

const Collapsible = defineComponent({
name: COLLAPSIBLE_NAME,
inheritAttrs: false,
props: {
modelValue: {
type: [Boolean, String, Number] as PropType<
boolean | string | number | undefined | 'indeterminate'
type: [Boolean] as PropType<
boolean
>,
default: undefined,
},
Expand All @@ -51,20 +51,26 @@ const Collapsible = defineComponent({
onOpenChange: {
type: Function as PropType<(open: boolean) => void>,
},
asChild: {
type: Boolean,
default: undefined,
},
},
emits: ['update:open', 'update:modelValue'],
setup(props, { attrs, slots, expose, emit }) {
const { ...collapsibleAttr } = attrs as CollapsibleElement
const { disabled, scopeCollapsible, open, defaultOpen } = toRefs(props)

const modelValue = useModel(props, 'modelValue')

const { $el, newRef } = useRef<CollapsibleElement>()

expose({
innerRef: $el,
})

const { state, updateValue } = useControllable({
prop: computed(() => open.value),
prop: computed(() => modelValue.value ?? open.value),
defaultProp: computed(() => defaultOpen.value),
onChange: (open) => {
emit('update:open', open)
Expand All @@ -88,6 +94,7 @@ const Collapsible = defineComponent({
'data-state': getState(state.value),
'data-disabled': disabled.value ? '' : undefined,
'ref': newRef,
'asChild': props.asChild,
...collapsibleAttr,
},
{
Expand All @@ -98,161 +105,11 @@ const Collapsible = defineComponent({
},
})

// ---CollapsibleTrigger---
const TRIGGER_NAME = 'CollapsibleTrigger'

const CollapsibleTrigger = defineComponent({
name: TRIGGER_NAME,
inheritAttrs: false,
props: {
scopeCollapsible: {
type: Object as unknown as PropType<Scope>,
required: false,
},
},
setup(props, { attrs, slots, expose }) {
const { scopeCollapsible } = toRefs(props)
const { ...triggerProps } = attrs as CollapsibleTriggerElement
const context = useCollapsibleInject(TRIGGER_NAME, scopeCollapsible.value)
const { $el, newRef } = useRef<CollapsibleElement>()

expose({
innerRef: $el,
})

const originalReturn = () => h(
Primitive.button,
{
'type': 'button',
'aria-controls': context.value.contentId,
'aria-expanded': context.value.open.value || false,
'data-state': getState(context.value.open.value || false),
'data-disabled': context.value.disabled?.value ? '' : undefined,
'disabled': context.value.disabled?.value,
...triggerProps,
'ref': newRef,
'onClick': composeEventHandlers(triggerProps.onClick, context.value.onOpenToggle),
},
{
default: () => slots.default && slots.default(),
},
)
return originalReturn
},
})

// ---CollapsibleContentImpl---
const CONTENT_NAME = 'CollapsibleContent'

const CollapsibleContentImpl = defineComponent({
inheritAttrs: false,
props: {
present: {
type: Boolean,
},
scopeCollapsible: {
type: Object as unknown as PropType<Scope>,
required: false,
},
},
setup(props, { attrs, slots, expose }) {
const { scopeCollapsible, present } = toRefs(props)
const { ...contentProps } = attrs
const context = useCollapsibleInject(CONTENT_NAME, scopeCollapsible.value)
const { $el, newRef } = useRef<CollapsibleElement>()

expose({
innerRef: $el,
})
const isPresent = ref(present.value)
const isOpen = computed(() => context.value.open.value || isPresent.value)

const originalReturn = () => h(
Primitive.div,
{
'data-state': getState(context.value.open.value),
'data-disabled': context.value.disabled?.value ? '' : undefined,
'id': context.value.contentId,
'hidden': !isOpen.value,
...contentProps,
'ref': newRef,
'style': {
// ...attrs.style,
},
},
isOpen.value
? {
default: () => slots.default && slots.default(),
}
: undefined,
)

return originalReturn
},
})

// ---CollapsibleContent---

const CollapsibleContent = defineComponent({
name: CONTENT_NAME,
components: {
CollapsibleContentImpl,
Transition,
},
inheritAttrs: false,
props: {
forceMount: {
type: Boolean,
default: true,
},
scopeCollapsible: {
type: Object as unknown as PropType<Scope>,
required: false,
},
},
setup(props, { attrs, slots, expose }) {
const { scopeCollapsible } = toRefs(props)
const { ...contentProps } = attrs

const { $el, newRef } = useRef<CollapsibleElement>()

expose({
innerRef: $el,
})

// TODO: Transition
const originalReturn = () => h(
Transition,
{},
{
default: () => h(
CollapsibleContentImpl,
{
...contentProps,
ref: newRef,
scopeCollapsible: scopeCollapsible.value,
},
{
default: () => slots.default && slots.default(),
},
),
},
)
return originalReturn
},
})

// ---export---
function getState(open?: boolean) {
return open ? 'open' : 'closed'
}
// TODO: https://github.com/vuejs/core/pull/7444 after delete
type _CollapsibleProps = MergeProps<CollapsibleProps, CollapsibleElement>
type CollapsibleRef = RefElement<typeof Collapsible>

export {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
}
const OkuCollapsible = Collapsible as typeof Collapsible & (new () => { $props: _CollapsibleProps })

export type {
CollapsibleProps,
}
export { OkuCollapsible }
export type { CollapsibleProps, CollapsibleElement, CollapsibleRef }
87 changes: 87 additions & 0 deletions packages/components/collapsible/src/collapsibleContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { PropType } from 'vue'
import { Transition, defineComponent, h, toRefs } from 'vue'
import type { Scope } from '@oku-ui/provide'

import { useRef } from '@oku-ui/use-composable'
import type { ElementType, MergeProps, PrimitiveProps, RefElement } from '@oku-ui/primitive'
import { OkuPresence } from '@oku-ui/presence'
import { OkuCollapsibleContentImpl } from './collapsibleContentImpl'
import { useCollapsibleInject } from './collapsible'

export const CONTENT_NAME = 'CollapsibleContent'

type CollapsibleContentElement = ElementType<'div'>
interface CollapsibleContentProps extends PrimitiveProps {
}

const CollapsibleContent = defineComponent({
name: CONTENT_NAME,
components: {
OkuCollapsibleContentImpl,
Transition,
},
inheritAttrs: false,
props: {
/**
* Used to force mounting when more control is needed. Useful when
* controlling animation with React animation libraries.
*/
forceMount: {
type: Boolean,
default: true,
},
scopeCollapsible: {
type: Object as unknown as PropType<Scope>,
required: false,
},
asChild: {
type: Boolean,
default: undefined,
},
},
setup(props, { attrs, slots, expose }) {
const { scopeCollapsible } = toRefs(props)
const { ...contentProps } = attrs as CollapsibleContentElement

const context = useCollapsibleInject(CONTENT_NAME, scopeCollapsible.value)

const { $el, newRef } = useRef<CollapsibleContentElement>()

expose({
innerRef: $el,
})

// TODO: Transition
const originalReturn = () => h(
OkuPresence,
{
present: props.forceMount || context.value.open.value,
},
{
default: () => h(
OkuCollapsibleContentImpl,
{
...contentProps,
ref: newRef,
asChild: props.asChild,
scopeCollapsible: scopeCollapsible.value,
},
{
default: () => slots.default && slots.default(),
},
),
},
)

return originalReturn
},
})

// TODO: https://github.com/vuejs/core/pull/7444 after delete
type _CollapsibleContentProps = MergeProps<CollapsibleContentProps, CollapsibleContentElement>
type CollapsibleContentRef = RefElement<typeof CollapsibleContent>

const OkuCollapsibleContent = CollapsibleContent as typeof CollapsibleContent & (new () => { $props: _CollapsibleContentProps })

export { OkuCollapsibleContent }
export type { CollapsibleContentProps, CollapsibleContentElement, CollapsibleContentRef }
Loading

0 comments on commit 6565829

Please sign in to comment.