-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core-components-fade): add fade
- Loading branch information
Showing
8 changed files
with
404 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "@alfalab/core-components-fade", | ||
"version": "1.0.0", | ||
"description": "Fade component", | ||
"keywords": [], | ||
"license": "ISC", | ||
"main": "dist/index.js", | ||
"module": "./dist/modern/index.js", | ||
"files": [ | ||
"dist" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"dependencies": { | ||
"classnames": "^2.2.6", | ||
"react-transition-group": "4.4.1" | ||
}, | ||
"devDependencies": { | ||
"@types/react-transition-group": "4.4.0" | ||
}, | ||
"peerDependencies": { | ||
"react": "^16.9.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks'; | ||
import { boolean, number } from '@storybook/addon-knobs'; | ||
import { Container } from 'storybook/blocks/grid'; | ||
import { ComponentHeader } from 'storybook/blocks/component-header'; | ||
|
||
import { Fade } from './Component'; | ||
import { version } from '../package.json'; | ||
|
||
<Meta | ||
title='Компоненты' | ||
component={Fade} | ||
parameters={{ 'theme-switcher': { themes: ['click'] } }} | ||
/> | ||
|
||
<!-- Canvas --> | ||
|
||
<Story name='Fade'> | ||
<Fade | ||
show={boolean('show', false)} | ||
delayTimeout={number('delayTimeout', 0)} | ||
> | ||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Officiis ducimus amet | ||
adipisci qui earum mollitia velit consectetur voluptate deserunt, quae tempora | ||
temporibus odit distinctio omnis quisquam saepe at molestias eum. | ||
</Fade> | ||
</Story> | ||
|
||
<!-- Docs --> | ||
|
||
<ComponentHeader | ||
name='Fade' | ||
version={version} | ||
package='@alfalab/core-components-fade' | ||
stage={1} | ||
/> | ||
|
||
```tsx | ||
import { Fade } from '@alfalab/core-components-fade'; | ||
``` | ||
Компонент `Fade`. Построен на `react-transition-group`. | ||
|
||
Используется в компоненте `Modal`. | ||
|
||
<Props of={Fade} /> | ||
|
||
<Preview> | ||
{React.createElement(() => { | ||
const [show, setShow] = React.useState(false); | ||
const handleClick = () => setShow(!show); | ||
return ( | ||
<Container> | ||
<button onClick={handleClick}>Click Me!</button> | ||
<Fade | ||
show={show} | ||
> | ||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Officiis ducimus amet | ||
adipisci qui earum mollitia velit consectetur voluptate deserunt, quae tempora | ||
temporibus odit distinctio omnis quisquam saepe at molestias eum. | ||
</Fade> | ||
</Container> | ||
); | ||
})} | ||
</Preview> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* eslint-disable import/no-extraneous-dependencies */ | ||
import React from 'react'; | ||
import { render } from '@testing-library/react'; | ||
|
||
import { Fade } from './index'; | ||
|
||
describe('Fade', () => { | ||
it('should use a children', () => { | ||
const text = 'Lorem ipsum'; | ||
const { getByText } = render(<Fade show={true}>{ text }</Fade>); | ||
|
||
expect(getByText(text)).toHaveTextContent(text); | ||
}); | ||
|
||
it('should use a className prop', () => { | ||
const className = 'test-class'; | ||
const { container } = render(<Fade show={true} className={className} />); | ||
|
||
expect(container.firstElementChild?.classList).toContain(className); | ||
}); | ||
|
||
it('should use a dataTestId prop', () => { | ||
const fadeTestId = 'fade-test-id'; | ||
const { getByTestId } = render(<Fade show={true} dataTestId={fadeTestId} />); | ||
|
||
expect(getByTestId(fadeTestId)).toHaveAttribute('data-test-id', fadeTestId); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import React, { ReactNode, forwardRef } from 'react'; | ||
import { Transition } from 'react-transition-group'; | ||
import { TransitionStatus } from 'react-transition-group/Transition'; | ||
import cn from 'classnames'; | ||
|
||
import { | ||
ComponentTransitionsProps, | ||
createTimeout, | ||
createTransitionStyles, | ||
reflow, | ||
useForkRef, | ||
} from './utils'; | ||
|
||
import styles from './index.module.css'; | ||
|
||
export type FadeProps = { | ||
/** | ||
* Управление видимостью элемента | ||
*/ | ||
show: boolean; | ||
|
||
/** | ||
* Дополнительный класс | ||
*/ | ||
className?: string; | ||
|
||
/** | ||
* Управление поведением фокуса | ||
*/ | ||
tabIndex?: number; | ||
|
||
/** | ||
* Контент | ||
*/ | ||
children?: ReactNode; | ||
|
||
/** | ||
* Идентификатор для систем автоматизированного тестирования | ||
*/ | ||
dataTestId?: string; | ||
} & ComponentTransitionsProps; | ||
|
||
/** | ||
* Заимствовано из [MUI](https://material-ui.com/) | ||
*/ | ||
export const Fade = forwardRef<Element, FadeProps>((props, ref) => { | ||
const { | ||
appear = false, | ||
children, | ||
className, | ||
dataTestId, | ||
delayTimeout = 0, | ||
show, | ||
onEnter, | ||
onExit, | ||
onExited, | ||
timeout = 500, | ||
tabIndex, | ||
} = props; | ||
|
||
const nodeRef = React.useRef(null); | ||
const foreignRef = useForkRef((children as { ref: React.Ref<typeof children> })?.ref, ref); | ||
const handleRef = useForkRef(nodeRef, foreignRef); | ||
|
||
const handleEnter = (node: HTMLElement, isAppearing: boolean) => { | ||
const element = node; | ||
reflow(element); // Для гарантии воспроизведения анимации с ее начала. | ||
|
||
const { duration, delay } = createTransitionStyles({ timeout, delayTimeout }, 'enter'); | ||
|
||
element.style.transitionDuration = duration; | ||
element.style.transitionDelay = delay; | ||
|
||
if (onEnter) { | ||
onEnter(element, isAppearing); | ||
} | ||
}; | ||
|
||
const handleExit = (node: HTMLElement) => { | ||
const element = node; | ||
const { duration, delay } = createTransitionStyles({ timeout, delayTimeout }, 'exit'); | ||
|
||
element.style.transitionDuration = duration; | ||
element.style.transitionDelay = delay; | ||
|
||
if (onExit) { | ||
onExit(element); | ||
} | ||
}; | ||
|
||
const handleExited = (node: HTMLElement) => { | ||
if (onExited) { | ||
onExited(node); | ||
} | ||
}; | ||
|
||
return ( | ||
<Transition | ||
appear={appear} | ||
in={show} | ||
timeout={createTimeout({ timeout, delayTimeout })} | ||
onEnter={handleEnter} | ||
onExit={handleExit} | ||
onExited={handleExited} | ||
> | ||
{ (status: string) => ( | ||
<div | ||
className={cn( | ||
styles.fade, | ||
{ [styles.hidden]: status === 'exited' && !show }, | ||
(styles as { [status: string]: TransitionStatus })[status], | ||
className, | ||
)} | ||
data-test-id={dataTestId} | ||
tabIndex={tabIndex} | ||
> | ||
{ | ||
React.isValidElement(children) | ||
? React.cloneElement(children, { ref: handleRef }) | ||
: children | ||
} | ||
</div> | ||
) } | ||
</Transition> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
.fade { | ||
transition-property: opacity; | ||
transition-timing-function: ease-out; | ||
} | ||
|
||
.hidden { | ||
display: none; | ||
} | ||
|
||
.exited { | ||
opacity: 0; | ||
} | ||
|
||
.entering { | ||
opacity: 1; | ||
} | ||
|
||
.entered { | ||
opacity: 1; | ||
} | ||
|
||
.exiting { | ||
opacity: 0; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Component'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import React from 'react'; | ||
import { | ||
TransitionProps, | ||
TransitionActions, | ||
EnterHandler, | ||
ExitHandler, | ||
} from 'react-transition-group/Transition'; | ||
|
||
// TODO: перенести в alfalab/utils | ||
|
||
/** | ||
* Типы для свойств компонентов с анимацией | ||
*/ | ||
export type ComponentTransitionsProps<RefElement extends undefined | HTMLElement = undefined> = { | ||
/** Таймауты задержек */ | ||
delayTimeout?: number | Timeout; | ||
|
||
/** | ||
* Колбэк, выполнеяемый перед назначением статуса `entering`. | ||
* Дополнительный парметр `isAppearing` необходим для того, чтобы определить, | ||
* выполняется ли `enter`-фаза при первичном маунте компонента. | ||
*/ | ||
onEnter?: EnterHandler<RefElement>; | ||
|
||
/** | ||
* Колбэк, выполнеяемый перед назначением статуса `exiting`. | ||
*/ | ||
onExit?: ExitHandler<RefElement>; | ||
|
||
/** | ||
* Колбэк, выполнеяемый после назначения статуса `exited`. | ||
*/ | ||
onExited?: ExitHandler<RefElement>; | ||
} & Partial<Pick<TransitionProps<RefElement>, 'timeout'>> & Pick<TransitionActions, 'appear'>; | ||
|
||
type Timeout = { enter?: number; exit?: number }; | ||
type TransitionMode = 'enter' | 'exit'; | ||
|
||
function formatMs(milliseconds: number) { | ||
return `${Math.round(milliseconds)}ms`; | ||
} | ||
|
||
function convertTransitionProp(prop?: Timeout, mode: TransitionMode = 'enter') { | ||
return prop ? prop[mode] || 0 : 0; | ||
} | ||
|
||
function getTransitionProp(prop?: Timeout | number, mode?: TransitionMode) { | ||
return typeof prop === 'number' ? prop : convertTransitionProp(prop, mode); | ||
} | ||
|
||
function getFullTimeout( | ||
{ timeout, delayTimeout }: ComponentTransitionsProps, | ||
mode: TransitionMode, | ||
) { | ||
return getTransitionProp(timeout, mode) + getTransitionProp(delayTimeout, mode); | ||
} | ||
|
||
/** | ||
* Создаёт стили для анимации | ||
*/ | ||
export const createTransitionStyles = ( | ||
{ timeout, delayTimeout }: ComponentTransitionsProps, | ||
mode: TransitionMode, | ||
) => ({ | ||
duration: formatMs(getTransitionProp(timeout, mode)), | ||
delay: formatMs(getTransitionProp(delayTimeout, mode)), | ||
}); | ||
|
||
/** | ||
* Создаёт таймауты для Transition c учётом задержки | ||
*/ | ||
export const createTimeout = (props: ComponentTransitionsProps) => { | ||
const { timeout } = props; | ||
const enter = getTransitionProp(timeout); | ||
|
||
return { | ||
appear: timeout === 'object' ? timeout.appear : enter, | ||
enter, | ||
exit: getFullTimeout(props, 'exit'), | ||
}; | ||
}; | ||
|
||
export const reflow = (node: HTMLElement) => node.scrollTop; | ||
|
||
/** | ||
* passes {value} to {ref} | ||
* | ||
* WARNING: Be sure to only call this inside a callback that is passed as a ref. | ||
* Otherwise make sure to cleanup previous {ref} if it changes. See | ||
* https://github.com/mui-org/material-ui/issues/13539 | ||
* | ||
* useful if you want to expose the ref of an inner component to the public api | ||
* while still using it inside the component | ||
* | ||
* @param ref a ref callback or ref object if anything falsy this is a no-op | ||
* @param value | ||
*/ | ||
export function setRef<T>( | ||
ref: React.RefObject<T> | ((instance: T | null) => void) | null | undefined, | ||
value: T | null, | ||
): void { | ||
if (typeof ref === 'function') { | ||
ref(value); | ||
} else if (ref) { | ||
// eslint-disable-next-line no-param-reassign | ||
(ref as React.MutableRefObject<T | null>).current = value; | ||
} | ||
} | ||
|
||
export function useForkRef<T>(refA: React.Ref<T>, refB: React.Ref<T>): React.Ref<T> { | ||
/** | ||
* This will create a new function if the ref props change and are defined. | ||
* This means react will call the old forkRef with `null` and the new forkRef | ||
* with the ref. Cleanup naturally emerges from this behavior | ||
*/ | ||
return React.useMemo(() => { | ||
if (refA == null && refB == null) { | ||
return null; | ||
} | ||
return (refValue) => { | ||
setRef(refA as React.MutableRefObject<T>, refValue); | ||
setRef(refB as React.MutableRefObject<T>, refValue); | ||
}; | ||
}, [refA, refB]); | ||
} |
Oops, something went wrong.