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

refactor(Hint): transfer to functional component & refactor tests/stories #2715

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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
249 changes: 98 additions & 151 deletions packages/react-ui/components/Hint/Hint.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import React from 'react';
import React, { useContext } from 'react';
import propTypes from 'prop-types';

import { forwardRefAndName } from '../../lib/forwardRefAndName';
import { withClassWrapper } from '../../lib/withClassWrapper';
import { ThemeContext } from '../../lib/theming/ThemeContext';
import { ThemeFactory } from '../../lib/theming/ThemeFactory';
import { Theme } from '../../lib/theming/Theme';
import { Popup, PopupPositionsType } from '../../internal/Popup';
import { Nullable } from '../../typings/utility-types';
import { MouseEventType } from '../../typings/event-types';
import { isTestEnv } from '../../lib/currentEnvironment';
import { CommonWrapper, CommonProps } from '../../internal/CommonWrapper';
import { cx } from '../../lib/theming/Emotion';
import { rootNode, TSetRootNode } from '../../lib/rootNode';
import { CommonProps } from '../../internal/CommonWrapper';

import { styles } from './Hint.styles';
import { HintContent } from './HintContent';
import { getPositions } from './getPositions';
import { useDelayDisplaying } from './useDelayDisplaying';

const HINT_BORDER_COLOR = 'transparent';

export interface HintProps extends CommonProps {
type HintInterface = {
children?: React.ReactNode;
/**
* Переводит отображение подсказки в _"ручной режим"_.
Expand Down Expand Up @@ -54,20 +53,18 @@ export interface HintProps extends CommonProps {
/**
* Отключает анимацию.
*/
disableAnimations: boolean;
disableAnimations?: boolean;
/**
* Явно указывает, что вложенные элементы должны быть обёрнуты в `<span/>`. <br/> Используется для корректного позиционирования тултипа при двух и более вложенных элементах.
*
* _Примечание_: при **двух и более** вложенных элементах обёртка будет добавлена автоматически.
*/
useWrapper: boolean;
}
useWrapper?: boolean;
};

export interface HintState {
opened: boolean;
}
export type HintProps = HintInterface & CommonProps;

