Skip to content

Commit

Permalink
refactor(tabs): ts to sfc (#538)
Browse files Browse the repository at this point in the history
  • Loading branch information
teleskop150750 authored Mar 9, 2024
1 parent 8e3d98f commit ea2447e
Show file tree
Hide file tree
Showing 15 changed files with 378 additions and 525 deletions.
66 changes: 66 additions & 0 deletions packages/vue/src/tabs/Tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { PrimitiveProps } from '@oku-ui/primitive'
import { type RovingFocusGroupProps, createRovingFocusGroupScope } from '@oku-ui/roving-focus'
import type { Ref } from 'vue'
import { createScope } from '@oku-ui/provide'
import type { ActivationMode, TabsScopeProps } from './types'
import { TAB_NAME } from './constants'

// Props

export interface TabsProps extends PrimitiveProps, TabsScopeProps {
/**
* The value for the selected tab, if controlled
*/
value?: string

/**
* The value of the tab to select by default, if uncontrolled
*/
defaultValue?: string

/**
* The orientation the tabs are layed out.
* Mainly so arrow navigation is done accordingly (left & right vs. up & down)
* @defaultValue horizontal
*/
orientation?: RovingFocusGroupProps['orientation']

/**
* The direction of navigation between toolbar items.
*/
dir?: RovingFocusGroupProps['dir']

/**
* Whether a tab is activated automatically or manually.
* @defaultValue automatic
*/
activationMode?: ActivationMode
}

// Emits

export type TabsEmits = {
/** Event handler called when the value changes */
'update:value': [payload: string]
}

// Context

export interface TabsContext {
baseId: string
value: Ref<string | undefined>
onValueChange: (value: string) => void
orientation: Ref<TabsProps['orientation']>
dir: Ref<TabsProps['dir']>
activationMode: Ref<TabsProps['activationMode']>
}

const [createTabsProvider, createTabsScope] = createScope(TAB_NAME, [
createRovingFocusGroupScope,
])

export { createTabsScope }

export const [tabsProvider, useTabsContext] = createTabsProvider<TabsContext>(TAB_NAME)

export const useRovingFocusGroupScope = createRovingFocusGroupScope()
48 changes: 48 additions & 0 deletions packages/vue/src/tabs/Tabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { type Ref, toRef } from 'vue'
import { Primitive } from '@oku-ui/primitive'
import { useDirection } from '@oku-ui/direction'
import { useId, useVModel } from '@oku-ui/use-composable'
import { type TabsEmits, type TabsProps, tabsProvider } from './Tabs'
import { TAB_NAME } from './constants'
defineOptions({
name: TAB_NAME,
})
const props = withDefaults(defineProps<TabsProps>(), {
orientation: 'horizontal',
activationMode: 'automatic',
})
const emit = defineEmits<TabsEmits>()
const dir = useDirection(toRef(props, 'dir'))
const value = useVModel(props, 'value', emit, {
defaultValue: props.defaultValue,
passive: (props.value === undefined) as false,
}) as Ref<typeof props.defaultValue>
tabsProvider({
scope: props.scopeOkuTabs,
baseId: useId(),
value,
onValueChange(payload) {
value.value = payload
},
orientation: toRef(props, 'orientation'),
dir: toRef(props, 'dir'),
activationMode: toRef(props, 'activationMode'),
})
</script>

<template>
<Primitive
:is="is"
:dir="dir"
:data-orientation="orientation"
:as-child="asChild"
>
<slot />
</Primitive>
</template>
14 changes: 14 additions & 0 deletions packages/vue/src/tabs/TabsContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { PrimitiveProps } from '@oku-ui/primitive'
import type { TabsScopeProps } from './types'

// Props

export interface TabsContentProps extends PrimitiveProps, TabsScopeProps {
value: string

/**
* Used to force mounting when more control is needed. Useful when
* controlling animation with React animation libraries.
*/
forceMount?: true
}
67 changes: 67 additions & 0 deletions packages/vue/src/tabs/TabsContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { makeContentId, makeTriggerId } from './utils'
import type { TabsContentProps } from './TabsContent'
import { useTabsContext } from './Tabs'
import { TAB_CONTENT_NAME } from './constants'
import { OkuPresence } from '@oku-ui/presence'
import { Primitive } from '@oku-ui/primitive'
import { usePrimitiveElement } from '@oku-ui/use-composable'
defineOptions({
name: TAB_CONTENT_NAME,
inheritAttrs: false,
})
const props = defineProps<TabsContentProps>()
const [$el, forwardedRef] = usePrimitiveElement()
const context = useTabsContext(TAB_CONTENT_NAME, props.scopeOkuTabs)
const triggerId = makeTriggerId(context.baseId, props.value)
const contentId = makeContentId(context.baseId, props.value)
const isSelected = computed(() => props.value === context.value.value)
const isMountAnimationPreventedRef = ref(isSelected.value)
const present = computed(() => props.forceMount || isSelected.value)
const rAf = ref()
onMounted(() => {
rAf.value = requestAnimationFrame(() => {
isMountAnimationPreventedRef.value = false
})
})
onBeforeUnmount(() => {
cancelAnimationFrame(rAf.value)
})
defineExpose({
$el,
})
</script>

<template>
<OkuPresence :present="isSelected" force-mount>
<template #default="{ isPresent }">
<Primitive
:is="is"
:id="contentId"
:ref="forwardedRef"
:as-child="asChild"
role="tabpanel"
:data-state="isSelected ? 'active' : 'inactive'"
:data-orientation="context.orientation.value"
:aria-labelledby="triggerId"
:hidden="!present"
v-bind="$attrs"
tabindex="0"
:style="{
animationDuration: isMountAnimationPreventedRef ? '0s' : undefined,
}"
>
<slot v-if="isPresent" />
</Primitive>
</template>
</OkuPresence>
</template>
8 changes: 8 additions & 0 deletions packages/vue/src/tabs/TabsList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PrimitiveProps } from '@oku-ui/primitive'
import type { TabsScopeProps } from './types'

