-
-
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.
- Loading branch information
1 parent
8e3d98f
commit ea2447e
Showing
15 changed files
with
378 additions
and
525 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 |
---|---|---|
@@ -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() |
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,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> |
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,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 | ||
} |
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,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> |
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,8 @@ | ||
import type { PrimitiveProps } from '@oku-ui/primitive' | ||
import type { TabsScopeProps } from './types' | ||
|
||
// Props | ||
|
||
export interface TabsListProps extends PrimitiveProps, TabsScopeProps { | ||
loop?: boolean | ||
} |
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,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> |
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,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] | ||
} |
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,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> |
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,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 |
Oops, something went wrong.