Skip to content

Commit

Permalink
feat: AppNavi を再設計 (#4823)
Browse files Browse the repository at this point in the history
  • Loading branch information
uknmr authored Sep 3, 2024
1 parent a17cf28 commit f64fbe5
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 351 deletions.
155 changes: 40 additions & 115 deletions packages/smarthr-ui/src/components/AppNavi/AppNavi.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { action } from '@storybook/addon-actions'
import { StoryFn } from '@storybook/react'
import React, { FC, ReactNode } from 'react'
import styled, { css } from 'styled-components'
import React, { type FC, type ReactNode } from 'react'

import { UnstyledButton } from '../Button'
import { FaBirthdayCakeIcon, FaChartPieIcon, FaCogIcon, FaFileIcon, FaUserAltIcon } from '../Icon'
import { AnchorButton, Button } from '../Button'

import { AppNavi } from './AppNavi'
import { AppNaviAnchor } from './AppNaviAnchor'
import { AppNaviButton } from './AppNaviButton'
import { AppNaviCustomTag } from './AppNaviCustomTag'
import { AppNaviDropdownMenuButton } from './AppNaviDropdownMenuButton'

import type { StoryFn } from '@storybook/react'

export default {
title: 'Navigation(ナビゲーション)/AppNavi',
component: AppNavi,
parameters: {
withTheming: true,
},
}

const Link: FC<{ to: string; children: ReactNode; disabled?: boolean; className?: string }> = ({
Expand All @@ -29,117 +29,42 @@ const Link: FC<{ to: string; children: ReactNode; disabled?: boolean; className?
</a>
)

const List: FC = () => (
<ListWrapper>
<li>
<UnstyledButton onClick={action('clicked item 1')}>ドロップダウンアイテム1</UnstyledButton>
</li>
<li>
<UnstyledButton onClick={action('clicked item 2')}>ドロップダウンアイテム2</UnstyledButton>
</li>
<li>
<UnstyledButton onClick={action('clicked item 3')}>ドロップダウンアイテム3</UnstyledButton>
</li>
<li>
<UnstyledButton onClick={action('clicked item 4')}>ドロップダウンアイテム4</UnstyledButton>
</li>
</ListWrapper>
)

const buttons = [
{
children: 'カレントボタン',
icon: FaFileIcon,
current: true,
onClick: action('click!!'),
},
{
children: 'ボタン',
icon: FaUserAltIcon,
onClick: action('click!!'),
},
{
children: 'アンカー',
icon: FaCogIcon,
href: '/',
},
{
children: 'ドロップダウン',
icon: FaChartPieIcon,
dropdownContent: <List />,
},
{
children: 'カスタムタグ',
icon: FaBirthdayCakeIcon,
tag: Link,
href: '/',
},
]
const withoutIconButtons = buttons.map(({ icon, ...button }) => button)

export const WithChildren: StoryFn = () => (
<Wrapper>
<AppNavi label="機能名" buttons={withoutIconButtons} displayDropdownCaret>
<Child>Some child components</Child>
</AppNavi>
</Wrapper>
)
WithChildren.storyName = 'with children'

export const WithoutChildren: StoryFn = () => (
<Wrapper>
<AppNavi label="機能名" buttons={withoutIconButtons} displayDropdownCaret />
</Wrapper>
export const Default: StoryFn = () => (
<AppNavi label="機能名">
<AppNaviButton onClick={action('click')} current>
カレントボタン
</AppNaviButton>
<AppNaviAnchor href="/">アンカーボタン</AppNaviAnchor>
<AppNaviDropdownMenuButton label="設定">
<Button>権限</Button>
<AnchorButton href="#">その他</AnchorButton>
</AppNaviDropdownMenuButton>
<AppNaviCustomTag tag={Link} href="/">
カスタムタグ
</AppNaviCustomTag>
<div className="shr-ms-auto">
<p>Some child components</p>
</div>
</AppNavi>
)
WithoutChildren.storyName = 'without children'

export const NoIconAndCaret: StoryFn = () => (
<Wrapper>
<AppNavi label="機能名" buttons={buttons} />
</Wrapper>
export const CurrentInMenu: StoryFn = () => (
<AppNavi label="機能名">
<AppNaviButton onClick={action('click')}>カレントボタン</AppNaviButton>
<AppNaviAnchor href="/">アンカーボタン</AppNaviAnchor>
<AppNaviDropdownMenuButton label="設定">
<Button onClick={action('click')} aria-current="page">
権限
</Button>
<AnchorButton href="#">その他</AnchorButton>
</AppNaviDropdownMenuButton>
</AppNavi>
)
NoIconAndCaret.storyName = 'アイコンありドロップダウン示唆なし'
CurrentInMenu.storyName = '現在地がDropdownMenu内にある場合'

export const ContainerScrollX: StoryFn = () => (
<OverflowWrapper>
<AppNavi label="機能名" buttons={withoutIconButtons} displayDropdownCaret>
<Child>Some child components</Child>
</AppNavi>
</OverflowWrapper>
<div className="shr-pb-0.25 shr-overflow-x-auto">
<Default />
</div>
)
ContainerScrollX.storyName = '横スクロールさせる場合'

const Wrapper = styled.div`
${({ theme }) => {
const { color } = theme
return css`
padding: 32px 0;
background-color: ${color.BACKGROUND};
`
}}
`
const OverflowWrapper = styled(Wrapper)`
overflow-x: auto;
`
const Child = styled.p`
margin: 0 0 0 auto;
`

const ListWrapper = styled.ul(
({ theme: { color } }) => css`
margin: 0;
padding: 8px 0;
list-style: none;
& > li > button {
line-height: 40px;
padding-inline: 20px;
background-color: ${color.WHITE};
&:hover {
background-color: ${color.hoverColor(color.WHITE)};
}
}
`,
)
4 changes: 3 additions & 1 deletion packages/smarthr-ui/src/components/AppNavi/AppNavi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ type ElementProps = Omit<ComponentPropsWithoutRef<'div'>, keyof Props>
type Props = PropsWithChildren<{
/** ラベルのテキスト */
label?: ReactNode
/** 表示するボタンの Props の配列 */
/** 表示するボタンの Props の配列
* @deprecated AppNaviButton などのコンポーネントを Composite させて構成させてください
*/
buttons?: Array<
AppNaviButtonProps | AppNaviAnchorProps | AppNaviDropdownProps | AppNaviCustomTagProps
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const appNaviDropdown = tv({
},
})

/** @deprecated AppNaviDropdownMenuButton を使ってください */
export const AppNaviDropdown: FC<AppNaviDropdownProps> = ({
children,
dropdownContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, {
type ComponentProps,
type FC,
type PropsWithChildren,
type ReactElement,
type ReactNode,
useMemo,
useState,
} from 'react'
import { tv } from 'tailwind-variants'

import { AnchorButton, Button, UnstyledButton } from '../Button'
import { RemoteDialogTrigger } from '../Dialog'
import { Dropdown, DropdownContent, DropdownScrollArea, DropdownTrigger } from '../Dropdown'
import { dropdownMenuButton } from '../Dropdown/DropdownMenuButton/DropdownMenuButton'
import { FaCaretDownIcon } from '../Icon'

import { appNaviItemStyle } from './style'

type AppNaviDropdownMenuButtonProps = PropsWithChildren<{
/** 引き金となるボタンラベル */
label: ReactNode
}>

type ActionItemTruthyType =
| ReactElement<ComponentProps<typeof Button>>
| ReactElement<ComponentProps<typeof AnchorButton>>
| ReactElement<ComponentProps<typeof RemoteDialogTrigger>>

const {
slots: { triggerButton, actionList },
} = dropdownMenuButton
const appNaviDropdownMenuButton = tv({
extend: appNaviItemStyle,
slots: {
wrapper: ['smarthr-ui-AppNavi-dropdownMenuButton', triggerButton],
actionList,
},
})

export const AppNaviDropdownMenuButton: FC<AppNaviDropdownMenuButtonProps> = ({
children,
label,
}) => {
const [hasCurrentPage, setHasCurrentPage] = useState(false)
const actualChildren = useMemo(
() =>
React.Children.map(children, (item, i) => {
if (React.isValidElement(item) && item.props['aria-current'] === 'page') {
setHasCurrentPage(true)
}

// MEMO: {flag && <Button/>}のような書き方に対応させる為、型を変換する
// itemの存在チェックでfalsyな値は弾かれている想定
return item ? <li key={i}>{item as ActionItemTruthyType}</li> : null
}),
[children],
)

const { wrapper: wrapperStyle, actionList: actionListStyle } = appNaviDropdownMenuButton({
active: hasCurrentPage,
})

return (
<Dropdown>
<DropdownTrigger>
<UnstyledButton className={wrapperStyle()}>
{label}
<FaCaretDownIcon />
</UnstyledButton>
</DropdownTrigger>
<DropdownContent>
<DropdownScrollArea as="ul" className={actionListStyle()}>
{actualChildren}
</DropdownScrollArea>
</DropdownContent>
</Dropdown>
)
}
Loading

0 comments on commit f64fbe5

Please sign in to comment.