diff --git a/.eslintrc.js b/.eslintrc.js index bc9579de..15cc4d55 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,25 @@ module.exports = { - extends: ['@gravity-ui/eslint-config'], + extends: ['@gravity-ui/eslint-config', '@gravity-ui/eslint-config/prettier'], root: true, rules: { 'no-param-reassign': 'off', + 'import/order': [ + 'error', + { + alphabetize: { + order: 'asc', + }, + 'newlines-between': 'always', + groups: [['builtin', 'external'], 'internal', 'parent', 'sibling', 'index'], + warnOnUnassignedImports: true, + pathGroups: [ + { + pattern: '*.s?css$', + group: 'index', + }, + ], + }, + ], }, overrides: [ { diff --git a/V3.md b/V3.md new file mode 100644 index 00000000..0a66afd1 --- /dev/null +++ b/V3.md @@ -0,0 +1,167 @@ +## Breaking + +- Update to `@gravity-ui/uilit@5` +- Remove peer dependency on `@doc-tools/transform`
+ `@doc-tools/transform` js and css bundles should be attached directly to final projects +- `DISLIKE_VARIANTS` not exported from package. Use `i18n['feedback-variants']` instead. +- Prop `lang` war removed from component. Now you should use `configure` helper + + ```js + import {configure} from '@doc-tools/components'; + + configure({ + lang: 'ru', + }); + ``` + +### BookmarkButton + +- `bookmarkedPage` prop was changed to `isBookmarked`. (Same as `isLiked`, `isDislaked` in Feedback component) +- `onChangeBookmarkPage` renamed to `onBookmark` + +### Contributors + +- Removed `lang` prop + +### Control + +- Replace `setRef` prop with `ref` (using forwardRef now) + +### Controls + +- Removed `lang` prop +- Removed `isVerticalView`, `controlSize`, `popupPosition` prop. Configure it with `ControlsLayout` wrapper component + ```jsx + + + + ``` +- Prop `showEditControl` replaced by `hideEditControl` + +### DividerControl + +- Removed `size`, `isVerticalView` props +- Value for `size`, `isVerticalView` now stored in `ControlsLayoutContext` + +### FullScreenControl + +- Removed `lang` prop +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop +- Icon replaced with equal from `@gravity-ui/uikit` + +### LangControl + +- Prop `onChangeLang` is required now +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop +- Icon replaced with equal from `@gravity-ui/uikit` + +### PdfControl + +- Prop `pdfLink` is required now +- Removed `lang` prop +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop + +### SettingsControl + +- Removed `lang` prop +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop + +### SinglePageControl + +- Prop `onChange` is required now +- Removed `lang` prop +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop +- Icons replaced with equal from `@gravity-ui/uikit` + +### DocLayout + +- Removed `lang` prop +- + +### DocLeadingPage + +- Removed `lang` prop + +### DocPageTitle + +- Ownership on bookmarks was removed. + Bookmarks should be passed as children. +- Removed `bookmarkedPage`, `onChangeBookmarkPage` props. + +### EditButton + +- Was removed in favor of EditControl + +### ErrorPage + +- Removed `lang` prop + +### Feedback + +- Removed `lang` prop +- Removed `singlePage` prop +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Removed `dislikeVariants` prop. Variants should be configured via `configure` util + ```js + configure({ + loc: { + en: { + 'feedback-variants': { + variant1: 'test1', + }, + }, + ru: { + 'feedback-variants': { + variant1: 'текс1', + }, + }, + }, + }); + ``` +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop +- Icons replaced with equal from `@gravity-ui/uikit` + +### MiniToc + +- Removed `lang` prop +- MiniToc returns `null` on empty headings. (Previously it render useless title) + +### SearchBar + +- Removed `lang` prop +- Icons replaced with equal from `@gravity-ui/uikit` + +### SearchItem + +- Removed `lang` prop + +### SearchPage + +- Removed `lang` prop + +### Subscribe + +- Prop `onSubscribe` is required now +- Removed `lang` prop +- Removed `size`, `isVerticalView`, `className`, `popupPosition` props +- Value for `size`, `isVerticalView`, `popupPosition` now stored in `ControlsLayoutContext` +- Value for `className` now stored in `ControlsLayoutContext.controlClassName` prop + +### Toc + +- Removed `lang` prop + +### TocNavPanel + +- Removed `lang` prop diff --git a/assets/icons/full-screen-clicked.svg b/assets/icons/full-screen-clicked.svg deleted file mode 100644 index 46628f63..00000000 --- a/assets/icons/full-screen-clicked.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/single-page-clicked.svg b/assets/icons/single-page-clicked.svg deleted file mode 100644 index 2d486fa9..00000000 --- a/assets/icons/single-page-clicked.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/single-page.svg b/assets/icons/single-page.svg deleted file mode 100644 index 63a0b1c3..00000000 --- a/assets/icons/single-page.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/subscribe.svg b/assets/icons/subscribe.svg deleted file mode 100644 index c7ce627b..00000000 --- a/assets/icons/subscribe.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/unsubscribe.svg b/assets/icons/unsubscribe.svg deleted file mode 100644 index 3ff24e61..00000000 --- a/assets/icons/unsubscribe.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/package.json b/package.json index f90a6542..ca882a30 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,11 @@ } }, "./assets/icons/*.svg": "./assets/icons/*.svg", - "./styles": "./build/index.css" + "./styles": "./build/index.css", + "./themes/*": { + "style": "./build/themes/*/index.css", + "default": "./build/themes/*/index.css" + } }, "sideEffects": [ "*.css", @@ -45,8 +49,8 @@ "deps:truncate": "npm prune --production", "lint": "run-p _lint:js _lint:styles _lint:prettier", "lint:fix": "run-s _lint:js:fix _lint:styles:fix _lint:prettier:fix", - "_lint:js": "eslint 'src/**/*.{js,jsx,ts,tsx}'", - "_lint:js:fix": "npm run _lint:js -- --quiet --fix", + "_lint:js": "eslint '**/*.{js,jsx,ts,tsx}'", + "_lint:js:fix": "npm run _lint:js -- --fix", "_lint:prettier": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'", "_lint:prettier:fix": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", "_lint:styles": "stylelint src/**/*.scss", @@ -61,7 +65,7 @@ "_build:declarations:cjs": "tsc --emitDeclarationOnly -p tsconfig.cjs.json", "build": "run-s build:clean build:compile _build:declarations:*", "_build:watch": "./esbuild/build.js --watch", - "_storybook:watch": "cd demo && start-storybook -p 7008", + "_storybook:watch": "cd demo && npm run storybook", "prepublishOnly": "npm run lint && npm run test && npm run build", "prepare": "husky install", "pre-commit": "lint-staged" @@ -78,7 +82,8 @@ "react-hotkeys-hook": "^3.3.1", "react-i18next": "11.15.6", "react-popper": "^2.2.5", - "scroll-into-view-if-needed": "2.2.29" + "scroll-into-view-if-needed": "2.2.29", + "url": "^0.11.1" }, "peerDependencies": { "react": ">=16.8.0 || >=17.0.0 || >=18.0.0", @@ -106,20 +111,22 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "rimraf": "^5.0.1", + "sass": "^1.66.1", "stylelint": "^15.10.3", "svgo": "2.8.0", "typescript": "^5.2.2" }, "lint-staged": { - "src/**/*.{js,jsx,ts,tsx}": [ - "eslint --max-warnings=0 --fix -c eslint.publish.json", + "**/*.{js,jsx,ts,tsx}": [ + "eslint --max-warnings=0 --fix -c .eslintrc.publish.js", "prettier --write" ], - "src/**/*.{css,scss}": [ + "**/*.{css,scss}": [ "stylelint --fix", "prettier --write" ], - "src/**/*.{json,yaml,yml,md}": [ + "**/*.{json,yaml,yml,md}": [ "prettier --write" ], "*.svg": [ diff --git a/src/components/BookmarkButton/BookmarkButton.tsx b/src/components/BookmarkButton/BookmarkButton.tsx index 375e6669..be8045c9 100644 --- a/src/components/BookmarkButton/BookmarkButton.tsx +++ b/src/components/BookmarkButton/BookmarkButton.tsx @@ -1,10 +1,8 @@ -import React from 'react'; - -import {Button, Icon} from '@gravity-ui/uikit'; -import block from 'bem-cn-lite'; - import StarActive from '@gravity-ui/icons/svgs/star-fill.svg'; import StarInactive from '@gravity-ui/icons/svgs/star.svg'; +import {Button, Icon} from '@gravity-ui/uikit'; +import block from 'bem-cn-lite'; +import React from 'react'; import './BookmarkButton.scss'; @@ -12,24 +10,21 @@ const b = block('dc-bookmark-button'); export interface BookmarkButtonProps { className?: string; - bookmarkedPage: boolean; - onChangeBookmarkPage: (value: boolean) => void; + isBookmarked: boolean; + onBookmark: (value: boolean) => void; } -export const BookmarkButton: React.FC = ({ - bookmarkedPage, - onChangeBookmarkPage, -}) => { +export const BookmarkButton: React.FC = ({isBookmarked, onBookmark}) => { return ( ); diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx index c41bc5e7..f80b23b0 100644 --- a/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -1,6 +1,5 @@ -import React from 'react'; - import block from 'bem-cn-lite'; +import React from 'react'; import {BreadcrumbItem} from '../../models'; @@ -12,6 +11,7 @@ export interface BreadcrumbsProps { items: BreadcrumbItem[]; className?: string; } + export const Breadcrumbs: React.FC = ({items, className}) => { if (!items || !items.length) { return null; diff --git a/src/components/Contributors/Contributors.tsx b/src/components/Contributors/Contributors.tsx index f472c87c..d8dc3fda 100644 --- a/src/components/Contributors/Contributors.tsx +++ b/src/components/Contributors/Contributors.tsx @@ -1,16 +1,15 @@ -import React, {useEffect} from 'react'; import block from 'bem-cn-lite'; -import {useTranslation} from 'react-i18next'; +import React from 'react'; +import {useTranslation} from '../../hooks'; +import {Contributor} from '../../models'; import {ContributorAvatars} from '../ContributorAvatars'; -import {Lang, Contributor} from '../../models'; import './Contributors.scss'; const b = block('contributors'); export interface ContributorsProps { - lang: Lang; users: Contributor[]; onlyAuthor?: boolean; isAuthor?: boolean; @@ -18,18 +17,8 @@ export interface ContributorsProps { } const Contributors: React.FC = (props) => { - const { - users, - lang, - onlyAuthor = false, - isAuthor = false, - translationName = 'contributors', - } = props; - const {t, i18n} = useTranslation(translationName); - - useEffect(() => { - i18n.changeLanguage(lang); - }, [i18n, lang]); + const {users, onlyAuthor = false, isAuthor = false, translationName = 'contributors'} = props; + const {t} = useTranslation(translationName); return (
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 ( -
-