Skip to content

Commit

Permalink
feat: Table に table-layout 相当の props を追加 ほか (#4757)
Browse files Browse the repository at this point in the history
* feat: Table に table-layout 相当の props を、Th と Td に幅を与える props を渡せるようにする

* feat: Th と Td に水平整列を追加

* docs: Story を整理
uknmr authored Jul 9, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent cb11898 commit 35db275
Showing 5 changed files with 257 additions and 65 deletions.
158 changes: 132 additions & 26 deletions packages/smarthr-ui/src/components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import styled from 'styled-components'

import { Base } from '../Base'
import { Button } from '../Button'
import { Cluster } from '../Layout'
import { Text } from '../Text'

import { TdCheckbox } from './TdCheckbox'
@@ -21,6 +22,111 @@ export default {
},
}

const employeeData = [
{
employeeNo: 'SMP001',
name: '須磨英知',
divisionName:
'table の幅計算は UA に依って仕様が異なるため、幅の指定は最小限にすることをおすすめします。',
date: '2024-06-29',
amount: '999,999円',
},
{
employeeNo: 'SMP002',
name: '須磨叡智郎',
divisionName:
'Th や Td の contentWidth に number か string を与えると width に反映されます。number は文字数 string は任意の値を指定できます。',
date: '2024-06-29',
amount: '999,999円',
},
{
employeeNo: 'SMP003',
name: '須磨端央',
divisionName:
'Td の contentWidth には { base: number | string, min: number | string, max: number | string } という object を与えられます。base は width に、min / max はそれぞれ min-width / max-width に対応します。',
date: '2024-06-29',
amount: '999,999円',
},
{
employeeNo: 'SMP004',
name: '須磨英千尋',
divisionName:
'社員番号や日付、操作列など、絶対に改行させたくない場合は Text[whitespace="nowrap"] を使ってください。合わせて contentWidth に 1px など小さい値を与えると、内包要素の最大幅で列幅が固定されます。',
date: '2024-06-29',
amount: '999,999円',
},
{
employeeNo: 'SHR001',
name: '田中 太郎',
divisionName: '経営企画部/マーケティング部/デジタルマーケティング課/ソーシャルメディアチーム',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR002',
name: '鈴木 花子',
divisionName: '営業部/国内営業部/西日本営業課/大阪支店/法人営業チーム',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR003',
name: '佐藤 健一',
divisionName: '研究開発部/製品開発部/新製品開発課/プロジェクト管理チーム',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR004',
name: '伊藤 美咲',
divisionName: '人事部/人事管理部/採用課/新卒採用チーム/東京オフィス',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR005',
name: '渡辺 次郎',
divisionName: 'IT部/システム開発部/インフラ部/ネットワーク課/セキュリティチーム',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR006',
name: 'ジョナサン・アレハンドロ・マルティネス・サンチェス',
divisionName: '総務部/総務課/オフィスマネジメントチーム',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR007',
name: 'アレクサンドラ・マリー・アンジェリーナ・ド・ソウザ',
divisionName: '財務部/経理部/財務管理課/支払チーム',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR008',
name: 'マイケル・フランシス・ジョンソン・ウィリアムズ',
divisionName: '法務部/契約管理部/コンプライアンス課',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR009',
name: 'カタリーナ・エリザベス・ロドリゲス・ガルシア',
divisionName: '営業部/国際営業部/アジア営業課/中国支店',
date: '2021-04-01',
amount: '999,999円',
},
{
employeeNo: 'SHR010',
name: 'クリスティーナ・マリー・アンジェラ・マクドナルド',
divisionName: '研究開発部/技術開発部/AI技術課/データサイエンステーム',
date: '2021-04-01',
amount: '999,999円',
},
]