// Props

export interface TabsListProps extends PrimitiveProps, TabsScopeProps {
loop?: boolean
}
46 changes: 46 additions & 0 deletions packages/vue/src/tabs/TabsList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { OkuRovingFocusGroup } from '@oku-ui/roving-focus'
import { useRovingFocusGroupScope, useTabsContext } from './Tabs'
import type { TabsListProps } from './TabsList'
import { TAB_LIST_NAME } from './constants'
import { Primitive } from '@oku-ui/primitive'
import { usePrimitiveElement } from '@oku-ui/use-composable'
defineOptions({
name: TAB_LIST_NAME,
inheritAttrs: false,
})
const props = withDefaults(defineProps<TabsListProps>(), {
loop: true,
})
const [$el, forwardedRef] = usePrimitiveElement()
const context = useTabsContext(TAB_LIST_NAME, props.scopeOkuTabs)
const rovingFocusGroupScope = useRovingFocusGroupScope(props.scopeOkuTabs)
defineExpose({
$el,
})
</script>

<template>
<OkuRovingFocusGroup
v-bind="rovingFocusGroupScope"
as-child
:dir="context.dir.value"
:loop="loop"
:orientation="context.orientation.value"
>
<Primitive
:is="is"
:ref="forwardedRef"
:aria-orientation="context.orientation.value"
role="tablist"
:as-child="asChild"
v-bind="$attrs"
>
<slot />
</Primitive>
</OkuRovingFocusGroup>
</template>
17 changes: 17 additions & 0 deletions packages/vue/src/tabs/TabsTrigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { PrimitiveProps } from '@oku-ui/primitive'
import type { TabsScopeProps } from './types'

// Props

export interface TabsTriggerProps extends PrimitiveProps, TabsScopeProps {
value: string
disabled?: boolean
}

// Emits

export type TabsTriggerEmits = {
mousedown: [event: MouseEvent]
keydown: [event: KeyboardEvent]
focus: [event: FocusEvent]
}
87 changes: 87 additions & 0 deletions packages/vue/src/tabs/TabsTrigger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { TabsTriggerEmits, TabsTriggerProps } from './TabsTrigger'
import { OkuRovingFocusGroupItem } from '@oku-ui/roving-focus'
import { useRovingFocusGroupScope, useTabsContext } from './Tabs'
import { Primitive } from '@oku-ui/primitive'
import { TAB_TRIGGER_NAME } from './constants'
import { makeContentId, makeTriggerId } from './utils'
import { composeEventHandlers } from '@oku-ui/utils'
import { usePrimitiveElement } from '@oku-ui/use-composable'
defineOptions({
name: TAB_TRIGGER_NAME,
inheritAttrs: false,
})
const props = withDefaults(defineProps<TabsTriggerProps>(), {
disabled: false,
is: 'button',
})
const emit = defineEmits<TabsTriggerEmits>()
const [$el, forwardedRef] = usePrimitiveElement()
const context = useTabsContext(TAB_TRIGGER_NAME, props.scopeOkuTabs)
const rovingFocusGroupScope = useRovingFocusGroupScope(props.scopeOkuTabs)
const triggerId = makeTriggerId(context.baseId, props.value)
const contentId = makeContentId(context.baseId, props.value)
const isSelected = computed(() => props.value === context.value.value)
const mousedownHandler = composeEventHandlers<MouseEvent>((e) => {
emit('mousedown', e)
}, (event) => {
// only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
// but not when the control key is pressed (avoiding MacOS right click)
if (!props.disabled && event.button === 0 && event.ctrlKey === false) {
context.onValueChange(props.value)
}
else {
// prevent focus to avoid accidental activation
event.preventDefault()
}
})
const keydownHandler = composeEventHandlers<KeyboardEvent>((e) => {
emit('keydown', e)
}, (event) => {
if ([' ', 'Enter'].includes(event.key))
context.onValueChange(props.value)
})
function focusHandler() {
// handle 'automatic' activation if necessary
// ie. activate tab following focus
const isAutomaticActivation = context.activationMode?.value !== 'manual'
if (!isSelected.value && !props.disabled && isAutomaticActivation)
context.onValueChange(props.value)
}
defineExpose({
$el,
})
</script>

<template>
<OkuRovingFocusGroupItem v-bind="rovingFocusGroupScope" as-child :focusable="!disabled" :active="isSelected">
<Primitive
:is="is"
:id="triggerId"
:ref="forwardedRef"
role="tab"
:type="is === 'button' ? 'button' : undefined"
:as-child="asChild"
:aria-selected="isSelected"
:aria-controls="contentId"
:data-state="isSelected ? 'active' : 'inactive'"
:data-disabled="disabled ? '' : undefined"
:disabled="disabled"
v-bind="$attrs"
@mousedown="mousedownHandler"
@keydown="keydownHandler"
@focus="focusHandler"
>
<slot />
</Primitive>
</OkuRovingFocusGroupItem>
</template>
4 changes: 4 additions & 0 deletions packages/vue/src/tabs/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const TAB_NAME = 'OkuTabs'
export const TAB_CONTENT_NAME = 'OkuTabsContent' as const
export const TAB_TRIGGER_NAME = 'OkuTabsTrigger' as const
export const TAB_LIST_NAME = 'OkuTabsList' as const
Loading

0 comments on commit ea2447e

Please sign in to comment.