Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: AppHeader/mobile の内部ロジックを整理 vol4 #5398

Draft
wants to merge 53 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4c0c033
chore: AppHeaderのisChildNavigationGroupの型を調整する
AtsushiM Jan 30, 2025
7a2e7d5
chore: AppHeaderのREADMEのフォーマットを修正
AtsushiM Jan 30, 2025
a87d247
chore: AppHeaderのbuildDisplayNameのロジックを整理
AtsushiM Jan 30, 2025
5fb3030
chore: AppHeaderのuseMediaQueryのロジックを整理
AtsushiM Jan 30, 2025
aca1e98
chore: AppHeaderのuseAppLancherのlooseIncludeの処理を改善する
AtsushiM Jan 31, 2025
1581148
chore: AppHeaderのuseAppLancherのsortFeaturesのifをswitchに置き換える
AtsushiM Jan 31, 2025
85e2560
chore: AppHeaderのuseAppLancher内のif条件を整理
AtsushiM Jan 31, 2025
062c617
chore: fix ci
AtsushiM Jan 31, 2025
de16079
chore: fix ci
AtsushiM Feb 2, 2025
e205a9b
Merge branch 'master' of https://github.com/kufu/smarthr-ui into chor…
AtsushiM Feb 3, 2025
5cdcedb
Merge branch 'master' of https://github.com/kufu/smarthr-ui into chor…
AtsushiM Feb 12, 2025
f814266
Merge branch 'master' of https://github.com/kufu/smarthr-ui into chor…
AtsushiM Feb 17, 2025
ce7b044
chore: Headerの表示ロジックを調整
AtsushiM Feb 17, 2025
ba481d2
chore: ActualUserInfoを切り出す
AtsushiM Feb 17, 2025
f265092
chore: displayNameをuseMemo
AtsushiM Feb 17, 2025
e11b8b5
chore: classNameの生成を調整
AtsushiM Feb 17, 2025
3e62c14
chore: translatedを定義
AtsushiM Feb 17, 2025
5dd3b3c
chore: memo化
AtsushiM Feb 17, 2025
28a4eec
chore: memo化
AtsushiM Feb 17, 2025
37100cc
chore: ActualTenantSelectorを切り出す
AtsushiM Feb 17, 2025
e00b546
chore: MemoizedTenantDropdownTriggerを切り出す
AtsushiM Feb 17, 2025
0b8e84f
chore: ActualReleaseNoteを切り出す
AtsushiM Feb 17, 2025
e83c032
chore: classNamesを調整
AtsushiM Feb 17, 2025
139760b
chore: translatedを定義
AtsushiM Feb 17, 2025
032a8d2
chore: fix ci
AtsushiM Feb 17, 2025
3dba808
chore: classNamesの生成を調整
AtsushiM Feb 18, 2025
ab30299
chore: onClickCloseの生成方法を修正
AtsushiM Feb 18, 2025
0d9a367
chore: onClickLocaleを最適化する
AtsushiM Feb 18, 2025
8e87032
chore: LocaleButtonを切り出す
AtsushiM Feb 18, 2025
ce26ccc
chore: SelectorHeadingを切り出す
AtsushiM Feb 18, 2025
9b94944
chore: LanguageSelectorをmemo化
AtsushiM Feb 18, 2025
b7d5765
chore: ActualHelpを切り出す
AtsushiM Feb 18, 2025
1fd7768
chore: translatedを定義
AtsushiM Feb 18, 2025
a6fc7ca
chore: ContentBodyを切り出す
AtsushiM Feb 18, 2025
791b78b
chore: MemoizedDropdownTriggerを切り出す
AtsushiM Feb 18, 2025
7557b81
chore: classNamesの定義を調整
AtsushiM Feb 18, 2025
d30eaff
chore: translatedを定義
AtsushiM Feb 18, 2025
e164526
chore: onClickButtonをmemo
AtsushiM Feb 18, 2025
3fdfe7d
chore: flexのためにStackを使うことを辞める
AtsushiM Feb 18, 2025
9fa631e
chore: Objectのループ順は保証されないため、ベタ書きにする
AtsushiM Feb 18, 2025
cb489f3
chore: ContentBodyを切り出す
AtsushiM Feb 18, 2025
2d61187
chore: MemoizedDropdownTriggerを切り出す
AtsushiM Feb 18, 2025
6da1db2
chore: AppLauncherFilterDropdown自体をmemo化する
AtsushiM Feb 18, 2025
72c38dc
chore: classNamesを定義
AtsushiM Feb 18, 2025
67bce84
chore: translatedを定義
AtsushiM Feb 18, 2025
c4eeaa4
chore: 関数をcustom hookに押し込める
AtsushiM Feb 18, 2025
14b68ce
chore: BottomAreaを切り出す
AtsushiM Feb 18, 2025
2af5697
chore: SearchResultTextを切り出す
AtsushiM Feb 18, 2025
67dc684
chore: ClearSearchButtonを切り出す
AtsushiM Feb 18, 2025
ec385d9
Merge branch 'chore-refactoring-AppHeader-mobile' into chore-refactor…
AtsushiM Feb 18, 2025
aea8225
chore: fix ci
AtsushiM Feb 18, 2025
20a85ba
chore: roleを指定し、aria-selectedをvalidにする
AtsushiM Feb 19, 2025
bc1b4bb
Merge branch 'master' of https://github.com/kufu/smarthr-ui into chor…
AtsushiM Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ export const AppHeader: FC<HeaderProps> = ({ locale, children, ...props }) => {
// wovn の言語切替カスタム UI の挿入対象となる DOM ("wovn-embedded-widget-anchor" クラスを持った div) が複数描画されていると、
// wovn のスクリプトの仕様上1つ目の DOM にしか UI が挿入されないため、やむを得ず children のみ React のレンダリングレベルでの出し分けをしています。
const isDesktop = useMediaQuery(mediaQuery.desktop)
const isMobile = useMediaQuery(mediaQuery.mobile)
const Header = isDesktop ? DesktopHeader : MobileHeader

return (
<LocaleContextProvider locale={locale}>
<DesktopHeader {...props}>{isDesktop && children}</DesktopHeader>
<MobileHeader {...props}>{isMobile && children}</MobileHeader>
<Header {...props}>{children}</Header>
</LocaleContextProvider>
)
}
42 changes: 21 additions & 21 deletions packages/smarthr-ui/src/components/AppHeader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,34 @@ export type HeaderProps = ComponentProps<typeof Header> & {
下記に、少し特殊な動きをするものやパッと見分かりづらいであろうと思われる props だけ補足説明を書きます。

- `locale`
- 多言語対応に wovn を使っている場合はこの props は不要です。
- 多言語対応に wovn を使っている場合はこの props は不要です。
- `tenants`
- デスクトップ表示時
- Header コンポーネントと同じです。
- モバイル表示時
- ハンバーガーメニューが表示されている場合はメニューの中に、そうでない場合はデスクトップ表示時と同じ箇所 (ロゴの横) に表示されます。
- もし既存の独自実装ハンバーガーメニュー内にテナント選択の UI があるなどの理由で「ハンバーガーメニューは表示しないがモバイル表示時にヘッダーにテナント選択の UI を表示したくない」という場合は、ウィンドウサイズが 751px 以下のときに tenants props に undefined を渡すようにしてください。
- デスクトップ表示時
- Header コンポーネントと同じです。
- モバイル表示時
- ハンバーガーメニューが表示されている場合はメニューの中に、そうでない場合はデスクトップ表示時と同じ箇所 (ロゴの横) に表示されます。
- もし既存の独自実装ハンバーガーメニュー内にテナント選択の UI があるなどの理由で「ハンバーガーメニューは表示しないがモバイル表示時にヘッダーにテナント選択の UI を表示したくない」という場合は、ウィンドウサイズが 751px 以下のときに tenants props に undefined を渡すようにしてください。
- `navigations`
- ヘッダーの下にナビゲーションが表示されるようになります。
- AppNavi コンポーネントの buttons props とほぼ同じ型のデータを取ります。
- AppNavi コンポーネントの buttons にはなかった、ドロップダウン内でのナビゲーションのグルーピングができるようになっています。
- storybook の「VRT Navigation Dropdown Group」を参考にしてください。
- **navigations props に値が渡されているときのみ、モバイル表示時にハンバーガーメニューが表示されます。独自実装の ハンバーガーメニューが存在する場合は、navigations props を利用するタイミングで移行してください。**
- ヘッダーの下にナビゲーションが表示されるようになります。
- AppNavi コンポーネントの buttons props とほぼ同じ型のデータを取ります。
- AppNavi コンポーネントの buttons にはなかった、ドロップダウン内でのナビゲーションのグルーピングができるようになっています。
- storybook の「VRT Navigation Dropdown Group」を参考にしてください。
- **navigations props に値が渡されているときのみ、モバイル表示時にハンバーガーメニューが表示されます。独自実装の ハンバーガーメニューが存在する場合は、navigations props を利用するタイミングで移行してください。**
- `desktopAdditionalContent`
- ユーザー名をクリックしたときのドロップダウンの、「個人設定」の下に入れたいものがある場合に使います。
- 見た目の共通化のため、乱用は避けてください
- ユーザー名をクリックしたときのドロップダウンの、「個人設定」の下に入れたいものがある場合に使います。
- 見た目の共通化のため、乱用は避けてください
- `desktopNavigationAdditionalContent`
- ナビゲーション内で右寄せ、かつリリースノートの左側に入れたい物がある場合に使います。
- 見た目の共通化のため、乱用は避けてください
- ナビゲーション内で右寄せ、かつリリースノートの左側に入れたい物がある場合に使います。
- 見た目の共通化のため、乱用は避けてください
- `mobileAdditionalContent`
- モバイル表示時に、メニュー内に何か追加で起きたいものがある場合に使います。
- 見た目の共通化のため、乱用は避けてほしいですが、もし何かしらのパーツを配置する必要がある場合は、デザイナーと相談しながら実装してください。
- モバイル表示時に、メニュー内に何か追加で起きたいものがある場合に使います。
- 見た目の共通化のため、乱用は避けてほしいですが、もし何かしらのパーツを配置する必要がある場合は、デザイナーと相談しながら実装してください。

## 多言語対応について

- wovn を使っているアプリの場合
- 内部で表示されているテキストに関しては、すべて `woven-enabled="true"` がついています。
- 外部から渡すテキストは全て `ReactNode` 型で受け取るようになっているので、`<span woven-enabled="true">ほげ</span>` みたいなものを渡すようにしてください。
- 内部で表示されているテキストに関しては、すべて `woven-enabled="true"` がついています。
- 外部から渡すテキストは全て `ReactNode` 型で受け取るようになっているので、`<span woven-enabled="true">ほげ</span>` みたいなものを渡すようにしてください。
- 辞書を持っているアプリの場合
- コンポーネント側で辞書を持っているので、`locale` の props を埋めると内部的に持っているテキストは翻訳されます。
- 外部から渡すテキストはアプリケーション側で翻訳されたものを渡すようにしてください。
- コンポーネント側で辞書を持っているので、`locale` の props を埋めると内部的に持っているテキストは翻訳されます。
- 外部から渡すテキストはアプリケーション側で翻訳されたものを渡すようにしてください。
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC } from 'react'
import React, { type FC, type PropsWithChildren, memo, useMemo } from 'react'
import { tv } from 'tailwind-variants'

