Skip to content

Commit

Permalink
feat!: SideMenuを実装 (#4895)
Browse files Browse the repository at this point in the history
Co-authored-by: KANAMORI Yu <[email protected]>
  • Loading branch information
Qs-F and uknmr authored Sep 10, 2024
1 parent 01d127a commit b02531a
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import { StoryFn } from '@storybook/react'
import React from 'react'

import { SideMenu } from './SideMenu'
import { FaGearIcon } from '../../Icon'

import { SideMenu, SideMenuGroup, SideMenuItem } from '.'

export default {
title: 'Experimental(実験的)/SideMenu',
component: SideMenu,
parameters: {
layout: 'fullscreen',
withTheming: true,
},
}

export const Default: StoryFn = () => (
<div className="shr-bg-background shr-p-2">
<div className="shr-bg-background shr-p-2 shr-max-w-[15rem]">
<SideMenu>
<SideMenu.Group name="基本設定">
<SideMenu.Item href="#">評価シート</SideMenu.Item>
<SideMenu.Item href="#" current>
評価ロール
</SideMenu.Item>
<SideMenu.Item href="#">評価フロー</SideMenu.Item>
</SideMenu.Group>
<SideMenu.Group name="その他の設定">
<SideMenu.Item href="#">評価項目の入力必須設定</SideMenu.Item>
<SideMenu.Item href="#">評価ロールの閲覧・編集権限設定</SideMenu.Item>
<SideMenu.Item href="#">評価対象者の表示項目設定</SideMenu.Item>
</SideMenu.Group>
<SideMenuGroup title="個人設定">
<SideMenuItem href="#">アカウント</SideMenuItem>
<SideMenuItem href="#" current>
認証設定
</SideMenuItem>
</SideMenuGroup>
<SideMenuGroup title="共通設定">
<SideMenuItem href="#" prefix={<FaGearIcon />}>
評価項目の表示設定
</SideMenuItem>
<SideMenuItem href="#" prefix={<FaGearIcon />}>
評価対象者の入力必須項目設定
</SideMenuItem>
</SideMenuGroup>
</SideMenu>
</div>
)
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
import React, { ComponentProps, FC, useMemo } from 'react'
import React, { ComponentPropsWithoutRef, FC, useMemo } from 'react'
import { tv } from 'tailwind-variants'

import { Stack } from '../../Layout'

import { SideMenuGroup } from './SideMenuGroup'
import { SideMenuItem } from './SideMenuItem'

type SubComponents = {
/** @deprecated SideMenu は削除予定です */
Group: typeof SideMenuGroup
/** @deprecated SideMenu は削除予定です */
Item: typeof SideMenuItem
}
import { Base } from '../../Base'

const sideMenu = tv({
base: 'smarthr-ui-SideMenu shr-list-none',
base: 'smarthr-ui-SideMenu shr-list-none shr-py-0.5',
})

/** @deprecated SideMenu コンポーネントは 2024/01 に削除予定です。別コンポーネントで代替するか、UI を見直してください。 */
export const SideMenu: FC<ComponentProps<typeof Stack>> & SubComponents = ({
className,
...rest
}) => {
type Props = Pick<ComponentPropsWithoutRef<typeof Base>, 'radius' | 'layer' | 'className'> & {
/**
* @default ul
*/
elementAs?: 'ul' | 'ol'
}

export const SideMenu: FC<Props> = ({ elementAs = 'ul', className, ...rest }) => {
const styles = useMemo(() => sideMenu({ className }), [className])
return (
// eslint-disable-next-line smarthr/best-practice-for-layouts
<Stack {...rest} as="ul" inline gap={0.75} className={styles} />
)
return <Base {...rest} as={elementAs} className={styles} />
}
SideMenu.Group = SideMenuGroup
SideMenu.Item = SideMenuItem
Original file line number Diff line number Diff line change
@@ -1,48 +1,60 @@
import React, { ComponentProps, PropsWithChildren, ReactNode, useMemo } from 'react'
import React, {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
ReactNode,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'

import { Stack } from '../../Layout'
import { Text } from '../../Text'

type Props = PropsWithChildren<{
/** 分類ラベル */
name: ReactNode
/** 分類ラベルの HTML タグ */
nameTag?: ComponentProps<typeof Text>['as']
}>
type ElementProps = ComponentProps<typeof Stack>
type Props<TitleElement extends ElementType = 'p'> = PropsWithChildren<{
title: ReactNode
titleElementAs?: TitleElement

/**
* @default ul
*/
listElementAs?: 'ul' | 'ol'
}> &
ComponentPropsWithoutRef<TitleElement>

const sideMenuGroup = tv({
slots: {
wrapper: ['smarthr-ui-SideMenu-group', '[&_+_&]:shr-border-t-shorthand [&_+_&]:shr-pt-1.25'],
wrapper: ['smarthr-ui-SideMenu-group', '[&:not(:first-of-type)]:shr-mt-1'],
list: 'shr-list-none',
groupTitle: 'shr-block shr-px-1 shr-py-0.5',
},
})

export const SideMenuGroup: React.FC<Props & ElementProps> = ({
name,
nameTag = 'h3',
export const SideMenuGroup = <TitleElement extends ElementType = 'p'>({
title,
titleElementAs,
listElementAs,
children,
className,
...rest
}) => {
const { wrapperStyle, listStyle } = useMemo(() => {
const { wrapper, list } = sideMenuGroup()
}: Props<TitleElement>) => {
const { wrapperStyle, listStyle, groupTitleStyle } = useMemo(() => {
const { wrapper, list, groupTitle } = sideMenuGroup()
return {
wrapperStyle: wrapper({ className }),
listStyle: list(),
groupTitleStyle: groupTitle(),
}
}, [className])

const TitleComponent = titleElementAs ?? 'p'
const ListComponent = listElementAs ?? 'ul'

return (
<Stack {...rest} as="li" gap={0.5} className={wrapperStyle}>
<Text color="TEXT_GREY" leading="TIGHT" size="S" weight="normal" as={nameTag}>
{name}
</Text>
{/* eslint-disable-next-line smarthr/best-practice-for-layouts */}
<Stack as="ul" gap={0} className={listStyle}>
{children}
</Stack>
</Stack>
<li className={wrapperStyle}>
<TitleComponent>
<Text color="TEXT_BLACK" leading="TIGHT" size="S" weight="bold" className={groupTitleStyle}>
{title}
</Text>
</TitleComponent>
<ListComponent className={listStyle}>{children}</ListComponent>
</li>
)
}
Original file line number Diff line number Diff line change
@@ -1,56 +1,77 @@
import React, { ComponentPropsWithoutRef, PropsWithChildren, useMemo } from 'react'
import React, {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
ReactNode,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'

type Props = PropsWithChildren<{
/** 現在地かどうか */
import { Text } from '../../Text'

type BaseProps<AsElement extends ElementType> = PropsWithChildren<{
elementAs?: AsElement
current?: boolean
prefix?: ReactNode
}>
type ElementProps = Omit<ComponentPropsWithoutRef<'li'>, keyof Props>
type InnerLinkProps = Omit<ComponentPropsWithoutRef<'a'>, keyof Props & keyof ElementProps>

type Props<AsElement extends ElementType = 'a'> = BaseProps<AsElement> &
Omit<ComponentPropsWithoutRef<AsElement>, keyof BaseProps<AsElement>>

const sideMenuItem = tv({
slots: {
wrapper: ['smarthr-ui-SideMenu-item', 'shr-relative shr-ps-0.75'],
innerLink: [
// 親要素ではなくリンクにスタイリングするため block でいっぱいに広げている
'shr-block',
'shr-rounded-m shr-px-1 shr-py-0.75 shr-no-underline',
wrapper: [
'smarthr-ui-SideMenu-item',
'[&>a]:shr-no-underline [&>a]:shr-block',
'[&>*:focus-visible]:shr-focus-indicator',
],
content: [
'shr-flex shr-gap-0.5 shr-p-0.75 shr-items-baseline',
'aria-current-page:shr-bg-grey-9 aria-current-page:shr-font-bold',
'hover:shr-bg-grey-9-darken',
// フォーカスリングを前に出したいので、スタッキングコンテキストを発生させている
'focus-visible:shr-focus-indicator focus-visible:shr-relative focus-visible:shr-z-1',
'hover:shr-bg-head-darken',
],
// 視覚調整のためのtranslate 参考: https://github.com/kufu/smarthr-ui/blob/01d127a4888f5698b2bf17be855ce1e985b575ea/packages/smarthr-ui/src/components/Icon/generateIcon.tsx#L73C81-L73C106
iconWrapper: ['shr-text-grey shr-translate-y-[0.125em]'],
},
variants: {
current: {
true: {
wrapper:
'before:shr-absolute before:shr-inset-y-0 before:shr-left-0 before:shr-block before:shr-w-[3px] before:shr-bg-main before:shr-content-[""]',
content:
'shr-ps-1.25 shr-border-[theme(colors.main)] shr-border-0 shr-border-s-4 shr-border-solid shr-bg-over-background',
},
false: {
content: 'shr-ps-1.5',
},
},
},
})

export const SideMenuItem: React.FC<Props & ElementProps & InnerLinkProps> = ({
href,
children,
export const SideMenuItem = <AsElement extends ElementType = 'a'>({
elementAs,
current,
prefix,
children,
className,
...rest
}) => {
const { wrapperStyle, innerLinkStyle } = useMemo(() => {
const { wrapper, innerLink } = sideMenuItem()
}: Props<AsElement>) => {
const Component = elementAs ?? 'a'
const { wrapperStyle, contentStyle, iconWrapperStyle } = useMemo(() => {
const { wrapper, content, iconWrapper } = sideMenuItem()
return {
wrapperStyle: wrapper({ current, className }),
innerLinkStyle: innerLink(),
contentStyle: content({ current }),
iconWrapperStyle: iconWrapper(),
}
}, [className, current])

return (
<li {...rest} className={wrapperStyle}>
<a href={href} aria-current={current && 'page'} className={innerLinkStyle}>
{children}
</a>
<li className={wrapperStyle}>
<Component {...rest}>
<Text size="M" leading="TIGHT" className={contentStyle}>
{prefix && <span className={iconWrapperStyle}>{prefix}</span>}
<Text weight={current ? 'bold' : undefined}>{children}</Text>
</Text>
</Component>
</li>
)
}

0 comments on commit b02531a

Please sign in to comment.