From ae2b6859b89783f3e8b9f8f8df56910485177ac6 Mon Sep 17 00:00:00 2001 From: Ejiro Asiuwhu Date: Sun, 9 Jul 2023 17:30:36 +0100 Subject: [PATCH 01/22] feat(wip): add base for tab component --- packages/components/tabs/README.md | 10 ++ packages/components/tabs/build.config.ts | 12 ++ packages/components/tabs/package.json | 44 +++++++ packages/components/tabs/src/index.ts | 7 ++ packages/components/tabs/src/tab-content.ts | 30 +++++ packages/components/tabs/src/tab-list.ts | 69 +++++++++++ packages/components/tabs/src/tab-trigger.ts | 27 +++++ packages/components/tabs/src/tabs.ts | 121 ++++++++++++++++++++ packages/components/tabs/tsconfig.json | 10 ++ packages/components/tabs/tsup.config.ts | 22 ++++ 10 files changed, 352 insertions(+) create mode 100644 packages/components/tabs/README.md create mode 100644 packages/components/tabs/build.config.ts create mode 100644 packages/components/tabs/package.json create mode 100644 packages/components/tabs/src/index.ts create mode 100644 packages/components/tabs/src/tab-content.ts create mode 100644 packages/components/tabs/src/tab-list.ts create mode 100644 packages/components/tabs/src/tab-trigger.ts create mode 100644 packages/components/tabs/src/tabs.ts create mode 100644 packages/components/tabs/tsconfig.json create mode 100644 packages/components/tabs/tsup.config.ts diff --git a/packages/components/tabs/README.md b/packages/components/tabs/README.md new file mode 100644 index 000000000..31237339f --- /dev/null +++ b/packages/components/tabs/README.md @@ -0,0 +1,10 @@ +# `@oku-ui/tabs` + +## Installation + +```sh +$ pnpm add @oku-ui/tabs +``` + +## Usage +... \ No newline at end of file diff --git a/packages/components/tabs/build.config.ts b/packages/components/tabs/build.config.ts new file mode 100644 index 000000000..b972b9a78 --- /dev/null +++ b/packages/components/tabs/build.config.ts @@ -0,0 +1,12 @@ +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + entries: [ + { + builder: 'mkdist', + input: './src/', + pattern: ['**/!(*.test|*.stories).ts'], + }, + ], + declaration: true, +}) diff --git a/packages/components/tabs/package.json b/packages/components/tabs/package.json new file mode 100644 index 000000000..ed6a62940 --- /dev/null +++ b/packages/components/tabs/package.json @@ -0,0 +1,44 @@ +{ + "name": "@oku-ui/tabs", + "type": "module", + "version": "0.1.0", + "license": "MIT", + "source": "src/index.ts", + "funding": "https://github.com/sponsors/productdevbook", + "homepage": "https://oku-ui.com/primitives", + "repository": { + "type": "git", + "url": "git+https://github.com/oku-ui/primitives.git", + "directory": "packages/components/tabs" + }, + "bugs": { + "url": "https://github.com/oku-ui/primitives/issues" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs" + } + }, + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch" + }, + "peerDependencies": { + "vue": "^3.3.4" + }, + "dependencies": { + "@oku-ui/primitive": "workspace:^", + "@oku-ui/provide": "workspace:^", + "@oku-ui/use-composable": "workspace:^", + "@oku-ui/utils": "workspace:^" + }, + "devDependencies": { + "tsconfig": "workspace:^" + } +} diff --git a/packages/components/tabs/src/index.ts b/packages/components/tabs/src/index.ts new file mode 100644 index 000000000..7648ba21f --- /dev/null +++ b/packages/components/tabs/src/index.ts @@ -0,0 +1,7 @@ +export { OkuTabs } from './tabs' +export { OkuTabList } from './tab-list' +export { OkuTabTrigger } from './tab-trigger' +export { OkuTabContent } from './tab-content' + +export type { TabsProps } from './tabs' +export { type TabListProps } from './tab-list' diff --git a/packages/components/tabs/src/tab-content.ts b/packages/components/tabs/src/tab-content.ts new file mode 100644 index 000000000..b2004a4df --- /dev/null +++ b/packages/components/tabs/src/tab-content.ts @@ -0,0 +1,30 @@ +import { type PropType, defineComponent, h } from 'vue' + +const TAB_CONTENT_NAME = 'TabContent' as const + +const TabContent = defineComponent({ + name: TAB_CONTENT_NAME, + inheritAttrs: false, + props: { + activeTab: { + type: String as PropType, + required: true, + }, + value: { + type: String as PropType, + required: true, + }, + }, + setup(props, { slots }) { + const slot = slots.default ? slots.default() : [] + return () => { + if (props.activeTab === props.value) + return h('div', { class: 'tab-content' }, slot) + else return null + } + }, +}) + +const OkuTabContent = TabContent as typeof TabContent + +export { OkuTabContent } diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts new file mode 100644 index 000000000..59d8b40c4 --- /dev/null +++ b/packages/components/tabs/src/tab-list.ts @@ -0,0 +1,69 @@ +import type { MergeProps, PrimitiveProps } from '@oku-ui/primitive' +import { type PropType, defineComponent, h } from 'vue' + +/* ------------------------------------------------------------------------------------------------- + * TabList + * ----------------------------------------------------------------------------------------------- */ + +const TAB_LIST_NAME = 'TabList' as const + +interface TabListProps extends PrimitiveProps { + /** + * The active tab value. + * @default 'tab1' + * @type string + * @example + * ```vue + * + * + * // ... + * + * + * ``` + * @see link-to-oku-docs/tab + * */ + activeTab: string + /** + * The callback function that is called when the tab value changes. + * @default () => {} + * @type (value: string) => void + * @example + * ```vue + * console.log(value)}> + * console.log(value)}> + // ... + * + * + * ``` + * @see link-to-oku-docs/tab + * */ + onChange: (value: string) => void +} + +const TabList = defineComponent({ + name: TAB_LIST_NAME, + inheritAttrs: false, + props: { + activeTab: { + type: String as PropType, + required: true, + }, + onChange: { + type: Function as PropType<(value: string) => void>, + required: true, + }, + }, + setup(_, { slots }) { + const slot = slots.default ? slots.default() : [] + + return () => h('div', { 'data-attr': 'tab-list' }, slot) + }, +}) + +type _TabListProps = MergeProps + +const OkuTabList = TabList as typeof TabList & (new () => { $props: _TabListProps }) + +export { OkuTabList } + +export type { TabListProps } diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts new file mode 100644 index 000000000..676113f25 --- /dev/null +++ b/packages/components/tabs/src/tab-trigger.ts @@ -0,0 +1,27 @@ +import { type PropType, defineComponent, h } from 'vue' + +const TAB_TRIGGER_NAME = 'TabTrigger' as const + +const TabTrigger = defineComponent({ + name: TAB_TRIGGER_NAME, + inheritAttrs: false, + props: { + value: { + type: String as PropType, + required: true, + }, + }, + setup(props, { slots, emit }) { + const handleClick = () => { + emit('change', props.value) + } + + const slot = slots.default ? slots.default() : [] + + return () => h('button', { onClick: handleClick }, slot) + }, +}) + +const OkuTabTrigger = TabTrigger as typeof TabTrigger + +export { OkuTabTrigger } diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts new file mode 100644 index 000000000..ffd00d6d7 --- /dev/null +++ b/packages/components/tabs/src/tabs.ts @@ -0,0 +1,121 @@ +import type { MergeProps, PrimitiveProps } from '@oku-ui/primitive' +import { type PropType, defineComponent, h, ref } from 'vue' +import { OkuTabContent } from './tab-content' +import { OkuTabList } from './tab-list' + +/* ------------------------------------------------------------------------------------------------- + * Tabs + * ----------------------------------------------------------------------------------------------- */ + +const TAB_NAME = 'TAB' as const + +interface TabsProps extends PrimitiveProps { + /** + * The default value of the tab. + * @default 'tab1' + * @type string + * @example + * ```vue + * + // ... + * + * ``` + * @see link-to-oku-docs/tab + */ + defaultValue?: string + /** + * The callback function that is called when the tab value changes. + * @default () => {} + * @type (value: string) => void + * @example + * ```vue + * console.log(value)}> + // ... + * + * */ + onValueChange?: (value: string) => void + /** + * The orientation of the tabs. + * @default 'horizontal' + * @type 'horizontal' | 'vertical' + * @example + * ```vue + * + // ... + * + * ``` + * @see link-to-oku-docs/tab + * */ + orientation?: 'horizontal' | 'vertical' +} + +const Tabs = defineComponent({ + name: TAB_NAME, + inheritAttrs: false, + props: { + defaultValue: { + type: String as PropType, + default: 'tab1', + }, + onValueChange: { + type: Function as PropType<(value: string) => void>, + default: () => {}, + }, + orientation: { + type: String as PropType<'horizontal' | 'vertical'>, + default: 'horizontal', + }, + }, + setup(props, { slots }) { + const activeTab = ref(props.defaultValue) + + const handleTabChange = (value: string) => { + activeTab.value = value + props.onValueChange?.(value) + } + + return () => { + const tabListSlot = slots.default + ? slots.default({ activeTab: activeTab.value }) + : [] + const tabContentSlot = slots.tabContent + ? slots.tabContent({ activeTab: activeTab.value }) + : [] + + return h('div', {}, [ + h( + // TODO: fix this + // @ts-expect-error + OkuTabList, + { + activeTab: activeTab.value, + onChange: handleTabChange, + }, + tabListSlot, + ), + h( + // TODO: fix this + // @ts-expect-error + OkuTabContent, + { + activeTab: activeTab.value, + }, + tabContentSlot.map(contentVNode => + h(OkuTabContent, { + activeTab: activeTab.value, + value: contentVNode.props?.value, + }), + ), + ), + ]) + } + }, +}) + +type _TabsProps = MergeProps + +const OkuTabs = Tabs as typeof Tabs & (new () => { $props: _TabsProps }) + +export { OkuTabs } + +export type { TabsProps } diff --git a/packages/components/tabs/tsconfig.json b/packages/components/tabs/tsconfig.json new file mode 100644 index 000000000..b8dfa9041 --- /dev/null +++ b/packages/components/tabs/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "tsconfig/node16.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": [ + "src" + ] +} diff --git a/packages/components/tabs/tsup.config.ts b/packages/components/tabs/tsup.config.ts new file mode 100644 index 000000000..a2f7a0d8b --- /dev/null +++ b/packages/components/tabs/tsup.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'tsup' +import pkg from './package.json' + +const external = [ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), +] + +export default defineConfig((options) => { + return [ + { + ...options, + entryPoints: ['src/index.ts'], + external, + dts: true, + clean: true, + target: 'node16', + format: ['esm'], + outExtension: () => ({ js: '.mjs' }), + }, + ] +}) From 95acd89fd29919f3922cb5f693f46d83d7162be0 Mon Sep 17 00:00:00 2001 From: Ejiro Asiuwhu Date: Thu, 20 Jul 2023 03:33:42 +0100 Subject: [PATCH 02/22] feat: add function for tab components --- package.json | 4 + packages/components/tabs/src/index.ts | 4 +- .../components/tabs/src/stories/TabsDemo.vue | 134 ++++++++++++++++++ .../tabs/src/stories/tabs.stories.ts | 41 ++++++ packages/components/tabs/src/tab-content.ts | 65 +++++++-- packages/components/tabs/src/tab-list.ts | 80 +++++------ packages/components/tabs/src/tab-trigger.ts | 99 ++++++++++++- packages/components/tabs/src/tabs.ts | 129 ++++++++++------- packages/core/use-composable/src/index.ts | 2 + .../use-composable/src/useArrowNavigation.ts | 130 +++++++++++++++++ .../use-composable/src/usePrimitiveElement.ts | 12 ++ playground/vue3/package.json | 1 + playground/vue3/src/pages/index.vue | 4 + playground/vue3/src/pages/tabs.vue | 7 + pnpm-lock.yaml | 93 ++++++++++-- 15 files changed, 680 insertions(+), 125 deletions(-) create mode 100644 packages/components/tabs/src/stories/TabsDemo.vue create mode 100644 packages/components/tabs/src/stories/tabs.stories.ts create mode 100644 packages/core/use-composable/src/useArrowNavigation.ts create mode 100644 packages/core/use-composable/src/usePrimitiveElement.ts create mode 100644 playground/vue3/src/pages/tabs.vue diff --git a/package.json b/package.json index 6b9472e6b..ac7f06671 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,9 @@ "clean:dist": "rimraf 'packages/**/dist' 'playground/**/dist'", "clean:dts": "rimraf 'playground/vue3/src/components.d.ts' 'playground/vue3/src/auto-imports.d.ts' 'playground/nuxt3/.nuxt'" }, + "dependencies": { + "@vueuse/core": "^10.2.1" + }, "devDependencies": { "@egoist/tailwindcss-icons": "^1.1.0", "@iconify-json/ph": "^1.1.5", @@ -37,6 +40,7 @@ "@oku-ui/label": "workspace:^", "@oku-ui/progress": "workspace:^", "@oku-ui/separator": "workspace:^", + "@oku-ui/tabs": "workspace:^", "@oku-ui/toggle": "workspace:^", "@storybook/addon-essentials": "^7.0.26", "@storybook/addon-interactions": "^7.0.26", diff --git a/packages/components/tabs/src/index.ts b/packages/components/tabs/src/index.ts index 7648ba21f..e28afb251 100644 --- a/packages/components/tabs/src/index.ts +++ b/packages/components/tabs/src/index.ts @@ -3,5 +3,7 @@ export { OkuTabList } from './tab-list' export { OkuTabTrigger } from './tab-trigger' export { OkuTabContent } from './tab-content' -export type { TabsProps } from './tabs' +export type { TabsProps, TabsProvideValue } from './tabs' export { type TabListProps } from './tab-list' +export { type TabsTriggerProps } from './tab-trigger' +export { type TabsContentProps } from './tab-content' diff --git a/packages/components/tabs/src/stories/TabsDemo.vue b/packages/components/tabs/src/stories/TabsDemo.vue new file mode 100644 index 000000000..ac3fe5aae --- /dev/null +++ b/packages/components/tabs/src/stories/TabsDemo.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/packages/components/tabs/src/stories/tabs.stories.ts b/packages/components/tabs/src/stories/tabs.stories.ts new file mode 100644 index 000000000..f776aee34 --- /dev/null +++ b/packages/components/tabs/src/stories/tabs.stories.ts @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/vue3' + +import type { OkuTabs } from '../tabs' +import OkuTabsComponent from './TabsDemo.vue' + +const meta = { + title: 'Components/Tabs', + component: OkuTabsComponent, + args: { + orientation: 'horizontal', + defaultValue: 'tab1', + }, + tags: ['autodocs'], +} satisfies Meta> + +export default meta +type Story = StoryObj + +export const Styled: Story = { + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Indeterminate: Story = { + render: (args: any) => ({ + components: { OkuTabsComponent }, + setup() { + return { args } + }, + template: ` + + `, + }), +} diff --git a/packages/components/tabs/src/tab-content.ts b/packages/components/tabs/src/tab-content.ts index b2004a4df..ad4e02119 100644 --- a/packages/components/tabs/src/tab-content.ts +++ b/packages/components/tabs/src/tab-content.ts @@ -1,30 +1,71 @@ -import { type PropType, defineComponent, h } from 'vue' +import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' +import { type PropType, computed, defineComponent, h, inject } from 'vue' +import type { TabsProvideValue } from './tabs' +import { TABS_INJECTION_KEY } from './tabs' + +/* ------------------------------------------------------------------------------------------------- + * TabTrigger + * ----------------------------------------------------------------------------------------------- */ const TAB_CONTENT_NAME = 'TabContent' as const +interface TabsContentProps extends PrimitiveProps { + value?: string + forceMount?: boolean +} + const TabContent = defineComponent({ name: TAB_CONTENT_NAME, inheritAttrs: false, props: { - activeTab: { - type: String as PropType, - required: true, - }, value: { type: String as PropType, required: true, }, + forceMount: { + type: Boolean as PropType, + default: false, + }, }, setup(props, { slots }) { - const slot = slots.default ? slots.default() : [] - return () => { - if (props.activeTab === props.value) - return h('div', { class: 'tab-content' }, slot) - else return null - } + const injectedValue = inject(TABS_INJECTION_KEY) + + const dataState = computed<'active' | 'inactive'>(() => { + return injectedValue?.modelValue?.value === props.value + ? 'active' + : 'inactive' + }) + + const shouldRender = computed(() => { + return ( + injectedValue?.modelValue?.value === props.value || props.forceMount + ) + }) + + return () => + h('div', [ + shouldRender.value + ? h( + Primitive.div, + { + 'vIf': injectedValue?.modelValue?.value === props.value, + 'role': 'tab-content', + 'data-state': dataState.value, + 'data-orientation': injectedValue?.orientation, + 'tabindex': '0', + }, + slots.default && slots.default(), + ) + : null, + ]) }, }) -const OkuTabContent = TabContent as typeof TabContent +type _TabsProps = MergeProps + +const OkuTabContent = TabContent as typeof TabContent & +(new () => { $props: _TabsProps }) export { OkuTabContent } + +export type { TabsContentProps } diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts index 59d8b40c4..2a88561b8 100644 --- a/packages/components/tabs/src/tab-list.ts +++ b/packages/components/tabs/src/tab-list.ts @@ -1,5 +1,13 @@ -import type { MergeProps, PrimitiveProps } from '@oku-ui/primitive' -import { type PropType, defineComponent, h } from 'vue' +import { + type MergeProps, + Primitive, + type PrimitiveProps, +} from '@oku-ui/primitive' +import type { PropType } from 'vue' +import { defineComponent, h, inject, onMounted } from 'vue' +import { usePrimitiveElement } from '@oku-ui/use-composable' +import type { TabsProvideValue } from './tabs' +import { TABS_INJECTION_KEY } from './tabs' /* ------------------------------------------------------------------------------------------------- * TabList @@ -8,55 +16,41 @@ import { type PropType, defineComponent, h } from 'vue' const TAB_LIST_NAME = 'TabList' as const interface TabListProps extends PrimitiveProps { - /** - * The active tab value. - * @default 'tab1' - * @type string - * @example - * ```vue - * - * - * // ... - * - * - * ``` - * @see link-to-oku-docs/tab - * */ - activeTab: string - /** - * The callback function that is called when the tab value changes. - * @default () => {} - * @type (value: string) => void - * @example - * ```vue - * console.log(value)}> - * console.log(value)}> - // ... - * - * - * ``` - * @see link-to-oku-docs/tab - * */ - onChange: (value: string) => void + loop?: boolean } const TabList = defineComponent({ name: TAB_LIST_NAME, inheritAttrs: false, props: { - activeTab: { - type: String as PropType, - required: true, - }, - onChange: { - type: Function as PropType<(value: string) => void>, - required: true, + loop: { + type: Boolean as PropType, + default: true, }, }, - setup(_, { slots }) { - const slot = slots.default ? slots.default() : [] - - return () => h('div', { 'data-attr': 'tab-list' }, slot) + setup(props, { slots }) { + const injectedValue = inject(TABS_INJECTION_KEY) + const { primitiveElement, currentElement: parentElement } + = usePrimitiveElement() + + onMounted(() => { + injectedValue!.parentElement.value = parentElement.value + injectedValue!.loop = props.loop + }) + + return () => + h( + Primitive.div, + { + 'role': 'tab-list', + 'ref': primitiveElement, + 'aria-orientation': injectedValue?.orientation, + 'tabindex': 0, + 'data-orientation': injectedValue?.orientation, + 'style': 'outline: none', + }, + slots.default && slots.default(), + ) }, }) diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts index 676113f25..d7ab86609 100644 --- a/packages/components/tabs/src/tab-trigger.ts +++ b/packages/components/tabs/src/tab-trigger.ts @@ -1,7 +1,23 @@ -import { type PropType, defineComponent, h } from 'vue' +import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' +import { type PropType, computed, defineComponent, h, inject } from 'vue' +import { + useArrowNavigation, + usePrimitiveElement, +} from '@oku-ui/use-composable' +import type { TabsProvideValue } from './tabs' +import { TABS_INJECTION_KEY } from './tabs' + +/* ------------------------------------------------------------------------------------------------- + * TabTrigger + * ----------------------------------------------------------------------------------------------- */ const TAB_TRIGGER_NAME = 'TabTrigger' as const +interface TabsTriggerProps extends PrimitiveProps { + value: string + disabled: boolean +} + const TabTrigger = defineComponent({ name: TAB_TRIGGER_NAME, inheritAttrs: false, @@ -10,18 +26,87 @@ const TabTrigger = defineComponent({ type: String as PropType, required: true, }, + disabled: { + type: Boolean as PropType, + default: false, + }, }, - setup(props, { slots, emit }) { - const handleClick = () => { - emit('change', props.value) + setup(props, { slots }) { + const injectedValue = inject(TABS_INJECTION_KEY) + const { primitiveElement, currentElement } = usePrimitiveElement() + + function changeTab(value: string) { + injectedValue?.changeModelValue(value) + } + + function handleKeydown(e: KeyboardEvent) { + if (!injectedValue?.parentElement.value || !currentElement.value) + return + const newSelectedElement = useArrowNavigation( + e, + currentElement.value, + injectedValue?.parentElement.value, + { + arrowKeyOptions: injectedValue?.orientation, + loop: injectedValue?.loop, + }, + ) + + if (newSelectedElement) { + newSelectedElement.focus() + injectedValue!.currentFocusedElement!.value = newSelectedElement + + if (injectedValue?.activationMode === 'automatic') { + changeTab( + newSelectedElement.getAttribute('data-oku-ui-tab-value')!, + ) + } + } } - const slot = slots.default ? slots.default() : [] + const getTabIndex = computed(() => { + if (!injectedValue?.currentFocusedElement?.value) { + return injectedValue?.modelValue?.value === props.value ? '0' : '-1' + } + else { + return injectedValue?.currentFocusedElement?.value + === currentElement.value + ? '0' + : '-1' + } + }) - return () => h('button', { onClick: handleClick }, slot) + return () => + h( + Primitive.button, + { + 'ref': primitiveElement, + 'type': Primitive.button, + 'role': 'tab', + 'aria-selected': + injectedValue?.modelValue?.value === props.value ? 'true' : 'false', + 'data-state': + injectedValue?.modelValue?.value === props.value + ? 'active' + : 'inactive', + 'disabled': props.disabled, + 'data-disabled': props.disabled ? '' : undefined, + 'tabindex': getTabIndex.value, + 'data-orientation': injectedValue?.orientation, + 'data-oku-ui-collection-item': true, + 'data-oku-ui-tab-value': props.value, + 'onClick': () => changeTab(props.value!), + 'onKeydown': handleKeydown, + }, + slots.default && slots.default(), + ) }, }) -const OkuTabTrigger = TabTrigger as typeof TabTrigger +type _TabsProps = MergeProps + +const OkuTabTrigger = TabTrigger as typeof TabTrigger & (new () => { $props: _TabsProps }) export { OkuTabTrigger } + +export type { TabsTriggerProps } diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index ffd00d6d7..613c2b1d1 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -1,7 +1,7 @@ -import type { MergeProps, PrimitiveProps } from '@oku-ui/primitive' -import { type PropType, defineComponent, h, ref } from 'vue' -import { OkuTabContent } from './tab-content' -import { OkuTabList } from './tab-list' +import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' +import { defineComponent, h, provide, ref } from 'vue' +import type { InjectionKey, PropType, Ref } from 'vue' +import { useVModel } from '@vueuse/core' /* ------------------------------------------------------------------------------------------------- * Tabs @@ -9,6 +9,9 @@ import { OkuTabList } from './tab-list' const TAB_NAME = 'TAB' as const +type Orientation = 'horizontal' | 'vertical' +type Direction = 'ltr' | 'rtl' +type ActivationMode = 'automatic' | 'manual' interface TabsProps extends PrimitiveProps { /** * The default value of the tab. @@ -46,69 +49,91 @@ interface TabsProps extends PrimitiveProps { * ``` * @see link-to-oku-docs/tab * */ - orientation?: 'horizontal' | 'vertical' + orientation?: Orientation + dir?: Direction + activationMode?: ActivationMode + modelValue?: string } +interface TabsProvideValue { + modelValue?: Readonly> + currentFocusedElement?: Ref + changeModelValue: (value: string) => void + parentElement: Ref + orientation: Orientation + dir: Direction + activationMode: ActivationMode + loop: boolean +} + +const TABS_INJECTION_KEY = Symbol(`${TAB_NAME} provide key`) as InjectionKey + const Tabs = defineComponent({ name: TAB_NAME, inheritAttrs: false, props: { defaultValue: { type: String as PropType, - default: 'tab1', - }, - onValueChange: { - type: Function as PropType<(value: string) => void>, - default: () => {}, + default: undefined, }, orientation: { - type: String as PropType<'horizontal' | 'vertical'>, + type: String as PropType, default: 'horizontal', }, + dir: { + type: String as PropType, + default: 'ltr', + required: false, + }, + activationMode: { + type: String as PropType, + default: 'automatic', + required: false, + }, + modelValue: { + type: String as PropType, + required: false, + }, + onValueChange: { + type: Function as PropType<(value: string) => void>, + required: false, + }, }, - setup(props, { slots }) { - const activeTab = ref(props.defaultValue) + emits: ['update:modelValue'], + setup(props, { slots, emit }) { + const parentElementRef = ref() + const currentFocusedElementRef = ref() - const handleTabChange = (value: string) => { - activeTab.value = value - props.onValueChange?.(value) - } + const modelValue = useVModel(props, 'modelValue', emit, { + defaultValue: props.defaultValue, + passive: true, + }) - return () => { - const tabListSlot = slots.default - ? slots.default({ activeTab: activeTab.value }) - : [] - const tabContentSlot = slots.tabContent - ? slots.tabContent({ activeTab: activeTab.value }) - : [] + provide(TABS_INJECTION_KEY, { + modelValue, + changeModelValue: (value: string) => { + modelValue.value = value + if (value && props.onValueChange) + props.onValueChange(value) + }, + currentFocusedElement: currentFocusedElementRef, + parentElement: parentElementRef, + orientation: props.orientation, + dir: props.dir, + loop: true, + activationMode: props.activationMode, + }) - return h('div', {}, [ - h( - // TODO: fix this - // @ts-expect-error - OkuTabList, - { - activeTab: activeTab.value, - onChange: handleTabChange, - }, - tabListSlot, - ), - h( - // TODO: fix this - // @ts-expect-error - OkuTabContent, - { - activeTab: activeTab.value, - }, - tabContentSlot.map(contentVNode => - h(OkuTabContent, { - activeTab: activeTab.value, - value: contentVNode.props?.value, - }), - ), - ), - ]) - } + return () => + h( + Primitive.div, + { + 'dir': props.dir, + 'data-orientation': props.orientation, + 'role': 'tab-group', + }, + slots.default && slots.default(), + ) }, }) @@ -116,6 +141,6 @@ type _TabsProps = MergeProps const OkuTabs = Tabs as typeof Tabs & (new () => { $props: _TabsProps }) -export { OkuTabs } +export { OkuTabs, TABS_INJECTION_KEY, TabsProvideValue } export type { TabsProps } diff --git a/packages/core/use-composable/src/index.ts b/packages/core/use-composable/src/index.ts index 77cc1079b..513bd2611 100644 --- a/packages/core/use-composable/src/index.ts +++ b/packages/core/use-composable/src/index.ts @@ -6,3 +6,5 @@ export { useRef } from './useRef' export { unrefElement } from './unrefElement' export { useId } from './useId' export type { MaybeComputedElementRef } from './unrefElement' +export { usePrimitiveElement } from './usePrimitiveElement' +export { useArrowNavigation } from './useArrowNavigation' diff --git a/packages/core/use-composable/src/useArrowNavigation.ts b/packages/core/use-composable/src/useArrowNavigation.ts new file mode 100644 index 000000000..7b6813d8b --- /dev/null +++ b/packages/core/use-composable/src/useArrowNavigation.ts @@ -0,0 +1,130 @@ +export type ArrowKeyOptions = 'horizontal' | 'vertical' | 'both' +type Direction = 'ltr' | 'rtl' + +export interface ArrowNavigationOptions { + /** + * The arrow key options to allow navigation + * + * @default "both" + */ + arrowKeyOptions?: ArrowKeyOptions + + /** + * The attribute name to find the collection items in the parent element. + * + * @default "data-oku-ui-collection-item" + */ + attributeName?: string + + /** + * The parent element where contains all the collection items, this will collect every item to be used when nav + * It will be ignored if attributeName is provided + * + * @default [] + */ + itemsArray?: HTMLElement[] + + /** + * Allow loop navigation. If false, it will stop at the first and last element + * + * @default true + */ + loop?: boolean + + /** + * The orientation of the collection + * + * @default "ltr" + */ + dir?: Direction + + /** + * Prevent the scroll when navigating. This happens when the direction of the + * key matches the scroll direction of any ancestor scrollable elements. + * + * @default true + */ + preventScroll?: boolean +} + +export function useArrowNavigation( + e: KeyboardEvent, + currentElement: HTMLElement, + parentElement: HTMLElement | undefined, + options: ArrowNavigationOptions = {}, +): HTMLElement | null { + const { + arrowKeyOptions = 'both', + attributeName = 'data-oku-ui-collection-item', + itemsArray = [], + loop = true, + dir = 'ltr', + preventScroll = true, + } = options + + const [right, left, up, down] = [ + e.key === 'ArrowRight', + e.key === 'ArrowLeft', + e.key === 'ArrowUp', + e.key === 'ArrowDown', + ] + const goingVertical = up || down + const goingHorizontal = right || left + if ( + (!goingVertical && !goingHorizontal) + || (arrowKeyOptions === 'vertical' && goingHorizontal) + || (arrowKeyOptions === 'horizontal' && goingVertical) + ) + return null + + const allCollectionItems: HTMLElement[] = parentElement + ? Array.from(parentElement.querySelectorAll(`[${attributeName}]`)) + : itemsArray + + if (!allCollectionItems.length) + return null + + if (preventScroll) + e.preventDefault() + + const goForward = goingVertical ? down : dir === 'ltr' ? right : left + + return findNextFocusableElement(allCollectionItems, currentElement, { + goForward, + loop, + }) +} + +function findNextFocusableElement( + elements: HTMLElement[], + currentElement: HTMLElement, + { goForward, loop }: { goForward: boolean; loop?: boolean }, + iterations = elements.length, +): HTMLElement | null { + if (--iterations === 0) + return null + + const index = elements.indexOf(currentElement) + const newIndex = goForward ? index + 1 : index - 1 + + if (!loop && (newIndex < 0 || newIndex >= elements.length)) + return null + + const adjustedNewIndex = (newIndex + elements.length) % elements.length + const candidate = elements[adjustedNewIndex] + if (!candidate) + return null + + const isDisabled + = candidate.hasAttribute('disabled') + && candidate.getAttribute('disabled') !== 'false' + if (isDisabled) { + return findNextFocusableElement( + elements, + candidate, + { goForward, loop }, + iterations, + ) + } + return candidate +} diff --git a/packages/core/use-composable/src/usePrimitiveElement.ts b/packages/core/use-composable/src/usePrimitiveElement.ts new file mode 100644 index 000000000..d9601c7c8 --- /dev/null +++ b/packages/core/use-composable/src/usePrimitiveElement.ts @@ -0,0 +1,12 @@ +import { computed, ref } from 'vue' +import { unrefElement } from './unrefElement' + +export function usePrimitiveElement() { + const primitiveElement = ref() + const currentElement = computed(() => unrefElement(primitiveElement)) + + return { + primitiveElement, + currentElement, + } +} diff --git a/playground/vue3/package.json b/playground/vue3/package.json index 001dfa67e..b6e336c1f 100644 --- a/playground/vue3/package.json +++ b/playground/vue3/package.json @@ -17,6 +17,7 @@ "@oku-ui/label": "workspace:^", "@oku-ui/progress": "workspace:^", "@oku-ui/separator": "workspace:^", + "@oku-ui/tabs": "workspace:^", "vite-plugin-pages": "^0.31.0", "vue": "^3.3.4", "vue-router": "^4.2.4" diff --git a/playground/vue3/src/pages/index.vue b/playground/vue3/src/pages/index.vue index b98cc19f9..fec797b26 100644 --- a/playground/vue3/src/pages/index.vue +++ b/playground/vue3/src/pages/index.vue @@ -32,6 +32,10 @@ const pages: Page[] = [ name: 'OkuToggle', path: '/toggle', }, + { + name: 'OkuTabs', + path: '/tabs', + }, ] diff --git a/playground/vue3/src/pages/tabs.vue b/playground/vue3/src/pages/tabs.vue new file mode 100644 index 000000000..01ce69ffd --- /dev/null +++ b/playground/vue3/src/pages/tabs.vue @@ -0,0 +1,7 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cee0501d..6cc613bee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -12,6 +12,10 @@ overrides: importers: .: + dependencies: + '@vueuse/core': + specifier: ^10.2.1 + version: 10.2.1(vue@3.3.4) devDependencies: '@egoist/tailwindcss-icons': specifier: ^1.1.0 @@ -40,6 +44,9 @@ importers: '@oku-ui/separator': specifier: workspace:^ version: link:packages/components/separator + '@oku-ui/tabs': + specifier: workspace:^ + version: link:packages/components/tabs '@oku-ui/toggle': specifier: workspace:^ version: link:packages/components/toggle @@ -141,7 +148,7 @@ importers: version: 1.2.1 unplugin-vue-macros: specifier: ^2.3.3 - version: 2.3.3(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5)(vue@3.3.4) + version: 2.3.3(@vueuse/core@10.2.1)(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5)(vue@3.3.4) vite: specifier: 4.3.5 version: 4.3.5(@types/node@18.16.19) @@ -294,6 +301,28 @@ importers: specifier: workspace:^ version: link:../../tsconfig + packages/components/tabs: + dependencies: + '@oku-ui/primitive': + specifier: workspace:^ + version: link:../../core/primitive + '@oku-ui/provide': + specifier: workspace:^ + version: link:../../core/provide + '@oku-ui/use-composable': + specifier: workspace:^ + version: link:../../core/use-composable + '@oku-ui/utils': + specifier: workspace:^ + version: link:../../core/utils + vue: + specifier: ^3.3.4 + version: 3.3.4 + devDependencies: + tsconfig: + specifier: workspace:^ + version: link:../../tsconfig + packages/components/toggle: dependencies: '@oku-ui/primitive': @@ -427,6 +456,9 @@ importers: '@oku-ui/separator': specifier: workspace:^ version: link:../../packages/components/separator + '@oku-ui/tabs': + specifier: workspace:^ + version: link:../../packages/components/tabs vite-plugin-pages: specifier: ^0.31.0 version: 0.31.0(vite@4.3.5) @@ -445,7 +477,7 @@ importers: version: 5.1.6 unplugin-auto-import: specifier: ^0.16.6 - version: 0.16.6(rollup@3.21.0) + version: 0.16.6(@vueuse/core@10.2.1)(rollup@3.21.0) unplugin-vue-components: specifier: ^0.25.1 version: 0.25.1(rollup@3.21.0)(vue@3.3.4) @@ -4323,7 +4355,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.3.4 - vue-component-type-helpers: 1.8.4 + vue-component-type-helpers: 1.8.5 transitivePeerDependencies: - encoding - supports-color @@ -4638,6 +4670,9 @@ packages: /@types/unist@2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + /@types/web-bluetooth@0.0.17: + resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} + /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -5108,7 +5143,7 @@ packages: vue: 3.3.4 dev: true - /@vue-macros/define-models@1.0.8(rollup@3.21.0)(vue@3.3.4): + /@vue-macros/define-models@1.0.8(@vueuse/core@10.2.1)(rollup@3.21.0)(vue@3.3.4): resolution: {integrity: sha512-tL8A49Fz34m8+63uoJYS/OpB+gApu49ZX4kZ0LY0daZ1sFhnzexNihljaGacoUQ3GIXelPwlJ/yqxv8KM8zPQQ==} engines: {node: '>=16.14.0'} peerDependencies: @@ -5118,6 +5153,7 @@ packages: optional: true dependencies: '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) + '@vueuse/core': 10.2.1(vue@3.3.4) ast-walker-scope: 0.4.2 unplugin: 1.3.2 transitivePeerDependencies: @@ -5460,6 +5496,28 @@ packages: - typescript dev: true + /@vueuse/core@10.2.1(vue@3.3.4): + resolution: {integrity: sha512-c441bfMbkAwTNwVRHQ0zdYZNETK//P84rC01aP2Uy/aRFCiie9NE/k9KdIXbno0eDYP5NPUuWv0aA/I4Unr/7w==} + dependencies: + '@types/web-bluetooth': 0.0.17 + '@vueuse/metadata': 10.2.1 + '@vueuse/shared': 10.2.1(vue@3.3.4) + vue-demi: 0.14.5(vue@3.3.4) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + /@vueuse/metadata@10.2.1: + resolution: {integrity: sha512-3Gt68mY/i6bQvFqx7cuGBzrCCQu17OBaGWS5JdwISpMsHnMKKjC2FeB5OAfMcCQ0oINfADP3i9A4PPRo0peHdQ==} + + /@vueuse/shared@10.2.1(vue@3.3.4): + resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==} + dependencies: + vue-demi: 0.14.5(vue@3.3.4) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + /@webassemblyjs/ast@1.11.5: resolution: {integrity: sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==} dependencies: @@ -13510,7 +13568,7 @@ packages: engines: {node: '>= 0.8'} dev: true - /unplugin-auto-import@0.16.6(rollup@3.21.0): + /unplugin-auto-import@0.16.6(@vueuse/core@10.2.1)(rollup@3.21.0): resolution: {integrity: sha512-M+YIITkx3C/Hg38hp8HmswP5mShUUyJOzpifv7RTlAbeFlO2Tyw0pwrogSSxnipHDPTtI8VHFBpkYkNKzYSuyA==} engines: {node: '>=14'} peerDependencies: @@ -13524,6 +13582,7 @@ packages: dependencies: '@antfu/utils': 0.7.5 '@rollup/pluginutils': 5.0.2(rollup@3.21.0) + '@vueuse/core': 10.2.1(vue@3.3.4) fast-glob: 3.3.0 local-pkg: 0.4.3 magic-string: 0.30.1 @@ -13600,7 +13659,7 @@ packages: - vue dev: true - /unplugin-vue-macros@2.3.3(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5)(vue@3.3.4): + /unplugin-vue-macros@2.3.3(@vueuse/core@10.2.1)(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5)(vue@3.3.4): resolution: {integrity: sha512-+aE2BIH2CZ1LVI/JdPxn8QPp386ZaRa8/q9I/aXnCav7UfhMiq7XW5GbpqWUP/yY/rjF+rdfXdkvUKUMTxy7mw==} engines: {node: '>=16.14.0'} peerDependencies: @@ -13610,7 +13669,7 @@ packages: '@vue-macros/chain-call': 0.0.3(rollup@3.21.0)(vue@3.3.4) '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) '@vue-macros/define-emit': 0.1.8(vue@3.3.4) - '@vue-macros/define-models': 1.0.8(rollup@3.21.0)(vue@3.3.4) + '@vue-macros/define-models': 1.0.8(@vueuse/core@10.2.1)(rollup@3.21.0)(vue@3.3.4) '@vue-macros/define-prop': 0.1.10(vue@3.3.4) '@vue-macros/define-props': 1.0.10(@vue-macros/reactivity-transform@0.3.12)(rollup@3.21.0)(vue@3.3.4) '@vue-macros/define-props-refs': 1.1.2(rollup@3.21.0)(vue@3.3.4) @@ -14217,10 +14276,24 @@ packages: resolution: {integrity: sha512-iGdlqtajmiqed8ptURKPJ/Olz0/mwripVZszg6tygfZSIL9kYFPJTNY6+Q6OjWGznl2L06vxG5HvNvAnWrnzbg==} dev: true - /vue-component-type-helpers@1.8.4: - resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==} + /vue-component-type-helpers@1.8.5: + resolution: {integrity: sha512-SBNsskF7L5x604V1BN4ZzdTtWgCqo5cfl//YuBXtc3LLyPdFRqUeJn2Q+FPNmCtl23LBT2tH79M/uv13fL0MgQ==} dev: true + /vue-demi@0.14.5(vue@3.3.4): + resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.3.4 + /vue-devtools-stub@0.1.0: resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} dev: true From 8751a8287b2e5c5c671c4524bd1b02153c71442d Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 06:14:54 +0300 Subject: [PATCH 03/22] fix: merge --- package.json | 2 +- packages/core/use-composable/src/index.ts | 1 - pnpm-lock.yaml | 326 +--------------------- 3 files changed, 4 insertions(+), 325 deletions(-) diff --git a/package.json b/package.json index fcb1c9940..9a63e35e3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "primitives", "type": "module", "version": "0.3.0", - "packageManager": "pnpm@8.6.6", + "packageManager": "pnpm@8.6.9", "repository": "oku-ui/primitives", "engines": { "node": ">=18" diff --git a/packages/core/use-composable/src/index.ts b/packages/core/use-composable/src/index.ts index 2d710e33a..2c7d34f8b 100644 --- a/packages/core/use-composable/src/index.ts +++ b/packages/core/use-composable/src/index.ts @@ -11,4 +11,3 @@ export type { MaybeComputedElementRef } from './unrefElement' export { usePrimitiveElement } from './usePrimitiveElement' export { useArrowNavigation } from './useArrowNavigation' export { computedEager, syncRef } -\ \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d054a20e9..f518bc91e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -170,12 +170,9 @@ importers: unbuild: specifier: ^1.2.1 version: 1.2.1 - unplugin-vue-macros: - specifier: ^2.3.3 - version: 2.3.3(@vueuse/core@10.2.1)(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5)(vue@3.3.4) vite: specifier: 4.3.5 - version: 4.3.5(@types/node@18.16.18) + version: 4.3.5(@types/node@18.16.19) vite-plugin-dts: specifier: ^3.3.0 version: 3.3.0(rollup@3.21.0)(typescript@5.1.6)(vite@4.3.5) @@ -585,7 +582,7 @@ importers: version: 0.25.1(rollup@3.21.0)(vue@3.3.4) vite: specifier: 4.3.5 - version: 4.3.5(@types/node@18.16.18) + version: 4.3.5(@types/node@18.16.19) vue-tsc: specifier: ^1.8.5 version: 1.8.5(typescript@5.1.6) @@ -4855,7 +4852,6 @@ packages: /@types/web-bluetooth@0.0.17: resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} - dev: false /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -5217,265 +5213,6 @@ packages: '@volar/source-map': 1.8.0 dev: true - /@volar/source-map@1.8.0: - resolution: {integrity: sha512-d35aV0yFkIrkynRSKgrN5hgbMv6ekkFvcJsJGmOZ8UEjqLStto9zq7RSvpp6/PZ7/pa4Gn1f6K1qDt0bq0oUew==} - dependencies: - muggle-string: 0.3.1 - dev: true - - /@volar/typescript@1.8.0: - resolution: {integrity: sha512-T/U1XLLhXv6tNr40Awznfc6QZWizSL99t6M0DeXtIMbnvSCqjjCVRnwlsq+DK9C1RlO3k8+i0Z8iJn7O1GGtoA==} - dependencies: - '@volar/language-core': 1.8.0 - dev: true - - /@vue-macros/api@0.7.3(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-nxDltgnMqi0yX0eAL1iXPgWE6XrgJnghbdXhEf2qIYk6dvLUu3C3ECsKpfkZUC7LDVB0lQTlGSaTS1amm5Sd0w==} - engines: {node: '>=16.14.0'} - dependencies: - '@babel/types': 7.22.5 - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - transitivePeerDependencies: - - rollup - - vue - dev: true - - /@vue-macros/api@0.7.3(rollup@3.26.2)(vue@3.3.4): - resolution: {integrity: sha512-nxDltgnMqi0yX0eAL1iXPgWE6XrgJnghbdXhEf2qIYk6dvLUu3C3ECsKpfkZUC7LDVB0lQTlGSaTS1amm5Sd0w==} - engines: {node: '>=16.14.0'} - dependencies: - '@babel/types': 7.22.5 - '@vue-macros/common': 1.4.1(rollup@3.26.2)(vue@3.3.4) - transitivePeerDependencies: - - rollup - - vue - dev: true - - /@vue-macros/better-define@1.6.4(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-hHjVj6sjo1AGkPJ9wqBUHlpcHUMUfv//sPWirFbU89MVK4hxNM13THQ74GqzwmJgqLJ1mW0NQSkn3PMnzaaRhg==} - engines: {node: '>=16.14.0'} - dependencies: - '@vue-macros/api': 0.7.3(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - transitivePeerDependencies: - - rollup - - vue - dev: true - - /@vue-macros/chain-call@0.0.3(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-eeFHYc/fMswvBnKDCIVwNyZIuvU4DaMXYcS1Qrw6Tv69w5swdk/vBm4Ii1b9OjPjgRPg8XWQK5EauN+6sHjh+w==} - engines: {node: '>=16.14.0'} - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - transitivePeerDependencies: - - rollup - - vue - dev: true - - /@vue-macros/common@1.4.1(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-guxdL0TrAPRaRtbR5iWMkX9ojcyQQ0NBVGvl9ZSxk9ugArQB1z3dCyTCirhHAo5GJZBFJ8FlrqNmpVspZV0MEw==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - peerDependenciesMeta: - vue: - optional: true - dependencies: - '@babel/types': 7.22.5 - '@rollup/pluginutils': 5.0.2(rollup@3.21.0) - '@vue/compiler-sfc': 3.3.4 - ast-kit: 0.6.6(rollup@3.21.0) - local-pkg: 0.4.3 - magic-string-ast: 0.1.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/common@1.4.1(rollup@3.26.2)(vue@3.3.4): - resolution: {integrity: sha512-guxdL0TrAPRaRtbR5iWMkX9ojcyQQ0NBVGvl9ZSxk9ugArQB1z3dCyTCirhHAo5GJZBFJ8FlrqNmpVspZV0MEw==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - peerDependenciesMeta: - vue: - optional: true - dependencies: - '@babel/types': 7.22.5 - '@rollup/pluginutils': 5.0.2(rollup@3.26.2) - '@vue/compiler-sfc': 3.3.4 - ast-kit: 0.6.6(rollup@3.26.2) - local-pkg: 0.4.3 - magic-string-ast: 0.1.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/define-emit@0.1.8(vue@3.3.4): - resolution: {integrity: sha512-psQtZ9Kwyp8/c4DWRTMehyL7yWycTn7Xz1EP7zDrVBO6oWy+CyvcZaD9xswoj3rqHI+fXSG3unb71T5bNzDuYQ==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/api': 0.7.3(rollup@3.26.2)(vue@3.3.4) - '@vue-macros/common': 1.4.1(rollup@3.26.2)(vue@3.3.4) - rollup: 3.26.2 - unplugin: 1.3.2 - vue: 3.3.4 - dev: true - - /@vue-macros/define-models@1.0.8(@vueuse/core@10.2.1)(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-tL8A49Fz34m8+63uoJYS/OpB+gApu49ZX4kZ0LY0daZ1sFhnzexNihljaGacoUQ3GIXelPwlJ/yqxv8KM8zPQQ==} - engines: {node: '>=16.14.0'} - peerDependencies: - '@vueuse/core': '>=9.0.0' - peerDependenciesMeta: - '@vueuse/core': - optional: true - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - '@vueuse/core': 10.2.1(vue@3.3.4) - ast-walker-scope: 0.4.2 - unplugin: 1.3.2 - transitivePeerDependencies: - - rollup - - vue - dev: true - - /@vue-macros/define-prop@0.1.10(vue@3.3.4): - resolution: {integrity: sha512-zuATgnqUIKHykV9hpezrnQnUycrTiKLAoynkAqLJM9NNYLFAfjp/nqLFLqjkL7NcZjGihoKYzHmxCej3mvkjOg==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/api': 0.7.3(rollup@3.26.2)(vue@3.3.4) - '@vue-macros/common': 1.4.1(rollup@3.26.2)(vue@3.3.4) - rollup: 3.26.2 - unplugin: 1.3.2 - vue: 3.3.4 - dev: true - - /@vue-macros/define-props-refs@1.1.2(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-a7WECgZV8uoa0NLkKs7jkk+Y7DYCwEhQHKwmoLyL7azb17ntXcPFNWB+muGTFF7jXqZUBF9UgC1oUNVPRdeYaw==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/define-props@1.0.10(@vue-macros/reactivity-transform@0.3.12)(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-/y37j5pwuqxKfAbgIsD/4Sxmn1q7DWR0owsUeW8wIcMBFcUTdk1ikyUMgv+nWPGsNytW7d9uymtqIe/DzAYc9A==} - engines: {node: '>=16.14.0'} - peerDependencies: - '@vue-macros/reactivity-transform': ^0.3.12 - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/reactivity-transform': 0.3.12(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/define-render@1.3.11(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-qSd4nYTz9r2nefn7NXtWec6n84B7+vi84soSgNA/LVAjgYS1qhl9WwZdBvEy9bLN2f6m4Mxs6egJ6uDVwGbfzQ==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.0.0 - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/define-slots@1.0.7(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-9i2uemlGs9rhkU0DM6B6QDZ02d5FKHlY+wF2pLxuQ3+ODTiGJB1CPbSWabgdmSE39wct+YFB3ka4ZyCcbpyeAw==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.0.0 - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/devtools@0.1.3(vite@4.3.5): - resolution: {integrity: sha512-aQRC9/TfmQajTMbZZ1BJn61rrraQztJqf64JdXRIpotbGR+xufLY/KIyTTB4SgL1pE1eW/ar5FaZTSjMqyVGIg==} - engines: {node: '>=16.14.0'} - peerDependencies: - vite: ^4.0.0 - peerDependenciesMeta: - vite: - optional: true - dependencies: - sirv: 2.0.3 - vite: 4.3.5 - vue: 3.3.4 - dev: true - - /@vue-macros/export-expose@0.0.5(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-T9sxvfb1RKFN5QY9YcK/LrKyY0eou56R/sSkyaLsX2IMVIVQLdKV7aqp1qxZV3O5N+tLSE/C63KuxzI7yQbJQA==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - '@vue/compiler-sfc': 3.3.4 - unplugin: 1.3.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/export-props@0.3.10(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-psUlgsSh9ybmcIqJrJszan6nWeB+2ycKoZNyMgQl1ARiy2UrhSxGMWOLBpWFY1Fh6Ihz1hAglWvN7EIQsgGENw==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - vue: 3.3.4 - transitivePeerDependencies: - - rollup - dev: true - - /@vue-macros/hoist-static@1.4.4(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-FWMS/1oPferEq8ZlMAU5KVd+DD7Q/JnvYKoFu598huzTt1YmGt3jJE+mvH37qJaZle5C9do9Wp2JXRgq1Bu6fQ==} - engines: {node: '>=16.14.0'} - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - transitivePeerDependencies: - - rollup - - vue - dev: true - - /@vue-macros/named-template@0.3.11(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-f+JV3AxZsBOUR7bWaLxSeWxDu6WTKWmA9NHG/eDKphnURYeTK1BZozy/dG1qPMcJreB8wdU6Tt5DxsCgSGgEBw==} - engines: {node: '>=16.14.0'} - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - '@vue/compiler-dom': 3.3.4 - unplugin: 1.3.2 - transitivePeerDependencies: - - rollup - - vue - dev: true - /@volar/language-core@1.9.0: resolution: {integrity: sha512-+PTRrGanAD2PxqMty0ZC46xhgW5BWzb67RLHhZyB3Im4+eMXsKlYjFUt7Z8ZCwTWQQOnj8NQ6gSgUEoOTwAHrQ==} dependencies: @@ -5694,11 +5431,6 @@ packages: /@vueuse/metadata@10.2.1: resolution: {integrity: sha512-3Gt68mY/i6bQvFqx7cuGBzrCCQu17OBaGWS5JdwISpMsHnMKKjC2FeB5OAfMcCQ0oINfADP3i9A4PPRo0peHdQ==} - dev: false - - /@vueuse/metadata@10.2.1: - resolution: {integrity: sha512-3Gt68mY/i6bQvFqx7cuGBzrCCQu17OBaGWS5JdwISpMsHnMKKjC2FeB5OAfMcCQ0oINfADP3i9A4PPRo0peHdQ==} - dev: false /@vueuse/shared@10.2.1(vue@3.3.4): resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==} @@ -5707,7 +5439,6 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue - dev: false /@webassemblyjs/ast@1.11.5: resolution: {integrity: sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==} @@ -13804,56 +13535,6 @@ packages: - supports-color dev: true - /unplugin-vue-define-options@1.3.10(rollup@3.21.0)(vue@3.3.4): - resolution: {integrity: sha512-+s+7pEQ+HflSTwBj2fd+83BD7tsai08yzJJkciZlKr9JaYHPNr0t0Rb59PHuxHsfg+QNVBX4qOzYJ+o6ZAHGeQ==} - engines: {node: '>=16.14.0'} - dependencies: - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - ast-walker-scope: 0.4.2 - unplugin: 1.3.2 - transitivePeerDependencies: - - rollup - - vue - dev: true - - /unplugin-vue-macros@2.3.3(@vueuse/core@10.2.1)(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5)(vue@3.3.4): - resolution: {integrity: sha512-+aE2BIH2CZ1LVI/JdPxn8QPp386ZaRa8/q9I/aXnCav7UfhMiq7XW5GbpqWUP/yY/rjF+rdfXdkvUKUMTxy7mw==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - dependencies: - '@vue-macros/better-define': 1.6.4(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/chain-call': 0.0.3(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/common': 1.4.1(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/define-emit': 0.1.8(vue@3.3.4) - '@vue-macros/define-models': 1.0.8(@vueuse/core@10.2.1)(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/define-prop': 0.1.10(vue@3.3.4) - '@vue-macros/define-props': 1.0.10(@vue-macros/reactivity-transform@0.3.12)(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/define-props-refs': 1.1.2(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/define-render': 1.3.11(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/define-slots': 1.0.7(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/devtools': 0.1.3(vite@4.3.5) - '@vue-macros/export-expose': 0.0.5(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/export-props': 0.3.10(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/hoist-static': 1.4.4(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/named-template': 0.3.11(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/reactivity-transform': 0.3.12(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/setup-block': 0.2.10(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/setup-component': 0.16.11(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/setup-sfc': 0.15.11(rollup@3.21.0)(vue@3.3.4) - '@vue-macros/short-emits': 1.4.2(rollup@3.21.0)(vue@3.3.4) - unplugin: 1.3.2 - unplugin-combine: 0.7.0(esbuild@0.18.11)(rollup@3.21.0)(vite@4.3.5) - unplugin-vue-define-options: 1.3.10(rollup@3.21.0)(vue@3.3.4) - vue: 3.3.4 - transitivePeerDependencies: - - '@vueuse/core' - - esbuild - - rollup - - vite - - webpack - dev: true - /unplugin-vue-router@0.6.4(rollup@3.21.0)(vue-router@4.2.4)(vue@3.3.4): resolution: {integrity: sha512-9THVhhtbVFxbsIibjK59oPwMI1UCxRWRPX7azSkTUABsxovlOXJys5SJx0kd/0oKIqNJuYgkRfAgPuO77SqCOg==} peerDependencies: @@ -14447,7 +14128,6 @@ packages: optional: true dependencies: vue: 3.3.4 - dev: false /vue-devtools-stub@0.1.0: resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} From d95400c0cc5f5f95ec4608b841ace393dd4ce5dc Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 06:16:35 +0300 Subject: [PATCH 04/22] refactor: depencies workspace to latest --- packages/components/tabs/package.json | 8 ++++---- playground/vue3/src/pages/index.vue | 4 ++-- pnpm-lock.yaml | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/components/tabs/package.json b/packages/components/tabs/package.json index ed6a62940..a344c1d38 100644 --- a/packages/components/tabs/package.json +++ b/packages/components/tabs/package.json @@ -33,10 +33,10 @@ "vue": "^3.3.4" }, "dependencies": { - "@oku-ui/primitive": "workspace:^", - "@oku-ui/provide": "workspace:^", - "@oku-ui/use-composable": "workspace:^", - "@oku-ui/utils": "workspace:^" + "@oku-ui/primitive": "latest", + "@oku-ui/provide": "latest", + "@oku-ui/use-composable": "latest", + "@oku-ui/utils": "latest" }, "devDependencies": { "tsconfig": "workspace:^" diff --git a/playground/vue3/src/pages/index.vue b/playground/vue3/src/pages/index.vue index 7f0da6e2a..92fceede7 100644 --- a/playground/vue3/src/pages/index.vue +++ b/playground/vue3/src/pages/index.vue @@ -35,11 +35,11 @@ const pages: Page[] = [ { name: 'OkuTabs', path: '/tabs', - }, + }, { name: 'OkuPopper', path: '/popper', - } + }, ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f518bc91e..9a02cf353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,16 +397,16 @@ importers: packages/components/tabs: dependencies: '@oku-ui/primitive': - specifier: workspace:^ + specifier: latest version: link:../../core/primitive '@oku-ui/provide': - specifier: workspace:^ + specifier: latest version: link:../../core/provide '@oku-ui/use-composable': - specifier: workspace:^ + specifier: latest version: link:../../core/use-composable '@oku-ui/utils': - specifier: workspace:^ + specifier: latest version: link:../../core/utils vue: specifier: ^3.3.4 From 22d13711edc45b1c3a22e7717d6b9efb917f0ed0 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 06:20:26 +0300 Subject: [PATCH 05/22] fix: default slots --- packages/components/tabs/src/tab-content.ts | 4 +++- packages/components/tabs/src/tab-list.ts | 4 +++- packages/components/tabs/src/tab-trigger.ts | 4 +++- packages/components/tabs/src/tabs.ts | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/components/tabs/src/tab-content.ts b/packages/components/tabs/src/tab-content.ts index ad4e02119..9b9b2768f 100644 --- a/packages/components/tabs/src/tab-content.ts +++ b/packages/components/tabs/src/tab-content.ts @@ -54,7 +54,9 @@ const TabContent = defineComponent({ 'data-orientation': injectedValue?.orientation, 'tabindex': '0', }, - slots.default && slots.default(), + { + default: () => slots.default?.(), + }, ) : null, ]) diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts index 2a88561b8..74d56d1d4 100644 --- a/packages/components/tabs/src/tab-list.ts +++ b/packages/components/tabs/src/tab-list.ts @@ -49,7 +49,9 @@ const TabList = defineComponent({ 'data-orientation': injectedValue?.orientation, 'style': 'outline: none', }, - slots.default && slots.default(), + { + default: () => slots.default?.(), + }, ) }, }) diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts index d7ab86609..3d04f1c5e 100644 --- a/packages/components/tabs/src/tab-trigger.ts +++ b/packages/components/tabs/src/tab-trigger.ts @@ -98,7 +98,9 @@ const TabTrigger = defineComponent({ 'onClick': () => changeTab(props.value!), 'onKeydown': handleKeydown, }, - slots.default && slots.default(), + { + default: () => slots.default?.(), + }, ) }, }) diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index 613c2b1d1..41dc93084 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -132,7 +132,9 @@ const Tabs = defineComponent({ 'data-orientation': props.orientation, 'role': 'tab-group', }, - slots.default && slots.default(), + { + default: () => slots.default?.(), + }, ) }, }) From 85275b38a76b65bf368fe8d8b372cf44dabd4c2c Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 06:29:00 +0300 Subject: [PATCH 06/22] feat: add aschild --- packages/components/tabs/src/tab-content.ts | 5 +++++ packages/components/tabs/src/tab-list.ts | 5 +++++ packages/components/tabs/src/tab-trigger.ts | 5 +++++ packages/components/tabs/src/tabs.ts | 16 ++++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/packages/components/tabs/src/tab-content.ts b/packages/components/tabs/src/tab-content.ts index 9b9b2768f..f2e6e81a9 100644 --- a/packages/components/tabs/src/tab-content.ts +++ b/packages/components/tabs/src/tab-content.ts @@ -26,6 +26,10 @@ const TabContent = defineComponent({ type: Boolean as PropType, default: false, }, + asChild: { + type: Boolean as PropType, + default: false, + }, }, setup(props, { slots }) { const injectedValue = inject(TABS_INJECTION_KEY) @@ -53,6 +57,7 @@ const TabContent = defineComponent({ 'data-state': dataState.value, 'data-orientation': injectedValue?.orientation, 'tabindex': '0', + 'asChild': props.asChild, }, { default: () => slots.default?.(), diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts index 74d56d1d4..343e36911 100644 --- a/packages/components/tabs/src/tab-list.ts +++ b/packages/components/tabs/src/tab-list.ts @@ -27,6 +27,10 @@ const TabList = defineComponent({ type: Boolean as PropType, default: true, }, + asChild: { + type: Boolean as PropType, + default: false, + }, }, setup(props, { slots }) { const injectedValue = inject(TABS_INJECTION_KEY) @@ -48,6 +52,7 @@ const TabList = defineComponent({ 'tabindex': 0, 'data-orientation': injectedValue?.orientation, 'style': 'outline: none', + 'asChild': props.asChild, }, { default: () => slots.default?.(), diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts index 3d04f1c5e..11b451ab0 100644 --- a/packages/components/tabs/src/tab-trigger.ts +++ b/packages/components/tabs/src/tab-trigger.ts @@ -30,6 +30,10 @@ const TabTrigger = defineComponent({ type: Boolean as PropType, default: false, }, + asChild: { + type: Boolean as PropType, + default: false, + }, }, setup(props, { slots }) { const injectedValue = inject(TABS_INJECTION_KEY) @@ -97,6 +101,7 @@ const TabTrigger = defineComponent({ 'data-oku-ui-tab-value': props.value, 'onClick': () => changeTab(props.value!), 'onKeydown': handleKeydown, + 'asChild': props.asChild, }, { default: () => slots.default?.(), diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index 41dc93084..121b271bf 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -11,6 +11,10 @@ const TAB_NAME = 'TAB' as const type Orientation = 'horizontal' | 'vertical' type Direction = 'ltr' | 'rtl' +/** + * Whether a tab is activated automatically or manually. + * @defaultValue automatic + * */ type ActivationMode = 'automatic' | 'manual' interface TabsProps extends PrimitiveProps { /** @@ -50,7 +54,14 @@ interface TabsProps extends PrimitiveProps { * @see link-to-oku-docs/tab * */ orientation?: Orientation + /** + * The direction of navigation between toolbar items. + */ dir?: Direction + /** + * Whether a tab is activated automatically or manually. + * @defaultValue automatic + * */ activationMode?: ActivationMode modelValue?: string } @@ -98,6 +109,10 @@ const Tabs = defineComponent({ type: Function as PropType<(value: string) => void>, required: false, }, + asChild: { + type: Boolean as PropType, + default: false, + }, }, emits: ['update:modelValue'], setup(props, { slots, emit }) { @@ -131,6 +146,7 @@ const Tabs = defineComponent({ 'dir': props.dir, 'data-orientation': props.orientation, 'role': 'tab-group', + 'asChild': props.asChild, }, { default: () => slots.default?.(), From 24086fa79ad3a23d2bde1cf73fb4c0bedca3c8fb Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 06:59:37 +0300 Subject: [PATCH 07/22] refactor: inject and useRef --- packages/components/tabs/src/tab-content.ts | 28 ++++----- packages/components/tabs/src/tab-list.ts | 37 ++++++------ packages/components/tabs/src/tab-trigger.ts | 58 ++++++++++--------- packages/components/tabs/src/tabs.ts | 21 +++---- packages/core/use-composable/src/index.ts | 1 - .../use-composable/src/usePrimitiveElement.ts | 12 ---- 6 files changed, 76 insertions(+), 81 deletions(-) delete mode 100644 packages/core/use-composable/src/usePrimitiveElement.ts diff --git a/packages/components/tabs/src/tab-content.ts b/packages/components/tabs/src/tab-content.ts index f2e6e81a9..4ceeff918 100644 --- a/packages/components/tabs/src/tab-content.ts +++ b/packages/components/tabs/src/tab-content.ts @@ -1,13 +1,9 @@ import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' -import { type PropType, computed, defineComponent, h, inject } from 'vue' -import type { TabsProvideValue } from './tabs' -import { TABS_INJECTION_KEY } from './tabs' +import { type PropType, computed, defineComponent, h, toRefs } from 'vue' +import type { Scope } from '@oku-ui/provide' +import { useTabsInject } from './tabs' -/* ------------------------------------------------------------------------------------------------- - * TabTrigger - * ----------------------------------------------------------------------------------------------- */ - -const TAB_CONTENT_NAME = 'TabContent' as const +const TAB_CONTENT_NAME = 'OkuTabContent' as const interface TabsContentProps extends PrimitiveProps { value?: string @@ -30,19 +26,25 @@ const TabContent = defineComponent({ type: Boolean as PropType, default: false, }, + scopeTabs: { + type: Object as unknown as PropType, + required: false, + default: undefined, + }, }, setup(props, { slots }) { - const injectedValue = inject(TABS_INJECTION_KEY) + const { scopeTabs } = toRefs(props) + const injectTabs = useTabsInject(TAB_CONTENT_NAME, scopeTabs.value) const dataState = computed<'active' | 'inactive'>(() => { - return injectedValue?.modelValue?.value === props.value + return injectTabs.value.modelValue?.value === props.value ? 'active' : 'inactive' }) const shouldRender = computed(() => { return ( - injectedValue?.modelValue?.value === props.value || props.forceMount + injectTabs.value.modelValue?.value === props.value || props.forceMount ) }) @@ -52,10 +54,10 @@ const TabContent = defineComponent({ ? h( Primitive.div, { - 'vIf': injectedValue?.modelValue?.value === props.value, + 'vIf': injectTabs.value.modelValue?.value === props.value, 'role': 'tab-content', 'data-state': dataState.value, - 'data-orientation': injectedValue?.orientation, + 'data-orientation': injectTabs.value.orientation, 'tabindex': '0', 'asChild': props.asChild, }, diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts index 343e36911..7128c6070 100644 --- a/packages/components/tabs/src/tab-list.ts +++ b/packages/components/tabs/src/tab-list.ts @@ -4,16 +4,12 @@ import { type PrimitiveProps, } from '@oku-ui/primitive' import type { PropType } from 'vue' -import { defineComponent, h, inject, onMounted } from 'vue' -import { usePrimitiveElement } from '@oku-ui/use-composable' -import type { TabsProvideValue } from './tabs' -import { TABS_INJECTION_KEY } from './tabs' +import { defineComponent, h, onMounted, toRefs } from 'vue' +import { useRef } from '@oku-ui/use-composable' +import type { Scope } from '@oku-ui/provide' +import { useTabsInject } from './tabs' -/* ------------------------------------------------------------------------------------------------- - * TabList - * ----------------------------------------------------------------------------------------------- */ - -const TAB_LIST_NAME = 'TabList' as const +const TAB_LIST_NAME = 'OkuTabList' as const interface TabListProps extends PrimitiveProps { loop?: boolean @@ -31,15 +27,22 @@ const TabList = defineComponent({ type: Boolean as PropType, default: false, }, + scopeTabs: { + type: Object as unknown as PropType, + required: false, + default: undefined, + }, }, setup(props, { slots }) { - const injectedValue = inject(TABS_INJECTION_KEY) - const { primitiveElement, currentElement: parentElement } - = usePrimitiveElement() + const { scopeTabs } = toRefs(props) + const injectTabs = useTabsInject(TAB_LIST_NAME, scopeTabs.value) + + const { $el, newRef: parentElement } + = useRef() onMounted(() => { - injectedValue!.parentElement.value = parentElement.value - injectedValue!.loop = props.loop + injectTabs.value.parentElement.value = $el.value + injectTabs.value.loop = props.loop }) return () => @@ -47,10 +50,10 @@ const TabList = defineComponent({ Primitive.div, { 'role': 'tab-list', - 'ref': primitiveElement, - 'aria-orientation': injectedValue?.orientation, + 'ref': parentElement, + 'aria-orientation': injectTabs.value.orientation, 'tabindex': 0, - 'data-orientation': injectedValue?.orientation, + 'data-orientation': injectTabs.value.orientation, 'style': 'outline: none', 'asChild': props.asChild, }, diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts index 11b451ab0..05696254d 100644 --- a/packages/components/tabs/src/tab-trigger.ts +++ b/packages/components/tabs/src/tab-trigger.ts @@ -1,17 +1,12 @@ import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' -import { type PropType, computed, defineComponent, h, inject } from 'vue' +import { type PropType, computed, defineComponent, h, toRefs } from 'vue' import { - useArrowNavigation, - usePrimitiveElement, + useArrowNavigation, useRef, } from '@oku-ui/use-composable' -import type { TabsProvideValue } from './tabs' -import { TABS_INJECTION_KEY } from './tabs' +import type { Scope } from '@oku-ui/provide' +import { useTabsInject } from './tabs' -/* ------------------------------------------------------------------------------------------------- - * TabTrigger - * ----------------------------------------------------------------------------------------------- */ - -const TAB_TRIGGER_NAME = 'TabTrigger' as const +const TAB_TRIGGER_NAME = 'OkuTabTrigger' as const interface TabsTriggerProps extends PrimitiveProps { value: string @@ -34,33 +29,40 @@ const TabTrigger = defineComponent({ type: Boolean as PropType, default: false, }, + scopeTabs: { + type: Object as unknown as PropType, + required: false, + default: undefined, + }, }, setup(props, { slots }) { - const injectedValue = inject(TABS_INJECTION_KEY) - const { primitiveElement, currentElement } = usePrimitiveElement() + const { scopeTabs } = toRefs(props) + const injectedValue = useTabsInject(TAB_TRIGGER_NAME, scopeTabs.value) + + const { $el, newRef: currentElement } = useRef() function changeTab(value: string) { - injectedValue?.changeModelValue(value) + injectedValue.value.changeModelValue(value) } function handleKeydown(e: KeyboardEvent) { - if (!injectedValue?.parentElement.value || !currentElement.value) + if (!injectedValue.value.parentElement.value || $el.value) return const newSelectedElement = useArrowNavigation( e, - currentElement.value, - injectedValue?.parentElement.value, + $el.value, + injectedValue.value.parentElement.value, { - arrowKeyOptions: injectedValue?.orientation, - loop: injectedValue?.loop, + arrowKeyOptions: injectedValue.value.orientation, + loop: injectedValue.value.loop, }, ) if (newSelectedElement) { newSelectedElement.focus() - injectedValue!.currentFocusedElement!.value = newSelectedElement + injectedValue.value.currentFocusedElement!.value = newSelectedElement - if (injectedValue?.activationMode === 'automatic') { + if (injectedValue.value.activationMode === 'automatic') { changeTab( newSelectedElement.getAttribute('data-oku-ui-tab-value')!, ) @@ -69,12 +71,12 @@ const TabTrigger = defineComponent({ } const getTabIndex = computed(() => { - if (!injectedValue?.currentFocusedElement?.value) { - return injectedValue?.modelValue?.value === props.value ? '0' : '-1' + if (!injectedValue.value.currentFocusedElement?.value) { + return injectedValue.value.modelValue?.value === props.value ? '0' : '-1' } else { - return injectedValue?.currentFocusedElement?.value - === currentElement.value + return injectedValue.value.currentFocusedElement?.value + === $el.value ? '0' : '-1' } @@ -84,19 +86,19 @@ const TabTrigger = defineComponent({ h( Primitive.button, { - 'ref': primitiveElement, + 'ref': currentElement, 'type': Primitive.button, 'role': 'tab', 'aria-selected': - injectedValue?.modelValue?.value === props.value ? 'true' : 'false', + injectedValue.value.modelValue?.value === props.value ? 'true' : 'false', 'data-state': - injectedValue?.modelValue?.value === props.value + injectedValue.value.modelValue?.value === props.value ? 'active' : 'inactive', 'disabled': props.disabled, 'data-disabled': props.disabled ? '' : undefined, 'tabindex': getTabIndex.value, - 'data-orientation': injectedValue?.orientation, + 'data-orientation': injectedValue.value.orientation, 'data-oku-ui-collection-item': true, 'data-oku-ui-tab-value': props.value, 'onClick': () => changeTab(props.value!), diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index 121b271bf..0d6f7f83d 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -1,13 +1,10 @@ import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' -import { defineComponent, h, provide, ref } from 'vue' -import type { InjectionKey, PropType, Ref } from 'vue' +import { defineComponent, h, ref } from 'vue' +import type { PropType, Ref } from 'vue' import { useVModel } from '@vueuse/core' +import { createProvideScope } from '@oku-ui/provide' -/* ------------------------------------------------------------------------------------------------- - * Tabs - * ----------------------------------------------------------------------------------------------- */ - -const TAB_NAME = 'TAB' as const +const TAB_NAME = 'OkuTab' as const type Orientation = 'horizontal' | 'vertical' type Direction = 'ltr' | 'rtl' @@ -77,7 +74,10 @@ interface TabsProvideValue { loop: boolean } -const TABS_INJECTION_KEY = Symbol(`${TAB_NAME} provide key`) as InjectionKey +export const [createTabsProvider, _createTabsScope] = createProvideScope(TAB_NAME) + +export const [TabsProvider, useTabsInject] + = createTabsProvider(TAB_NAME) const Tabs = defineComponent({ name: TAB_NAME, @@ -124,7 +124,7 @@ const Tabs = defineComponent({ passive: true, }) - provide(TABS_INJECTION_KEY, { + TabsProvider({ modelValue, changeModelValue: (value: string) => { modelValue.value = value @@ -137,6 +137,7 @@ const Tabs = defineComponent({ dir: props.dir, loop: true, activationMode: props.activationMode, + scope: undefined, }) return () => @@ -159,6 +160,6 @@ type _TabsProps = MergeProps const OkuTabs = Tabs as typeof Tabs & (new () => { $props: _TabsProps }) -export { OkuTabs, TABS_INJECTION_KEY, TabsProvideValue } +export { OkuTabs, TabsProvideValue } export type { TabsProps } diff --git a/packages/core/use-composable/src/index.ts b/packages/core/use-composable/src/index.ts index 2c7d34f8b..bbcafe8a6 100644 --- a/packages/core/use-composable/src/index.ts +++ b/packages/core/use-composable/src/index.ts @@ -8,6 +8,5 @@ export { useRef } from './useRef' export { unrefElement } from './unrefElement' export { useId } from './useId' export type { MaybeComputedElementRef } from './unrefElement' -export { usePrimitiveElement } from './usePrimitiveElement' export { useArrowNavigation } from './useArrowNavigation' export { computedEager, syncRef } diff --git a/packages/core/use-composable/src/usePrimitiveElement.ts b/packages/core/use-composable/src/usePrimitiveElement.ts deleted file mode 100644 index d9601c7c8..000000000 --- a/packages/core/use-composable/src/usePrimitiveElement.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { computed, ref } from 'vue' -import { unrefElement } from './unrefElement' - -export function usePrimitiveElement() { - const primitiveElement = ref() - const currentElement = computed(() => unrefElement(primitiveElement)) - - return { - primitiveElement, - currentElement, - } -} From 01ee3f45b5bce038fab4a68946525ea15ab20076 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 07:10:34 +0300 Subject: [PATCH 08/22] fix: type --- packages/components/tabs/src/index.ts | 2 +- packages/components/tabs/src/tabs.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/components/tabs/src/index.ts b/packages/components/tabs/src/index.ts index e28afb251..517b59858 100644 --- a/packages/components/tabs/src/index.ts +++ b/packages/components/tabs/src/index.ts @@ -3,7 +3,7 @@ export { OkuTabList } from './tab-list' export { OkuTabTrigger } from './tab-trigger' export { OkuTabContent } from './tab-content' -export type { TabsProps, TabsProvideValue } from './tabs' +export type { TabsProps } from './tabs' export { type TabListProps } from './tab-list' export { type TabsTriggerProps } from './tab-trigger' export { type TabsContentProps } from './tab-content' diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index 0d6f7f83d..cbbe47d77 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -1,4 +1,5 @@ -import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' +import type { MergeProps, PrimitiveProps, RefElement } from '@oku-ui/primitive' +import { Primitive } from '@oku-ui/primitive' import { defineComponent, h, ref } from 'vue' import type { PropType, Ref } from 'vue' import { useVModel } from '@vueuse/core' @@ -158,8 +159,10 @@ const Tabs = defineComponent({ type _TabsProps = MergeProps +type TabsRef = RefElement + const OkuTabs = Tabs as typeof Tabs & (new () => { $props: _TabsProps }) -export { OkuTabs, TabsProvideValue } +export { OkuTabs } -export type { TabsProps } +export type { TabsProps, TabsRef } From 61e953e74b7a3d4ad3573efecbb819ebab68f388 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 20 Jul 2023 08:12:14 +0300 Subject: [PATCH 09/22] chore: delete @vueuse/core root --- package.json | 3 --- pnpm-lock.yaml | 14 +++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 9a63e35e3..981bc9e3e 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,6 @@ "clean:dts": "rimraf 'playground/vue3/src/components.d.ts' 'playground/vue3/src/auto-imports.d.ts' 'playground/nuxt3/.nuxt'", "update:version": "esno scripts/update-version.ts" }, - "dependencies": { - "@vueuse/core": "^10.2.1" - }, "devDependencies": { "@egoist/tailwindcss-icons": "^1.1.0", "@iconify-json/ph": "^1.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a02cf353..847b64b8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,10 +12,6 @@ overrides: importers: .: - dependencies: - '@vueuse/core': - specifier: ^10.2.1 - version: 10.2.1(vue@3.3.4) devDependencies: '@egoist/tailwindcss-icons': specifier: ^1.1.0 @@ -576,7 +572,7 @@ importers: version: 5.1.6 unplugin-auto-import: specifier: ^0.16.6 - version: 0.16.6(@vueuse/core@10.2.1)(rollup@3.21.0) + version: 0.16.6(rollup@3.21.0) unplugin-vue-components: specifier: ^0.25.1 version: 0.25.1(rollup@3.21.0)(vue@3.3.4) @@ -4852,6 +4848,7 @@ packages: /@types/web-bluetooth@0.0.17: resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} + dev: false /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -5428,9 +5425,11 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue + dev: false /@vueuse/metadata@10.2.1: resolution: {integrity: sha512-3Gt68mY/i6bQvFqx7cuGBzrCCQu17OBaGWS5JdwISpMsHnMKKjC2FeB5OAfMcCQ0oINfADP3i9A4PPRo0peHdQ==} + dev: false /@vueuse/shared@10.2.1(vue@3.3.4): resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==} @@ -5439,6 +5438,7 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue + dev: false /@webassemblyjs/ast@1.11.5: resolution: {integrity: sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==} @@ -13481,7 +13481,7 @@ packages: engines: {node: '>= 0.8'} dev: true - /unplugin-auto-import@0.16.6(@vueuse/core@10.2.1)(rollup@3.21.0): + /unplugin-auto-import@0.16.6(rollup@3.21.0): resolution: {integrity: sha512-M+YIITkx3C/Hg38hp8HmswP5mShUUyJOzpifv7RTlAbeFlO2Tyw0pwrogSSxnipHDPTtI8VHFBpkYkNKzYSuyA==} engines: {node: '>=14'} peerDependencies: @@ -13495,7 +13495,6 @@ packages: dependencies: '@antfu/utils': 0.7.5 '@rollup/pluginutils': 5.0.2(rollup@3.21.0) - '@vueuse/core': 10.2.1(vue@3.3.4) fast-glob: 3.3.0 local-pkg: 0.4.3 magic-string: 0.30.1 @@ -14128,6 +14127,7 @@ packages: optional: true dependencies: vue: 3.3.4 + dev: false /vue-devtools-stub@0.1.0: resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} From 781ea4ed744f367f41c8f6c686e7778b1c2018a5 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 9 Aug 2023 12:59:08 +0300 Subject: [PATCH 10/22] fix: lint --- package.json | 2 +- playground/vue3/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 30fea2b01..62bb1719e 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,9 @@ "@oku-ui/provide": "workspace:^", "@oku-ui/roving-focus": "workspace:^", "@oku-ui/separator": "workspace:^", - "@oku-ui/tabs": "workspace:^", "@oku-ui/slot": "workspace:^", "@oku-ui/switch": "workspace:^", + "@oku-ui/tabs": "workspace:^", "@oku-ui/toggle": "workspace:^", "@oku-ui/use-composable": "workspace:^", "@oku-ui/utils": "workspace:^", diff --git a/playground/vue3/package.json b/playground/vue3/package.json index 552848ee6..f4c489fb2 100644 --- a/playground/vue3/package.json +++ b/playground/vue3/package.json @@ -18,9 +18,9 @@ "@oku-ui/progress": "workspace:^", "@oku-ui/roving-focus": "workspace:^", "@oku-ui/separator": "workspace:^", - "@oku-ui/tabs": "workspace:^", "@oku-ui/slot": "workspace:^", "@oku-ui/switch": "workspace:^", + "@oku-ui/tabs": "workspace:^", "vite-plugin-pages": "^0.31.0", "vue": "^3.3.4", "vue-router": "^4.2.4" From 9fa26b5350a9f7744ae8988e9afba443de90608f Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 9 Aug 2023 20:44:55 +0300 Subject: [PATCH 11/22] rafactor: change struct --- packages/components/roving-focus/src/index.ts | 2 +- packages/components/tabs/package.json | 3 + packages/components/tabs/src/tab-content.ts | 82 +++++----- packages/components/tabs/src/tab-list.ts | 70 ++++---- packages/components/tabs/src/tab-trigger.ts | 151 ++++++++---------- packages/components/tabs/src/tabs.ts | 80 ++++++---- packages/components/tabs/src/utils.ts | 7 + pnpm-lock.yaml | 9 ++ 8 files changed, 216 insertions(+), 188 deletions(-) create mode 100644 packages/components/tabs/src/utils.ts diff --git a/packages/components/roving-focus/src/index.ts b/packages/components/roving-focus/src/index.ts index a124a06c8..f614de1ec 100644 --- a/packages/components/roving-focus/src/index.ts +++ b/packages/components/roving-focus/src/index.ts @@ -1,2 +1,2 @@ -export { OkuRovingFocusGroup } from './RovingFocusGroup' +export { OkuRovingFocusGroup, createRovingFocusGroupScope } from './RovingFocusGroup' export { OkuRovingFocusGroupItem } from './RovingFocusGroupItem' diff --git a/packages/components/tabs/package.json b/packages/components/tabs/package.json index a344c1d38..561893bcf 100644 --- a/packages/components/tabs/package.json +++ b/packages/components/tabs/package.json @@ -33,8 +33,11 @@ "vue": "^3.3.4" }, "dependencies": { + "@oku-ui/direction": "latest", + "@oku-ui/presence": "latest", "@oku-ui/primitive": "latest", "@oku-ui/provide": "latest", + "@oku-ui/roving-focus": "latest", "@oku-ui/use-composable": "latest", "@oku-ui/utils": "latest" }, diff --git a/packages/components/tabs/src/tab-content.ts b/packages/components/tabs/src/tab-content.ts index 4ceeff918..83876cd66 100644 --- a/packages/components/tabs/src/tab-content.ts +++ b/packages/components/tabs/src/tab-content.ts @@ -1,13 +1,20 @@ -import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' -import { type PropType, computed, defineComponent, h, toRefs } from 'vue' -import type { Scope } from '@oku-ui/provide' -import { useTabsInject } from './tabs' +import type { IPrimitiveProps, MergeProps } from '@oku-ui/primitive' +import { computed, defineComponent, ref, toRefs, watchEffect } from 'vue' +import type { PropType } from 'vue' +import type { ScopedPropsInterface } from './tabs' +import { ScopedProps, useTabsInject } from './tabs' +import { makeContentId, makeTriggerId } from './utils' const TAB_CONTENT_NAME = 'OkuTabContent' as const -interface TabsContentProps extends PrimitiveProps { - value?: string - forceMount?: boolean +interface TabsContentProps extends ScopedPropsInterface { + value: string + + /** + * Used to force mounting when more control is needed. Useful when + * controlling animation with React animation libraries. + */ + forceMount?: true } const TabContent = defineComponent({ @@ -26,47 +33,40 @@ const TabContent = defineComponent({ type: Boolean as PropType, default: false, }, - scopeTabs: { - type: Object as unknown as PropType, - required: false, - default: undefined, - }, + ...ScopedProps, }, setup(props, { slots }) { - const { scopeTabs } = toRefs(props) + const { scopeTabs, value } = toRefs(props) const injectTabs = useTabsInject(TAB_CONTENT_NAME, scopeTabs.value) - const dataState = computed<'active' | 'inactive'>(() => { - return injectTabs.value.modelValue?.value === props.value - ? 'active' - : 'inactive' - }) + const triggerId = makeTriggerId(injectTabs.value.baseId, value.value) + const contentId = makeContentId(injectTabs.value.baseId, value.value) + const isSelected = computed(() => value.value === injectTabs.value.value) - const shouldRender = computed(() => { - return ( - injectTabs.value.modelValue?.value === props.value || props.forceMount - ) + const isMountAnimationPreventedRef = ref(isSelected.value) + + watchEffect((onClean) => { + const rAF = requestAnimationFrame(() => (isMountAnimationPreventedRef.value = false)) + onClean(() => cancelAnimationFrame(rAF)) }) - return () => - h('div', [ - shouldRender.value - ? h( - Primitive.div, - { - 'vIf': injectTabs.value.modelValue?.value === props.value, - 'role': 'tab-content', - 'data-state': dataState.value, - 'data-orientation': injectTabs.value.orientation, - 'tabindex': '0', - 'asChild': props.asChild, - }, - { - default: () => slots.default?.(), - }, - ) - : null, - ]) + // TODO: presence + // h(OkuPresence, { + + // }, { + // default: () => h( + // Primitive.div, + // { + // 'role': 'tab-content', + // 'data-orientation': injectTabs.value.orientation, + // 'tabindex': '0', + // 'asChild': props.asChild, + // }, + // { + // default: () => slots.default?.(), + // }, + // ), + // }) }, }) diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts index 7128c6070..1f36bd543 100644 --- a/packages/components/tabs/src/tab-list.ts +++ b/packages/components/tabs/src/tab-list.ts @@ -1,20 +1,23 @@ import { + type IPrimitiveProps, type MergeProps, Primitive, - type PrimitiveProps, } from '@oku-ui/primitive' import type { PropType } from 'vue' -import { defineComponent, h, onMounted, toRefs } from 'vue' -import { useRef } from '@oku-ui/use-composable' -import type { Scope } from '@oku-ui/provide' -import { useTabsInject } from './tabs' +import { defineComponent, h, toRefs } from 'vue' +import { useForwardRef } from '@oku-ui/use-composable' +import { OkuRovingFocusGroup, createRovingFocusGroupScope } from '@oku-ui/roving-focus' +import type { ScopedPropsInterface } from './tabs' +import { ScopedProps, useTabsInject } from './tabs' const TAB_LIST_NAME = 'OkuTabList' as const -interface TabListProps extends PrimitiveProps { +interface TabListProps extends ScopedPropsInterface { loop?: boolean } +const useRovingFocusGroupScope = createRovingFocusGroupScope() + const TabList = defineComponent({ name: TAB_LIST_NAME, inheritAttrs: false, @@ -27,40 +30,39 @@ const TabList = defineComponent({ type: Boolean as PropType, default: false, }, - scopeTabs: { - type: Object as unknown as PropType, - required: false, - default: undefined, - }, + ...ScopedProps, }, - setup(props, { slots }) { + setup(props, { slots, attrs }) { const { scopeTabs } = toRefs(props) - const injectTabs = useTabsInject(TAB_LIST_NAME, scopeTabs.value) + const { ...listAttrs } = attrs - const { $el, newRef: parentElement } - = useRef() + const injectTabs = useTabsInject(TAB_LIST_NAME, scopeTabs.value) + const forwardedRef = useForwardRef() - onMounted(() => { - injectTabs.value.parentElement.value = $el.value - injectTabs.value.loop = props.loop - }) + const rovingFocusGroupScope = useRovingFocusGroupScope(props.scopeTabs) return () => - h( - Primitive.div, - { - 'role': 'tab-list', - 'ref': parentElement, - 'aria-orientation': injectTabs.value.orientation, - 'tabindex': 0, - 'data-orientation': injectTabs.value.orientation, - 'style': 'outline: none', - 'asChild': props.asChild, - }, - { - default: () => slots.default?.(), - }, - ) + h(OkuRovingFocusGroup, { + asChild: props.asChild, + ...rovingFocusGroupScope, + scope: rovingFocusGroupScope, + dir: injectTabs.value.dir, + loop: props.loop, + }, { + default: () => h( + Primitive.div, + { + 'role': 'tablist', + 'aria-orientation': injectTabs.value.orientation, + ...listAttrs, + 'asChild': props.asChild, + 'ref': forwardedRef, + }, + { + default: () => slots.default?.(), + }, + ), + }) }, }) diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts index 05696254d..4be143f63 100644 --- a/packages/components/tabs/src/tab-trigger.ts +++ b/packages/components/tabs/src/tab-trigger.ts @@ -1,16 +1,21 @@ -import { type MergeProps, Primitive, type PrimitiveProps } from '@oku-ui/primitive' +import { Primitive } from '@oku-ui/primitive' +import type { IPrimitiveProps, MergeProps } from '@oku-ui/primitive' import { type PropType, computed, defineComponent, h, toRefs } from 'vue' -import { - useArrowNavigation, useRef, -} from '@oku-ui/use-composable' -import type { Scope } from '@oku-ui/provide' -import { useTabsInject } from './tabs' +import { useForwardRef } from '@oku-ui/use-composable' +import { OkuRovingFocusGroupItem } from '@oku-ui/roving-focus' +import { composeEventHandlers } from '@oku-ui/utils' +import type { ScopedPropsInterface } from './tabs' +import { ScopedProps, useRovingFocusGroupScope, useTabsInject } from './tabs' +import { makeContentId, makeTriggerId } from './utils' const TAB_TRIGGER_NAME = 'OkuTabTrigger' as const -interface TabsTriggerProps extends PrimitiveProps { +interface TabsTriggerProps extends ScopedPropsInterface { value: string disabled: boolean + onMousedown?: (event: MouseEvent) => void + onKeydown?: (event: KeyboardEvent) => void + onFocus?: (event: FocusEvent) => void } const TabTrigger = defineComponent({ @@ -29,86 +34,70 @@ const TabTrigger = defineComponent({ type: Boolean as PropType, default: false, }, - scopeTabs: { - type: Object as unknown as PropType, - required: false, - default: undefined, - }, + onMousedown: Function as PropType<(e: MouseEvent) => void>, + onKeydown: Function as PropType<(e: KeyboardEvent) => void>, + onFocus: Function as PropType<(e: FocusEvent) => void>, + ...ScopedProps, }, - setup(props, { slots }) { - const { scopeTabs } = toRefs(props) + setup(props, { slots, attrs }) { + const { scopeTabs, value, disabled } = toRefs(props) + const { ...triggerAttrs } = attrs const injectedValue = useTabsInject(TAB_TRIGGER_NAME, scopeTabs.value) - const { $el, newRef: currentElement } = useRef() - - function changeTab(value: string) { - injectedValue.value.changeModelValue(value) - } - - function handleKeydown(e: KeyboardEvent) { - if (!injectedValue.value.parentElement.value || $el.value) - return - const newSelectedElement = useArrowNavigation( - e, - $el.value, - injectedValue.value.parentElement.value, - { - arrowKeyOptions: injectedValue.value.orientation, - loop: injectedValue.value.loop, - }, - ) - - if (newSelectedElement) { - newSelectedElement.focus() - injectedValue.value.currentFocusedElement!.value = newSelectedElement - - if (injectedValue.value.activationMode === 'automatic') { - changeTab( - newSelectedElement.getAttribute('data-oku-ui-tab-value')!, - ) - } - } - } + const forwardedRef = useForwardRef() - const getTabIndex = computed(() => { - if (!injectedValue.value.currentFocusedElement?.value) { - return injectedValue.value.modelValue?.value === props.value ? '0' : '-1' - } - else { - return injectedValue.value.currentFocusedElement?.value - === $el.value - ? '0' - : '-1' - } - }) + const rovingFocusGroupScope = useRovingFocusGroupScope(props.scopeTabs) + const triggerId = makeTriggerId(injectedValue.value.baseId, value.value) + const contentId = makeContentId(injectedValue.value.baseId, value.value) + const isSelected = computed(() => (value.value === injectedValue.value.value)) return () => - h( - Primitive.button, - { - 'ref': currentElement, - 'type': Primitive.button, - 'role': 'tab', - 'aria-selected': - injectedValue.value.modelValue?.value === props.value ? 'true' : 'false', - 'data-state': - injectedValue.value.modelValue?.value === props.value - ? 'active' - : 'inactive', - 'disabled': props.disabled, - 'data-disabled': props.disabled ? '' : undefined, - 'tabindex': getTabIndex.value, - 'data-orientation': injectedValue.value.orientation, - 'data-oku-ui-collection-item': true, - 'data-oku-ui-tab-value': props.value, - 'onClick': () => changeTab(props.value!), - 'onKeydown': handleKeydown, - 'asChild': props.asChild, - }, - { - default: () => slots.default?.(), - }, - ) + h(OkuRovingFocusGroupItem, { + asChild: props.asChild, + ...rovingFocusGroupScope, + active: isSelected.value, + focusable: !disabled.value, + }, { + default: () => h( + Primitive.button, + { + 'type': 'button', + 'role': 'tab', + 'aria-selected': isSelected.value, + 'aria-controls': contentId, + 'data-state': isSelected.value ? 'active' : 'inactive', + 'disabled': disabled.value, + 'id': triggerId, + ...triggerAttrs, + 'ref': forwardedRef, + 'onMousedown': composeEventHandlers(props.onMousedown, (event: MouseEvent) => { + // 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 (!disabled.value && event.button === 0 && event.ctrlKey === false) { + injectedValue.value.onValueChange(value.value) + } + else { + // prevent focus to avoid accidental activation + event.preventDefault() + } + }), + 'onKeydown': composeEventHandlers(props.onKeydown, (event: KeyboardEvent) => { + if ([' ', 'Enter'].includes(event.key)) + injectedValue.value.onValueChange(value.value) + }), + 'onFocus': composeEventHandlers(props.onFocus, () => { + // handle "automatic" activation if necessary + // ie. activate tab following focus + const isAutomaticActivation = injectedValue.value.activationMode !== 'manual' + if (!isSelected.value && !disabled.value && isAutomaticActivation) + injectedValue.value.onValueChange(value.value) + }), + }, + { + default: () => slots.default?.(), + }, + ), + }) }, }) diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index cbbe47d77..440e56892 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -1,12 +1,22 @@ -import type { MergeProps, PrimitiveProps, RefElement } from '@oku-ui/primitive' +import type { IPrimitiveProps, MergeProps, RefElement } from '@oku-ui/primitive' import { Primitive } from '@oku-ui/primitive' -import { defineComponent, h, ref } from 'vue' -import type { PropType, Ref } from 'vue' -import { useVModel } from '@vueuse/core' +import { computed, defineComponent, h, toRefs, useModel } from 'vue' +import type { PropType } from 'vue' +import type { Scope } from '@oku-ui/provide' import { createProvideScope } from '@oku-ui/provide' +import { createRovingFocusGroupScope } from '@oku-ui/roving-focus' +import { useControllable, useForwardRef, useId } from '@oku-ui/use-composable' +import { useDirection } from '@oku-ui/direction' const TAB_NAME = 'OkuTab' as const +export type ScopedPropsInterface

