Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
51 changes: 42 additions & 9 deletions packages/accordion/src/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import React, {
type KeyboardEvent,
type ReactNode,
useCallback,
useRef,
useState,
} from 'react';
import cn from 'classnames';

import { useAccordionSpringAnimation } from '@alfalab/core-components-shared';
import { TypographyText } from '@alfalab/core-components-typography';

import { DefaultControlIcon } from './components';
Expand Down Expand Up @@ -86,6 +88,8 @@ export type AccordionProps = {
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;

animationVariant?: 'spring' | 'css';
} & AnchorHTMLAttributes<HTMLDivElement>;

export const Accordion: FC<AccordionProps> = ({
Expand All @@ -103,6 +107,7 @@ export const Accordion: FC<AccordionProps> = ({
onExpandedChange,
dataTestId,
bodyContentClassName,
animationVariant = 'css',
...rest
}) => {
const uncontrolled = expanded === undefined;
Expand All @@ -111,7 +116,17 @@ export const Accordion: FC<AccordionProps> = ({

const isStartPosition = controlPosition === 'start';

const [contentHeight, contentRef] = useMeasureHeight();
const [contentHeight, measureRef] = useMeasureHeight();
const bodyRef = useRef<HTMLDivElement>(null);
const contentAnimRef = useRef<HTMLDivElement | null>(null);

const contentRef = useCallback(
(el: HTMLDivElement | null) => {
contentAnimRef.current = el;
if (typeof measureRef === 'function') measureRef(el);
},
[measureRef],
);

const controlContent =
control === undefined ? (
Expand All @@ -136,13 +151,21 @@ export const Accordion: FC<AccordionProps> = ({
children
);

const { playEnter, playExit } = useAccordionSpringAnimation(bodyRef, contentAnimRef);

const handleExpandedChange = useCallback(() => {
if (uncontrolled) {
setExpanded(!isExpanded);
}

if (isExpanded) {
playExit();
} else {
playEnter();
}

onExpandedChange?.(!isExpanded);
}, [isExpanded, onExpandedChange, uncontrolled]);
}, [isExpanded, onExpandedChange, playEnter, playExit, uncontrolled]);

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
Expand Down Expand Up @@ -177,14 +200,24 @@ export const Accordion: FC<AccordionProps> = ({
</div>
</div>

<div
className={cn(styles.body, bodyClassName, { [styles.expandedBody]: isExpanded })}
style={{ height: isExpanded ? contentHeight : 0 }}
>
<div className={cn(styles.bodyContent, bodyContentClassName)} ref={contentRef}>
{bodyContent}
{animationVariant === 'spring' ? (
<div ref={bodyRef} className={cn(styles.spring, styles.container)}>
<div className={cn(styles.content)} ref={contentRef}>
{bodyContent}
</div>
</div>
</div>
) : (
<div
className={cn(styles.body, bodyClassName, {
[styles.expandedBody]: isExpanded,
})}
style={{ height: isExpanded ? contentHeight : 0 }}
>
<div className={cn(styles.bodyContent, bodyContentClassName)} ref={contentRef}>
{bodyContent}
</div>
</div>
)}
</div>
);
};
128 changes: 45 additions & 83 deletions packages/accordion/src/docs/description.mdx
Original file line number Diff line number Diff line change
@@ -1,96 +1,58 @@
## Анатомия
# Приведённый код и live-демо имеют демонстрационный характер и не являются финальной реализацией

Компонент состоит из только 3 слотов: Control - элемента управления, Header - основной элемент для отображения содержимого и Body - содержимое.
Слоты могут принимать в себя как готовые компоненты, так и кастомную вёрстку.
## CSS

```jsx live
<Accordion
className='accordion-wrapper'
header={<div className='accordion-header' />}
control={<div className='accordion-control' />}
>
<div className='accordion-body' />
</Accordion>
render(() => {
return (
<React.Fragment>
<Accordion
header='Когда лучше использовать аккордeон?'
className='accordion-container'
animationVariant={'css'}
>
Подходит для организации сложной информации в ограниченном пространстве,
представления большого объема данных, иерархической структуры с возможностью скрытия
и открытия разделов, удовлетворения потребностей пользователей в доступе к
информации по запросу и просмотра нескольких связанных разделов контента.
</Accordion>
</React.Fragment>
);
});
```

## Контрол
Control может быть в виде кнопки, иконки или любым другим интерактивным элементом, который инициирует раскрытие или сворачивание секции.
По умолчанию Control размещается сбоку справа от Header, но может располагаться и слева, что больше напоминает дерево.
## Spring

```jsx live

render(() => {
const [controlPosition, setControlPosition] = React.useState('end');

const handleControlPositionChange = React.useCallback((_, payload) => {
setControlPosition(payload.value);
}, []);

return <Space>
<Accordion
className='accordion-wrapper'
header={<div className='accordion-header' />}
controlPosition={controlPosition}
>
<div className='accordion-body' />
</Accordion>

<Accordion
className='accordion-wrapper'
header={<div className='accordion-header' />}
controlPosition={controlPosition}
>
<div className='accordion-body' />
</Accordion>

<Accordion
className='accordion-wrapper'
header={<div className='accordion-header' />}
controlPosition={controlPosition}
>
<div className='accordion-body' />
</Accordion>

<RadioGroup
label='Расположение контрола'
direction='vertical'
name='radioGroup'
onChange={handleControlPositionChange}
value={controlPosition}
>
<Radio size={24} label='Справа' value='end' />
<Radio size={24} label='Слева' value='start' />
</RadioGroup>
</Space>
})
return (
<React.Fragment>
<Accordion
header='Когда лучше использовать аккордeон?'
className='accordion-container'
animationVariant={'spring'}
>
Подходит для организации сложной информации в ограниченном пространстве,
представления большого объема данных, иерархической структуры с возможностью скрытия
и открытия разделов, удовлетворения потребностей пользователей в доступе к
информации по запросу и просмотра нескольких связанных разделов контента.
</Accordion>
</React.Fragment>
);
});
```

## Примеры
В качестве пресетов в компонент заложены текстовые контейнеры, как самый распространённый вариант использования.

## Stub
```jsx live

render(() => {
return <Space>
<Accordion
header='Зачем нужен аккордeон?'
className='accordion-container'
>
Используется для создания интерактивных списков,
которые можно разворачивать и сворачивать для
отображения дополнительной информации.
</Accordion>

<Accordion
header='Когда лучше использовать аккордeон?'
className='accordion-container'
>
Подходит для организации сложной информации в ограниченном пространстве,
представления большого объема данных, иерархической структуры с
возможностью скрытия и открытия разделов,
удовлетворения потребностей пользователей в доступе к информации
по запросу и просмотра нескольких связанных разделов контента.
</Accordion>
</Space>
})
return (
<React.Fragment>
<Gap size={64} />
<Gap size={64} />
<Gap size={64} />
<Gap size={64} />
<Gap size={64} />
</React.Fragment>
);
});
```
11 changes: 11 additions & 0 deletions packages/accordion/src/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@
visibility 0s linear 400ms;
}

.spring {
&.container {
overflow: hidden;
height: 0;

& .content {
padding-top: var(--gap-12);
}
}
}

.expandedBody {
visibility: visible;
height: auto;
Expand Down
13 changes: 7 additions & 6 deletions packages/backdrop/src/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,31 @@
bottom: var(--gap-0);
top: var(--gap-0);
left: var(--gap-0);
background-color: var(--backdrop-visible-background);
-webkit-tap-highlight-color: transparent; /* убирает хайлайт */
}

.appear,
.enter {
background-color: var(--backdrop-hidden-background);
opacity: 0;
}

.appearActive,
.enterActive,
.appearDone,
.enterDone {
background-color: var(--backdrop-visible-background);
transition: background-color 200ms ease-in;
opacity: 1;
transition: opacity 200ms ease-in;
}

.exit {
background-color: var(--backdrop-visible-background);
opacity: 1;
}

.exitActive,
.exitDone {
background-color: var(--backdrop-hidden-background);
transition: background-color 200ms ease-out;
opacity: 0;
transition: opacity 200ms ease-out;
}

.transparent.transparent {
Expand Down
Loading
Loading