diff --git a/src/components/Control/Control.tsx b/src/components/Control/Control.tsx
index 8ee101bf..11960ad5 100644
--- a/src/components/Control/Control.tsx
+++ b/src/components/Control/Control.tsx
@@ -1,9 +1,9 @@
-import React, {useCallback, useState, useRef} from 'react';
+import {Button, Popup} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {Popup, Button} from '@gravity-ui/uikit';
+import React, {forwardRef, useCallback, useImperativeHandle, useRef} from 'react';
+import {PopperPosition, usePopupState} from '../../hooks';
import {ControlSizes} from '../../models';
-import {PopperPosition} from '../../hooks';
import './Control.scss';
@@ -30,23 +30,21 @@ const ICONS_SIZES = {
[ControlSizes.L]: 20,
};
-const Control = (props: ControlProps) => {
+const Control = forwardRef((props: ControlProps, ref) => {
const {
onClick,
className,
tooltipText,
isVerticalView,
- setRef,
size = ControlSizes.M,
icon,
popupPosition,
} = props;
const controlRef = useRef
(null);
- const [isVisibleTooltip, setIsVisibleTooltip] = useState(false);
- const showTooltip = () => setIsVisibleTooltip(true);
- const hideTooltip = () => setIsVisibleTooltip(false);
+ const popupState = usePopupState({autoclose: 3000});
+
const getTooltipAlign = useCallback(() => {
if (popupPosition) {
return popupPosition;
@@ -54,16 +52,8 @@ const Control = (props: ControlProps) => {
return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
}, [isVerticalView, popupPosition]);
- const _setRef = useCallback(
- (ref: HTMLButtonElement) => {
- controlRef.current = ref;
-
- if (setRef) {
- setRef(ref);
- }
- },
- [setRef],
- );
+
+ useImperativeHandle(ref, () => controlRef.current, [controlRef]);
const position = getTooltipAlign();
const Icon = icon;
@@ -74,9 +64,9 @@ const Control = (props: ControlProps) => {
-
- {tooltipText}
-
+ {controlRef.current && (
+
+ {tooltipText}
+
+ )}
);
-};
+});
Control.displayName = 'DCControl';
diff --git a/src/components/Controls/Controls.tsx b/src/components/Controls/Controls.tsx
index 6d350b6e..08326215 100644
--- a/src/components/Controls/Controls.tsx
+++ b/src/components/Controls/Controls.tsx
@@ -1,28 +1,27 @@
-import React from 'react';
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
+import React, {memo, useContext} from 'react';
-import {Control} from '../Control';
+import {FeedbackSendData, Lang, SubscribeData, TextSizes, Theme} from '../../models';
import {Feedback, FeedbackView} from '../Feedback';
import {Subscribe, SubscribeView} from '../Subscribe';
+
+import './Controls.scss';
+import {ControlsLayoutContext} from './ControlsLayout';
+
import {
+ DividerControl,
+ EditControl,
FullScreenControl,
- SettingsControl,
- SinglePageControl,
LangControl,
- DividerControl,
PdfControl,
+ SettingsControl,
+ SinglePageControl,
} from './';
-import {PopperPosition} from '../../hooks';
-import {Lang, TextSizes, Theme, FeedbackSendData, ControlSizes, SubscribeData} from '../../models';
-import EditIcon from '@gravity-ui/icons/svgs/pencil.svg';
-import './Controls.scss';
-
const b = block('dc-controls');
export interface ControlsProps {
- lang: Lang;
+ lang?: Lang;
langs?: string[];
fullScreen?: boolean;
singlePage?: boolean;
@@ -32,10 +31,8 @@ export interface ControlsProps {
textSize?: TextSizes;
vcsUrl?: string;
vcsType?: string;
- showEditControl?: boolean;
isLiked?: boolean;
isDisliked?: boolean;
- dislikeVariants?: string[];
onChangeLang?: (lang: Lang) => void;
onChangeFullScreen?: (value: boolean) => void;
onChangeSinglePage?: (value: boolean) => void;
@@ -47,213 +44,137 @@ export interface ControlsProps {
onSubscribe?: (data: SubscribeData) => void;
pdfLink?: string;
className?: string;
- isVerticalView?: boolean;
- controlSize?: ControlSizes;
+ hideEditControl?: boolean;
hideFeedbackControls?: boolean;
- popupPosition?: PopperPosition;
-}
-
-type ControlsInnerProps = ControlsProps & WithTranslation & WithTranslationProps;
-
-class Controls extends React.Component {
- componentDidUpdate(prevProps: ControlsProps) {
- const {i18n, lang} = this.props;
-
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
-
- render() {
- const {lang, i18n, className, isVerticalView} = this.props;
-
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
- return (
-
- {this.renderCommonControls()}
- {this.renderEditLink()}
- {this.renderFeedbackControls()}
- {this.renderSubscribeControls()}
-
- );
- }
-
- private renderEditLink() {
- const {
- vcsUrl,
- vcsType,
- showEditControl,
- singlePage,
- isVerticalView,
- controlSize,
- popupPosition,
- t,
- } = this.props;
-
- if (!showEditControl || singlePage) {
- return null;
- }
-
- return (
-
-
-
-
-
-
- );
- }
-
- private renderCommonControls() {
- const {
- fullScreen,
- singlePage,
- theme,
- wideFormat,
- showMiniToc,
- textSize,
- onChangeFullScreen,
- onChangeTheme,
- onChangeShowMiniToc,
- onChangeTextSize,
- onChangeWideFormat,
- onChangeLang,
- onChangeSinglePage,
- isVerticalView,
- controlSize,
- lang,
- langs,
- popupPosition,
- pdfLink,
- } = this.props;
-
- return (
-
-
-
-
-
-
-
- );
- }
-
- private renderFeedbackControls = () => {
- const {
- lang,
- singlePage,
- onSendFeedback,
- isLiked,
- isDisliked,
- dislikeVariants,
- isVerticalView,
- hideFeedbackControls,
- popupPosition,
- } = this.props;
-
- if (singlePage || !onSendFeedback || hideFeedbackControls) {
- return null;
- }
-
- return (
-
-
-
-
- );
- };
-
- private renderSubscribeControls = () => {
- const {lang, singlePage, onSubscribe, isVerticalView, popupPosition} = this.props;
-
- if (singlePage || !onSubscribe) {
- return null;
- }
-
- return (
-
-
-
-
- );
- };
}
-export default withTranslation('controls')(Controls);
+type Defined = {
+ [P in keyof ControlsProps]-?: ControlsProps[P];
+};
+
+const Controls = memo((props) => {
+ const {isVerticalView} = useContext(ControlsLayoutContext);
+ const {
+ className,
+ fullScreen,
+ singlePage,
+ theme,
+ wideFormat,
+ showMiniToc,
+ hideEditControl,
+ hideFeedbackControls,
+ textSize,
+ onChangeFullScreen,
+ onChangeTheme,
+ onChangeShowMiniToc,
+ onChangeTextSize,
+ onChangeWideFormat,
+ onChangeLang,
+ onChangeSinglePage,
+ onSendFeedback,
+ onSubscribe,
+ lang,
+ langs,
+ pdfLink,
+ vcsUrl,
+ vcsType,
+ isLiked,
+ isDisliked,
+ } = props;
+
+ const withFullscreenControl = Boolean(onChangeFullScreen);
+ const withSettingsControl = Boolean(
+ onChangeWideFormat || onChangeTheme || onChangeShowMiniToc || onChangeTextSize,
+ );
+ const withLangControl = Boolean(lang && onChangeLang);
+ const withSinglePageControl = Boolean(onChangeSinglePage);
+ const withPdfControl = Boolean(pdfLink);
+ const withEditControl = Boolean(!singlePage && !hideEditControl && vcsUrl);
+ const withFeedbackControl = Boolean(!singlePage && !hideFeedbackControls && onSendFeedback);
+ const withSubscribeControls = Boolean(!singlePage && onSubscribe);
+
+ const controls = [
+ withFullscreenControl && (
+
+ ),
+ withSettingsControl && (
+
+ ),
+ withLangControl && (
+
+ ),
+ withSinglePageControl && (
+
+ ),
+ withPdfControl && ,
+ '---',
+ withEditControl && (
+
+ ),
+ '---',
+ withFeedbackControl && (
+
+ ),
+ '---',
+ withSubscribeControls && (
+
+ ),
+ ]
+ .filter(Boolean)
+ .reduce((result, control, index, array) => {
+ if (control === '---') {
+ if (array[index - 1] && array[index + 1] && array[index + 1] !== '---') {
+ result.push(
+ ,
+ );
+ }
+ } else {
+ result.push(control as React.ReactElement);
+ }
+
+ return result;
+ }, [] as React.ReactElement[]);
+
+ return {controls}
;
+});
+
+Controls.displayName = 'DCControls';
+
+export default Controls;
diff --git a/src/components/Controls/ControlsLayout.tsx b/src/components/Controls/ControlsLayout.tsx
new file mode 100644
index 00000000..0b9e8004
--- /dev/null
+++ b/src/components/Controls/ControlsLayout.tsx
@@ -0,0 +1,42 @@
+import block from 'bem-cn-lite';
+import React, {PropsWithChildren, createContext} from 'react';
+
+import {PopperPosition} from '../../hooks';
+import {ControlSizes} from '../../models';
+
+type ControlsLayoutProps = {
+ isVerticalView?: boolean;
+ controlClassName?: string;
+ controlSize?: ControlSizes;
+ popupPosition?: PopperPosition;
+};
+
+export const ControlsLayoutContext = createContext({
+ controlClassName: '',
+ isVerticalView: false,
+ controlSize: ControlSizes.M,
+ popupPosition: PopperPosition.BOTTOM_END,
+});
+
+const b = block('dc-controls');
+
+export const ControlsLayout: React.FC> = ({
+ isVerticalView,
+ controlClassName,
+ controlSize,
+ popupPosition,
+ children,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Controls/index.ts b/src/components/Controls/index.ts
index 362e8750..92651153 100644
--- a/src/components/Controls/index.ts
+++ b/src/components/Controls/index.ts
@@ -1,3 +1,4 @@
export * from './Controls';
export {default as Controls} from './Controls';
export * from './single-controls';
+export {ControlsLayout} from './ControlsLayout';
diff --git a/src/components/Controls/single-controls/DividerControl/DividerControl.tsx b/src/components/Controls/single-controls/DividerControl/DividerControl.tsx
index 9c58c159..830f955a 100644
--- a/src/components/Controls/single-controls/DividerControl/DividerControl.tsx
+++ b/src/components/Controls/single-controls/DividerControl/DividerControl.tsx
@@ -1,24 +1,20 @@
-import React from 'react';
import cn from 'bem-cn-lite';
+import React, {useContext} from 'react';
-import {ControlSizes} from '../../../../models';
+import {ControlsLayoutContext} from '../../ControlsLayout';
import './DividerControl.scss';
const b = cn('dc-divider-control');
interface DividerControlProps {
- size?: ControlSizes;
className?: string;
- isVerticalView?: boolean;
}
-const DividerControl = ({
- size = ControlSizes.M,
- className,
- isVerticalView = true,
-}: DividerControlProps) => {
- return ;
+const DividerControl: React.FC = ({className}) => {
+ const {isVerticalView, controlSize} = useContext(ControlsLayoutContext);
+
+ return ;
};
export default DividerControl;
diff --git a/src/components/Controls/single-controls/EditControl.tsx b/src/components/Controls/single-controls/EditControl.tsx
new file mode 100644
index 00000000..1eaf1ea5
--- /dev/null
+++ b/src/components/Controls/single-controls/EditControl.tsx
@@ -0,0 +1,58 @@
+import EditIcon from '@gravity-ui/icons/svgs/pencil.svg';
+import {Button, Icon} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {memo, useContext} from 'react';
+
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {ControlsLayoutContext} from '../ControlsLayout';
+
+interface EditControlProps {
+ vcsUrl: string;
+ vcsType?: string;
+ view?: string;
+ className?: string;
+}
+
+const b = block('dc-controls');
+
+const EditControl = memo(({vcsUrl, vcsType = 'github', view, className}) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(ControlsLayoutContext);
+
+ if (view === 'wide') {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+});
+
+EditControl.displayName = 'EditControl';
+
+export default EditControl;
diff --git a/src/components/Controls/single-controls/FullScreenControl.tsx b/src/components/Controls/single-controls/FullScreenControl.tsx
index df913e4b..2650652c 100644
--- a/src/components/Controls/single-controls/FullScreenControl.tsx
+++ b/src/components/Controls/single-controls/FullScreenControl.tsx
@@ -1,27 +1,22 @@
-import React, {useCallback, useEffect} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Icon as IconComponent} from '@gravity-ui/uikit';
+import FullScreenClickedIcon from '@gravity-ui/icons/svgs/square-dashed-circle.svg';
+import FullScreenIcon from '@gravity-ui/icons/svgs/square-dashed.svg';
+import {Icon} from '@gravity-ui/uikit';
+import React, {memo, useCallback, useContext, useEffect} from 'react';
+import {useTranslation} from '../../../hooks';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
-
-import FullScreenClickedIcon from '../../../../assets/icons/full-screen-clicked.svg';
-import FullScreenIcon from '@gravity-ui/icons/svgs/square-dashed.svg';
+import {ControlsLayoutContext} from '../ControlsLayout';
interface ControlProps {
- lang: Lang;
value?: boolean;
onChange?: (value: boolean) => void;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- popupPosition?: PopperPosition;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const FullScreenControl = (props: ControlInnerProps) => {
- const {className, isVerticalView, size, value, onChange, lang, popupPosition, i18n, t} = props;
+const FullScreenControl = memo((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(ControlsLayoutContext);
+ const {value, onChange} = props;
const onClick = useCallback(() => {
if (onChange) {
@@ -46,29 +41,23 @@ const FullScreenControl = (props: ControlInnerProps) => {
};
}, [onKeyDown]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
const activeMode = value ? 'enabled' : 'disabled';
- if (!onChange) {
- return null;
- }
-
return (
(
-
+
)}
popupPosition={popupPosition}
/>
);
-};
+});
+
+FullScreenControl.displayName = 'FullScreenControl';
-export default withTranslation('controls')(FullScreenControl);
+export default FullScreenControl;
diff --git a/src/components/Controls/single-controls/LangControl.tsx b/src/components/Controls/single-controls/LangControl.tsx
index 8fe65c8a..f09ecc1a 100644
--- a/src/components/Controls/single-controls/LangControl.tsx
+++ b/src/components/Controls/single-controls/LangControl.tsx
@@ -1,18 +1,22 @@
-import React, {useCallback, useEffect, useState, useRef} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import allLangs from 'langs';
-import {Popup, Icon as IconComponent, List} from '@gravity-ui/uikit';
+import LangIcon from '@gravity-ui/icons/svgs/globe.svg';
+import {Icon, List, Popup} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
+import allLangs from 'langs';
+import React, {useCallback, useContext, useMemo, useRef} from 'react';
+import {usePopupState, useTranslation} from '../../../hooks';
+import {Lang} from '../../../models';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
-import {getPopupPosition} from './utils';
-import {PopperPosition} from '../../../hooks';
-
-import LangIcon from '@gravity-ui/icons/svgs/globe.svg';
-
import '../Controls.scss';
+import {ControlsLayoutContext} from '../ControlsLayout';
+
+import {getPopupPosition} from './utils';
+const ICONS: Record = {
+ en: '🇬🇧',
+ ru: '🇷🇺',
+};
+const DEFAULT_LANGS = ['en', 'ru'];
const LEGACY_LANG_ITEMS = [
{value: Lang.En, text: 'English', icon: '🇬🇧'},
{value: Lang.Ru, text: 'Русский', icon: '🇷🇺'},
@@ -23,11 +27,7 @@ const b = block('dc-controls');
interface ControlProps {
lang: Lang;
langs?: string[];
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- onChangeLang?: (lang: Lang) => void;
- popupPosition?: PopperPosition;
+ onChangeLang: (lang: Lang) => void;
}
interface ListItem {
@@ -38,37 +38,16 @@ interface ListItem {
const LIST_ITEM_HEIGHT = 36;
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const LangControl = (props: ControlInnerProps) => {
- const {
- className,
- isVerticalView,
- size,
- lang,
- langs = [],
- i18n,
- onChangeLang,
- popupPosition,
- t,
- } = props;
+const LangControl = (props: ControlProps) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(ControlsLayoutContext);
+ const {lang, langs = DEFAULT_LANGS, onChangeLang} = props;
- const [langItems, setLangItems] = useState(LEGACY_LANG_ITEMS);
const controlRef = useRef(null);
- const [isVisiblePopup, setIsVisiblePopup] = useState(false);
- const showPopup = () => setIsVisiblePopup(true);
- const hidePopup = () => setIsVisiblePopup(false);
- const _onChangeLang = useCallback(
- (value: Lang) => {
- if (onChangeLang) {
- onChangeLang(value);
- }
- },
- [onChangeLang],
- );
-
- useEffect(() => {
+ const popupState = usePopupState();
+ const langItems = useMemo(() => {
const preparedLangs = langs
.map((code) => {
const langData = allLangs.where('1', code);
@@ -77,29 +56,28 @@ const LangControl = (props: ControlInnerProps) => {
? {
text: langData.name,
value: langData['1'],
+ icon: ICONS[code] || '',
}
: undefined;
})
.filter(Boolean) as ListItem[];
- if (preparedLangs.length) {
- setLangItems(preparedLangs);
- } else {
- setLangItems(LEGACY_LANG_ITEMS);
- }
+ return preparedLangs.length ? preparedLangs : LEGACY_LANG_ITEMS;
}, [langs]);
-
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- const setRef = useCallback((ref: HTMLButtonElement) => {
- controlRef.current = ref;
+ const renderItem = useCallback((item: ListItem) => {
+ return (
+
+
{item.icon}
+ {item.text}
+
+ );
}, []);
-
- if (!onChangeLang) {
- return null;
- }
+ const onItemClick = useCallback(
+ (item: ListItem) => {
+ onChangeLang(item.value as Lang);
+ },
+ [onChangeLang],
+ );
const itemsHeight = LIST_ITEM_HEIGHT * langItems.length;
const selectedItemIndex = langItems.findIndex(({value}) => value === lang);
@@ -107,42 +85,36 @@ const LangControl = (props: ControlInnerProps) => {
return (
}
- setRef={setRef}
+ icon={(args) => }
popupPosition={popupPosition}
/>
-
- {
- _onChangeLang(item.value as Lang);
- }}
- selectedItemIndex={selectedItemIndex}
- itemHeight={LIST_ITEM_HEIGHT}
- itemsHeight={itemsHeight}
- renderItem={(item) => {
- return (
-
-
{item.icon}
{item.text}
-
- );
- }}
- />
-
+ {popupState.visible && (
+
+
+
+ )}
);
};
-export default withTranslation('controls')(LangControl);
+export default LangControl;
diff --git a/src/components/Controls/single-controls/PdfControl.tsx b/src/components/Controls/single-controls/PdfControl.tsx
index 1b5a4acb..3090abc8 100644
--- a/src/components/Controls/single-controls/PdfControl.tsx
+++ b/src/components/Controls/single-controls/PdfControl.tsx
@@ -1,47 +1,35 @@
-import React, {useEffect} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Icon as IconComponent} from '@gravity-ui/uikit';
-
-import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
-import {PopperPosition} from '../../../hooks';
+import {Icon} from '@gravity-ui/uikit';
+import React, {memo, useContext} from 'react';
import PdfIcon from '../../../../assets/icons/pdf.svg';
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {ControlsLayoutContext} from '../ControlsLayout';
interface ControlProps {
- lang: Lang;
- pdfLink?: string;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- popupPosition?: PopperPosition;
+ pdfLink: string;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const PdfControl = (props: ControlInnerProps) => {
- const {className, isVerticalView, size, pdfLink, lang, i18n, popupPosition, t} = props;
-
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- if (!pdfLink) {
- return null;
- }
+const PdfControl = memo((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(ControlsLayoutContext);
+ const {pdfLink} = props;
return (
}
+ icon={(args) => }
popupPosition={popupPosition}
/>
);
-};
+});
+
+PdfControl.displayName = 'PdfControl';
-export default withTranslation('controls')(PdfControl);
+export default PdfControl;
diff --git a/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx b/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx
index 7a04f6e3..b1e180c4 100644
--- a/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx
+++ b/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx
@@ -1,16 +1,14 @@
-import React, {useCallback, useEffect, useState, useRef} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
+import SettingsIcon from '@gravity-ui/icons/svgs/gear.svg';
+import {Button, Icon, List, Popup, Switch} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';
-import {Button, Popup, Switch, List, Icon as IconComponent} from '@gravity-ui/uikit';
+import React, {ReactElement, useCallback, useContext, useRef, useState} from 'react';
+import {useTranslation} from '../../../../hooks';
+import {TextSizes, Theme} from '../../../../models';
import {Control} from '../../../Control';
-import {ControlSizes, Lang, TextSizes, Theme} from '../../../../models';
-import {PopperPosition} from '../../../../hooks';
-
+import {ControlsLayoutContext} from '../../ControlsLayout';
import {getPopupPosition} from '../utils';
-import SettingsIcon from '@gravity-ui/icons/svgs/gear.svg';
-
import './SettingsControl.scss';
const ITEM_HEIGHT = 48;
@@ -23,32 +21,23 @@ interface ControlProps {
showMiniToc?: boolean;
theme?: Theme;
textSize?: TextSizes;
- lang: Lang;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
onChangeWideFormat?: (value: boolean) => void;
onChangeShowMiniToc?: (value: boolean) => void;
onChangeTheme?: (theme: Theme) => void;
onChangeTextSize?: (textSize: TextSizes) => void;
- popupPosition?: PopperPosition;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
interface SettingControlItem {
text: string;
description: string;
- control: Element;
+ control: ReactElement;
}
-const SettingsControl = (props: ControlInnerProps) => {
+const SettingsControl = (props: ControlProps) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(ControlsLayoutContext);
const {
- className,
- isVerticalView,
- size,
- lang,
- i18n,
textSize,
theme,
wideFormat,
@@ -59,8 +48,6 @@ const SettingsControl = (props: ControlInnerProps) => {
onChangeWideFormat,
onChangeShowMiniToc,
onChangeTextSize,
- popupPosition,
- t,
} = props;
const controlRef = useRef(null);
@@ -68,8 +55,8 @@ const SettingsControl = (props: ControlInnerProps) => {
const showPopup = () => setIsVisiblePopup(true);
const hidePopup = () => setIsVisiblePopup(false);
- const makeOnChangeTextSize = useCallback(
- (textSizeKey) => () => {
+ const _onChangeTextSize = useCallback(
+ (textSizeKey: TextSizes) => () => {
if (onChangeTextSize) {
onChangeTextSize(textSizeKey);
}
@@ -142,7 +129,7 @@ const SettingsControl = (props: ControlInnerProps) => {
[textSizeKey]: true,
})}
view="flat"
- onClick={makeOnChangeTextSize(textSizeKey)}
+ onClick={_onChangeTextSize(textSizeKey)}
>
{
showMiniToc,
fullScreen,
singlePage,
- onChangeTheme,
- onChangeWideFormat,
- onChangeShowMiniToc,
_onChangeTheme,
_onChangeWideFormat,
_onChangeShowMiniToc,
+ _onChangeTextSize,
+ onChangeWideFormat,
+ onChangeShowMiniToc,
+ onChangeTheme,
onChangeTextSize,
- makeOnChangeTextSize,
]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- const setRef = useCallback((ref: HTMLButtonElement) => {
- controlRef.current = ref;
- }, []);
-
- if (!(onChangeWideFormat || onChangeTheme || onChangeShowMiniToc || onChangeTextSize)) {
- return null;
- }
-
const settingsItems = getSettingsItems();
return (
}
+ icon={(args) => }
/>
{
itemHeight={ITEM_HEIGHT}
itemsHeight={ITEM_HEIGHT * settingsItems.length}
filterable={false}
- renderItem={(item) => {
+ renderItem={(item: SettingControlItem) => {
return (
@@ -234,4 +209,4 @@ const SettingsControl = (props: ControlInnerProps) => {
);
};
-export default withTranslation('controls')(SettingsControl);
+export default SettingsControl;
diff --git a/src/components/Controls/single-controls/SinglePageControl.tsx b/src/components/Controls/single-controls/SinglePageControl.tsx
index bed7cd43..b2f2d75e 100644
--- a/src/components/Controls/single-controls/SinglePageControl.tsx
+++ b/src/components/Controls/single-controls/SinglePageControl.tsx
@@ -1,58 +1,44 @@
-import React, {useCallback, useEffect} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Icon as IconComponent} from '@gravity-ui/uikit';
+import {ChevronsCollapseToLine, ChevronsExpandToLines} from '@gravity-ui/icons';
+import block from 'bem-cn-lite';
+import React, {memo, useCallback, useContext} from 'react';
+import {useTranslation} from '../../../hooks';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
-import {PopperPosition} from '../../../hooks';
-
-import SinglePageIcon from '../../../../assets/icons/single-page.svg';
-import SinglePageClickedIcon from '../../../../assets/icons/single-page-clicked.svg';
+import {ControlsLayoutContext} from '../ControlsLayout';
interface ControlProps {
- lang: Lang;
value?: boolean;
- onChange?: (value: boolean) => void;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- popupPosition?: PopperPosition;
+ onChange: (value: boolean) => void;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
+const b = block('dc-controls');
-const SinglePageControl = (props: ControlInnerProps) => {
- const {className, isVerticalView, size, value, onChange, lang, i18n, popupPosition, t} = props;
+const SinglePageControl = memo
((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(ControlsLayoutContext);
+ const {value, onChange} = props;
const onClick = useCallback(() => {
- if (onChange) {
- onChange(!value);
- }
+ onChange(!value);
}, [value, onChange]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
const activeMode = value ? 'enabled' : 'disabled';
-
- if (!onChange) {
- return null;
- }
+ const Icon = value ? ChevronsExpandToLines : ChevronsCollapseToLine;
return (
(
-
- )}
+ icon={(args) => }
popupPosition={popupPosition}
/>
);
-};
+});
+
+SinglePageControl.displayName = 'SinglePageControl';
-export default withTranslation('controls')(SinglePageControl);
+export default SinglePageControl;
diff --git a/src/components/Controls/single-controls/index.ts b/src/components/Controls/single-controls/index.ts
index b0c435a8..e3211962 100644
--- a/src/components/Controls/single-controls/index.ts
+++ b/src/components/Controls/single-controls/index.ts
@@ -4,3 +4,4 @@ export {default as SinglePageControl} from './SinglePageControl';
export {default as LangControl} from './LangControl';
export {default as DividerControl} from './DividerControl/DividerControl';
export {default as PdfControl} from './PdfControl';
+export {default as EditControl} from './EditControl';
diff --git a/src/components/DocLayout/DocLayout.tsx b/src/components/DocLayout/DocLayout.tsx
index ad9ee9f7..87ea494d 100644
--- a/src/components/DocLayout/DocLayout.tsx
+++ b/src/components/DocLayout/DocLayout.tsx
@@ -1,8 +1,7 @@
-import React, {PropsWithChildren, ReactElement} from 'react';
-
import block from 'bem-cn-lite';
+import React, {PropsWithChildren, ReactElement} from 'react';
-import {TocData, Router, Lang} from '../../models';
+import {Router, TocData} from '../../models';
import {getStateKey} from '../../utils';
import {Toc} from '../Toc';
@@ -17,7 +16,6 @@ const Right: React.FC = () => null;
export interface DocLayoutProps {
toc: TocData;
router: Router;
- lang: Lang;
children: (ReactElement | null)[] | ReactElement;
fullScreen?: boolean;
hideRight?: boolean;
@@ -104,7 +102,6 @@ export class DocLayout extends React.Component {
wideFormat,
hideTocHeader,
hideToc,
- lang,
singlePage,
onChangeSinglePage,
pdfLink,
@@ -124,7 +121,6 @@ export class DocLayout extends React.Component {
headerHeight={headerHeight}
tocTitleIcon={tocTitleIcon}
hideTocHeader={hideTocHeader}
- lang={lang}
singlePage={singlePage}
onChangeSinglePage={onChangeSinglePage}
pdfLink={pdfLink}
diff --git a/src/components/DocLeadingPage/DocLeadingPage.tsx b/src/components/DocLeadingPage/DocLeadingPage.tsx
index eea825f7..01bc4bfb 100644
--- a/src/components/DocLeadingPage/DocLeadingPage.tsx
+++ b/src/components/DocLeadingPage/DocLeadingPage.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-
import block from 'bem-cn-lite';
+import React from 'react';
-import {DocLeadingPageData, DocLeadingLinks, Router, Lang} from '../../models';
import {DEFAULT_SETTINGS} from '../../constants';
+import {DocLeadingLinks, DocLeadingPageData, Router} from '../../models';
import {DocLayout} from '../DocLayout';
import {DocPageTitle} from '../DocPageTitle';
import {HTML} from '../HTML';
@@ -17,7 +16,6 @@ const {wideFormat: defaultWideFormat} = DEFAULT_SETTINGS;
export interface DocLeadingPageProps extends DocLeadingPageData {
router: Router;
- lang: Lang;
headerHeight?: number;
wideFormat?: boolean;
hideTocHeader?: boolean;
@@ -106,7 +104,6 @@ export const DocLeadingPage: React.FC = ({
data: {title, description, links},
toc,
router,
- lang,
headerHeight,
wideFormat = defaultWideFormat,
hideTocHeader,
@@ -122,7 +119,6 @@ export const DocLeadingPage: React.FC = ({
{
const {
toc,
router,
- lang,
headerHeight,
wideFormat,
fullScreen,
@@ -155,7 +152,6 @@ class DocPage extends React.Component {
{
}
private addLinksToOriginalArticle = () => {
- const {singlePage, lang, convertPathToOriginalArticle, generatePathToVcs} = this.props;
+ const {singlePage, convertPathToOriginalArticle, generatePathToVcs, vcsType} = this.props;
if (singlePage) {
const elements = document.querySelectorAll('[data-original-article]');
@@ -291,7 +287,12 @@ class DocPage extends React.Component {
const vcsHref = callSafe(generatePathToVcs, href);
const linkToVcs = createElementFromHTML(
ReactDOMServer.renderToStaticMarkup(
- ,
+ ,
),
);
linkWrapperEl.append(linkToVcs);
@@ -336,19 +337,21 @@ class DocPage extends React.Component {
private renderTitle() {
const {title, meta, bookmarkedPage, onChangeBookmarkPage} = this.props;
+ const withBookmarks = onChangeBookmarkPage;
if (!title) {
return null;
}
return (
-
+
{title}
+ {withBookmarks && (
+
+ )}
);
}
@@ -373,7 +376,7 @@ class DocPage extends React.Component {
}
private renderAuthor(onlyAuthor: boolean) {
- const {meta, lang} = this.props;
+ const {meta} = this.props;
if (!isContributor(meta?.author)) {
return null;
@@ -381,7 +384,6 @@ class DocPage extends React.Component {
return (
{
}
private renderContributors() {
- const {meta, lang} = this.props;
+ const {meta} = this.props;
if (!meta?.contributors || meta.contributors.length === 0) {
return null;
}
- return ;
+ return ;
}
private renderContentMiniToc() {
@@ -463,7 +465,7 @@ class DocPage extends React.Component {
}
private renderAsideMiniToc() {
- const {headings, router, headerHeight, lang} = this.props;
+ const {headings, router, headerHeight} = this.props;
const {keyDOM} = this.state;
return (
@@ -472,7 +474,6 @@ class DocPage extends React.Component {
headings={headings}
router={router}
headerHeight={headerHeight}
- lang={lang}
key={keyDOM}
/>
@@ -480,32 +481,17 @@ class DocPage extends React.Component
{
}
private renderFeedback() {
- const {
- toc,
- lang,
- singlePage,
- isLiked,
- isDisliked,
- onSendFeedback,
- dislikeVariants,
- hideFeedbackControls,
- } = this.props;
+ const {singlePage, isLiked, isDisliked, onSendFeedback, hideFeedbackControls} = this.props;
- if (!toc || toc.singlePage || hideFeedbackControls) {
+ if (singlePage || hideFeedbackControls || !onSendFeedback) {
return null;
}
- const isVerticalView = this.getIsVerticalView();
-
return (
@@ -514,16 +500,15 @@ class DocPage extends React.Component
{
}
private renderTocNavPanel() {
- const {toc, router, fullScreen, lang} = this.props;
+ const {toc, singlePage, router, fullScreen} = this.props;
- if (!toc || toc.singlePage) {
+ if (singlePage) {
return null;
}
return (
{
onClickPrevSearch,
onClickNextSearch,
onCloseSearchBar,
- lang,
singlePage,
} = this.props;
@@ -567,7 +551,6 @@ class DocPage extends React.Component {
return (
{
onSubscribe,
isLiked,
isDisliked,
- dislikeVariants,
hideControls,
hideEditControl,
hideFeedbackControls,
@@ -610,37 +592,36 @@ class DocPage extends React.Component {
return null;
}
- const showEditControl = !hideEditControl && !fullScreen && this.isEditable();
const isVerticalView = this.getIsVerticalView();
return (
-
+
+
+
);
}
diff --git a/src/components/DocPageTitle/DocPageTitle.tsx b/src/components/DocPageTitle/DocPageTitle.tsx
index 82a6f6d1..3378f01e 100644
--- a/src/components/DocPageTitle/DocPageTitle.tsx
+++ b/src/components/DocPageTitle/DocPageTitle.tsx
@@ -1,8 +1,7 @@
-import React, {PropsWithChildren} from 'react';
-
import block from 'bem-cn-lite';
+import React, {PropsWithChildren} from 'react';
-import {BookmarkButton} from '../BookmarkButton';
+import {StageLabel, StageType} from '../StageLabel';
import './DocPageTitle.scss';
@@ -11,16 +10,12 @@ const b = block('dc-doc-page-title');
export interface DocPageTitleProps {
stage?: string;
className?: string;
- bookmarkedPage?: boolean;
- onChangeBookmarkPage?: (value: boolean) => void;
}
export const DocPageTitle: React.FC> = ({
children,
stage,
className,
- bookmarkedPage,
- onChangeBookmarkPage,
}) => {
const visibleStage = stage === 'tech-preview' ? 'preview' : (stage as StageType | undefined);
const label = ;
@@ -29,13 +24,6 @@ export const DocPageTitle: React.FC> = ({
{label}
{children}
- {typeof bookmarkedPage !== 'undefined' &&
- typeof onChangeBookmarkPage !== 'undefined' && (
-
- )}
);
};
diff --git a/src/components/EditButton/EditButton.scss b/src/components/EditButton/EditButton.scss
deleted file mode 100644
index 13c046e1..00000000
--- a/src/components/EditButton/EditButton.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.dc-edit-button {
- position: absolute;
- right: 0;
-
- &__text {
- margin-left: 8px;
- }
-}
diff --git a/src/components/EditButton/EditButton.tsx b/src/components/EditButton/EditButton.tsx
deleted file mode 100644
index 0d8e188a..00000000
--- a/src/components/EditButton/EditButton.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-import {Button, Icon as IconComponent} from '@gravity-ui/uikit';
-
-
-import {Lang} from '../../models';
-import EditIcon from '@gravity-ui/icons/svgs/pencil.svg';
-
-import './EditButton.scss';
-
-const b = block('dc-edit-button');
-
-export interface EditButtonProps {
- lang: Lang;
- href: string;
-}
-
-type EditButtonInnerProps = EditButtonProps & WithTranslation & WithTranslationProps;
-
-class EditButton extends React.Component {
- componentDidUpdate(prevProps: EditButtonProps) {
- const {i18n, lang} = this.props;
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
-
- render() {
- const {t, href} = this.props;
-
- return (
-
-
-
- );
- }
-}
-
-export default withTranslation('controls')(EditButton);
diff --git a/src/components/EditButton/index.ts b/src/components/EditButton/index.ts
deleted file mode 100644
index 77e2f618..00000000
--- a/src/components/EditButton/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export {default as EditButton} from './EditButton';
-export * from './EditButton';
diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx
index 980ea549..4f9a3778 100644
--- a/src/components/ErrorPage/ErrorPage.tsx
+++ b/src/components/ErrorPage/ErrorPage.tsx
@@ -1,11 +1,9 @@
-import React from 'react';
-
import {Button, Link} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
+import React from 'react';
-import {Lang} from '../../models';
import {ERROR_CODES} from '../../constants';
+import {useTranslation} from '../../hooks';
import './ErrorPage.scss';
@@ -13,29 +11,20 @@ const b = block('ErrorPage');
export interface ErrorPageProps {
code?: number;
- lang?: Lang;
pageGroup?: string;
homeUrl?: string;
receiveAccessUrl?: string;
}
-type ErrorPagePropsInnerProps = ErrorPageProps & WithTranslation & WithTranslationProps;
-
-const ErrorPage = ({
+const ErrorPage: React.FC = ({
code = 500,
- lang = Lang.En,
- i18n,
- t,
homeUrl,
receiveAccessUrl,
pageGroup,
-}: ErrorPagePropsInnerProps): JSX.Element => {
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
+}) => {
let title;
let description;
+ const {t} = useTranslation('error');
const href = homeUrl || '/';
const homeLink = (
@@ -91,4 +80,4 @@ const ErrorPage = ({
);
};
-export default withTranslation('error')(ErrorPage);
+export default ErrorPage;
diff --git a/src/components/Feedback/Feedback.scss b/src/components/Feedback/Feedback.scss
index b3bea843..92469aaf 100644
--- a/src/components/Feedback/Feedback.scss
+++ b/src/components/Feedback/Feedback.scss
@@ -39,12 +39,6 @@ $popupWidth: 320px;
}
}
- &__like-button {
- &_active {
- color: var(--g-color-base-brand);
- }
- }
-
&__success-popup {
padding: $popupPadding 50px $popupPadding $popupPadding;
width: $popupWidth;
@@ -145,10 +139,4 @@ $popupWidth: 320px;
}
}
}
-
- &__feedback-button {
- &_active {
- color: var(--g-color-base-brand);
- }
- }
}
diff --git a/src/components/Feedback/Feedback.tsx b/src/components/Feedback/Feedback.tsx
index 08ef49f3..98ec9657 100644
--- a/src/components/Feedback/Feedback.tsx
+++ b/src/components/Feedback/Feedback.tsx
@@ -1,17 +1,14 @@
-import React, {useCallback, useState, useEffect, useRef} from 'react';
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-import {Checkbox, Popup, TextArea, Button, Icon as IconComponent} from '@gravity-ui/uikit';
+import React, {PropsWithChildren, useCallback, useEffect, useRef, useState} from 'react';
-import {Control} from '../Control';
-import {PopperPosition} from '../../hooks';
-import {FeedbackSendData, FeedbackType, Lang} from '../../models';
-import {DISLIKE_VARIANTS} from '../../constants';
-
-import DislikeActiveIcon from '@gravity-ui/icons/svgs/thumbs-down-fill.svg';
-import DislikeIcon from '@gravity-ui/icons/svgs/thumbs-down.svg';
+import {usePopupState, useTranslation} from '../../hooks';
+import {FeedbackSendData, FeedbackType} from '../../models';
import './Feedback.scss';
+import DislikeControl from './controls/DislikeControl';
+import DislikeVariantsPopup, {FormData} from './controls/DislikeVariantsPopup';
+import LikeControl from './controls/LikeControl';
+import SuccessPopup from './controls/SuccessPopup';
const b = block('dc-feedback');
@@ -21,455 +18,143 @@ export enum FeedbackView {
}
export interface FeedbackProps {
- lang: Lang;
- singlePage?: boolean;
isLiked?: boolean;
isDisliked?: boolean;
- dislikeVariants?: string[];
- isVerticalView?: boolean;
- onSendFeedback?: (data: FeedbackSendData) => void;
+ onSendFeedback: (data: FeedbackSendData) => void;
view?: FeedbackView;
- classNameControl?: string;
- popupPosition?: PopperPosition;
}
-interface FeedbackCheckboxes {
- [key: string]: boolean;
-}
+const getInnerState = (isLiked: boolean, isDisliked: boolean) => {
+ switch (true) {
+ case Boolean(isDisliked):
+ return FeedbackType.dislike;
+ case Boolean(isLiked):
+ return FeedbackType.like;
+ default:
+ return FeedbackType.indeterminate;
+ }
+};
+
+const ControlsLayout: React.FC> = ({view, children}) => {
+ const {t} = useTranslation('feedback');
-type FeedbackInnerProps = FeedbackProps & WithTranslation & WithTranslationProps;
+ if (view === FeedbackView.Regular) {
+ return {children};
+ }
+
+ return (
+
+
+
{t('main-question')}
+
{children}
+
+
+ );
+};
-const Feedback: React.FC = (props) => {
+const Feedback: React.FC = (props) => {
const {
- lang,
- singlePage,
- isLiked,
- isDisliked,
- dislikeVariants = DISLIKE_VARIANTS[lang],
- isVerticalView,
+ isLiked = false,
+ isDisliked = false,
onSendFeedback,
- view,
- classNameControl,
- i18n,
- popupPosition,
- t,
+ view = FeedbackView.Regular,
} = props;
const likeControlRef = useRef(null);
const dislikeControlRef = useRef(null);
- const timerId = useRef();
- const timeout = 3000;
-
- const [innerIsDisliked, setInnerIsDisliked] = useState(isDisliked);
- const [feedbackComment, setFeedbackComment] = useState('');
- const [feedbackCheckboxes, setFeedbackCheckboxes] = useState({} as FeedbackCheckboxes);
- const [showLikeSuccessPopup, setShowLikeSuccessPopup] = useState(false);
- const [showDislikeSuccessPopup, setShowDislikeSuccessPopup] = useState(false);
- const [showDislikeVariantsPopup, setShowDislikeVariantsPopup] = useState(false);
-
- const hideFeedbackPopups = useCallback(() => {
- setShowDislikeSuccessPopup(false);
- setShowLikeSuccessPopup(false);
- setShowDislikeVariantsPopup(false);
- }, []);
-
- const resetFeedbackAdditionalInfo = useCallback(() => {
- setFeedbackComment('');
- setFeedbackCheckboxes({});
- }, []);
+ const [innerState, setInnerState] = useState(getInnerState(isLiked, isDisliked));
useEffect(() => {
- setInnerIsDisliked(isDisliked);
- }, [isDisliked]);
+ setInnerState(getInnerState(isLiked, isDisliked));
+ }, [isLiked, isDisliked, setInnerState]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- const setTimer = useCallback((callback: () => void) => {
- timerId.current = setTimeout(async () => {
- callback();
- }, timeout);
- }, []);
-
- const clearTimer = useCallback(() => {
- clearTimeout(timerId.current as number);
- timerId.current = undefined;
- }, []);
+ const likeSuccessPopup = usePopupState({autoclose: 3000});
+ const dislikeSuccessPopup = usePopupState({autoclose: 3000});
+ const dislikeVariantsPopup = usePopupState();
- useEffect(() => {
- if (showLikeSuccessPopup || showDislikeSuccessPopup) {
- setTimer(() => {
- setShowDislikeSuccessPopup(false);
- setShowLikeSuccessPopup(false);
- clearTimer();
- });
- }
- }, [isDisliked, clearTimer, setTimer, showLikeSuccessPopup, showDislikeSuccessPopup]);
-
- const setLikeControlRef = useCallback((ref) => {
- likeControlRef.current = ref;
- }, []);
-
- const setDislikeControlRef = useCallback((ref) => {
- dislikeControlRef.current = ref;
- }, []);
-
- const onOutsideClick = useCallback(() => {
- hideFeedbackPopups();
-
- if (showDislikeVariantsPopup && innerIsDisliked && !isDisliked) {
- setInnerIsDisliked(false);
- }
- }, [
- isDisliked,
- innerIsDisliked,
- hideFeedbackPopups,
- setInnerIsDisliked,
- showDislikeVariantsPopup,
- ]);
-
- const onSendDislikeInformation = useCallback(() => {
- setShowDislikeSuccessPopup(true);
- setShowLikeSuccessPopup(false);
- setShowDislikeVariantsPopup(false);
-
- if (onSendFeedback) {
- const type = FeedbackType.dislike;
-
- const additionalInfo = getPreparedFeedbackAdditionalInfo(
- feedbackComment,
- feedbackCheckboxes,
- );
- const data = {
- type,
- ...additionalInfo,
- };
-
- onSendFeedback(data);
-
- resetFeedbackAdditionalInfo();
- }
- }, [onSendFeedback, feedbackComment, feedbackCheckboxes, resetFeedbackAdditionalInfo]);
-
- const getPopupPosition = useCallback(() => {
- if (!view || view === FeedbackView.Regular) {
- return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
- }
-
- return PopperPosition.RIGHT;
- }, [isVerticalView, view]);
+ const hideFeedbackPopups = useCallback(() => {
+ likeSuccessPopup.close();
+ dislikeSuccessPopup.close();
+ dislikeVariantsPopup.close();
+ }, [likeSuccessPopup, dislikeSuccessPopup, dislikeVariantsPopup]);
const onChangeLike = useCallback(() => {
- setShowLikeSuccessPopup(true);
- setShowDislikeSuccessPopup(false);
- setShowDislikeVariantsPopup(false);
- setInnerIsDisliked(false);
+ hideFeedbackPopups();
- if (onSendFeedback) {
- onSendFeedback({
- type: isLiked ? FeedbackType.indeterminate : FeedbackType.like,
- });
+ if (innerState === FeedbackType.like) {
+ setInnerState(FeedbackType.indeterminate);
+ onSendFeedback({type: FeedbackType.indeterminate});
+ } else {
+ setInnerState(FeedbackType.like);
+ onSendFeedback({type: FeedbackType.like});
+ likeSuccessPopup.open();
}
- }, [isLiked, onSendFeedback]);
+ }, [onSendFeedback, setInnerState, innerState, likeSuccessPopup, hideFeedbackPopups]);
const onChangeDislike = useCallback(() => {
- if (!isDisliked && !innerIsDisliked) {
- // Нажать дизлайк и показать окно с доп. информацией
- setShowDislikeVariantsPopup(true);
- setInnerIsDisliked(true);
- setShowLikeSuccessPopup(false);
- setShowDislikeSuccessPopup(false);
-
- if (isLiked && onSendFeedback) {
- onSendFeedback({type: FeedbackType.indeterminate});
- }
- } else if (!isDisliked && innerIsDisliked) {
- hideFeedbackPopups();
- setInnerIsDisliked(false);
- } else if (isDisliked && innerIsDisliked) {
- // Отжать дизлайк и отправить событие в неопределенное состояние
- hideFeedbackPopups();
- setInnerIsDisliked(false);
-
- if (onSendFeedback) {
- onSendFeedback({type: FeedbackType.indeterminate});
- }
- }
- }, [innerIsDisliked, isDisliked, isLiked, onSendFeedback, hideFeedbackPopups]);
-
- const renderLikeControl = useCallback(() => {
- return (
- (
-
- )}
- popupPosition={popupPosition}
- />
- );
- }, [
- onChangeLike,
- classNameControl,
- view,
- isVerticalView,
- isLiked,
- setLikeControlRef,
- popupPosition,
- t,
- ]);
-
- const renderDislikeControl = useCallback(() => {
- return (
- (
-
- )}
- />
- );
- }, [
- innerIsDisliked,
- onChangeDislike,
- classNameControl,
- view,
- isVerticalView,
- setDislikeControlRef,
- t,
- ]);
-
- const renderRegularFeedbackControls = useCallback(() => {
- return (
-
- {renderLikeControl()}
- {renderDislikeControl()}
-
- );
- }, [renderLikeControl, renderDislikeControl]);
-
- const renderWideFeedbackControls = useCallback(() => {
- return (
-
-
-
{t('main-question')}
-
-
-
-
-
-
- );
- }, [
- innerIsDisliked,
- isLiked,
- view,
- t,
- setLikeControlRef,
- setDislikeControlRef,
- onChangeLike,
- onChangeDislike,
- ]);
-
- const renderFeedbackControls = useCallback(() => {
- return view === FeedbackView.Regular
- ? renderRegularFeedbackControls()
- : renderWideFeedbackControls();
- }, [view, renderRegularFeedbackControls, renderWideFeedbackControls]);
-
- const renderFeedbackSuccessPopup = useCallback(() => {
- const anchor = showLikeSuccessPopup ? likeControlRef : dislikeControlRef;
- const visible = showLikeSuccessPopup || showDislikeSuccessPopup;
-
- if (!visible) {
- return null;
- }
-
- return (
-
- {t('success-title')}
- {t('success-text')}
-
- );
- }, [
- showLikeSuccessPopup,
- showDislikeSuccessPopup,
- hideFeedbackPopups,
- view,
- getPopupPosition,
- t,
- ]);
-
- const renderDislikeVariantsList = useCallback(() => {
- if (!dislikeVariants.length) {
- return null;
- }
-
- return (
-
- {dislikeVariants.map((variant, index) => (
- {
- setFeedbackCheckboxes({
- ...feedbackCheckboxes,
- [variant]: checked,
- });
- }}
- content={variant}
- />
- ))}
-
- );
- }, [dislikeVariants, feedbackCheckboxes]);
-
- const renderDislikeVariantsTextArea = useCallback(() => {
- return (
-
-
- );
- }, [feedbackComment, t]);
-
- const renderDislikeVariantsActions = useCallback(() => {
- return (
-
-
-
- );
- }, [onSendDislikeInformation, t]);
-
- const renderDislikeVariantsContent = useCallback(() => {
- return (
-
- {t('dislike-variants-title')}
- {renderDislikeVariantsList()}
- {renderDislikeVariantsTextArea()}
- {renderDislikeVariantsActions()}
-
- );
- }, [t, renderDislikeVariantsList, renderDislikeVariantsTextArea, renderDislikeVariantsActions]);
+ hideFeedbackPopups();
- const renderDislikeVariantsPopup = useCallback(() => {
- if (!showDislikeVariantsPopup) {
- return null;
+ if (innerState === FeedbackType.dislike) {
+ setInnerState(FeedbackType.indeterminate);
+ onSendFeedback({type: FeedbackType.indeterminate});
+ } else {
+ dislikeVariantsPopup.open();
}
+ }, [onSendFeedback, setInnerState, innerState, dislikeVariantsPopup, hideFeedbackPopups]);
- return (
-
- {renderDislikeVariantsContent()}
-
- );
- }, [
- showDislikeVariantsPopup,
- onOutsideClick,
- view,
- getPopupPosition,
- renderDislikeVariantsContent,
- ]);
-
- if (singlePage || !onSendFeedback) {
- return null;
- }
+ const onSendDislikeInformation = useCallback(
+ (data: FormData) => {
+ hideFeedbackPopups();
+ dislikeSuccessPopup.open();
+ setInnerState(FeedbackType.dislike);
+ onSendFeedback({type: FeedbackType.dislike, ...data});
+ },
+ [onSendFeedback, setInnerState, dislikeSuccessPopup, hideFeedbackPopups],
+ );
return (
- {renderFeedbackControls()}
- {renderFeedbackSuccessPopup()}
- {renderDislikeVariantsPopup()}
+
+
+
+
+ {likeControlRef.current && (
+
+ )}
+ {dislikeControlRef.current && (
+
+ )}
+ {dislikeControlRef.current && (
+
+ )}
);
};
-function getPreparedFeedbackAdditionalInfo(
- feedbackComment: string,
- feedbackCheckboxes: FeedbackCheckboxes,
-) {
- const answers = Object.keys(feedbackCheckboxes).reduce((acc, key) => {
- if (feedbackCheckboxes[key]) {
- acc.push(key);
- }
-
- return acc;
- }, [] as string[]);
-
- return {
- comment: feedbackComment,
- answers,
- };
-}
-
-export default withTranslation('feedback')(Feedback);
+export default Feedback;
diff --git a/src/components/Feedback/controls/DislikeControl.tsx b/src/components/Feedback/controls/DislikeControl.tsx
new file mode 100644
index 00000000..77a90c84
--- /dev/null
+++ b/src/components/Feedback/controls/DislikeControl.tsx
@@ -0,0 +1,56 @@
+import DislikeActiveIcon from '@gravity-ui/icons/svgs/thumbs-down-fill.svg';
+import DislikeIcon from '@gravity-ui/icons/svgs/thumbs-down.svg';
+import {Button, Icon} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {forwardRef, memo, useContext} from 'react';
+
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {ControlsLayoutContext} from '../../Controls/ControlsLayout';
+import {FeedbackView} from '../Feedback';
+
+type DislikeControlProps = {
+ isVerticalView?: boolean | undefined;
+ isDisliked: boolean | undefined;
+ className?: string | undefined;
+ view: FeedbackView | undefined;
+ onClick: () => void;
+};
+
+const b = block('dc-feedback');
+
+const DislikeControl = memo(
+ forwardRef(({isDisliked, view, onClick}, ref) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView, controlClassName} = useContext(ControlsLayoutContext);
+ const tooltipText = isDisliked ? t('cancel-dislike-text') : t('dislike-text');
+
+ if (view === FeedbackView.Wide) {
+ return (
+
+ );
+ }
+
+ return (
+ (
+
+ )}
+ />
+ );
+ }),
+);
+
+DislikeControl.displayName = 'DislikeControl';
+
+export default DislikeControl;
diff --git a/src/components/Feedback/controls/DislikeVariantsPopup.tsx b/src/components/Feedback/controls/DislikeVariantsPopup.tsx
new file mode 100644
index 00000000..bfc03c26
--- /dev/null
+++ b/src/components/Feedback/controls/DislikeVariantsPopup.tsx
@@ -0,0 +1,184 @@
+import {Button, Checkbox, Popup, TextArea} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {
+ RefObject,
+ SyntheticEvent,
+ forwardRef,
+ memo,
+ useCallback,
+ useContext,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import {PopperPosition, useTranslation} from '../../../hooks';
+import {ControlsLayoutContext} from '../../Controls/ControlsLayout';
+import {FeedbackView} from '../Feedback';
+
+export interface FeedbackCheckboxes {
+ [key: string]: boolean;
+}
+
+const b = block('dc-feedback');
+
+const DislikeVariantsList = memo(
+ forwardRef>((_props, ref) => {
+ const {t, i18n} = useTranslation('feedback-variants');
+ const variants = i18n.getResourceBundle(i18n.language, 'feedback-variants');
+ const [checked, setChecked] = useState({} as FeedbackCheckboxes);
+
+ useImperativeHandle(ref, () => {
+ return {
+ data() {
+ return Object.keys(checked)
+ .filter((key) => Boolean(checked[key]))
+ .map((key) => variants[key]);
+ },
+
+ clean() {
+ setChecked({});
+ },
+ };
+ });
+
+ if (!Object.keys(variants).length) {
+ return null;
+ }
+
+ return (
+
+ {Object.keys(variants).map((variant) => (
+ {
+ setChecked({
+ ...checked,
+ [variant]: state,
+ });
+ }}
+ content={t(variant)}
+ />
+ ))}
+
+ );
+ }),
+);
+
+DislikeVariantsList.displayName = 'DislikeVariantsList';
+
+const DislikeVariantsInput = memo(
+ forwardRef>((_props, ref) => {
+ const {t} = useTranslation('feedback');
+ const [feedbackComment, setFeedbackComment] = useState('');
+ const onChange = useCallback((event: SyntheticEvent) => {
+ setFeedbackComment((event.target as HTMLTextAreaElement).value);
+ }, []);
+
+ useImperativeHandle(ref, () => {
+ return {
+ data() {
+ return feedbackComment;
+ },
+
+ clean() {
+ setFeedbackComment('');
+ },
+ };
+ });
+
+ return (
+
+
+
+ );
+ }),
+);
+
+DislikeVariantsInput.displayName = 'DislikeVariantsInput';
+
+type FormPart = {
+ data(): T;
+ clean(): void;
+};
+
+export type FormData = {
+ comment: string;
+ answers: string[];
+};
+
+type DislikeVariantsPopupProps = {
+ view: FeedbackView;
+ visible: boolean;
+ anchor: RefObject;
+ onOutsideClick: () => void;
+ onSubmit: (data: FormData) => void;
+};
+
+const DislikeVariantsPopup: React.FC = memo(
+ ({anchor, visible, view, onOutsideClick, onSubmit}) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView} = useContext(ControlsLayoutContext);
+ const position = useMemo(() => {
+ if (!view || view === FeedbackView.Regular) {
+ return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
+ }
+
+ return PopperPosition.RIGHT;
+ }, [isVerticalView, view]);
+
+ const feedbackComment = useRef | null>(null);
+ const feedbackCheckboxes = useRef | null>(null);
+
+ const onFormSubmit = useCallback(
+ (event: SyntheticEvent) => {
+ event.preventDefault();
+
+ onSubmit({
+ comment: feedbackComment.current?.data() || '',
+ answers: feedbackCheckboxes.current?.data() || [],
+ });
+
+ feedbackComment.current?.clean();
+ feedbackCheckboxes.current?.clean();
+ },
+ [onSubmit],
+ );
+
+ return (
+
+ {t('dislike-variants-title')}
+
+
+ );
+ },
+);
+
+DislikeVariantsPopup.displayName = 'FeedbackDislikeVariantsPopup';
+
+export default DislikeVariantsPopup;
diff --git a/src/components/Feedback/controls/LikeControl.tsx b/src/components/Feedback/controls/LikeControl.tsx
new file mode 100644
index 00000000..bfa83a50
--- /dev/null
+++ b/src/components/Feedback/controls/LikeControl.tsx
@@ -0,0 +1,63 @@
+import LikeActiveIcon from '@gravity-ui/icons/svgs/thumbs-up-fill.svg';
+import LikeIcon from '@gravity-ui/icons/svgs/thumbs-up.svg';
+import {Button, Icon} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {forwardRef, memo, useContext} from 'react';
+
+import type {PopperPosition} from '../../../hooks';
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {ControlsLayoutContext} from '../../Controls/ControlsLayout';
+import {FeedbackView} from '../Feedback';
+
+type LikeControlProps = {
+ isVerticalView?: boolean | undefined;
+ isLiked: boolean | undefined;
+ className?: string | undefined;
+ view: FeedbackView | undefined;
+ onClick: () => void;
+ popupPosition?: PopperPosition | undefined;
+};
+
+const b = block('dc-feedback');
+
+const LikeControl = memo(
+ forwardRef(({isLiked, view, onClick}, ref) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView, popupPosition, controlClassName} = useContext(ControlsLayoutContext);
+ const tooltipText = isLiked ? t('cancel-like-text') : t('like-text');
+
+ if (view === FeedbackView.Wide) {
+ return (
+
+ );
+ }
+
+ return (
+ }
+ popupPosition={popupPosition}
+ />
+ );
+ }),
+);
+
+LikeControl.displayName = 'LikeControl';
+
+export default LikeControl;
diff --git a/src/components/Feedback/controls/SuccessPopup.tsx b/src/components/Feedback/controls/SuccessPopup.tsx
new file mode 100644
index 00000000..5caa784d
--- /dev/null
+++ b/src/components/Feedback/controls/SuccessPopup.tsx
@@ -0,0 +1,45 @@
+import {Popup} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {RefObject, memo, useContext, useMemo} from 'react';
+
+import {PopperPosition, useTranslation} from '../../../hooks';
+import {ControlsLayoutContext} from '../../Controls/ControlsLayout';
+import {FeedbackView} from '../Feedback';
+
+type SuccessPopupProps = {
+ view: FeedbackView;
+ visible: boolean;
+ anchor: RefObject;
+ onOutsideClick: () => void;
+};
+
+const b = block('dc-feedback');
+
+const SuccessPopup = memo(({visible, anchor, onOutsideClick, view}) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView} = useContext(ControlsLayoutContext);
+ const position = useMemo(() => {
+ if (!view || view === FeedbackView.Regular) {
+ return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
+ }
+
+ return PopperPosition.RIGHT;
+ }, [isVerticalView, view]);
+
+ return (
+
+ {t('success-title')}
+ {t('success-text')}
+
+ );
+});
+
+SuccessPopup.displayName = 'FeedbackSuccessPopup';
+
+export default SuccessPopup;
diff --git a/src/components/MiniToc/MiniToc.tsx b/src/components/MiniToc/MiniToc.tsx
index 980492f5..ef7f09e5 100644
--- a/src/components/MiniToc/MiniToc.tsx
+++ b/src/components/MiniToc/MiniToc.tsx
@@ -1,10 +1,8 @@
-import React, {ReactElement} from 'react';
-import PropTypes from 'prop-types';
import block from 'bem-cn-lite';
+import React, {memo, useMemo} from 'react';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-
-import {DocHeadingItem, Router, Lang} from '../../models';
+import {useTranslation} from '../../hooks';
+import {DocHeadingItem, Router} from '../../models';
import {Scrollspy} from '../Scrollspy';
import './MiniToc.scss';
@@ -12,59 +10,44 @@ import './MiniToc.scss';
const b = block('dc-mini-toc');
export interface MinitocProps {
- lang: Lang;
headings: DocHeadingItem[];
router: Router;
headerHeight?: number;
}
-type MinitocInnerProps = MinitocProps & WithTranslation & WithTranslationProps;
-
-class MiniToc extends React.Component {
- static propTypes = {
- headings: PropTypes.array.isRequired,
- };
+export interface MinitocSectionProps {
+ headings: DocHeadingItem[];
+ router: Router;
+ headerHeight?: number;
+}
- componentDidUpdate(prevProps: MinitocProps) {
- const {i18n, lang} = this.props;
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
+interface FlatHeadingItem {
+ title: string;
+ href: string;
+ isChild: boolean;
+}
- render() {
- const {lang, i18n, t} = this.props;
+function getFlatHeadings(items: DocHeadingItem[], isChild = false): FlatHeadingItem[] {
+ return items.reduce((result, {href, title, items: subItems}) => {
+ return result.concat({title, href, isChild}, getFlatHeadings(subItems || [], true));
+ }, [] as FlatHeadingItem[]);
+}
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
+const MiniToc = memo(({headings, router, headerHeight}) => {
+ const {t} = useTranslation('mini-toc');
+ const flatHeadings = useMemo(() => getFlatHeadings(headings), [headings]);
+ const sectionHrefs = useMemo(
+ () => flatHeadings.map(({href}) => href, []),
+ [flatHeadings],
+ );
- return (
-
-
{t('title')}:
- {this.renderSections()}
-
- );
+ if (flatHeadings.length === 0) {
+ return null;
}
- private renderSections() {
- const {headings, router, headerHeight} = this.props;
-
- if (headings.length === 0) {
- return null;
- }
-
- const sectionHrefs = headings.reduce((prevHrefs, {href, items}) => {
- const children = items ? items.map(({href: itemHref}) => itemHref) : [];
-
- return prevHrefs.concat(href, children);
- }, []);
-
- if (sectionHrefs.length === 0) {
- return null;
- }
-
- return (
+ return (
+
+
{t('title')}:
{
router={router}
headerHeight={headerHeight}
>
- {headings.reduce(this.renderSection, [])}
+ {flatHeadings.map(({href, title, isChild}) => (
+
+
+ {title}
+
+
+ ))}
- );
- }
+
+ );
+});
- private renderSection = (prevSections: ReactElement[], heading: DocHeadingItem) => {
- return prevSections.concat(
- this.renderItem(heading),
- heading.items ? heading.items.map((item) => this.renderItem(item, true)) : [],
- );
- };
-
- private renderItem = ({title, href}: DocHeadingItem, isChild = false) => {
- return (
-
-
- {title}
-
-
- );
- };
-}
+MiniToc.displayName = 'MiniToc';
-export default withTranslation('mini-toc')(MiniToc);
+export default MiniToc;
diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx
index 81d660d5..7d254af7 100644
--- a/src/components/SearchBar/SearchBar.tsx
+++ b/src/components/SearchBar/SearchBar.tsx
@@ -1,22 +1,18 @@
-import React from 'react';
-
+import ArrowLeftIcon from '@gravity-ui/icons/svgs/chevron-left.svg';
+import CloseIcon from '@gravity-ui/icons/svgs/xmark.svg';
import {Icon} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
+import React, {memo} from 'react';
import {useHotkeys} from 'react-hotkeys-hook';
+import {useTranslation} from '../../hooks';
import {Control} from '../Control';
-import {Lang} from '../../models';
-import ArrowLeftIcon from '@gravity-ui/icons/svgs/chevron-left.svg';
-import CloseIcon from '@gravity-ui/icons/svgs/xmark.svg';
-
import './SearchBar.scss';
const b = block('dc-search-bar');
export interface SearchBarProps {
- lang: Lang;
searchQuery?: string;
onClickPrevSearch?: () => void;
onClickNextSearch?: () => void;
@@ -25,15 +21,11 @@ export interface SearchBarProps {
searchCountResults?: number;
}
-type SearchBarInnerProps = SearchBarProps & WithTranslation & WithTranslationProps;
-
const noop = () => {};
-const SearchBar: React.FC = (props) => {
+const SearchBar = memo((props) => {
+ const {t} = useTranslation('search-bar');
const {
- t,
- i18n,
- lang,
searchQuery,
searchCurrentIndex,
searchCountResults,
@@ -48,10 +40,6 @@ const SearchBar: React.FC = (props) => {
useHotkeys(hotkeysPrev, onClickPrevSearch, hotkeysOptions, [onClickPrevSearch]);
useHotkeys(hotkeysNext, onClickNextSearch, hotkeysOptions, [onClickNextSearch]);
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
return (
@@ -59,9 +47,7 @@ const SearchBar: React.FC = (props) => {
(
-
- )}
+ icon={(args) => }
/>
{searchCurrentIndex}/{searchCountResults}
@@ -83,7 +69,7 @@ const SearchBar: React.FC = (props) => {
}
+ icon={() => }
/>
@@ -92,4 +78,4 @@ const SearchBar: React.FC = (props) => {
SearchBar.displayName = 'DCSearchBar';
-export default withTranslation('search-bar')(SearchBar);
+export default SearchBar;
diff --git a/src/components/SearchItem/SearchItem.tsx b/src/components/SearchItem/SearchItem.tsx
index 76e3025d..8bd95520 100644
--- a/src/components/SearchItem/SearchItem.tsx
+++ b/src/components/SearchItem/SearchItem.tsx
@@ -1,12 +1,12 @@
-import React, {useState} from 'react';
-import block from 'bem-cn-lite';
import {Button} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {memo, useState} from 'react';
-import './SearchItem.scss';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Lang} from '../../models';
+import {useTranslation} from '../../hooks';
import {HTML} from '../HTML';
+import './SearchItem.scss';
+
const b = block('SearchItem');
export interface ISearchItem {
@@ -24,86 +24,74 @@ export interface SearchOnClickProps {
export interface SearchItemProps {
item: ISearchItem;
className?: string;
- lang?: Lang;
}
-type SearchPageInnerProps = SearchItemProps &
- SearchOnClickProps &
- WithTranslation &
- WithTranslationProps;
-
-const SearchItem = ({
- item,
- className,
- lang = Lang.En,
- i18n,
- t,
- itemOnClick,
- irrelevantOnClick,
- relevantOnClick,
-}: SearchPageInnerProps) => {
- const {url, title, description} = item;
+type SearchPageInnerProps = SearchItemProps & SearchOnClickProps;
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
+const SearchItem = memo(
+ ({item, className, itemOnClick, irrelevantOnClick, relevantOnClick}) => {
+ const {t} = useTranslation('search');
+ const {url, title, description} = item;
- const [markedItem, setMarkedItem] = useState(false);
+ const [markedItem, setMarkedItem] = useState(false);
- const renderItem = () => {
- return (
-
-
(itemOnClick ? itemOnClick(item) : undefined)}
- >
- {title}
- {description}
-
- {irrelevantOnClick && relevantOnClick && (
-
-
- {markedItem ? (
-
- {t('search_mark-result-text')}
-
- ) : (
-
-
-
-
- )}
+ const renderItem = () => {
+ return (
+
+
(itemOnClick ? itemOnClick(item) : undefined)}
+ >
+ {title}
+ {description}
+
+ {irrelevantOnClick && relevantOnClick && (
+
+
+ {markedItem ? (
+
+ {t('search_mark-result-text')}
+
+ ) : (
+
+
+
+
+ )}
+
-
- )}
-
- );
- };
+ )}
+
+ );
+ };
+
+ return
{renderItem()}
;
+ },
+);
- return
{renderItem()}
;
-};
+SearchItem.displayName = 'SearchItem';
-export default withTranslation('search')(SearchItem);
+export default SearchItem;
diff --git a/src/components/SearchPage/SearchPage.tsx b/src/components/SearchPage/SearchPage.tsx
index 2c5e21d4..fe2950a6 100644
--- a/src/components/SearchPage/SearchPage.tsx
+++ b/src/components/SearchPage/SearchPage.tsx
@@ -1,21 +1,15 @@
-import React, {useRef, useState} from 'react';
-
import {Button, Loader, TextInput} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {TFunction, withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
+import React, {useRef, useState} from 'react';
+import {useTranslation} from '../../hooks';
import {Paginator, PaginatorProps} from '../Paginator';
import {ISearchItem, SearchItem, SearchOnClickProps} from '../SearchItem';
-import {Lang} from '../../models';
import './SearchPage.scss';
const b = block('dc-search-page');
-interface Translation {
- t: TFunction<'translation'>;
-}
-
interface Loading {
loading?: boolean;
}
@@ -28,88 +22,22 @@ interface InputProps {
type RenderInput = {
inputRef: React.MutableRefObject
;
onQueryUpdate: (arg: string) => void;
-} & InputProps &
- Translation;
+} & InputProps;
-type RenderNoContent = Loading & Translation;
+type RenderNoContent = Loading;
interface SearchPageProps extends Loading {
items: ISearchItem[];
page: number;
- lang?: Lang;
isMobile?: boolean;
loading?: boolean;
}
-type RenderFoundProps = SearchPageProps & SearchOnClickProps & PaginatorProps & Translation;
+type RenderFoundProps = SearchPageProps & SearchOnClickProps & PaginatorProps;
-type SearchPageInnerProps = SearchPageProps &
- SearchOnClickProps &
- InputProps &
- PaginatorProps &
- WithTranslation &
- WithTranslationProps;
+type SearchPageInnerProps = SearchPageProps & SearchOnClickProps & InputProps & PaginatorProps;
-const SearchPage = ({
- query = '',
- items = [],
- page = 1,
- lang = Lang.En,
- isMobile,
- i18n,
- t,
- totalItems,
- maxPages,
- itemsPerPage,
- onPageChange,
- onSubmit,
- itemOnClick,
- irrelevantOnClick,
- relevantOnClick,
- loading,
-}: SearchPageInnerProps) => {
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
- const inputRef = useRef(null);
- const [currentQuery, setCurrentQuery] = useState(query);
-
- return (
-
-
- {renderInput({
- query: currentQuery,
- onQueryUpdate: setCurrentQuery,
- onSubmit,
- inputRef,
- t,
- })}
-
-
- {items?.length && query
- ? renderFound({
- items,
- page,
- lang,
- isMobile,
- t,
- totalItems,
- maxPages,
- itemsPerPage,
- itemOnClick,
- onPageChange,
- irrelevantOnClick,
- relevantOnClick,
- })
- : renderWithoutContent({loading, t})}
-
-
- );
-};
-
-function renderFound({
- lang,
+const FoundBlock: React.FC = ({
items,
itemOnClick,
irrelevantOnClick,
@@ -120,25 +48,21 @@ function renderFound({
onPageChange,
itemsPerPage,
isMobile,
- t,
-}: RenderFoundProps) {
+}) => {
+ const {t} = useTranslation('search');
+
return (
{t('search_request-query')}
{items.map((item: ISearchItem) => (
itemOnClick(arg) : undefined}
- irrelevantOnClick={
- irrelevantOnClick ? (arg) => irrelevantOnClick(arg) : undefined
- }
- relevantOnClick={
- relevantOnClick ? (arg) => relevantOnClick(arg) : undefined
- }
+ itemOnClick={itemOnClick}
+ irrelevantOnClick={irrelevantOnClick}
+ relevantOnClick={relevantOnClick}
/>
))}
@@ -154,9 +78,11 @@ function renderFound({
);
-}
+};
+
+const WithoutContentBlock: React.FC = ({loading}) => {
+ const {t} = useTranslation('search');
-function renderWithoutContent({loading, t}: RenderNoContent) {
return loading ? (
) : (
@@ -165,9 +91,11 @@ function renderWithoutContent({loading, t}: RenderNoContent) {
{t('search_not-found-text')}
);
-}
+};
+
+const InputBlock: React.FC = ({query, onQueryUpdate, onSubmit, inputRef}) => {
+ const {t} = useTranslation('search');
-function renderInput({query, onQueryUpdate, onSubmit, inputRef, t}: RenderInput) {
return (
);
-}
+};
+
+const SearchPage = ({
+ query = '',
+ items = [],
+ page = 1,
+ isMobile,
+ totalItems,
+ maxPages,
+ itemsPerPage,
+ onPageChange,
+ onSubmit,
+ itemOnClick,
+ irrelevantOnClick,
+ relevantOnClick,
+ loading,
+}: SearchPageInnerProps) => {
+ const inputRef = useRef(null);
+ const [currentQuery, setCurrentQuery] = useState(query);
+
+ return (
+
+
+
+
+
+ {items?.length && query ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
-export default withTranslation('search')(SearchPage);
+export default SearchPage;
diff --git a/src/components/Subscribe/Subscribe.tsx b/src/components/Subscribe/Subscribe.tsx
index 6964ecc4..f1819fbb 100644
--- a/src/components/Subscribe/Subscribe.tsx
+++ b/src/components/Subscribe/Subscribe.tsx
@@ -1,18 +1,16 @@
-import React, {useCallback, useState, useRef} from 'react';
+import SubscribeIcon from '@gravity-ui/icons/svgs/envelope.svg';
+import {Button, Icon} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {WithTranslation, withTranslation} from 'react-i18next';
+import React, {PropsWithChildren, forwardRef, memo, useCallback, useContext, useRef} from 'react';
-import {SubscribeSuccessPopup} from './SubscribeSuccessPopup';
-import {SubscribeVariantsPopup} from './SubscribeVariantsPopup';
-import {Button} from '@gravity-ui/uikit';
+import {usePopupState, useTranslation} from '../../hooks';
+import {SubscribeData} from '../../models';
import {Control} from '../Control';
-
-import {SubscribeData, Lang} from '../../models';
-
-import SubscribeIcon from '../../../assets/icons/subscribe.svg';
+import {ControlsLayoutContext} from '../Controls/ControlsLayout';
import './Subscribe.scss';
-import {PopperPosition} from '../../hooks';
+import {SubscribeSuccessPopup} from './SubscribeSuccessPopup';
+import {SubscribeVariantsPopup} from './SubscribeVariantsPopup';
const b = block('dc-subscribe');
@@ -22,100 +20,116 @@ export enum SubscribeView {
}
export interface SubscribeProps {
- lang: Lang;
- isVerticalView?: boolean;
- onSubscribe?: (data: SubscribeData) => void;
+ onSubscribe: (data: SubscribeData) => void;
view?: SubscribeView;
- classNameControl?: string;
- popupPosition?: PopperPosition;
}
-const Subscribe: React.FC = React.memo((props) => {
- const {isVerticalView, onSubscribe, view, classNameControl, popupPosition, t} = props;
-
- const subscribeControlRef = useRef(null);
-
- const [showSubscribeSuccessPopup, setShowSubscribeSuccessPopup] = useState(false);
- const [showSubscribeVariantsPopup, setShowSubscribeVariantsPopup] = useState(false);
-
- const setSubscribeControlRef = useCallback((ref) => {
- subscribeControlRef.current = ref;
- }, []);
-
- const onChangeSubscribe = useCallback(() => {
- setShowSubscribeVariantsPopup(!showSubscribeVariantsPopup);
- setShowSubscribeSuccessPopup(false);
- }, [showSubscribeVariantsPopup]);
+type SubscribeControlProps = {
+ view: SubscribeView;
+ onChangeSubscribe: () => void;
+};
+
+const SubscribeControl = memo(
+ forwardRef(({view, onChangeSubscribe}, ref) => {
+ const {isVerticalView, popupPosition, controlClassName, controlSize} =
+ useContext(ControlsLayoutContext);
+ const {t} = useTranslation('controls');
+
+ if (view === SubscribeView.Wide) {
+ return (
+
+ );
+ }
- const renderSubscribeControl = useCallback(() => {
return (
}
popupPosition={popupPosition}
/>
);
- }, [
- classNameControl,
- view,
- isVerticalView,
- setSubscribeControlRef,
- onChangeSubscribe,
- popupPosition,
- t,
- ]);
-
- const renderWideSubscribeControls = useCallback(() => {
+ }),
+);
+
+SubscribeControl.displayName = 'SubscribeControl';
+
+const SubscribeControlsLayout = memo>(
+ ({view, children}) => {
+ const {t} = useTranslation('controls');
+
+ if (view === SubscribeView.Regular) {
+ return {children};
+ }
+
return (
-
{t('main-question')}
-
-
-
+
{t('main-question')}
+
{children}
);
- }, [view, setSubscribeControlRef, onChangeSubscribe, t]);
+ },
+);
+
+SubscribeControlsLayout.displayName = 'SubscribeControlsLayout';
+
+const Subscribe = memo((props) => {
+ const {view = SubscribeView.Regular, onSubscribe} = props;
- const renderSubscribeControls = useCallback(() => {
- return view === SubscribeView.Regular
- ? renderSubscribeControl()
- : renderWideSubscribeControls();
- }, [view, renderSubscribeControl, renderWideSubscribeControls]);
+ const subscribeControlRef = useRef(null);
+
+ const successPopup = usePopupState({autoclose: 60000});
+ const variantsPopup = usePopupState();
+
+ const onChangeSubscribe = useCallback(() => {
+ variantsPopup.toggle();
+ successPopup.close();
+ }, [successPopup, variantsPopup]);
+
+ const onSubmitVariants = useCallback(() => {
+ variantsPopup.close();
+ successPopup.open();
+ }, [successPopup, variantsPopup]);
return (
- {renderSubscribeControls()}
- {showSubscribeSuccessPopup && subscribeControlRef.current && (
+
+
+
+ {successPopup.visible && subscribeControlRef.current && (
)}
- {showSubscribeVariantsPopup && subscribeControlRef.current && (
+ {variantsPopup.visible && subscribeControlRef.current && (
{
- setShowSubscribeVariantsPopup(false);
- setShowSubscribeSuccessPopup(true);
- }}
- {...{view, isVerticalView, anchor: subscribeControlRef, t}}
+ onSubmit={onSubmitVariants}
/>
)}
@@ -124,4 +138,4 @@ const Subscribe: React.FC = React.memo((props)
Subscribe.displayName = 'Subscribe';
-export default withTranslation('controls')(Subscribe);
+export default Subscribe;
diff --git a/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx b/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx
index 2f8d4733..6243ebeb 100644
--- a/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx
+++ b/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx
@@ -1,43 +1,36 @@
-import React, {memo, useCallback} from 'react';
-import {WithTranslation, withTranslation} from 'react-i18next';
-import block from 'bem-cn-lite';
import {Popup} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {memo, useContext} from 'react';
+import {useTranslation} from '../../../hooks';
+import {ControlsLayoutContext} from '../../Controls/ControlsLayout';
import {SubscribeView} from '../Subscribe';
-import useTimeout from '../../../hooks/useTimeout';
import {getPopupPosition} from '../utils';
const b = block('dc-subscribe');
-const SubscribeSuccessPopup: React.FC<
- {
- anchor: React.RefObject;
- isVerticalView?: boolean;
- view?: SubscribeView;
- visible: boolean;
- setVisible: (value: boolean) => void;
- } & WithTranslation
-> = memo((props) => {
- const {t, visible, setVisible, anchor, isVerticalView, view} = props;
-
- const hide = useCallback(() => setVisible(false), [setVisible]);
-
- useTimeout(hide, 60000);
+const SubscribeSuccessPopup = memo<{
+ anchor: React.RefObject;
+ view?: SubscribeView;
+ onOutsideClick: () => void;
+}>(({anchor, view, onOutsideClick}) => {
+ const {t} = useTranslation('controls');
+ const {isVerticalView} = useContext(ControlsLayoutContext);
return (
- {t('verify-title')}
- {t('verify-text')}
+ {t('verify-title')}
+ {t('verify-text')}
);
});
SubscribeSuccessPopup.displayName = 'SubscribeSuccessPopup';
-export default withTranslation('controls')(SubscribeSuccessPopup);
+export default SubscribeSuccessPopup;
diff --git a/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx b/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx
index d5b87f6d..bc502916 100644
--- a/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx
+++ b/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx
@@ -1,32 +1,28 @@
-import React, {memo, useCallback, useState} from 'react';
-import {WithTranslation, withTranslation} from 'react-i18next';
-import block from 'bem-cn-lite';
import {Button, List, Popup, TextInput} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import React, {SyntheticEvent, memo, useCallback, useContext, useState} from 'react';
-import {SubscribeView} from '../Subscribe';
+import {useTranslation} from '../../../hooks';
+import {SubscribeData, SubscribeType} from '../../../models';
import {isInvalidEmail} from '../../../utils';
+import {ControlsLayoutContext} from '../../Controls/ControlsLayout';
+import {SubscribeView} from '../Subscribe';
import {getPopupPosition} from '../utils';
-import {SubscribeData, SubscribeType} from '../../../models';
-
const b = block('dc-subscribe');
const LIST_ITEM_HEIGHT = 36;
-const SubscribeVariantsPopup: React.FC<
- {
- anchor: React.RefObject;
- visible: boolean;
- setVisible: (value: boolean) => void;
- isVerticalView?: boolean;
- view?: SubscribeView;
- onSubscribe?: (data: SubscribeData) => void;
- onSubmit: () => void;
- } & WithTranslation
-> = memo((props) => {
- const {t, visible, setVisible, anchor, view, isVerticalView, onSubscribe, onSubmit} = props;
-
- const hide = useCallback(() => setVisible(false), [setVisible]);
+const SubscribeVariantsPopup = memo<{
+ anchor: React.RefObject;
+ view?: SubscribeView;
+ onSubscribe?: (data: SubscribeData) => void;
+ onSubmit: () => void;
+ onOutsideClick: () => void;
+}>((props) => {
+ const {t} = useTranslation('controls');
+ const {isVerticalView} = useContext(ControlsLayoutContext);
+ const {anchor, view, onSubscribe, onSubmit, onOutsideClick} = props;
const [email, setEmail] = useState('');
const [showError, setShowError] = useState('');
@@ -37,7 +33,7 @@ const SubscribeVariantsPopup: React.FC<
}, []);
const onSendSubscribeInformation = useCallback(
- (event) => {
+ (event: SyntheticEvent) => {
event.preventDefault();
if (isInvalidEmail(email)) {
@@ -56,7 +52,6 @@ const SubscribeVariantsPopup: React.FC<
onSubmit();
} catch (e) {
- console.error(e);
setShowError(t('email-request-fail'));
}
@@ -102,7 +97,7 @@ const SubscribeVariantsPopup: React.FC<
@@ -112,8 +107,8 @@ const SubscribeVariantsPopup: React.FC<
return (
@@ -125,4 +120,4 @@ const SubscribeVariantsPopup: React.FC<
SubscribeVariantsPopup.displayName = 'SubscribeVariantsPopup';
-export default withTranslation('controls')(SubscribeVariantsPopup);
+export default SubscribeVariantsPopup;
diff --git a/src/components/Toc/Toc.tsx b/src/components/Toc/Toc.tsx
index ec9e4822..84ccf50a 100644
--- a/src/components/Toc/Toc.tsx
+++ b/src/components/Toc/Toc.tsx
@@ -1,18 +1,16 @@
-import React from 'react';
-
import block from 'bem-cn-lite';
import {omit} from 'lodash';
-import {ControlSizes, Lang, Router, TocData, TocItem} from '../../models';
+import React from 'react';
import {PopperPosition} from '../../hooks';
+import {ControlSizes, Router, TocData, TocItem} from '../../models';
import {isActiveItem, normalizeHash, normalizePath} from '../../utils';
-import {Controls} from '../Controls';
+import {Controls, ControlsLayout} from '../Controls';
import {HTML} from '../HTML';
import {TocItem as Item} from '../TocItem';
-import {TocItemRegistry} from './TocItemRegistry';
-
import './Toc.scss';
+import {TocItemRegistry} from './TocItemRegistry';
const b = block('dc-toc');
const HEADER_DEFAULT_HEIGHT = 0;
@@ -26,7 +24,6 @@ export interface TocProps extends TocData {
headerHeight?: number;
tocTitleIcon?: React.ReactNode;
hideTocHeader?: boolean;
- lang: Lang;
singlePage?: boolean;
onChangeSinglePage?: (value: boolean) => void;
pdfLink?: string;
@@ -242,19 +239,21 @@ class Toc extends React.Component {
}
private renderBottom() {
- const {lang, singlePage, onChangeSinglePage, pdfLink} = this.props;
+ const {singlePage, onChangeSinglePage, pdfLink} = this.props;
const {contentScrolled} = this.state;
return (
-
+ popupPosition={PopperPosition.TOP_START}
+ >
+
+
);
}
diff --git a/src/components/TocItem/TocItem.tsx b/src/components/TocItem/TocItem.tsx
index 5979fb21..4e5379b8 100644
--- a/src/components/TocItem/TocItem.tsx
+++ b/src/components/TocItem/TocItem.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
import block from 'bem-cn-lite';
+import React from 'react';
import {TocItem as ITocItem} from '../../models';
import {isExternalHref} from '../../utils';
diff --git a/src/components/TocNavPanel/TocNavPanel.tsx b/src/components/TocNavPanel/TocNavPanel.tsx
index eddf89c9..49c51b8d 100644
--- a/src/components/TocNavPanel/TocNavPanel.tsx
+++ b/src/components/TocNavPanel/TocNavPanel.tsx
@@ -1,14 +1,12 @@
-import React from 'react';
+import ArrowLeftIcon from '@gravity-ui/icons/svgs/arrow-left.svg';
+import ArrowRightIcon from '@gravity-ui/icons/svgs/arrow-right.svg';
+import {Icon} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
+import React, {memo, useMemo} from 'react';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-
-import {TocData, TocItem, Router, Lang} from '../../models';
-
-
-import {isExternalHref, isActiveItem} from '../../utils';
-import ArrowLeft from '@gravity-ui/icons/svgs/arrow-left.svg';
-import ArrowRight from '@gravity-ui/icons/svgs/arrow-right.svg';
+import {useTranslation} from '../../hooks';
+import {Router, TocData, TocItem} from '../../models';
+import {isActiveItem, isExternalHref} from '../../utils';
import './TocNavPanel.scss';
@@ -16,130 +14,96 @@ const b = block('dc-nav-toc-panel');
export interface TocNavPanelProps extends TocData {
router: Router;
- lang: Lang;
fixed?: boolean;
className?: string;
}
-type TocNavPanelInnerProps = TocNavPanelProps & WithTranslation & WithTranslationProps;
-
interface FlatTocItem {
name: string;
href: string;
}
-interface TocNavPanelState {
- flatToc: FlatTocItem[];
- activeItemIndex: number;
-}
-
-class TocNavPanel extends React.Component {
- state: TocNavPanelState = {
- flatToc: [],
- activeItemIndex: 0,
- };
-
- componentDidMount() {
- this.setState(this.getState(this.props));
- }
-
- componentDidUpdate(prevProps: TocNavPanelProps) {
- const {router, i18n, lang} = this.props;
+function getFlatToc(items: TocItem[]): FlatTocItem[] {
+ return items.reduce((result, {href, name, items: subItems}) => {
+ const part: FlatTocItem[] = subItems ? getFlatToc(subItems) : [];
- if (prevProps.router.pathname !== router.pathname) {
- this.setState(this.getState(this.props));
+ if (href) {
+ part.push({name, href});
}
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
+ return result.concat(part);
+ }, [] as FlatTocItem[]);
+}
- render() {
- const {flatToc, activeItemIndex} = this.state;
- const {fixed = false, className} = this.props;
+function getBoundingItems(flatToc: FlatTocItem[], router: Router) {
+ for (let i = 0; i < flatToc.length; i++) {
+ const {href} = flatToc[i];
- if (!flatToc.length) {
- return null;
+ if (isActiveItem(router, href)) {
+ return {
+ prevItem: flatToc[i - 1] || null,
+ nextItem: flatToc[i + 1] || null,
+ };
}
-
- const prevItem = activeItemIndex > 0 ? flatToc[activeItemIndex - 1] : null;
- const nextItem = activeItemIndex < flatToc.length - 1 ? flatToc[activeItemIndex + 1] : null;
-
- return (
-
-
- {this.renderControl(prevItem)}
- {this.renderControl(nextItem, true)}
-
-
- );
}
- private renderControl(tocItem: FlatTocItem | null, isNext?: boolean) {
- const {t} = this.props;
- const keyHint = isNext ? 'hint_next' : 'hint_previous';
-
- return (
-
- {tocItem && (
-
- {t(keyHint)}
- {this.renderLink(tocItem, isNext)}
-
- )}
-
- );
- }
+ return {
+ prevItem: null,
+ nextItem: null,
+ };
+}
- private renderLink(item: FlatTocItem | null, isNext?: boolean) {
- if (!item) {
- return null;
- }
+const TocNavControl = memo<{isNext?: boolean; item: FlatTocItem}>(({item, isNext}) => {
+ const {t} = useTranslation('toc-nav-panel');
+ const keyHint = isNext ? 'hint_next' : 'hint_previous';
+ const isExternal = isExternalHref(item.href);
+ const linkAttributes = {
+ href: item.href,
+ target: isExternal ? '_blank' : '_self',
+ rel: isExternal ? 'noopener noreferrer' : undefined,
+ };
- const isExternal = isExternalHref(item.href);
- const linkAttributes = {
- href: item.href,
- target: isExternal ? '_blank' : '_self',
- rel: isExternal ? 'noopener noreferrer' : undefined,
- };
-
- return (
-
- {!isNext && }
- {item.name}
- {isNext && }
-
- );
+ return (
+
+ {item && (
+
+ {t(keyHint)}
+
+
+ )}
+
+ );
+});
+
+TocNavControl.displayName = 'TocNavControl';
+
+const TocNavPanel = memo(({items, router, fixed, className}) => {
+ const flatToc = useMemo(() => getFlatToc(items), [items]);
+ const {prevItem, nextItem} = useMemo(
+ () => getBoundingItems(flatToc, router),
+ [flatToc, router],
+ );
+
+ if (!flatToc.length) {
+ return null;
}
- private getState(props: TocNavPanelProps): TocNavPanelState {
- const flatToc: FlatTocItem[] = [];
- let activeItemIndex = 0;
-
- function processItems(items: TocItem[]) {
- items.forEach(({href, name, items: subItems}) => {
- if (subItems) {
- processItems(subItems);
- }
-
- if (href) {
- flatToc.push({
- name,
- href,
- });
- }
-
- if (href && isActiveItem(props.router, href)) {
- activeItemIndex = flatToc.length - 1;
- }
- });
- }
-
- processItems(props.items);
+ return (
+
+
+ {prevItem && }
+ {nextItem && }
+
+
+ );
+});
- return {flatToc, activeItemIndex};
- }
-}
+TocNavPanel.displayName = 'TocNavPanel';
-export default withTranslation('toc-nav-panel')(TocNavPanel);
+export default TocNavPanel;
diff --git a/src/components/ToggleArrow/ToggleArrow.tsx b/src/components/ToggleArrow/ToggleArrow.tsx
index 0749bba2..82bcfdaa 100644
--- a/src/components/ToggleArrow/ToggleArrow.tsx
+++ b/src/components/ToggleArrow/ToggleArrow.tsx
@@ -1,7 +1,6 @@
-import React from 'react';
-import block from 'bem-cn-lite';
-
import ChevronIcon from '@gravity-ui/icons/svgs/chevron-right.svg';
+import block from 'bem-cn-lite';
+import React from 'react';
import './ToggleArrow.scss';
diff --git a/src/constants.ts b/src/constants.ts
index a9a2c779..95fed168 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,26 +1,12 @@
import {Lang, TextSizes, Theme} from './models';
-export const DISLIKE_VARIANTS = {
- [Lang.Ru]: [
- 'Нет ответа на мой вопрос',
- 'Рекомендации не помогли',
- 'Содержание не соответствует заголовку',
- 'Другое',
- ],
- [Lang.En]: [
- 'No answer to my question',
- "Recommendations didn't help",
- "The content doesn't match the title",
- 'Other',
- ],
-};
-
export const DEFAULT_SETTINGS = {
fullScreen: false,
singlePage: false,
wideFormat: false,
showMiniToc: true,
bookmarkedPage: false,
+ lang: Lang.En,
theme: Theme.Dark,
textSize: TextSizes.M,
isLiked: false,
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 10e96b6b..705684b9 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -1,3 +1,4 @@
-export * from './useForkRef';
export * from './usePopper';
+export * from './useTimer';
export * from './useTranslation';
+export * from './usePopupState';
diff --git a/src/hooks/useForkRef.ts b/src/hooks/useForkRef.ts
deleted file mode 100644
index 4d6f070b..00000000
--- a/src/hooks/useForkRef.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import {MutableRefObject, Ref, RefCallback, useMemo} from 'react';
-
-function setRef(
- value: T | null,
- ref?: MutableRefObject | RefCallback | null,
-): void {
- if (typeof ref === 'function') {
- ref(value);
- } else if (ref) {
- ref.current = value;
- }
-}
-
-export function useForkRef(refA?: Ref | null, refB?: Ref | null): Ref | null {
- return useMemo(() => {
- if (refA === null && refB === null) {
- return null;
- }
-
- return (value) => {
- setRef(value, refA);
- setRef(value, refB);
- };
- }, [refA, refB]);
-}
diff --git a/src/hooks/useIsomorphicLayoutEffect.ts b/src/hooks/useIsomorphicLayoutEffect.ts
deleted file mode 100644
index c8a149b0..00000000
--- a/src/hooks/useIsomorphicLayoutEffect.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {useEffect, useLayoutEffect} from 'react';
-
-const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
-
-export default useIsomorphicLayoutEffect;
diff --git a/src/hooks/usePopper.ts b/src/hooks/usePopper.ts
index 4b709032..7aa1014a 100644
--- a/src/hooks/usePopper.ts
+++ b/src/hooks/usePopper.ts
@@ -1,5 +1,5 @@
-import React from 'react';
import popper from '@popperjs/core';
+import React from 'react';
import {Modifier, usePopper as useReactPopper} from 'react-popper';
export type PopperPlacement = popper.Placement | popper.Placement[];
diff --git a/src/hooks/usePopupState.ts b/src/hooks/usePopupState.ts
new file mode 100644
index 00000000..2210f242
--- /dev/null
+++ b/src/hooks/usePopupState.ts
@@ -0,0 +1,42 @@
+import {useCallback, useMemo, useState} from 'react';
+
+import {useTimer} from './useTimer';
+
+export const usePopupState = ({autoclose = 0}: {autoclose?: number} = {}) => {
+ const [visible, setVisible] = useState(false);
+ const [setTimer, clearTimer] = useTimer(autoclose);
+
+ const close = useCallback(() => {
+ setVisible(false);
+
+ if (autoclose) {
+ clearTimer();
+ }
+ }, [autoclose, clearTimer, setVisible]);
+ const open = useCallback(() => {
+ setVisible(true);
+
+ if (autoclose) {
+ setTimer(close);
+ }
+ }, [autoclose, close, setTimer, setVisible]);
+ const toggle = useCallback(() => {
+ if (visible) {
+ close();
+ } else {
+ open();
+ }
+ }, [visible, close, open]);
+
+ return useMemo(
+ () => ({
+ open,
+ close,
+ toggle,
+ get visible() {
+ return visible;
+ },
+ }),
+ [visible, open, close, toggle],
+ );
+};
diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts
deleted file mode 100644
index 3c16752c..00000000
--- a/src/hooks/useTimeout.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {useEffect, useRef} from 'react';
-import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
-
-function useTimeout(callback: () => void, delay: number | null) {
- const savedCallback = useRef(callback);
-
- useIsomorphicLayoutEffect(() => {
- savedCallback.current = callback;
- }, [callback]);
-
- useEffect(() => {
- if (!delay && delay !== 0) {
- return undefined;
- }
-
- const id = setTimeout(() => savedCallback.current(), delay);
-
- return () => clearTimeout(id);
- }, [delay]);
-}
-
-export default useTimeout;
diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts
new file mode 100644
index 00000000..d682c5c5
--- /dev/null
+++ b/src/hooks/useTimer.ts
@@ -0,0 +1,22 @@
+import {useCallback, useRef} from 'react';
+
+export function useTimer(timeout = 3000) {
+ const timerId = useRef();
+
+ const clearTimer = useCallback(() => {
+ clearTimeout(timerId.current as number);
+ timerId.current = undefined;
+ }, []);
+
+ const setTimer = useCallback(
+ (callback: () => void) => {
+ timerId.current = setTimeout(() => {
+ clearTimer();
+ callback();
+ }, timeout);
+ },
+ [timeout, clearTimer],
+ );
+
+ return [setTimer, clearTimer] as const;
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index aa542aa6..f5392ce1 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -72,6 +72,12 @@
"button-dislike-text": "No",
"main-question": "Was the article helpful?"
},
+ "feedback-variants": {
+ "irrelevant-answer": "No answer to my question",
+ "useless-recs": "Recommendations didn't help",
+ "content-mismatch": "The content doesn't match the title",
+ "other": "Other"
+ },
"search-bar": {
"search-query-label": "Found on request",
"close": "Close",
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
deleted file mode 100644
index 88508290..00000000
--- a/src/i18n/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import i18n from 'i18next';
-import {initReactI18next} from 'react-i18next';
-import {Lang} from '../models';
-
-import ru from './ru.json';
-import en from './en.json';
-
-export default i18n.use(initReactI18next).init({
- fallbackLng: Lang.En,
- ns: ['controls', 'mini-toc', 'toc-nav-panel', 'authors'],
- resources: {ru, en},
- interpolation: {
- escapeValue: false,
- },
-});
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 7de7fb27..56638a00 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -72,6 +72,12 @@
"button-dislike-text": "Нет",
"main-question": "Была ли статья полезна?"
},
+ "feedback-variants": {
+ "irrelevant-answer": "Нет ответа на мой вопрос",
+ "useless-recs": "Рекомендации не помогли",
+ "content-mismatch": "Содержание не соответствует заголовку",
+ "other": "Другое"
+ },
"search-bar": {
"search-query-label": "Найдено по запросу",
"close": "Закрыть",
diff --git a/src/index.ts b/src/index.ts
index afd416ca..09e61511 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,3 @@
-// init i18n
-import './i18n';
-
export * from './components/Breadcrumbs';
export * from './components/DocLayout';
export * from './components/DocLeadingPage';
@@ -27,4 +24,6 @@ export * from './components/Paginator';
export * from './components/SearchItem';
export * from './components/SearchPage';
+export * from './config';
export * from './models';
+export * from './constants';
diff --git a/src/models/index.ts b/src/models/index.ts
index 13f1f0a2..46cc22bf 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -1,3 +1,5 @@
+import type {Loc} from '../config/i18n';
+
export enum Theme {
Light = 'light',
Dark = 'dark',
@@ -14,6 +16,11 @@ export enum ControlSizes {
L = 'l',
}
+export interface Config {
+ lang?: string;
+ loc?: Loc;
+}
+
export interface DocSettings {
fullScreen?: boolean;
singlePage?: boolean;