const Positions: PopupPositionsType[] = [
const positions: PopupPositionsType[] = [
'top center',
'top left',
'top right',
Expand All @@ -82,140 +79,90 @@ const Positions: PopupPositionsType[] = [
'right bottom',
];

export const DEFAULT_POSITION = 'top';
export const DEFAULT_MAX_WIDTH = 200;
export const DISPLAY_DELAY = 400;

const HintFC = forwardRefAndName<HTMLDivElement, HintProps>('HintFC', (props, ref) => {
const {
children,
disableAnimations = isTestEnv,
useWrapper = false,
manual = false,
text,
opened = false,
pos = DEFAULT_POSITION,
maxWidth = DEFAULT_MAX_WIDTH,
onMouseEnter,
onMouseLeave,
...rest
} = props;

const theme = useContext(ThemeContext);
const { isOpen, handleMouseEnter, handleMouseLeave } = useDelayDisplaying(manual, opened, onMouseLeave, onMouseEnter);

return (
// TODO: Pass `ref` down to `Popup` when it'll be possible
<ThemeContext.Provider
value={ThemeFactory.create(
{
popupPinOffset: theme.hintPinOffset,
popupMargin: theme.hintMargin,
popupBorder: theme.hintBorder,
popupBorderRadius: theme.hintBorderRadius,
},
theme,
)}
>
<Popup
hasPin
opened={!!isOpen}
anchorElement={children}
positions={getPositions(positions, pos)}
backgroundColor={theme.hintBgColor}
borderColor="transparent"
disableAnimations={disableAnimations}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
useWrapper={useWrapper}
{...rest}
>
<HintContent text={text} pos={pos} maxWidth={maxWidth} />
</Popup>
</ThemeContext.Provider>
);
});

HintFC.propTypes = {
children: propTypes.node,
manual: propTypes.bool,
maxWidth: propTypes.oneOfType([propTypes.string, propTypes.number]),
opened: propTypes.bool,
pos: propTypes.oneOf<HintProps['pos']>([
'top',
'right',
'bottom',
'left',
'top left',
'top center',
'top right',
'right top',
'right middle',
'right bottom',
'bottom right',
'bottom center',
'bottom left',
'left bottom',
'left middle',
'left top',
]).isRequired,
text: propTypes.node.isRequired,
disableAnimations: propTypes.bool,
useWrapper: propTypes.bool,
};

/**
* Всплывающая подсказка, которая по умолчанию отображается при наведении на элемент. <br/> Можно задать другие условия отображения.
*/
@rootNode
export class Hint extends React.PureComponent<HintProps, HintState> {
public static __KONTUR_REACT_UI__ = 'Hint';

public static defaultProps = {
pos: 'top',
manual: false,
opened: false,
maxWidth: 200,
disableAnimations: isTestEnv,
useWrapper: false,
};

public state: HintState = {
opened: this.props.manual ? !!this.props.opened : false,
};

private timer: Nullable<number> = null;
private theme!: Theme;
private setRootNode!: TSetRootNode;

public componentDidUpdate(prevProps: HintProps) {
if (!this.props.manual) {
return;
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.props.opened !== prevProps.opened) {
this.setState({ opened: !!this.props.opened });
}
}

public componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}

public render() {
return (
<ThemeContext.Consumer>
{(theme) => {
this.theme = theme;
return (
<ThemeContext.Provider
value={ThemeFactory.create(
{
popupPinOffset: theme.hintPinOffset,
popupMargin: theme.hintMargin,
popupBorder: theme.hintBorder,
popupBorderRadius: theme.hintBorderRadius,
},
this.theme,
)}
>
{this.renderMain()}
</ThemeContext.Provider>
);
}}
</ThemeContext.Consumer>
);
}

public renderMain() {
return (
<CommonWrapper rootNodeRef={this.setRootNode} {...this.props}>
<Popup
hasPin
opened={this.state.opened}
anchorElement={this.props.children}
positions={this.getPositions()}
backgroundColor={this.theme.hintBgColor}
borderColor={HINT_BORDER_COLOR}
disableAnimations={this.props.disableAnimations}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
useWrapper={this.props.useWrapper}
>
{this.renderContent()}
</Popup>
</CommonWrapper>
);
}

private renderContent() {
if (!this.props.text) {
return null;
}

const { pos, maxWidth } = this.props;
const className = cx({
[styles.content(this.theme)]: true,
[styles.contentCenter(this.theme)]: pos === 'top' || pos === 'bottom',
});
return (
<div className={className} style={{ maxWidth }}>
{this.props.text}
</div>
);
}

private getPositions = (): PopupPositionsType[] => {
return Positions.filter((x) => x.startsWith(this.props.pos));
};

private handleMouseEnter = (e: MouseEventType) => {
if (!this.props.manual && !this.timer) {
this.timer = window.setTimeout(this.open, 400);
}

if (this.props.onMouseEnter) {
this.props.onMouseEnter(e);
}
};

private handleMouseLeave = (e: MouseEventType) => {
if (!this.props.manual && this.timer) {
clearTimeout(this.timer);
this.timer = null;
this.setState({ opened: false });
}

if (this.props.onMouseLeave) {
this.props.onMouseLeave(e);
}
};

private open = () => {
this.setState({ opened: true });
};
}
export const Hint = withClassWrapper(HintFC);
export type Hint = InstanceType<typeof Hint>;
29 changes: 29 additions & 0 deletions packages/react-ui/components/Hint/HintContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useContext } from 'react';

import { cx } from '../../lib/theming/Emotion';
import { ThemeContext } from '../../lib/theming/ThemeContext';

import { HintProps } from './Hint';
import { styles } from './Hint.styles';

export type HintContentProps = Pick<HintProps, 'text' | 'pos' | 'maxWidth'>;

export const HintContent = ({ text, pos, maxWidth }: HintContentProps) => {
const theme = useContext(ThemeContext);

if (!text) {
return null;
}

return (
<div
className={cx({
[styles.content(theme)]: true,
[styles.contentCenter(theme)]: pos === 'top' || pos === 'bottom',
})}
style={{ maxWidth }}
>
{text}
</div>
);
};
Loading