diff --git a/packages/frontend/src/components/primitives/Tab.tsx b/packages/frontend/src/components/primitives/Tab.tsx index 0133ed1..251b503 100644 --- a/packages/frontend/src/components/primitives/Tab.tsx +++ b/packages/frontend/src/components/primitives/Tab.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { useState } from 'react'; -import { useLayoutEffect } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; + import { styled } from '@/libs/stitches'; export type TabProp = { children: React.ReactElement | React.ReactElement[] value?: string; + direction?: 'vertical' | 'horizontal'; onChange?: (newValue: string) => void; }; @@ -31,29 +31,115 @@ const Ul = styled('ul', { display: 'flex', margin: 0, padding: 0, + + defaultVariants: { + direction: 'horizontal', + }, + + variants: { + direction: { + horizontal: { + flexDirection: 'row', + }, + vertical: { + flexDirection: 'column', + }, + }, + }, }); const Li = styled('li', { display: 'flex', alignItems: 'center', justifyContent: 'center', - padding: '$m', + padding: '$s $l', margin: 0, - borderBottom: '2px solid $muted', color: '$muted', + defaultVariants: { + direction: 'horizontal', + }, + variants: { active: { true: { color: '$primary', }, }, + + direction: { + horizontal: { + borderBottom: '2px solid $divider', + }, + vertical: { + borderLeft: '2px solid $divider', + justifyContent: 'flex-start', + }, + }, + }, +}); + +const Indicator = styled('div', { + position: 'absolute', + background: '$primary', + transition: 'all 0.2s $timingFunction$default', + + defaultVariants: { + direction: 'horizontal', + }, + + variants: { + direction: { + horizontal: { + bottom: 0, + height: 2, + }, + vertical: { + left: 0, + width: 2, + }, + }, }, }); export const Tab: React.FC = (p) => { - const c = Array.isArray(p.children) ? p.children.map(c => c.props) : [p.children.props]; - const [value, setValue] = useState(c[0].value); + const items = useMemo(() => Array.isArray(p.children) ? p.children.map(c => c.props) : [p.children.props], [p.children]); + + const [value, setValue] = useState(null); + + const itemRefs = useRef<(HTMLLIElement | null)[]>([]); + + const position = useMemo(() => { + let sum = 0; + const max = items.findIndex(item => item.value === value); + for (let i = 0; i < max; i++) { + const ref = itemRefs.current[i]; + if (ref == null) return 0; + sum += p.direction === 'vertical' ? ref.clientHeight : ref.clientWidth; + } + console.log(`left: ${sum}`); + return sum; + }, [items, p.direction, value]); + + const size = useMemo(() => { + const i = items.findIndex(item => item.value === value); + const ref = itemRefs.current[i]; + if (ref == null) return 0; + return p.direction === 'vertical' ? ref.clientHeight : ref.clientWidth; + }, [items, p.direction, value]); + + const indicatorStyle = useMemo(() => (p.direction === 'vertical' ? { + top: position, height: size, + }: { + left: position, width: size, + }), [p.direction, position, size]); + + useEffect(() => { + itemRefs.current = itemRefs.current.slice(0, items.length); + if (value === null) { + setValue(items[0].value); + } + }, [items, value]); useLayoutEffect(() => { if (!p.value) return; @@ -62,9 +148,9 @@ export const Tab: React.FC = (p) => { return ( -
    - {c.map(item => ( -
  • { +
      + {items.map((item, i) => ( +
    • itemRefs.current[i] = el} role="button" active={item.value === value} onClick={() => { if (item.value === value) return; console.log(item.value); setValue(item.value); @@ -74,6 +160,7 @@ export const Tab: React.FC = (p) => {
    • ))}
    + ); }; diff --git a/packages/frontend/src/stories/components/primitives/Tab.stories.tsx b/packages/frontend/src/stories/components/primitives/Tab.stories.tsx index 6593835..0d06fc6 100644 --- a/packages/frontend/src/stories/components/primitives/Tab.stories.tsx +++ b/packages/frontend/src/stories/components/primitives/Tab.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type { Meta } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react'; import { Tab, TabItem } from '@/components/primitives/Tab'; @@ -9,14 +9,30 @@ const meta = { parameters: { layouts: 'centered', }, + args: { + children: ([ + ホーム, + 通知, + みつける, + ]), + }, } satisfies Meta; export default meta; -export const Default = () => ( - - ホーム - 通知 - みつける - -); +type Story = StoryObj; + +export const Default: Story = {}; + +export const Horizontal: Story = { + args: { + direction: 'horizontal', + }, +}; + +export const Vertical: Story = { + args: { + direction: 'vertical', + }, +}; +