diff --git a/demo/package-lock.json b/demo/package-lock.json index b1af6af8..d2072b94 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -38,17 +38,20 @@ } }, "..": { - "version": "3.10.0-beta.0", + "name": "@diplodoc/components", + "version": "4.7.0", "license": "MIT", "dependencies": { + "@gravity-ui/components": "^3.6.0", "@gravity-ui/icons": "^2.5.0", - "@gravity-ui/uikit": "^6.0.0", + "@gravity-ui/uikit": "^6.2.0", "@popperjs/core": "^2.11.2", "bem-cn-lite": "4.1.0", "i18next": "^19.9.2", "langs": "^2.0.0", "lodash": "^4.17.21", "mark.ts": "^1.0.5", + "react-gtm-module": "^2.0.11", "react-hotkeys-hook": "^3.3.1", "react-i18next": "11.15.6", "react-popper": "^2.2.5", @@ -64,12 +67,13 @@ "@types/lodash": "4.14.179", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "@types/react-gtm-module": "^2.0.3", "autoprefixer": "^10.4.15", "esbuild": "^0.19.2", "esbuild-sass-plugin": "^2.13.0", "eslint": "^8.48.0", "husky": "^8.0.3", - "lint-staged": "^14.0.1", + "lint-staged": "^12.5.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.28", "postcss-preset-env": "^9.1.2", @@ -77,7 +81,6 @@ "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", @@ -18708,17 +18711,19 @@ "@diplodoc/components": { "version": "file:..", "requires": { + "@gravity-ui/components": "^3.6.0", "@gravity-ui/eslint-config": "^2.2.0", "@gravity-ui/icons": "^2.5.0", "@gravity-ui/prettier-config": "^1.0.1", "@gravity-ui/stylelint-config": "^3.0.0", "@gravity-ui/tsconfig": "^1.0.0", - "@gravity-ui/uikit": "^6.0.0", + "@gravity-ui/uikit": "^6.2.0", "@popperjs/core": "^2.11.2", "@types/langs": "^2.0.1", "@types/lodash": "4.14.179", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "@types/react-gtm-module": "^2.0.3", "autoprefixer": "^10.4.15", "bem-cn-lite": "4.1.0", "esbuild": "^0.19.2", @@ -18727,7 +18732,7 @@ "husky": "^8.0.3", "i18next": "^19.9.2", "langs": "^2.0.0", - "lint-staged": "^14.0.1", + "lint-staged": "^12.5.0", "lodash": "^4.17.21", "mark.ts": "^1.0.5", "npm-run-all": "^4.1.5", @@ -18737,10 +18742,10 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-gtm-module": "^2.0.11", "react-hotkeys-hook": "^3.3.1", "react-i18next": "11.15.6", "react-popper": "^2.2.5", - "rimraf": "^5.0.1", "sass": "^1.66.1", "scroll-into-view-if-needed": "2.2.29", "stylelint": "^15.10.3", diff --git a/src/components/DocLayout/DocLayout.scss b/src/components/DocLayout/DocLayout.scss index d623134d..7690e681 100644 --- a/src/components/DocLayout/DocLayout.scss +++ b/src/components/DocLayout/DocLayout.scss @@ -81,8 +81,19 @@ @media (max-width: map-get($screenBreakpoints, 'md') - 1) { flex-direction: column-reverse; + &__center + &__right:has(> .dc-subnavigation_invisible) { + margin-top: calc(0px - var(--dc-subheader-height, 52px)); + } + &__right { - display: none; + position: sticky; + top: calc(var(--dc-header-height) - 1px); + left: 0; + z-index: 119; + + height: fit-content; + width: 100%; + padding: 0; } &__left { @@ -93,6 +104,7 @@ position: static; width: auto; height: auto; + padding: 0 20px; .dc-toc { /* stylelint-disable-next-line declaration-no-important */ diff --git a/src/components/DocLeadingPage/DocLeadingPage.scss b/src/components/DocLeadingPage/DocLeadingPage.scss index 60845b62..cb602e78 100644 --- a/src/components/DocLeadingPage/DocLeadingPage.scss +++ b/src/components/DocLeadingPage/DocLeadingPage.scss @@ -20,6 +20,10 @@ margin: 40px 0 $normalOffset; @include text-size(display-2); + + @media (max-width: map-get($screenBreakpoints, 'md') - 1) { + margin-top: 20px; + } } &__description { @@ -43,6 +47,10 @@ flex-direction: row; margin-top: 70px; margin-bottom: -$blockMarginBottomLarge; + + @media (max-width: map-get($screenBreakpoints, 'md') - 1) { + margin-top: 20px; + } } } diff --git a/src/components/DocPage/DocPage.scss b/src/components/DocPage/DocPage.scss index 5aff0a1c..c631c417 100644 --- a/src/components/DocPage/DocPage.scss +++ b/src/components/DocPage/DocPage.scss @@ -27,6 +27,41 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); @include text-size(body-1); + @media (max-width: map-get($screenBreakpoints, 'md') - 1) { + position: absolute; + top: var(--dc-subheader-height); + z-index: 117; + + display: flex; + flex-direction: column; + align-items: flex-start; + + box-sizing: border-box; + max-height: 0px; + width: 100%; + padding: 0 16px 0 16px; + + background: var(--g-color-base-background, #FFF); + box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.15); + + transition: max-height 300ms 0s;; + + overflow-y: hidden; + + &_open-mini-toc { + max-height: 80vh; + } + + &_hidden-mini-toc { + padding: 0; + } + + &-mini-toc { + width: 100%; + padding: 16px 0 16px 0; + } + } + @media screen and (min-width: 1280px) { & { width: 200px; @@ -57,11 +92,11 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); } &__controls { - display: flex; + display: none; top: 0; align-items: center; height: 40px; - z-index: 102; + z-index: 121; position: absolute; right: 7px; @@ -80,6 +115,8 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); } @media (min-width: map-get($screenBreakpoints, 'md')) { + display: flex; + &_vertical { top: calc(6px + var(--dc-header-height, #{$headerHeight})); justify-content: center; @@ -145,9 +182,13 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); } &__title { - margin-bottom: 12px; + margin-top: 20px; @include text-size(display-2); + + @media (min-width: map-get($screenBreakpoints, 'md')) { + margin-bottom: 12px; + } } &__content { @@ -222,15 +263,16 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); @media (max-width: map-get($screenBreakpoints, 'md') - 1) { &__main { + margin-top: var(--dc-subheader-height); padding: 0 20px; } &__breadcrumbs { padding: 0 20px; } - + /* CHECK display: block; temporarily hidden */ &__content-mini-toc { - display: block; + display: none; } &__toc-nav-panel { @@ -327,7 +369,7 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); } & > p { - margin: 0 0 10px 0; + margin: 0 0 10px; &:first-child { &::before { @@ -348,10 +390,10 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); } $colors: ( - dc-accent-info: $infoColor, - dc-accent-tip: $tipColor, - dc-accent-alert: $importantColor, - dc-accent-warning: $warningColor + dc-accent-info: $infoColor, + dc-accent-tip: $tipColor, + dc-accent-alert: $importantColor, + dc-accent-warning: $warningColor, ); @each $type, $color in $colors { @@ -365,10 +407,10 @@ $importantBackgroundColor: rgba(235, 50, 38, 0.08); } $backgroundColors: ( - dc-accent-info: $infoBackgroundColor, - dc-accent-tip: $tipBackgroundColor, - dc-accent-alert: $importantBackgroundColor, - dc-accent-warning: $warningBackgroundColor + dc-accent-info: $infoBackgroundColor, + dc-accent-tip: $tipBackgroundColor, + dc-accent-alert: $importantBackgroundColor, + dc-accent-warning: $warningBackgroundColor, ); @each $type, $color in $backgroundColors { diff --git a/src/components/DocPage/DocPage.tsx b/src/components/DocPage/DocPage.tsx index fd7bf48e..1c363aff 100644 --- a/src/components/DocPage/DocPage.tsx +++ b/src/components/DocPage/DocPage.tsx @@ -28,6 +28,7 @@ import {Feedback, FeedbackView} from '../Feedback'; import {HTML} from '../HTML'; import {MiniToc} from '../MiniToc'; import {SearchBar, withHighlightedSearchWords} from '../SearchBar'; +import {SubNavigation} from '../SubNavigation'; import {TocNavPanel} from '../TocNavPanel'; import UpdatedAtDate from '../UpdatedAtDate/UpdatedAtDate'; @@ -84,6 +85,7 @@ export interface DocPageProps extends DocPageData, DocSettings { type DocPageInnerProps = InnerProps; type DocPageState = { + mobileMiniTocOpen: boolean; loading: boolean; keyDOM: number; showNotification: boolean; @@ -100,6 +102,7 @@ class DocPage extends React.Component { super(props); this.state = { + mobileMiniTocOpen: false, loading: props.singlePage, keyDOM: getRandomKey(), showNotification: true, @@ -155,6 +158,7 @@ class DocPage extends React.Component { 'full-screen': fullScreen, 'hidden-mini-toc': hideMiniToc, 'single-page': singlePage, + 'open-mini-toc': this.state.mobileMiniTocOpen, }; return ( @@ -164,7 +168,6 @@ class DocPage extends React.Component { headerHeight={headerHeight} className={b(modes)} fullScreen={fullScreen} - hideRight={hideMiniToc} tocTitleIcon={tocTitleIcon} wideFormat={wideFormat} hideTocHeader={hideTocHeader} @@ -194,9 +197,18 @@ class DocPage extends React.Component { {this.renderSinglePageControls()} + + this.setState({mobileMiniTocOpen: !this.state.mobileMiniTocOpen}) + } + closeMiniToc={() => this.setState({mobileMiniTocOpen: false})} + /> {/* This key allows recalculating the offset for the mini-toc for Safari */}
{hideMiniToc ? null : this.renderAsideMiniToc()} diff --git a/src/components/MiniToc/MiniToc.scss b/src/components/MiniToc/MiniToc.scss index 121813d4..203dee03 100644 --- a/src/components/MiniToc/MiniToc.scss +++ b/src/components/MiniToc/MiniToc.scss @@ -10,6 +10,10 @@ color: var(--g-color-text-primary); margin-bottom: 12px; margin-top: 0; + + @media (max-width: map-get($screenBreakpoints, 'md') - 1) { + margin-bottom: 8px; + } } &__sections { @@ -18,8 +22,14 @@ height: calc( 100vh - var(--dc-header-height, #{$headerHeight}) - #{$miniTocOffset} - var(--dc-subheader-height) ); - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; + + @media (max-width: map-get($screenBreakpoints, 'md') - 1) { + height: fit-content; + max-height: calc( + 80vh - var(--dc-header-height, #{$headerHeight}) - #{$miniTocOffset} - var(--dc-subheader-height) + ); + } } &__section { diff --git a/src/components/SubNavigation/SubNavigation.tsx b/src/components/SubNavigation/SubNavigation.tsx new file mode 100644 index 00000000..1f7bee6f --- /dev/null +++ b/src/components/SubNavigation/SubNavigation.tsx @@ -0,0 +1,189 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; + +import {ArrowShapeTurnUpRight, SquareListUl} from '@gravity-ui/icons'; +import {Button} from '@gravity-ui/uikit'; +import block from 'bem-cn-lite'; + +import './SubNavigation.scss'; + +const b = block('dc-subnavigation'); + +export type ShareData = { + title: string | undefined; + url?: string; +}; + +const useVisibility = (miniTocOpened: boolean, closeMiniToc: () => void) => { + const [visibility, setVisibility] = useState(true); + const [hiddingTimeout, setHiddingTimeout] = useState(undefined); + const [lastScrollY, setLastScrollY] = useState(window.screenY); + + const clickOutsideMiniToc = useCallback( + (event: MouseEvent) => { + /* + * func "composedPath" returns an array in which the last two elements are "HTML" and "#document", + * which do not have the classList property, so they are subtracted before checking by slice() + */ + const isOutside = !event + .composedPath() + .slice(0, -2) + .some((item) => { + const el = item as HTMLElement; + const classes = el.classList ?? []; + + return classes?.contains('dc-doc-layout__right'); + }); + + if (isOutside) { + closeMiniToc(); + } + }, + [closeMiniToc], + ); + + const controlVisibility = useCallback(() => { + if (miniTocOpened) { + setVisibility(true); + return; + } + + if (lastScrollY === 0) { + setVisibility(true); + } + + if (window.scrollY > lastScrollY) { + if (hiddingTimeout) { + return; + } + + setVisibility(false); + + setHiddingTimeout( + window.setTimeout(() => { + window.clearTimeout(hiddingTimeout); + setHiddingTimeout(undefined); + }, 300), + ); + } else if (window.scrollY < lastScrollY) { + setVisibility(true); + } + + setLastScrollY(window.scrollY); + }, [ + miniTocOpened, + lastScrollY, + hiddingTimeout, + setLastScrollY, + setVisibility, + setHiddingTimeout, + ]); + + useEffect(() => { + if (window.scrollY === 0) { + return; + } + + setHiddingTimeout( + window.setTimeout(() => { + setLastScrollY(window.scrollY); + setHiddingTimeout(undefined); + }, 100), + ); + }, []); + + useEffect(() => { + window.addEventListener('scroll', controlVisibility); + + return () => { + window.removeEventListener('scroll', controlVisibility); + }; + }, [controlVisibility]); + + useEffect(() => { + document.addEventListener('click', clickOutsideMiniToc, true); + + return () => { + document.removeEventListener('click', clickOutsideMiniToc, true); + }; + }, [clickOutsideMiniToc]); + + return visibility; +}; + +const useShareHandler = (title: string | undefined) => { + const shareData = useMemo(() => { + return { + title, + url: window.location.href, + }; + }, [title]); + + const shareHandler = useCallback(() => { + if (navigator && navigator.share) { + navigator + .share(shareData) + .then(() => {}) + .catch((error) => console.error('Error sharing', error)); + } else { + console.log('Share not supported', shareData); + } + }, [shareData]); + + return shareHandler; +}; + +export interface SubNavigationProps { + title: string | undefined; + hideMiniToc: boolean; + miniTocOpened: boolean; + toggleMiniTocOpen: () => void; + closeMiniToc: () => void; +} + +export const SubNavigation = ({ + title, + hideMiniToc, + miniTocOpened, + toggleMiniTocOpen, + closeMiniToc, +}: SubNavigationProps) => { + const visibility = useVisibility(miniTocOpened, closeMiniToc); + const shareHandler = useShareHandler(title); + + return ( +
+ + +
+ ); +}; + +export default SubNavigation; diff --git a/src/components/SubNavigation/index.ts b/src/components/SubNavigation/index.ts new file mode 100644 index 00000000..49a548a7 --- /dev/null +++ b/src/components/SubNavigation/index.ts @@ -0,0 +1,2 @@ +export * from './SubNavigation'; +export {default as SubNavigation} from './SubNavigation'; diff --git a/src/components/SubNavigation/subnavigation.scss b/src/components/SubNavigation/subnavigation.scss new file mode 100644 index 00000000..e16d011b --- /dev/null +++ b/src/components/SubNavigation/subnavigation.scss @@ -0,0 +1,98 @@ +@import '../../styles/variables'; +@import '../../styles/mixins'; + +.dc-subnavigation { + position: absolute; + z-index: 119; + + display: flex; + + width: calc(100% - 12px - 12px); + height: 44px; + padding: 4px 12px; + + gap: 4px; + + background: var(--g-color-base-background, #fff); + border-bottom: 1px solid var(--g-color-line-generic, #0000001a); + + @media (min-width: map-get($screenBreakpoints, 'md')) { + display: none; + } + + &__left { + display: flex; + align-items: center; + justify-content: flex-start; + flex: 1 0 0; + + width: 100%; + padding: 0; + gap: 4px; + + text-decoration: none; + + color: var(--g-color-text-primary, rgba(0, 0, 0, 0.85)); + background: none; + border-radius: var(--g-border-radius-xs, 3px); + border: none; + outline: none; + + transition: transform 0.1s ease-out, color 0.15s linear; + transform: scale(1); + + touch-action: manipulation; + + &:hover { + background: var(--g-color-base-simple-hover, rgba(0, 0, 0, 0.05)); + } + + &:active { + transition: none; + transform: scale(0.98); + } + + &:focus-visible { + outline: var(--g-color-line-focus, rgb(78, 121, 235)); + outline-width: 2px; + outline-style: solid; + } + + &_hidden { + visibility: hidden; + } + } + + &__icon { + display: flex; + justify-content: center; + align-items: center; + + padding: 12px; + } + + &__title { + @include text-size(subheader-2); + font-weight: 400; + } + + transition: margin-top 300ms 0s; + + &_hidden { + --dc-header-height: 64px; + margin-top: calc(0px - var(--dc-header-height, 64px)); + } + + &_visible { + margin-top: 0; + } + + &_invisible { + border: none; + background: none; + } + + &_invisible &__button { + margin-top: 8px; + } +} diff --git a/src/themes/common/index.scss b/src/themes/common/index.scss index d8cc549a..e8a3c139 100644 --- a/src/themes/common/index.scss +++ b/src/themes/common/index.scss @@ -16,7 +16,7 @@ --dc-text-highlight: var(--g-color-base-warning-heavy); --dc-text-highlight-selected: #ffab3b; --dc-header-height: 0px; - --dc-subheader-height: 40px; + --dc-subheader-height: 52px; --dc-error-image-403: url('data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); --dc-error-image-404: url('data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=');