= P & { scopeTabs?: Scope } +export const ScopedProps = { + scopeTabs: { + type: Object as PropType, + }, +} + type Orientation = 'horizontal' | 'vertical' type Direction = 'ltr' | 'rtl' /** @@ -14,7 +24,9 @@ type Direction = 'ltr' | 'rtl' * @defaultValue automatic * */ type ActivationMode = 'automatic' | 'manual' -interface TabsProps extends PrimitiveProps { +interface TabsProps extends ScopedPropsInterface { + /** The value for the selected tab, if controlled */ + value?: string /** * The default value of the tab. * @default 'tab1' @@ -61,18 +73,15 @@ interface TabsProps extends PrimitiveProps { * @defaultValue automatic * */ activationMode?: ActivationMode - modelValue?: string } interface TabsProvideValue { - modelValue?: Readonly> - currentFocusedElement?: Ref - changeModelValue: (value: string) => void - parentElement: Ref - orientation: Orientation - dir: Direction - activationMode: ActivationMode - loop: boolean + baseId: string + value?: string + onValueChange: (value: string) => void + orientation?: TabsProps['orientation'] + dir?: TabsProps['dir'] + activationMode?: TabsProps['activationMode'] } export const [createTabsProvider, _createTabsScope] = createProvideScope(TAB_NAME) @@ -80,10 +89,16 @@ export const [createTabsProvider, _createTabsScope] = createProvideScope(TAB_NAM export const [TabsProvider, useTabsInject] = createTabsProvider(TAB_NAME) +export const useRovingFocusGroupScope = createRovingFocusGroupScope() + const Tabs = defineComponent({ name: TAB_NAME, inheritAttrs: false, props: { + value: { + type: String as PropType, + required: false, + }, defaultValue: { type: String as PropType, default: undefined, @@ -114,41 +129,44 @@ const Tabs = defineComponent({ type: Boolean as PropType, default: false, }, + ...ScopedProps, }, emits: ['update:modelValue'], setup(props, { slots, emit }) { - const parentElementRef = ref() - const currentFocusedElementRef = ref() + const direction = useDirection(props.dir) + const { value: valueProp, defaultValue } = toRefs(props) + + const forwardedRef = useForwardRef() + + const modelValue = useModel(props, 'modelValue') - const modelValue = useVModel(props, 'modelValue', emit, { - defaultValue: props.defaultValue, - passive: true, + const { state, updateValue } = useControllable({ + prop: computed(() => modelValue.value ?? valueProp.value), + defaultProp: computed(() => defaultValue.value), + onChange: (result: any) => { + emit('update:modelValue', result) + }, }) TabsProvider({ - modelValue, - changeModelValue: (value: string) => { - modelValue.value = value - if (value && props.onValueChange) - props.onValueChange(value) - }, - currentFocusedElement: currentFocusedElementRef, - parentElement: parentElementRef, + onValueChange: updateValue, orientation: props.orientation, - dir: props.dir, - loop: true, + dir: direction, + value: state.value, activationMode: props.activationMode, - scope: undefined, + baseId: useId(), + scope: props.scopeTabs, }) return () => h( Primitive.div, { - 'dir': props.dir, + 'dir': direction, 'data-orientation': props.orientation, 'role': 'tab-group', 'asChild': props.asChild, + 'ref': forwardedRef, }, { default: () => slots.default?.(), diff --git a/packages/components/tabs/src/utils.ts b/packages/components/tabs/src/utils.ts new file mode 100644 index 000000000..a865868d7 --- /dev/null +++ b/packages/components/tabs/src/utils.ts @@ -0,0 +1,7 @@ +export function makeTriggerId(baseId: string, value: string) { + return `${baseId}-trigger-${value}` +} + +export function makeContentId(baseId: string, value: string) { + return `${baseId}-content-${value}` +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d96cdbb9..9c871681a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -511,12 +511,21 @@ importers: packages/components/tabs: dependencies: + '@oku-ui/direction': + specifier: workspace:^ + version: link:../direction + '@oku-ui/presence': + specifier: workspace:^ + version: link:../presence '@oku-ui/primitive': specifier: latest version: link:../../core/primitive '@oku-ui/provide': specifier: latest version: link:../../core/provide + '@oku-ui/roving-focus': + specifier: workspace:^ + version: link:../roving-focus '@oku-ui/use-composable': specifier: latest version: link:../../core/use-composable From 5e024b540088d392c7dc327d520ed31c74abdf8c Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 10 Aug 2023 07:22:38 +0300 Subject: [PATCH 12/22] refaktor: presence --- .../components/presence/src/presence.test.ts | 47 ++++++++++++-- packages/components/presence/src/presence.ts | 62 +++++-------------- .../presence/src/stories/PresenceDemo.vue | 6 +- .../components/presence/src/usePresence.ts | 39 +++++++----- pnpm-lock.yaml | 6 +- 5 files changed, 89 insertions(+), 71 deletions(-) diff --git a/packages/components/presence/src/presence.test.ts b/packages/components/presence/src/presence.test.ts index f1731232d..11a22de44 100644 --- a/packages/components/presence/src/presence.test.ts +++ b/packages/components/presence/src/presence.test.ts @@ -33,6 +33,7 @@ describe('presence', async () => { }, } as Component const wrapper = mount(component, {}) + expect(wrapper.html()).toContain(`

`) @@ -66,12 +67,12 @@ describe('presence', async () => { } }, } as Component + const wrapper = mount(component, {}) await wrapper.find('button').trigger('click') - // present="[object Object] because of the ref expect(wrapper.html()).toContain(`
-
content
+
content
`) }) @@ -103,16 +104,52 @@ describe('presence', async () => { } }, } as Component + const wrapper = mount(component, {}) expect(wrapper.html()).toContain(`
`) await wrapper.find('button').trigger('click') - - // present="[object Object] because of the ref expect(wrapper.html()).toContain(`
-
content
+
content
+
`) + }) + + it('close content', async () => { + const component = { + components: { + OkuPresence, + }, + template: ` +
+ + +
+ content - {{ isPresent }} +
+
+
+ `, + setup() { + const open = ref(false) + const toggle = () => { + open.value = !open.value + } + return { + open, + toggle, + } + }, + } as Component + const wrapper = mount(component, {}) + + expect(wrapper.html()).toContain(`
+
content - { + "isPresent": true + }
`) }) diff --git a/packages/components/presence/src/presence.ts b/packages/components/presence/src/presence.ts index 22338e611..958093b2e 100644 --- a/packages/components/presence/src/presence.ts +++ b/packages/components/presence/src/presence.ts @@ -1,6 +1,5 @@ -import type { Directive } from 'vue' -import { defineComponent, h, ref, toRefs, withDirectives } from 'vue' -import { syncRef } from '@oku-ui/use-composable' +import { defineComponent, h, toRefs } from 'vue' +import { useComposedRefs, useForwardRef } from '@oku-ui/use-composable' import { usePresence } from './usePresence' interface PresenceProps { @@ -18,53 +17,24 @@ const presence = defineComponent({ default: false, }, }, - setup(props, { slots, attrs }) { + setup(props, { slots }) { const { present } = toRefs(props) - const elementRef = ref() - const element: Directive = { - created(el) { - const { isPresent } = usePresence(present, el) - syncRef(isPresent, elementRef, { direction: 'ltr' }) - }, - } + const forwardedRef = useForwardRef() + const { isPresent, ref: presenceRef } = usePresence(present) + const composedRefs = useComposedRefs(presenceRef, forwardedRef) return () => { - const children = slots.default?.() - - if (children?.length === 1) { - const [firstChild] = children || [] - - const directVNodeChildren = withDirectives( - h( - firstChild, - { - present, - ...attrs, - }, - ), - [ - [element], - ]) - - return present.value ? directVNodeChildren : null - } - else { - throw new Error( - [ - `Now you can only pass one child to \`${NAME}\`.`, - '', - 'Note: All components accepting `Presence` expect only one direct child of valid VNode type.', - 'You can apply a few solutions:', - [ - 'Provide a single child element so that we can forward the props onto that element.', - 'Ensure the first child is an actual element instead of a raw text node or comment node.', - ] - .map(line => ` - ${line}`) - .join('\n'), - ].join('\n'), - ) - } + const ddd = slots.default?.({ + isPresent, + }) + const [child] = ddd ?? [] + + return isPresent.value + ? h(child, { + ref: composedRefs, + }) + : null } }, }) diff --git a/packages/components/presence/src/stories/PresenceDemo.vue b/packages/components/presence/src/stories/PresenceDemo.vue index d121a9328..fd0833166 100644 --- a/packages/components/presence/src/stories/PresenceDemo.vue +++ b/packages/components/presence/src/stories/PresenceDemo.vue @@ -18,7 +18,7 @@ function toggle() { open.value = !open.value } -const element = ref() +const element = ref() watch(element, () => { console.log('element', element.value) @@ -44,9 +44,9 @@ function handleToggleVisibility() { - +
- content + content - {{ isPresent }}
diff --git a/packages/components/presence/src/usePresence.ts b/packages/components/presence/src/usePresence.ts index f941051a9..69ba6c639 100644 --- a/packages/components/presence/src/usePresence.ts +++ b/packages/components/presence/src/usePresence.ts @@ -5,7 +5,8 @@ function getAnimationName(styles?: CSSStyleDeclaration) { return styles?.animationName || 'none' } -export function usePresence(present: Ref, el: HTMLElement) { +export function usePresence(present: Ref) { + const el = ref(undefined) const stylesRef = ref({} as any) const prevPresentRef = ref(present.value) const prevAnimationNameRef = ref('none') @@ -69,8 +70,8 @@ export function usePresence(present: Ref, el: HTMLElement) { } }) - watch(() => el, () => { - if (el) { + watch(el, () => { + if (el.value) { /** * Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel` * event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we @@ -81,7 +82,7 @@ export function usePresence(present: Ref, el: HTMLElement) { const isCurrentAnimation = currentAnimationName.includes( event.animationName, ) - if (event.target === el && isCurrentAnimation) { + if (event.target === el.value && isCurrentAnimation) { // With React 18 concurrency this update is applied // a frame after the animation ends, creating a flash of visible content. // By manually flushing we ensure they sync within a frame, removing the flash. @@ -89,19 +90,19 @@ export function usePresence(present: Ref, el: HTMLElement) { } } const handleAnimationStart = (event: AnimationEvent) => { - if (event.target === el) + if (event.target === el.value) // if animation occurred, store its name as the previous animation. prevAnimationNameRef.value = getAnimationName(stylesRef.value) } - el.addEventListener('animationstart', handleAnimationStart) - el.addEventListener('animationcancel', handleAnimationEnd) - el.addEventListener('animationend', handleAnimationEnd) + el.value.addEventListener('animationstart', handleAnimationStart) + el.value.addEventListener('animationcancel', handleAnimationEnd) + el.value.addEventListener('animationend', handleAnimationEnd) return () => { - if (el) { - el.removeEventListener('animationstart', handleAnimationStart) - el.removeEventListener('animationcancel', handleAnimationEnd) - el.removeEventListener('animationend', handleAnimationEnd) + if (el.value) { + el.value.removeEventListener('animationstart', handleAnimationStart) + el.value.removeEventListener('animationcancel', handleAnimationEnd) + el.value.removeEventListener('animationend', handleAnimationEnd) } } } @@ -116,10 +117,20 @@ export function usePresence(present: Ref, el: HTMLElement) { ['mounted', 'unmountSuspended'].includes(state.value), ) - if (el) - stylesRef.value = getComputedStyle(el) + if (el.value) + stylesRef.value = getComputedStyle(el.value) return { isPresent, + ref: computed({ + get() { + return el.value! + }, + set(node: HTMLElement) { + if (node) + stylesRef.value = getComputedStyle(node) + el.value = node + }, + }), } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c871681a..b7bf9671e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -512,10 +512,10 @@ importers: packages/components/tabs: dependencies: '@oku-ui/direction': - specifier: workspace:^ + specifier: latest version: link:../direction '@oku-ui/presence': - specifier: workspace:^ + specifier: latest version: link:../presence '@oku-ui/primitive': specifier: latest @@ -524,7 +524,7 @@ importers: specifier: latest version: link:../../core/provide '@oku-ui/roving-focus': - specifier: workspace:^ + specifier: latest version: link:../roving-focus '@oku-ui/use-composable': specifier: latest From f6d7772198c77aef21cac0ea8cecbb8b317a4d63 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 10 Aug 2023 08:00:53 +0300 Subject: [PATCH 13/22] fix: export ts --- .../components/tabs/src/stories/TabsDemo.vue | 1 - packages/components/tabs/src/tab-content.ts | 73 +++++++++++-------- packages/components/tabs/src/tab-list.ts | 22 +++--- packages/components/tabs/src/tab-trigger.ts | 18 +++-- packages/components/tabs/src/tabs.ts | 14 ++-- 5 files changed, 74 insertions(+), 54 deletions(-) diff --git a/packages/components/tabs/src/stories/TabsDemo.vue b/packages/components/tabs/src/stories/TabsDemo.vue index ac3fe5aae..2022b23dd 100644 --- a/packages/components/tabs/src/stories/TabsDemo.vue +++ b/packages/components/tabs/src/stories/TabsDemo.vue @@ -13,7 +13,6 @@ import { dir="ltr" orientation="horizontal" class="flex flex-col w-[300px] shadow-[0_2px_10px] shadow-black/10" - default-value="tab1" > +export type _TabsContentEl = HTMLDivElement + interface TabsContentProps extends ScopedPropsInterface { value: string @@ -29,52 +35,59 @@ const TabContent = defineComponent({ type: Boolean as PropType, default: false, }, - asChild: { - type: Boolean as PropType, - default: false, - }, + ...PrimitiveProps, ...ScopedProps, }, - setup(props, { slots }) { + setup(props, { slots, attrs }) { const { scopeTabs, value } = toRefs(props) + const { ...ContentAttrs } = attrs const injectTabs = useTabsInject(TAB_CONTENT_NAME, scopeTabs.value) const triggerId = makeTriggerId(injectTabs.value.baseId, value.value) const contentId = makeContentId(injectTabs.value.baseId, value.value) const isSelected = computed(() => value.value === injectTabs.value.value) + const forwardedRef = useForwardRef() const isMountAnimationPreventedRef = ref(isSelected.value) - watchEffect((onClean) => { - const rAF = requestAnimationFrame(() => (isMountAnimationPreventedRef.value = false)) - onClean(() => cancelAnimationFrame(rAF)) - }) - - // TODO: presence - // h(OkuPresence, { - - // }, { - // default: () => h( - // Primitive.div, - // { - // 'role': 'tab-content', - // 'data-orientation': injectTabs.value.orientation, - // 'tabindex': '0', - // 'asChild': props.asChild, - // }, - // { - // default: () => slots.default?.(), - // }, - // ), + // watchEffect((onClean) => { + // nextTick(() => { + // const rAF = requestAnimationFrame(() => (isMountAnimationPreventedRef.value = false)) + // onClean(() => cancelAnimationFrame(rAF)) + // }) // }) + + return () => h(OkuPresence, { + present: isSelected.value || props.forceMount, + }, { + default: ({ isPresent }: { isPresent: boolean }) => h(Primitive.div, { + 'data-state': isSelected.value ? 'active' : 'inactive', + 'data-orientation': injectTabs.value.orientation, + 'role': 'tabpanel', + 'aria-labelledby': triggerId, + 'hidden': !isPresent, + 'id': contentId, + 'tabindex': '0', + ...ContentAttrs, + 'ref': forwardedRef, + 'style': { + ...attrs.style ?? {}, + animationDuration: isMountAnimationPreventedRef.value ? '0s' : undefined, + }, + }, { + default: () => isSelected.value ? slots.default?.() : null, + }), + }) }, }) -type _TabsProps = MergeProps +type _TabsProps = MergeProps + +type InstanceTabsContent = InstanceTypeRef const OkuTabContent = TabContent as typeof TabContent & (new () => { $props: _TabsProps }) export { OkuTabContent } -export type { TabsContentProps } +export type { TabsContentProps, InstanceTabsContent } diff --git a/packages/components/tabs/src/tab-list.ts b/packages/components/tabs/src/tab-list.ts index 1f36bd543..29e7bc3d9 100644 --- a/packages/components/tabs/src/tab-list.ts +++ b/packages/components/tabs/src/tab-list.ts @@ -1,7 +1,9 @@ -import { - type IPrimitiveProps, - type MergeProps, - Primitive, +import { Primitive, PrimitiveProps } from '@oku-ui/primitive' +import type { + ElementType, + IPrimitiveProps, + InstanceTypeRef, + MergeProps, } from '@oku-ui/primitive' import type { PropType } from 'vue' import { defineComponent, h, toRefs } from 'vue' @@ -10,6 +12,9 @@ import { OkuRovingFocusGroup, createRovingFocusGroupScope } from '@oku-ui/roving import type { ScopedPropsInterface } from './tabs' import { ScopedProps, useTabsInject } from './tabs' +type TabListElement = ElementType<'div'> +export type _TabListEl = HTMLDivElement + const TAB_LIST_NAME = 'OkuTabList' as const interface TabListProps extends ScopedPropsInterface { @@ -26,10 +31,7 @@ const TabList = defineComponent({ type: Boolean as PropType, default: true, }, - asChild: { - type: Boolean as PropType, - default: false, - }, + ...PrimitiveProps, ...ScopedProps, }, setup(props, { slots, attrs }) { @@ -66,7 +68,9 @@ const TabList = defineComponent({ }, }) -type _TabListProps = MergeProps +type _TabListProps = MergeProps + +export type InstanceTabListType = InstanceTypeRef const OkuTabList = TabList as typeof TabList & (new () => { $props: _TabListProps }) diff --git a/packages/components/tabs/src/tab-trigger.ts b/packages/components/tabs/src/tab-trigger.ts index 4be143f63..30ddbd972 100644 --- a/packages/components/tabs/src/tab-trigger.ts +++ b/packages/components/tabs/src/tab-trigger.ts @@ -1,5 +1,5 @@ -import { Primitive } from '@oku-ui/primitive' -import type { IPrimitiveProps, MergeProps } from '@oku-ui/primitive' +import { Primitive, PrimitiveProps } from '@oku-ui/primitive' +import type { ElementType, IPrimitiveProps, InstanceTypeRef, MergeProps } from '@oku-ui/primitive' import { type PropType, computed, defineComponent, h, toRefs } from 'vue' import { useForwardRef } from '@oku-ui/use-composable' import { OkuRovingFocusGroupItem } from '@oku-ui/roving-focus' @@ -8,11 +8,14 @@ import type { ScopedPropsInterface } from './tabs' import { ScopedProps, useRovingFocusGroupScope, useTabsInject } from './tabs' import { makeContentId, makeTriggerId } from './utils' +type TabsTriggerElement = ElementType<'button'> +export type _TabsTriggerEl = HTMLButtonElement + const TAB_TRIGGER_NAME = 'OkuTabTrigger' as const interface TabsTriggerProps extends ScopedPropsInterface { value: string - disabled: boolean + disabled?: boolean onMousedown?: (event: MouseEvent) => void onKeydown?: (event: KeyboardEvent) => void onFocus?: (event: FocusEvent) => void @@ -30,14 +33,11 @@ const TabTrigger = defineComponent({ type: Boolean as PropType, default: false, }, - asChild: { - type: Boolean as PropType, - default: false, - }, onMousedown: Function as PropType<(e: MouseEvent) => void>, onKeydown: Function as PropType<(e: KeyboardEvent) => void>, onFocus: Function as PropType<(e: FocusEvent) => void>, ...ScopedProps, + ...PrimitiveProps, }, setup(props, { slots, attrs }) { const { scopeTabs, value, disabled } = toRefs(props) @@ -101,7 +101,9 @@ const TabTrigger = defineComponent({ }, }) -type _TabsProps = MergeProps +type _TabsProps = MergeProps + +export type InstanceTabsTriggerType = InstanceTypeRef const OkuTabTrigger = TabTrigger as typeof TabTrigger & (new () => { $props: _TabsProps }) diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts index 440e56892..95256515a 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.ts @@ -1,4 +1,4 @@ -import type { IPrimitiveProps, MergeProps, RefElement } from '@oku-ui/primitive' +import type { ElementType, IPrimitiveProps, InstanceTypeRef, MergeProps, RefElement } from '@oku-ui/primitive' import { Primitive } from '@oku-ui/primitive' import { computed, defineComponent, h, toRefs, useModel } from 'vue' import type { PropType } from 'vue' @@ -10,10 +10,14 @@ import { useDirection } from '@oku-ui/direction' const TAB_NAME = 'OkuTab' as const +type TabsElement = ElementType<'div'> +export type _TabsEl = HTMLDivElement + export type ScopedPropsInterface

= P & { scopeTabs?: Scope } export const ScopedProps = { scopeTabs: { type: Object as PropType, + required: false, }, } @@ -167,15 +171,13 @@ const Tabs = defineComponent({ 'role': 'tab-group', 'asChild': props.asChild, 'ref': forwardedRef, - }, - { - default: () => slots.default?.(), - }, + }, slots, ) }, }) -type _TabsProps = MergeProps +type _TabsProps = MergeProps +export type IstanceTabsType = InstanceTypeRef type TabsRef = RefElement From 46fc5a265a72aa0d05a09dcc884baf51b17a7d0e Mon Sep 17 00:00:00 2001 From: productdevbook Date: Fri, 11 Aug 2023 06:49:07 +0300 Subject: [PATCH 14/22] fix: tabs --- .eslintrc | 3 +- package.json | 4 +-- .../arrow/src/stories/ArrowDemo.vue | 1 - .../src/stories/AspectRatioDemo.vue | 1 - .../components/collection/src/collection.ts | 11 ++++--- .../collection/src/stories/CollectionDemo.vue | 2 +- packages/components/presence/package.json | 1 - .../components/presence/src/usePresence.ts | 17 +++++++--- .../roving-focus/src/RovingFocusGroupImpl.ts | 1 - .../roving-focus/src/RovingFocusGroupItem.ts | 8 ++--- .../switch/src/stories/SwitchDemo.vue | 5 ++- .../components/tabs/src/stories/TabsDemo.vue | 2 +- packages/components/tabs/src/tab-content.ts | 20 ++++++------ packages/components/tabs/src/tab-list.ts | 6 ++-- packages/components/tabs/src/tab-trigger.ts | 8 ++--- packages/components/tabs/src/tabs.ts | 32 +++++++++++++------ packages/core/provide/src/createProvide.ts | 21 ++++++------ packages/core/utils/src/index.ts | 2 +- .../core/utils/src/isValidVNodeElement.ts | 11 +++++++ scripts/dev.ts | 21 ++++++------ 20 files changed, 103 insertions(+), 74 deletions(-) create mode 100644 packages/core/utils/src/isValidVNodeElement.ts diff --git a/.eslintrc b/.eslintrc index 037b2702f..1496e459f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,6 +3,7 @@ // This tells ESLint to load the config from the package `eslint-config-custom` "extends": ["custom", "plugin:storybook/recommended"], "rules": { - "vue/one-component-per-file": "off" + "vue/one-component-per-file": "off", + "no-console": "off" } } diff --git a/package.json b/package.json index 62bb1719e..3c6d4f4aa 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "build:all": "turbo run build --filter='./packages/**'", "build:components": "turbo run build --filter='./packages/components/**'", "build:core": "turbo run build --filter='./packages/core/**'", - "dev:all": "turbo run dev --filter='./packages/**' --concurrency $(($(ls -1 packages/components packages/core | wc -l)+3))", + "dev:all": "turbo run dev --filter='./packages/**' --concurrency=50", "dev:core": "turbo run dev --filter='./packages/core/**'", - "dev:components": "turbo run dev --filter='./packages/components/**' --concurrency $(($(ls -1 packages/components | wc -l)+3))", + "dev:components": "turbo run dev --filter='./packages/components/**' --concurrency=50", "story": "pnpm storybook dev -p 6006 --no-open", "lint": "eslint . --cache ", "lint:fix": "eslint . --fix --cache", diff --git a/packages/components/arrow/src/stories/ArrowDemo.vue b/packages/components/arrow/src/stories/ArrowDemo.vue index ce4cb7bc1..8e221ec19 100644 --- a/packages/components/arrow/src/stories/ArrowDemo.vue +++ b/packages/components/arrow/src/stories/ArrowDemo.vue @@ -14,7 +14,6 @@ withDefaults(defineProps(), { const arrowRef = ref() onMounted(() => { - // eslint-disable-next-line no-console console.log(arrowRef.value?.$el) }) const alert = () => window.alert('clicked') diff --git a/packages/components/aspect-ratio/src/stories/AspectRatioDemo.vue b/packages/components/aspect-ratio/src/stories/AspectRatioDemo.vue index c7294217c..b638cb968 100644 --- a/packages/components/aspect-ratio/src/stories/AspectRatioDemo.vue +++ b/packages/components/aspect-ratio/src/stories/AspectRatioDemo.vue @@ -17,7 +17,6 @@ withDefaults(defineProps(), { const root = ref() onMounted(() => { - // eslint-disable-next-line no-console console.log(root.value?.$el) }) diff --git a/packages/components/collection/src/collection.ts b/packages/components/collection/src/collection.ts index 5a02c7623..8948b1100 100644 --- a/packages/components/collection/src/collection.ts +++ b/packages/components/collection/src/collection.ts @@ -1,5 +1,5 @@ import type { AllowedComponentProps, ComponentCustomProps, ComponentObjectPropsOptions, ComponentPublicInstance, Ref, VNodeProps } from 'vue' -import { computed, createVNode, defineComponent, h, ref, watchEffect } from 'vue' +import { computed, defineComponent, h, ref, toRefs, watchEffect } from 'vue' import { useComposedRefs, useForwardRef } from '@oku-ui/use-composable' import { createProvideScope } from '@oku-ui/provide' import { OkuSlot } from '@oku-ui/slot' @@ -49,12 +49,13 @@ function createCollection(name: string, Item ...CollectionProps, }, setup(props, { slots }) { + const { scope } = toRefs(props) const collectionRef = ref>() const itemMap = ref(new Map | null | undefined>, { ref: ComponentPublicInstanceRef } & T>()) CollectionProviderImpl({ collectionRef, itemMap, - scope: props.scope, + scope: scope.value, }) return () => slots.default?.() @@ -102,12 +103,12 @@ function createCollection(name: string, Item ...ItemData, }, setup(props, { attrs, slots }) { - const { scope, ...itemData } = props + const { scope, ...itemData } = toRefs(props) const refValue = ref | null>() const forwaredRef = useForwardRef() const composedRefs = useComposedRefs(refValue, forwaredRef) - const inject = useCollectionInject(ITEM_SLOT_NAME, scope) + const inject = useCollectionInject(ITEM_SLOT_NAME, scope.value) watchEffect((onClean) => { inject.value.itemMap.value.set(refValue, { ref: refValue, ...(itemData as any), ...attrs }) @@ -117,7 +118,7 @@ function createCollection(name: string, Item }) }) - return () => createVNode(OkuSlot, { ref: composedRefs, ...{ [ITEM_DATA_ATTR]: '' } }, slots) + return () => h(OkuSlot, { ref: composedRefs, ...{ [ITEM_DATA_ATTR]: '' } }, slots) }, }) diff --git a/packages/components/collection/src/stories/CollectionDemo.vue b/packages/components/collection/src/stories/CollectionDemo.vue index 426ce92da..eab2c0603 100644 --- a/packages/components/collection/src/stories/CollectionDemo.vue +++ b/packages/components/collection/src/stories/CollectionDemo.vue @@ -24,7 +24,7 @@ const alert = () => window.alert('clicked') function LogsItem() { const getItems = useCollection(undefined) - console.log(getItems.value[0].ref.value) + console.log(getItems.value) } diff --git a/packages/components/presence/package.json b/packages/components/presence/package.json index 5d0591e41..0e73b424a 100644 --- a/packages/components/presence/package.json +++ b/packages/components/presence/package.json @@ -36,7 +36,6 @@ "vue": "^3.3.0" }, "dependencies": { - "@oku-ui/primitive": "latest", "@oku-ui/use-composable": "latest", "@oku-ui/utils": "latest" }, diff --git a/packages/components/presence/src/usePresence.ts b/packages/components/presence/src/usePresence.ts index 69ba6c639..1f683bf88 100644 --- a/packages/components/presence/src/usePresence.ts +++ b/packages/components/presence/src/usePresence.ts @@ -1,4 +1,5 @@ import { type Ref, computed, nextTick, ref, watch } from 'vue' +import { isValidVNodeElement } from '@oku-ui/utils' import { useStateMachine } from './useStateMachine' function getAnimationName(styles?: CSSStyleDeclaration) { @@ -124,12 +125,20 @@ export function usePresence(present: Ref) { isPresent, ref: computed({ get() { - return el.value! + return el.value }, - set(node: HTMLElement) { - if (node) + set(node: any) { + if (!isValidVNodeElement(node)) + return + // node is ComponentPublicInstance + if (node && node.$el) { + stylesRef.value = getComputedStyle(node.$el) + el.value = node.$el as HTMLElement + } + else if (node) { stylesRef.value = getComputedStyle(node) - el.value = node + el.value = node as HTMLElement + } }, }), } diff --git a/packages/components/roving-focus/src/RovingFocusGroupImpl.ts b/packages/components/roving-focus/src/RovingFocusGroupImpl.ts index 39395d33b..eacf91a9a 100644 --- a/packages/components/roving-focus/src/RovingFocusGroupImpl.ts +++ b/packages/components/roving-focus/src/RovingFocusGroupImpl.ts @@ -95,7 +95,6 @@ const RovingFocusGroupImpl = defineComponent({ asChild, ...propsData } = toRefs(props) - const buttonRef = ref | null>(null) const forwardedRef = useForwardRef() const composedRefs = useComposedRefs(buttonRef, forwardedRef) diff --git a/packages/components/roving-focus/src/RovingFocusGroupItem.ts b/packages/components/roving-focus/src/RovingFocusGroupItem.ts index 2e4e9837c..0e820d09e 100644 --- a/packages/components/roving-focus/src/RovingFocusGroupItem.ts +++ b/packages/components/roving-focus/src/RovingFocusGroupItem.ts @@ -65,19 +65,19 @@ const RovingFocusGroupItem = defineComponent({ setup(props, { attrs, slots }) { const _attrs = attrs as any const { - scopeRovingFocusGroup, focusable, active, tabStopId, + scopeRovingFocusGroup, ...propsData } = toRefs(props) const attrsItems = _attrs const autoId = useId() const id = computed(() => tabStopId.value ?? autoId) - const inject = useRovingFocusInject(ITEM_NAME, scopeRovingFocusGroup.value) + const inject = useRovingFocusInject(ITEM_NAME, props.scopeRovingFocusGroup) const isCurrentTabStop = computed(() => inject.value.currentTabStopId.value === id.value) - const getItems = useCollection(scopeRovingFocusGroup.value) + const getItems = useCollection(props.scopeRovingFocusGroup) const forwardedRef = useForwardRef() watchEffect((onClean) => { @@ -91,7 +91,7 @@ const RovingFocusGroupItem = defineComponent({ }) }) }) - + console.log(props.asChild, 'props.asChild') const _props: ItemData = { id: id.value, focusable: focusable.value, diff --git a/packages/components/switch/src/stories/SwitchDemo.vue b/packages/components/switch/src/stories/SwitchDemo.vue index 0f5a5291d..c4fba9e5f 100644 --- a/packages/components/switch/src/stories/SwitchDemo.vue +++ b/packages/components/switch/src/stories/SwitchDemo.vue @@ -22,17 +22,16 @@ function setData(event: any) { ...data.value, [input.name]: input.value, } - // eslint-disable-next-line no-console + console.log(data.value) } function sendForm(event: any) { - // eslint-disable-next-line no-console console.log(event, 'sendForm') data.value = { ...data.value, [event.target.name]: event.target.value, } - // eslint-disable-next-line no-console + console.log(data.value) } diff --git a/packages/components/tabs/src/stories/TabsDemo.vue b/packages/components/tabs/src/stories/TabsDemo.vue index 2022b23dd..ba5fc3e26 100644 --- a/packages/components/tabs/src/stories/TabsDemo.vue +++ b/packages/components/tabs/src/stories/TabsDemo.vue @@ -9,7 +9,7 @@ import {