import { UnstyledButton } from '../../../Button'
Expand All @@ -20,7 +20,7 @@ type Props = {
features: Array<Launcher['feature']>
}

const appLauncher = tv({
const classNameGenerator = tv({
slots: {
wrapper: ['smarthr-ui-AppLauncher', 'shr-flex shr-flex-col shr-h-full'],
searchArea: [
Expand All @@ -35,7 +35,6 @@ const appLauncher = tv({
})

export const AppLauncher: FC<Props> = ({ features: baseFeatures }) => {
const translate = useTranslate()
const {
features,
page,
Expand All @@ -44,69 +43,88 @@ export const AppLauncher: FC<Props> = ({ features: baseFeatures }) => {
searchQuery,
changePage,
setSortType,
changeSearchQuery,
onChangeSearchQuery,
onClickClearSearchQuery,
} = useAppLauncher(baseFeatures)

const { wrapper, searchArea, headArea, scrollArea, bottomArea } = appLauncher()
const classNames = useMemo(() => {
const { wrapper, searchArea, headArea, scrollArea, bottomArea } = classNameGenerator()

return {
wrapper: wrapper(),
searchArea: searchArea(),
headArea: headArea(),
scrollArea: scrollArea(),
bottomArea: bottomArea(),
}
}, [])

const translate = useTranslate()
const translated = useMemo(
() => ({
searchInputTitle: translate('Launcher/searchInputTitle'),
searchResultText: translate('Launcher/searchResultText'),
helpText: translate('Launcher/helpText'),
}),
[translate],
)

return (
<div className={wrapper()}>
<div className={searchArea()}>
<div className={classNames.wrapper}>
<div className={classNames.searchArea}>
<SearchInput
name="search"
title={translate('Launcher/searchInputTitle')}
tooltipMessage={<Translate>{translate('Launcher/searchInputTitle')}</Translate>}
title={translated.searchInputTitle}
tooltipMessage={<Translate>{translated.searchInputTitle}</Translate>}
width="100%"
value={searchQuery}
suffix={
mode === 'search' && (
<UnstyledButton
onClick={() => {
// 別のキューにしないとドロップダウンが閉じてしまう
setTimeout(() => {
changeSearchQuery('')
}, 0)
}}
>
<FaCircleXmarkIcon />
</UnstyledButton>
)
}
onChange={(e) => changeSearchQuery(e.target.value)}
suffix={mode === 'search' && <ClearSearchButton onClick={onClickClearSearchQuery} />}
onChange={onChangeSearchQuery}
/>
</div>

<Cluster className={headArea()} justify="space-between" align="center">
<Cluster className={classNames.headArea} justify="space-between" align="center">
{mode === 'search' ? (
<Text size="S" weight="bold">
<Translate>{translate('Launcher/searchResultText')}</Translate>
</Text>
<SearchResultText>{translated.searchResultText}</SearchResultText>
) : (
<AppLauncherFilterDropdown page={page} onSelectPage={(p) => changePage(p)} />
<AppLauncherFilterDropdown page={page} onSelectPage={changePage} />
)}

{(mode === 'search' || page === 'all') && (
<AppLauncherSortDropdown
sortType={sortType}
onSelectSortType={(value) => setSortType(value)}
/>
<AppLauncherSortDropdown sortType={sortType} onSelectSortType={setSortType} />
)}
</Cluster>

<div className={scrollArea()}>
<div className={classNames.scrollArea}>
<AppLauncherFeatures features={features} page={page} />
</div>

<div className={bottomArea()}>
<Text size="XS">
<TextLink
href="https://support.smarthr.jp/ja/help/articles/2bfd350d-8e8b-4bbd-a209-426d2eb302cc/"
target="_blank"
>
<Translate>{translate('Launcher/helpText')}</Translate>
</TextLink>
</Text>
</div>
<BottomArea className={classNames.bottomArea}>{translated.helpText}</BottomArea>
</div>
)
}

const ClearSearchButton = memo<{ onClick: () => void }>(({ onClick }) => (
<UnstyledButton onClick={onClick}>
<FaCircleXmarkIcon />
</UnstyledButton>
))

const SearchResultText = memo<PropsWithChildren>(({ children }) => (
<Text size="S" weight="bold">
<Translate>{children}</Translate>
</Text>
))

const BottomArea = memo<PropsWithChildren<{ className: string }>>(({ children, className }) => (
<div className={className}>
<Text size="XS">
<TextLink
href="https://support.smarthr.jp/ja/help/articles/2bfd350d-8e8b-4bbd-a209-426d2eb302cc/"
target="_blank"
>
<Translate>{children}</Translate>
</TextLink>
</Text>
</div>
))
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { FC } from 'react'
import React, { type MouseEvent, type PropsWithChildren, memo, useCallback, useMemo } from 'react'
import { tv } from 'tailwind-variants'

import { textColor } from '../../../../themes'
import { Button } from '../../../Button'
import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown'
import { FaCaretDownIcon, FaCheckIcon } from '../../../Icon'
import { Stack } from '../../../Layout'
import { useTranslate } from '../../hooks/useTranslate'
import { Launcher } from '../../types'
import { Translate } from '../common/Translate'
Expand All @@ -15,69 +14,112 @@ type Props = {
onSelectPage: (page: Launcher['page']) => void
}

const filterDropdown = tv({
const classNameGenerator = tv({
slots: {
trigger: [
'smarthr-ui-AppLauncher-SortDropdown-trigger',
'shr-gap-0.25 shr-text-grey',
'[&[aria-expanded="true"]>.smarthr-ui-Icon]:shr-rotate-180',
],
stack: ['shr-px-0.25 shr-py-0.5'],
contentButton: ['shr-border-none shr-justify-start shr-py-0.75 shr-font-normal shr-pl-2.5'],
},
variants: {
selected: {
true: {
contentButton: ['shr-pl-1'],
},
},
contentBody: ['shr-px-0.25 shr-py-0.5 shr-flex shr-flex-col shr-items-stretch'],
contentButton: [
'shr-border-none shr-justify-start shr-py-0.75 shr-font-normal shr-pl-2.5',
'aria-selected:shr-pl-1',
],
},
})

export const AppLauncherFilterDropdown: FC<Props> = ({ page, onSelectPage }) => {
export const AppLauncherFilterDropdown = memo<Props>(({ page, onSelectPage }) => {
const classNames = useMemo(() => {
const { trigger, contentBody, contentButton } = classNameGenerator()

return {
trigger: trigger(),
contentBody: contentBody(),
contentButton: contentButton(),
}
}, [])

const translate = useTranslate()
const { trigger, stack, contentButton } = filterDropdown()
const filterMap: Record<Launcher['page'], string> = {
favorite: translate('Launcher/favoriteModeText'),
all: translate('MobileHeader/Menu/allAppButton'),
}
const translated = useMemo(
() => ({
favorite: translate('Launcher/favoriteModeText'),
all: translate('MobileHeader/Menu/allAppButton'),
checkIconAlt: translate('Launcher/sortDropdownSelected'),
}),
[translate],
)

return (
<Dropdown>
<DropdownTrigger>
<Button className={trigger()} size="s" suffix={<FaCaretDownIcon />}>
<Translate>{filterMap[page]}</Translate>
</Button>
</DropdownTrigger>

<MemoizedDropdownTrigger className={classNames.trigger}>
{translated[page]}
</MemoizedDropdownTrigger>
<DropdownContent>
{/* eslint-disable-next-line smarthr/best-practice-for-layouts */}
<Stack className={stack()} gap={0} align="stretch">
{Object.entries(filterMap).map(([key, value], i) => {
const isSelected = key === page

return (
<Button
key={i}
className={contentButton({ selected: isSelected })}
prefix={
isSelected && (
<FaCheckIcon
color={textColor.main}
alt={<Translate>{translate('Launcher/sortDropdownSelected')}</Translate>}
/>
)
}
onClick={() => {
onSelectPage(key as Launcher['page'])
}}
>
<Translate>{value}</Translate>
</Button>
)
})}
</Stack>
<ContentBody
page={page}
onSelectPage={onSelectPage}
translated={translated}
className={classNames.contentBody}
buttonClassName={classNames.contentButton}
/>
</DropdownContent>
</Dropdown>
)
}
})

const MemoizedDropdownTrigger = memo<PropsWithChildren<{ className: string }>>(
({ children, className }) => (
<DropdownTrigger>
<Button className={className} size="s" suffix={<FaCaretDownIcon />}>
<Translate>{children}</Translate>
</Button>
</DropdownTrigger>
),
)

const ContentBody = memo<
Props & {
translated: { favorite: string; all: string; checkIconAlt: string }
className: string
buttonClassName: string
}
>(({ page, onSelectPage, translated, className, buttonClassName }) => {
const isFavorite = page === 'favorite'

const onClickButton = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
onSelectPage(e.currentTarget.value as Launcher['page'])
},
[onSelectPage],
)

const buttonPrefix = (
<FaCheckIcon color={textColor.main} alt={<Translate>{translated.checkIconAlt}</Translate>} />
)

return (
<div role="listbox" className={className}>
<Button
value="favorite"
role="option"
aria-selected={isFavorite}
onClick={onClickButton}
className={buttonClassName}
prefix={isFavorite && buttonPrefix}
>
<Translate>{translated.favorite}</Translate>
</Button>
<Button
value="all"
role="option"
aria-selected={!isFavorite}
onClick={onClickButton}
className={buttonClassName}
prefix={!isFavorite && buttonPrefix}
>
<Translate>{translated.all}</Translate>
</Button>
</div>
)
})
Loading