const data = [
{
name: 'Tea',
@@ -83,35 +189,42 @@ const data = [
export const All: StoryFn = () => (
<Ul>
<li>
table
<Table>
<thead>
<tr>
<ThCheckbox name="tableAllCheckbox" />
<Th sort="asc" onSort={action('降順に並び替え')}>
Name
<Th sort="desc" onSort={action('昇順に並び替え')}>
<Text whiteSpace="nowrap">社員番号</Text>
</Th>
<Th>氏名</Th>
<Th>部署</Th>
<Th sort="none" onSort={action('昇順に並び替え')}>
Calories
申請日
</Th>
<Th>Fat (g)</Th>
<Th>Carbs (g)</Th>
<Th>Protein (g)</Th>
<Th>Button</Th>
<Th align="right">総額</Th>
<Th>操作</Th>
</tr>
<BulkActionRow>Bulk action area</BulkActionRow>
</thead>
<tbody>
{data.map(({ name, calories, fat, carbs, protein }, i) => (
<tr key={name}>
<TdCheckbox name={`tableCheckBox-${i}`} aria-labelledby={`name-${i}`} />
<Td id={`name-${i}`}>{name}</Td>
<Td>{calories}</Td>
<Td>{fat}</Td>
<Td>{carbs}</Td>
<Td>{protein}</Td>
<Td>
<Button size="s">Button</Button>
{employeeData.map(({ employeeNo, name, divisionName, date, amount }, i) => (
<tr key={employeeNo}>
<TdCheckbox name={`tableCheckbox-${i}`} aria-labelledby={`name-${employeeNo}`} />
<Td contentWidth="1px">
<Text whiteSpace="nowrap">{employeeNo}</Text>
</Td>
<Td contentWidth={{ min: 10, max: 20 }} id={`name-${employeeNo}`}>
{name}
</Td>
<Td contentWidth={{ min: 15, max: 30 }}>{divisionName}</Td>
<Td contentWidth="1px">
<Text whiteSpace="nowrap">{date}</Text>
</Td>
<Td align="right">{amount}</Td>
<Td contentWidth="1px">
<Cluster style={{ flexWrap: 'nowrap' }}>
<Button size="s">編集</Button>
<Button size="s">削除</Button>
</Cluster>
</Td>
</tr>
))}
@@ -266,13 +379,6 @@ export const All: StoryFn = () => (
Table with empty state
<Base overflow="auto">
<Table>
<thead>
<tr>
<Th>cell</Th>
<Th>cell</Th>
<Th>cell</Th>
</tr>
</thead>
<EmptyTableBody>
<Text>お探しの条件に該当する項目はありません。</Text>
<Text>別の条件をお試しください。</Text>
27 changes: 17 additions & 10 deletions packages/smarthr-ui/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import React, { ComponentProps, FC, PropsWithChildren, createContext, useMemo } from 'react'
import { tv } from 'tailwind-variants'
import { VariantProps, tv } from 'tailwind-variants'

export const TableGroupContext = createContext<{
group: 'head' | 'body'
}>({
group: 'body',
})

type Props = PropsWithChildren<{
/** `true` のとき、スクロール時にヘッダーを固定表示する */
fixedHead?: boolean
}>
type Props = PropsWithChildren<VariantProps<typeof table>>
type ElementProps = Omit<ComponentProps<'table'>, keyof Props>

const table = tv({
@@ -27,19 +24,29 @@ const table = tv({
'contrast-more:shr-border-shorthand contrast-more:shr-border-high-contrast',
],
variants: {
layout: {
auto: '',
fixed: 'shr-table-fixed',
},
fixedHead: {
true: [
'[&_thead]:shr-sticky',
'[&_thead]:shr-start-0',
'[&_thead]:shr-top-0',
'[&_thead]:shr-sticky [&_thead]:shr-start-0 [&_thead]:shr-top-0',
/* zIndexの値はセマンティックトークンとして管理しているため、明示的に値を指定しないと重なり順が崩れるため設定しています */
'[&_thead]:shr-z-fixed-menu',
],
},
},
})

export const Table: FC<Props & ElementProps> = ({ fixedHead = false, className, ...props }) => {
const styles = useMemo(() => table({ fixedHead, className }), [className, fixedHead])
export const Table: FC<Props & ElementProps> = ({
fixedHead = false,
layout = 'auto',
className,
...props
}) => {
const styles = useMemo(
() => table({ fixedHead, layout, className }),
[className, fixedHead, layout],
)
return <table {...props} className={styles} />
}
57 changes: 50 additions & 7 deletions packages/smarthr-ui/src/components/Table/Td.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import React, { ComponentPropsWithoutRef, FC, PropsWithChildren, useMemo } from 'react'
import { VariantProps, tv } from 'tailwind-variants'
import { type VariantProps, tv } from 'tailwind-variants'

import { reelShadowStyle } from './useReelShadow'

export type Props = PropsWithChildren<VariantProps<typeof td>>
import type { CellContentWidth } from './type'

export type Props = PropsWithChildren<
VariantProps<typeof td> & {
contentWidth?:
| CellContentWidth
| { base?: CellContentWidth; min?: CellContentWidth; max?: CellContentWidth }
}
>
type ElementProps = Omit<ComponentPropsWithoutRef<'td'>, keyof Props>

export const Td: FC<Props & ElementProps> = ({
align = 'left',
nullable = false,
fixed = false,
contentWidth,
className,
style,
...props
}) => {
const styles = useMemo(() => {
const tdStyles = td({ nullable, fixed, className })
const styleProps = useMemo(() => {
const tdStyles = td({ align, nullable, fixed, className })
const reelShadowStyles = fixed ? reelShadowStyle({ direction: 'right' }) : ''
return `${tdStyles} ${reelShadowStyles}`.trim()
}, [className, fixed, nullable])
return {
className: `${tdStyles} ${reelShadowStyles}`.trim(),
style: {
...style,
...getWidthStyle(contentWidth),
},
}
}, [align, className, contentWidth, fixed, nullable, style])

return <td {...props} className={styles} />
return <td {...props} {...styleProps} />
}

const td = tv({
@@ -27,6 +44,10 @@ const td = tv({
'shr-border-t-shorthand shr-h-[calc(1em_*_theme(lineHeight.normal))] shr-px-1 shr-py-0.5 shr-align-middle shr-text-base shr-leading-normal shr-text-black',
],
variants: {
align: {
left: '',
right: 'shr-text-right',
},
nullable: {
true: "empty:after:shr-content-['-----']",
},
@@ -38,3 +59,25 @@ const td = tv({
},
},
})

const convertContentWidth = (contentWidth?: CellContentWidth) => {
if (typeof contentWidth === 'number') {
return `${contentWidth}em`
}

return contentWidth
}

const getWidthStyle = (contentWidth: Props['contentWidth']) => {
if (typeof contentWidth === 'object') {
return {
width: convertContentWidth(contentWidth.base),
minWidth: convertContentWidth(contentWidth.min),
maxWidth: convertContentWidth(contentWidth.max),
}
}

return {
width: convertContentWidth(contentWidth),
}
}
79 changes: 57 additions & 22 deletions packages/smarthr-ui/src/components/Table/Th.tsx
Original file line number Diff line number Diff line change
@@ -7,27 +7,30 @@ import React, {
ReactNode,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'
import { type VariantProps, tv } from 'tailwind-variants'

import { UnstyledButton } from '../Button'
import { FaSortDownIcon, FaSortUpIcon } from '../Icon'
import { VisuallyHiddenText } from '../VisuallyHiddenText'

import { reelShadowStyle } from './useReelShadow'

import type { CellContentWidth } from './type'

type sortTypes = keyof typeof SORT_DIRECTION_LABEL
export type Props = PropsWithChildren<{
/** 並び替え状態 */
sort?: sortTypes
/** 並び替えをクリックした時に発火するコールバック関数 */
onSort?: () => void
/** 文言を変更するための関数 */
decorators?: {
sortDirectionIconAlt: (text: string, { sort }: { sort: sortTypes }) => ReactNode
}
/** `true` のとき、TableReel内で固定表示になる */
fixed?: boolean
}>
export type Props = PropsWithChildren<
{
/** 並び替え状態 */
sort?: sortTypes
/** 並び替えをクリックした時に発火するコールバック関数 */
onSort?: () => void
/** 文言を変更するための関数 */
decorators?: {
sortDirectionIconAlt: (text: string, { sort }: { sort: sortTypes }) => ReactNode
}
contentWidth?: CellContentWidth
} & VariantProps<typeof thWrapper>
>
type ElementProps = Omit<ComponentPropsWithoutRef<'th'>, keyof Props | 'onClick'>

const SORT_DIRECTION_LABEL = {
@@ -48,6 +51,10 @@ const thWrapper = tv({
'[&[aria-sort=descending]_.smarthr-ui-Icon:first-of-type]:forced-colors:shr-fill-[GrayText] [&[aria-sort=descending]_.smarthr-ui-Icon:last-of-type]:forced-colors:shr-fill-[CanvasText]',
],
variants: {
align: {
left: '',
right: 'shr-text-right',
},
fixed: {
true: [
/* これ以降の記述はTableReel内で'fixed'を利用した際に追従させるために必要 */
@@ -58,20 +65,38 @@ const thWrapper = tv({
},
})

const convertContentWidth = (contentWidth?: CellContentWidth) => {
if (typeof contentWidth === 'number') {
// Th は fontSize.S のため、rem で指定する
return `${contentWidth}rem`
}

return contentWidth
}

export const Th: FC<Props & ElementProps> = ({
children,
sort,
onSort,
decorators,
align = 'left',
fixed = false,
contentWidth,
className,
style,
...props
}) => {
const styles = useMemo(() => {
const thWrapperStyle = thWrapper({ className, fixed })
const styleProps = useMemo(() => {
const thWrapperStyle = thWrapper({ className, align, fixed })
const reelShadowStyles = fixed ? reelShadowStyle({ showShadow: false, direction: 'right' }) : ''
return `${thWrapperStyle} ${reelShadowStyles}`.trim()
}, [className, fixed])
return {
className: `${thWrapperStyle} ${reelShadowStyles}`.trim(),
style: {
...style,
width: convertContentWidth(contentWidth),
},
}
}, [align, className, contentWidth, fixed, style])

const sortLabel = useMemo(
() =>
@@ -94,9 +119,9 @@ export const Th: FC<Props & ElementProps> = ({
)

return (
<th {...ariaSortProps} {...props} className={styles}>
<th {...ariaSortProps} {...props} {...styleProps}>
{sort ? (
<SortButton onClick={onSort}>
<SortButton align={align} onClick={onSort}>
{children}
<SortIcon sort={sort} />
<VisuallyHiddenText>{sortLabel}</VisuallyHiddenText>
@@ -110,14 +135,24 @@ export const Th: FC<Props & ElementProps> = ({

const sortButton = tv({
base: [
'-shr-mx-1 -shr-my-0.75 shr-inline-flex shr-w-full shr-justify-between shr-gap-x-0.5 shr-px-1 shr-py-0.75 shr-font-bold',
'-shr-mx-1 -shr-my-0.75 shr-inline-flex shr-w-full shr-gap-x-0.5 shr-px-1 shr-py-0.75 shr-font-bold',
// UnstyledButton に stretch がなぜか指定されてて負けてしまうため(UnstyledButton を見直した方がよさそう)
'[&]:shr-items-center',
],
variants: {
align: {
left: 'shr-justify-between',
right: 'shr-justify-end',
},
},
})

const SortButton: FC<ComponentProps<typeof UnstyledButton>> = ({ className, ...props }) => {
const sortButtonStyle = useMemo(() => sortButton({ className }), [className])
const SortButton: FC<ComponentProps<typeof UnstyledButton> & Pick<Props, 'align'>> = ({
align,
className,
...props
}) => {
const sortButtonStyle = useMemo(() => sortButton({ align, className }), [align, className])
return <UnstyledButton {...props} className={sortButtonStyle} />
}

1 change: 1 addition & 0 deletions packages/smarthr-ui/src/components/Table/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type CellContentWidth = number | string

0 comments on commit 35db275

Please sign in to comment.