Skip to content

Commit

Permalink
refactor: AccordionPanel を Tailwind CSS 化 (#3935)
Browse files Browse the repository at this point in the history
* feat: AccordionPanel を Tailwind CSS 化

* feat: AccordionPanelItem を Tailwind CSS 化

* feat: AccordionPanelTrigger を Tailwind CSS 化

* feat: AccordionPanelContent を Tailwind CSS 化

* chore: 不要な useClassNames を削除

* fix: レビュー指摘修正

* fix: アニメーションを修正

* fix: アニメーションを調整
  • Loading branch information
uknmr authored Nov 28, 2023
1 parent b236718 commit 0cdd674
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 204 deletions.
42 changes: 21 additions & 21 deletions src/components/AccordionPanel/AccordionPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from 'react'
import React, {
ComponentProps,
PropsWithChildren,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { tv } from 'tailwind-variants'

import { flatArrayToMap } from '../../libs/map'

Expand All @@ -10,11 +19,8 @@ import {
getNewExpandedItems,
keycodes,
} from './accordionPanelHelper'
import { useClassNames } from './useClassNames'

type Props = {
/** アコーディオンの内容 */
children: React.ReactNode
type Props = PropsWithChildren<{
/** アイコンの左右位置 */
iconPosition?: 'left' | 'right'
/** アイコンを表示するかどうか */
Expand All @@ -23,12 +29,10 @@ type Props = {
expandableMultiply?: boolean
/** デフォルトで開いた状態にするアイテムの `name` の配列 */
defaultExpanded?: string[]
/** コンポーネントのクラス名 */
className?: string
/** トリガのクリックイベントを処理するハンドラ */
onClick?: (expandedItems: string[]) => void
}
type ElementProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Props>
}>
type ElementProps = Omit<ComponentProps<'div'>, keyof Props>

export const AccordionPanelContext = React.createContext<{
iconPosition: 'left' | 'right'
Expand All @@ -46,19 +50,22 @@ export const AccordionPanelContext = React.createContext<{
parentRef: null,
})

export const AccordionPanel: React.VFC<Props & ElementProps> = ({
children,
const accordionWrapper = tv({
base: 'smarthr-ui-AccordionPanel',
})

export const AccordionPanel: React.FC<Props & ElementProps> = ({
iconPosition = 'left',
displayIcon = true,
expandableMultiply = false,
defaultExpanded = [],
className = '',
className,
onClick: onClickProps,
...props
}) => {
const [expandedItems, setExpanded] = useState(flatArrayToMap(defaultExpanded))
const parentRef = useRef<HTMLDivElement>(null)
const classNames = useClassNames()
const styles = useMemo(() => accordionWrapper({ className }), [className])

const onClickTrigger = useCallback(
(itemName: string, isExpanded: boolean) => {
Expand Down Expand Up @@ -118,14 +125,7 @@ export const AccordionPanel: React.VFC<Props & ElementProps> = ({
}}
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
{...props}
className={`${className} ${classNames.wrapper}`}
ref={parentRef}
onKeyDown={handleKeyPress}
>
{children}
</div>
<div {...props} className={styles} ref={parentRef} onKeyDown={handleKeyPress} />
</AccordionPanelContext.Provider>
)
}
98 changes: 34 additions & 64 deletions src/components/AccordionPanel/AccordionPanelContent.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,56 @@
import React, { HTMLAttributes, VFC, useCallback, useContext, useRef } from 'react'
import React, {
ComponentPropsWithoutRef,
FC,
PropsWithChildren,
useContext,
useMemo,
useRef,
} from 'react'
import { Transition } from 'react-transition-group'
import styled from 'styled-components'
import { tv } from 'tailwind-variants'

import { getIsInclude } from '../../libs/map'

import { AccordionPanelContext } from './AccordionPanel'
import { AccordionPanelItemContext } from './AccordionPanelItem'
import { useClassNames } from './useClassNames'

type Props = {
/** パネル部分の内容 */
children: React.ReactNode
/** パネル部分のクラス名 */
className?: string
}
type ElementProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Props>

const duration = 200

export const AccordionPanelContent: VFC<Props & ElementProps> = ({
children,
className = '',
...props
}) => {
type Props = PropsWithChildren
type ElementProps = Omit<ComponentPropsWithoutRef<'div'>, keyof Props>

const accordionPanelContent = tv({
base: [
'smarthr-ui-AccordionPanel-content',
'shr-max-h-0',
'shr-transition-[max-height,_visible,_opacity]',
'shr-duration-150',
'shr-ease-in-out',
'shr-invisible',
'shr-opacity-0',
'[&.entered]:shr-max-h-screen',
'[&.entered]:shr-visible',
'[&.entered]:shr-opacity-100',
],
})

export const AccordionPanelContent: FC<Props & ElementProps> = ({ className, ...props }) => {
const { name } = useContext(AccordionPanelItemContext)
const { expandedItems } = useContext(AccordionPanelContext)
const isInclude = getIsInclude(expandedItems, name)
const wrapperRef = useRef<HTMLDivElement>(null)
const classNames = useClassNames()

const recalculateHeight = useCallback(
(node: HTMLElement) => {
const wrapperHeight = wrapperRef.current ? wrapperRef.current.clientHeight : 0
node.style.height = `${wrapperHeight}px`
},
[wrapperRef],
)

const handleEntered = (node: HTMLElement) => {
node.style.height = 'auto'
node.style.visibility = 'visible'
}

const handleExited = (node: HTMLElement) => {
node.style.height = '0px'
node.style.visibility = 'hidden'
}
const styles = useMemo(() => accordionPanelContent({ className }), [className])

return (
<Transition
in={isInclude}
onEntering={recalculateHeight}
onEntered={handleEntered}
onExit={recalculateHeight}
onExiting={recalculateHeight}
onExited={handleExited}
timeout={duration}
>
<Transition in={isInclude} timeout={150}>
{(status) => (
<CollapseContainer
<div
{...props}
id={`${name}-content`}
className={`${status} ${className} ${classNames.content}`}
className={`${styles} ${status}`}
aria-labelledby={`${name}-trigger`}
aria-hidden={!isInclude}
>
<div ref={wrapperRef}>{children}</div>
</CollapseContainer>
ref={wrapperRef}
/>
)}
</Transition>
)
}

const CollapseContainer = styled.div`
height: 0;
overflow: hidden;
transition: height ${duration}ms ease;
visibility: hidden;
&.entered {
height: auto;
overflow: visible;
visibility: visible;
}
`
39 changes: 19 additions & 20 deletions src/components/AccordionPanel/AccordionPanelItem.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import React, { HTMLAttributes, VFC, createContext } from 'react'
import React, {
ComponentPropsWithoutRef,
FC,
PropsWithChildren,
createContext,
useMemo,
} from 'react'
import { tv } from 'tailwind-variants'

import { Section } from '../SectioningContent'

import { useClassNames } from './useClassNames'

type Props = {
type Props = PropsWithChildren<{
/** アイテムを識別するための名前 */
name: string
/** アコーディオンのアイテムの内容 */
children: React.ReactNode
/** アイテムのクラス名 */
className?: string
}
type ElementProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Props>
}>

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

export const AccordionPanelItemContext = createContext<{ name: string }>({
name: '',
})

export const AccordionPanelItem: VFC<Props & ElementProps> = ({
name,
children,
className = '',
...props
}) => {
const classNames = useClassNames()
const accordionPanelItem = tv({
base: 'smarthr-ui-AccordionPanel-item',
})

export const AccordionPanelItem: FC<Props & ElementProps> = ({ name, className, ...props }) => {
const styles = useMemo(() => accordionPanelItem({ className }), [className])
return (
<AccordionPanelItemContext.Provider
value={{
name,
}}
>
<Section {...props} className={`${className} ${classNames.item}`}>
{children}
</Section>
<Section {...props} className={styles} />
</AccordionPanelItemContext.Provider>
)
}
Loading

0 comments on commit 0cdd674

Please sign in to comment.