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: ErrorScreen, SideMenu, FlashMessage, FloatArea, FormControlの内部ロジックを整理する #5435

Merged
merged 17 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
21 changes: 9 additions & 12 deletions packages/smarthr-ui/src/components/ErrorScreen/ErrorScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React, { useMemo } from 'react'
import React, { type ComponentPropsWithoutRef, type FC, type ReactNode, useMemo } from 'react'
import { tv } from 'tailwind-variants'

import { PageHeading } from '../Heading'
import { Center, Stack } from '../Layout'
import { SmartHRLogo } from '../SmartHRLogo'
import { TextLink } from '../TextLink'

import type { ComponentPropsWithoutRef, FC, ReactNode } from 'react'

type Props = {
/** ロゴ */
logo?: ReactNode
Expand All @@ -30,30 +28,29 @@ type Props = {

type ElementProps = Omit<ComponentPropsWithoutRef<'div'>, keyof Props>

const errorScreen = tv({
const classNameGenerator = tv({
base: 'smarthr-ui-ErrorScreen shr-bg-background',
})

export const ErrorScreen: FC<Props & ElementProps> = ({
logo = <SmartHRLogo fill="brand" className="shr-p-0.75" />,
logo,
title,
links,
children,
className,
...props
}) => {
const styles = useMemo(() => errorScreen({ className }), [className])
const actualClassName = useMemo(() => classNameGenerator({ className }), [className])

return (
<Center {...props} minHeight="100vh" verticalCentering className={styles}>
<Center {...props} minHeight="100vh" verticalCentering className={actualClassName}>
<Stack gap={1.5} align="center" className="[&&&]:shr-my-auto">
<div className="smarthr-ui-ErrorScreen-logo">{logo}</div>

<div className="smarthr-ui-ErrorScreen-logo">
{logo || <SmartHRLogo fill="brand" className="shr-p-0.75" />}
</div>
<Stack align="center">
{title && <PageHeading className="smarthr-ui-ErrorScreen-title">{title}</PageHeading>}

{children && <div className="smarthr-ui-ErrorScreen-content">{children}</div>}

{links?.length && (
<Stack
as="ul"
Expand All @@ -64,8 +61,8 @@ export const ErrorScreen: FC<Props & ElementProps> = ({
{links.map((link, index) => (
<li key={index}>
<TextLink
{...(link.target ? { target: link.target } : {})}
href={link.url}
target={link.target}
className="smarthr-ui-ErrorScreen-link"
>
{link.label}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React, { ComponentPropsWithoutRef, FC, PropsWithChildren, useMemo } from 'react'
import React, {
type ComponentPropsWithoutRef,
type FC,
type PropsWithChildren,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'

import { Base } from '../../Base'

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

Expand All @@ -17,6 +22,7 @@ type Props = PropsWithChildren<
}

export const SideMenu: FC<Props> = ({ elementAs = 'ul', className, ...rest }) => {
const styles = useMemo(() => sideMenu({ className }), [className])
return <Base {...rest} as={elementAs} className={styles} />
const actualClassName = useMemo(() => classNameGenerator({ className }), [className])

return <Base {...rest} as={elementAs} className={actualClassName} />
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
ReactNode,
type ComponentPropsWithoutRef,
type ElementType,
type PropsWithChildren,
type ReactNode,
memo,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'
Expand All @@ -20,7 +21,7 @@ type Props<TitleElement extends ElementType = 'p'> = PropsWithChildren<{
}> &
ComponentPropsWithoutRef<TitleElement>

const sideMenuGroup = tv({
const classNameGenerator = tv({
slots: {
wrapper: ['smarthr-ui-SideMenu-group', '[&:not(:first-of-type)]:shr-mt-1'],
list: 'shr-list-none',
Expand All @@ -35,26 +36,34 @@ export const SideMenuGroup = <TitleElement extends ElementType = 'p'>({
children,
className,
}: Props<TitleElement>) => {
const { wrapperStyle, listStyle, groupTitleStyle } = useMemo(() => {
const { wrapper, list, groupTitle } = sideMenuGroup()
const classNames = useMemo(() => {
const { wrapper, list, groupTitle } = classNameGenerator()

return {
wrapperStyle: wrapper({ className }),
listStyle: list(),
groupTitleStyle: groupTitle(),
wrapper: wrapper({ className }),
list: list(),
groupTitle: groupTitle(),
}
}, [className])

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

return (
<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 className={classNames.wrapper}>
<GroupTitleText titleElementAs={titleElementAs} className={classNames.groupTitle}>
{title}
</GroupTitleText>
<ListComponent className={classNames.list}>{children}</ListComponent>
</li>
)
}

const GroupTitleText = memo<PropsWithChildren<{ titleElementAs?: ElementType; className: string }>>(
({ titleElementAs: Component = 'p', children, className }) => (
<Component>
<Text color="TEXT_BLACK" leading="TIGHT" size="S" weight="bold" className={className}>
{children}
</Text>
</Component>
),
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
ReactNode,
type ComponentPropsWithoutRef,
type ElementType,
type PropsWithChildren,
type ReactNode,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'
Expand All @@ -18,7 +18,7 @@ type BaseProps<AsElement extends ElementType> = PropsWithChildren<{
type Props<AsElement extends ElementType = 'a'> = BaseProps<AsElement> &
Omit<ComponentPropsWithoutRef<AsElement>, keyof BaseProps<AsElement>>

const sideMenuItem = tv({
const classNameGenerator = tv({
slots: {
wrapper: [
'smarthr-ui-SideMenu-item',
Expand Down Expand Up @@ -55,20 +55,21 @@ export const SideMenuItem = <AsElement extends ElementType = 'a'>({
...rest
}: Props<AsElement>) => {
const Component = elementAs ?? 'a'
const { wrapperStyle, contentStyle, iconWrapperStyle } = useMemo(() => {
const { wrapper, content, iconWrapper } = sideMenuItem()
const classNames = useMemo(() => {
const { wrapper, content, iconWrapper } = classNameGenerator()

return {
wrapperStyle: wrapper({ current, className }),
contentStyle: content({ current }),
iconWrapperStyle: iconWrapper(),
wrapper: wrapper({ current, className }),
content: content({ current }),
iconWrapper: iconWrapper(),
}
}, [className, current])

return (
<li className={wrapperStyle}>
<li className={classNames.wrapper}>
<Component {...rest}>
<Text size="M" leading="TIGHT" className={contentStyle}>
{prefix && <span className={iconWrapperStyle}>{prefix}</span>}
<Text size="M" leading="TIGHT" className={classNames.content}>
{prefix && <span className={classNames.iconWrapper}>{prefix}</span>}
<Text weight={current ? 'bold' : undefined}>{children}</Text>
</Text>
</Component>
Expand Down
46 changes: 28 additions & 18 deletions packages/smarthr-ui/src/components/FlashMessage/FlashMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
'use client'

import React, { ComponentPropsWithoutRef, FC, ReactNode, useEffect, useMemo } from 'react'
import { VariantProps, tv } from 'tailwind-variants'
import React, {
type ComponentPropsWithoutRef,
type FC,
type ReactNode,
useEffect,
useMemo,
} from 'react'
import { type VariantProps, tv } from 'tailwind-variants'

import { Button } from '../Button'
import { FaXmarkIcon } from '../Icon'
Expand All @@ -23,13 +29,15 @@ export type Props = {
onClose: () => void
/** FlashMessage が表示されてから一定時間後に自動で閉じるかどうか */
autoClose?: boolean
} & VariantProps<typeof flashMessage>
} & VariantProps<typeof classNameGenerator>

type ElementProps = Omit<ComponentPropsWithoutRef<'div'>, keyof Props>

type ActualProps = Props & ElementProps

const REMOVE_DELAY = 8000

const flashMessage = tv({
const classNameGenerator = tv({
slots: {
wrapper: [
'smarthr-ui-FlashMessage',
Expand Down Expand Up @@ -57,8 +65,10 @@ const flashMessage = tv({
/**
* @deprecated `FlashMessage` は気づきにくいため、安易な使用はお勧めしません。`NotificationBar` や `Dialog` の使用を検討してください。
*/
export const FlashMessage: FC<Props & ElementProps> = ({
visible,
export const FlashMessage: FC<ActualProps> = ({ visible, ...rest }) =>
visible ? <ActualFlashMessage {...rest} /> : null

const ActualFlashMessage: FC<Omit<ActualProps, 'visible'>> = ({
type,
text,
animation = 'bounce',
Expand All @@ -69,33 +79,33 @@ export const FlashMessage: FC<Props & ElementProps> = ({
...rest
}) => {
useEffect(() => {
if (!visible || !autoClose) {
if (!autoClose) {
return
}

const timerId = setTimeout(onClose, REMOVE_DELAY)

return () => {
clearTimeout(timerId)
}
}, [autoClose, onClose, visible])
}, [autoClose, onClose])

const classNames = useMemo(() => {
const { wrapper, responseMessage, closeButton } = classNameGenerator()

const { wrapperStyle, responseMessageStyle, closeButtonStyle } = useMemo(() => {
const { wrapper, responseMessage, closeButton } = flashMessage()
return {
wrapperStyle: wrapper({ animation, className }),
responseMessageStyle: responseMessage(),
closeButtonStyle: closeButton(),
wrapper: wrapper({ animation, className }),
responseMessage: responseMessage(),
closeButton: closeButton(),
}
}, [animation, className])

if (!visible) return null

return (
<div {...rest} className={wrapperStyle} role={role}>
<ResponseMessage type={type} iconGap={0.5} className={responseMessageStyle}>
<div {...rest} role={role} className={classNames.wrapper}>
<ResponseMessage type={type} iconGap={0.5} className={classNames.responseMessage}>
{text}
</ResponseMessage>
<Button className={closeButtonStyle} onClick={onClose} size="s" square>
<Button onClick={onClose} size="s" square className={classNames.closeButton}>
<FaXmarkIcon alt="閉じる" />
</Button>
</div>
Expand Down
30 changes: 16 additions & 14 deletions packages/smarthr-ui/src/components/FloatArea/FloatArea.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { ComponentPropsWithoutRef, FC, ReactNode, useMemo } from 'react'
import React, { type ComponentPropsWithoutRef, type FC, type ReactNode, useMemo } from 'react'
import { tv } from 'tailwind-variants'

import { type ResponseMessageTypeWithoutProcessing } from '../../hooks/useResponseMessage'
import { AbstractSize, CharRelativeSize } from '../../themes/createSpacing'
import { Gap } from '../../types'
import { type AbstractSize, type CharRelativeSize } from '../../themes/createSpacing'
import { Base } from '../Base'
import { Cluster } from '../Layout'
import { ResponseMessage } from '../ResponseMessage'

const floatArea = tv({
import type { Gap } from '../../types'

const classNameGenerator = tv({
base: 'smarthr-ui-FloatArea shr-z-fixed-menu shr-sticky -shr-mx-0.5',
variants: {
bottom: {
Expand Down Expand Up @@ -41,13 +42,7 @@ const floatArea = tv({
},
})

type StyleProps = {
/** コンポーネントの下端から、包含ブロックの下端までの間隔(基準フォントサイズの相対値または抽象値) */
bottom?: CharRelativeSize | AbstractSize
/** コンポーネントの `z-index` 値 */
zIndex?: number
}
type Props = StyleProps & {
type Props = {
/** 表示する `Button` または `AnchorButton` コンポーネント */
primaryButton: ReactNode
/** 表示する `Button` または `AnchorButton` コンポーネント */
Expand All @@ -56,6 +51,10 @@ type Props = StyleProps & {
tertiaryButton?: ReactNode
/** 操作に対するフィードバックメッセージ */
responseMessage?: ResponseMessageTypeWithoutProcessing
/** コンポーネントの下端から、包含ブロックの下端までの間隔(基準フォントサイズの相対値または抽象値) */
bottom?: CharRelativeSize | AbstractSize
/** コンポーネントの `z-index` 値 */
zIndex?: number
}
type ElementProps = Omit<ComponentPropsWithoutRef<'div'>, keyof Props>

Expand All @@ -70,11 +69,14 @@ export const FloatArea: FC<Props & ElementProps> = ({
className,
...rest
}) => {
const styleAttr = useMemo(() => ({ ...style, zIndex }), [style, zIndex])
const actualClassName = useMemo(() => floatArea({ bottom, className }), [bottom, className])
const actualClassName = useMemo(
() => classNameGenerator({ bottom, className }),
[bottom, className],
)
const actualStyle = useMemo(() => ({ ...style, zIndex }), [style, zIndex])

return (
<Base {...rest} style={styleAttr} layer={3} padding={1} className={actualClassName}>
<Base {...rest} layer={3} padding={1} className={actualClassName} style={actualStyle}>
<Cluster gap={1}>
{tertiaryButton}
<div className="shr-ms-auto">
Expand Down
Loading