Merge pull request #48 from acid-info/theme/mobile-header

Theme/mobile header
This commit is contained in:
Hossein Mehrabi
2022-10-10 12:33:46 +03:30
committed by GitHub
25 changed files with 838 additions and 24 deletions
+6 -1
View File
@@ -10,5 +10,10 @@ export const Button = (
props: TProps & HTMLProps<HTMLButtonElement>,
): JSX.Element => {
const { children, className, ...rest } = props
return <button className={clsx('button', className)}>{children}</button>
return (
<button className={clsx('button', className)} {...(rest as any)}>
{children}
</button>
)
}
+14
View File
@@ -15,6 +15,8 @@ import LinkedinSvg from '@site/static/icons/linkedin.svg'
import DiscourseSvg from '@site/static/icons/discourse.svg'
import TelegramSvg from '@site/static/icons/telegram.svg'
import StatusSvg from '@site/static/icons/status.svg'
import SearchSvg from '@site/static/icons/search.svg'
import DropdownSvg from '@site/static/icons/dropdown.svg'
type TIconProps = {
size?: 's' | 'm' | 'l'
@@ -113,3 +115,15 @@ export const IconFolderFilled = (props: TIconProps): JSX.Element => (
<FolderSvg />
</Icon>
)
export const IconSearch = (props: TIconProps): JSX.Element => (
<Icon {...props}>
<SearchSvg />
</Icon>
)
export const IconDropdown = (props: TIconProps): JSX.Element => (
<Icon {...props}>
<DropdownSvg />
</Icon>
)
-3
View File
@@ -251,9 +251,6 @@ small {
.footer__logo {
margin: 0 !important;
}
.menu_src-theme-Navbar-Content-styles-module svg {
display: none;
}
.card {
padding: var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal);
+215
View File
@@ -0,0 +1,215 @@
import React, { type ComponentProps, useEffect, useMemo } from 'react'
import clsx from 'clsx'
import {
ThemeClassNames,
useThemeConfig,
usePrevious,
Collapsible,
useCollapsible,
} from '@docusaurus/theme-common'
import {
isActiveSidebarItem,
findFirstCategoryLink,
useDocSidebarItemsExpandedState,
isSamePath,
} from '@docusaurus/theme-common/internal'
import Link from '@docusaurus/Link'
import { translate } from '@docusaurus/Translate'
import useIsBrowser from '@docusaurus/useIsBrowser'
import DocSidebarItems from '@theme/DocSidebarItems'
import type { Props } from '@theme/DocSidebarItem/Category'
import {
IconArrowLeftCircle,
IconArrowRightCircle,
IconDropdown,
} from '@site/src/components/Icon'
// If we navigate to a category and it becomes active, it should automatically
// expand itself
function useAutoExpandActiveCategory({
isActive,
collapsed,
updateCollapsed,
}: {
isActive: boolean
collapsed: boolean
updateCollapsed: (b: boolean) => void
}) {
const wasActive = usePrevious(isActive)
useEffect(() => {
const justBecameActive = isActive && !wasActive
if (justBecameActive && collapsed) {
updateCollapsed(false)
}
}, [isActive, wasActive, collapsed, updateCollapsed])
}
/**
* When a collapsible category has no link, we still link it to its first child
* during SSR as a temporary fallback. This allows to be able to navigate inside
* the category even when JS fails to load, is delayed or simply disabled
* React hydration becomes an optional progressive enhancement
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
* see https://github.com/facebook/docusaurus/issues/3030
*/
function useCategoryHrefWithSSRFallback(
item: Props['item'],
): string | undefined {
const isBrowser = useIsBrowser()
return useMemo(() => {
if (item.href) {
return item.href
}
// In these cases, it's not necessary to render a fallback
// We skip the "findFirstCategoryLink" computation
if (isBrowser || !item.collapsible) {
return undefined
}
return findFirstCategoryLink(item)
}, [item, isBrowser])
}
function CollapseButton({
categoryLabel,
onClick,
}: {
categoryLabel: string
onClick: ComponentProps<'button'>['onClick']
}) {
return (
<button
aria-label={translate(
{
id: 'theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel',
message: "Toggle the collapsible sidebar category '{label}'",
description:
'The ARIA label to toggle the collapsible sidebar category',
},
{ label: categoryLabel },
)}
type="button"
className="clean-btn menu__caret"
onClick={onClick}
/>
)
}
export default function DocSidebarItemCategory({
item,
onItemClick,
activePath,
level,
index,
...props
}: Props): JSX.Element {
const { items, label, collapsible, className, href } = item
const {
docs: {
sidebar: { autoCollapseCategories },
},
} = useThemeConfig()
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item)
const isActive = isActiveSidebarItem(item, activePath)
const isCurrentPage = isSamePath(href, activePath)
const { collapsed, setCollapsed } = useCollapsible({
// Active categories are always initialized as expanded. The default
// (`item.collapsed`) is only used for non-active categories.
initialState: () => {
if (!collapsible) {
return false
}
return isActive ? false : item.collapsed
},
})
const { expandedItem, setExpandedItem } = useDocSidebarItemsExpandedState()
// Use this instead of `setCollapsed`, because it is also reactive
const updateCollapsed = (toCollapsed: boolean = !collapsed) => {
setExpandedItem(toCollapsed ? null : index)
setCollapsed(toCollapsed)
}
useAutoExpandActiveCategory({ isActive, collapsed, updateCollapsed })
useEffect(() => {
if (
collapsible &&
expandedItem != null &&
expandedItem !== index &&
autoCollapseCategories
) {
setCollapsed(true)
}
}, [collapsible, expandedItem, index, setCollapsed, autoCollapseCategories])
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemCategory,
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
'menu__list-item',
{
'menu__list-item--collapsed': collapsed,
},
className,
)}
>
<div
className={clsx('menu__list-item-collapsible', {
'menu__list-item-collapsible--active': isCurrentPage,
})}
>
<Link
className={clsx('menu__link', {
'menu__link--sublist': collapsible,
'menu__link--sublist-caret': !href && collapsible,
'menu__link--active': isActive,
})}
onClick={
collapsible
? (e) => {
onItemClick?.(item)
if (href) {
updateCollapsed(false)
} else {
e.preventDefault()
updateCollapsed()
}
}
: () => {
onItemClick?.(item)
}
}
aria-current={isCurrentPage ? 'page' : undefined}
aria-expanded={collapsible ? !collapsed : undefined}
href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback}
{...props}
>
{label}
<span className="dropdown-icon">
<IconDropdown />
</span>
</Link>
{href && collapsible && (
<CollapseButton
categoryLabel={label}
onClick={(e) => {
e.preventDefault()
updateCollapsed()
}}
/>
)}
</div>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
<DocSidebarItems
items={items}
tabIndex={collapsed ? -1 : 0}
onItemClick={onItemClick}
activePath={activePath}
level={level + 1}
/>
</Collapsible>
</li>
)
}
+7
View File
@@ -23,6 +23,13 @@
background-color: var(--ifm-background-surface-color);
}
:root {
&,
body {
overflow-x: hidden !important;
}
}
@include utils.responsive('lg', 'down') {
.background-underlay {
display: none;
@@ -0,0 +1,33 @@
.root {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
width: 100%;
padding: 0 1.875rem;
& > div {
flex: 0 0 !important;
}
}
.root > div:nth-child(2) {
width: 57%;
flex-basis: 57%;
margin-left: 12px;
flex-shrink: 0;
flex-grow: 1;
flex: 1 1 57% !important;
button {
width: auto;
}
}
.root > div:last-child > button {
font-size: 0.75rem;
font-weight: bold;
color: #909091;
margin-left: 8px;
}
@@ -0,0 +1,28 @@
import NavbarLogo from '@theme/Navbar/Logo'
import clsx from 'clsx'
import React, { useState } from 'react'
import { useDebounce } from 'react-use'
import { SearchBar } from '../../../SearchBar/SearchBar'
import styles from './MobileSearch.module.scss'
export const MobileSearch: React.FC<{
render: boolean
onCancel: () => void
}> = ({ render, onCancel }) => {
const [renderSearchBar, setRenderSearchBar] = useState(false)
useDebounce(() => setRenderSearchBar(render), render ? 0 : 1000, [render])
return (
<div className={clsx(styles.root)}>
<div>
<NavbarLogo />
</div>
<div>{renderSearchBar && <SearchBar />}</div>
<div>
<button className={clsx('clean-btn')} onClick={onCancel}>
Cancel
</button>
</div>
</div>
)
}
@@ -0,0 +1 @@
export * from './MobileSearch'
+47 -4
View File
@@ -1,5 +1,10 @@
import { useThemeConfig } from '@docusaurus/theme-common'
import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'
import {
useNavbarMobileSidebar,
useSearchPage,
} from '@docusaurus/theme-common/internal'
import { IconSearch } from '@site/src/components/Icon'
import { IconButton } from '@site/src/components/IconButton'
import {
globalStore,
selectHiddenSidebar,
@@ -12,7 +17,9 @@ import NavbarSearch from '@theme/Navbar/Search'
import NavbarItem, { Props as NavbarItemConfig } from '@theme/NavbarItem'
import SearchBar from '@theme/SearchBar'
import clsx from 'clsx'
import React from 'react'
import React, { useState } from 'react'
import { useSearch } from '../../SearchBar/hooks/useSearch'
import { MobileSearch } from './MobileSearch/MobileSearch'
import { ShareButton } from './ShareButton'
import styles from './styles.module.scss'
@@ -59,8 +66,16 @@ export default function NavbarContent(): JSX.Element {
const dispatch = globalStore.useDispatch()
const hiddenDesktopSidebar = globalStore.useSelector(selectHiddenSidebar)
const [mobileSearch, setMobileSearch] = useState(false)
return (
<div className={clsx('row', styles.root)}>
<div
className={clsx(
'row',
styles.root,
mobileSearch && styles.activeMobileSearch,
)}
>
<div className="col col--2">
<SidebarToggleButton
onToggle={mobileSidebar.toggle}
@@ -92,7 +107,7 @@ export default function NavbarContent(): JSX.Element {
</div>
</div>
<div>
<nav className={styles.menu}>
<nav className={clsx(styles.menu, styles.links)}>
<NavbarItems items={links} />
</nav>
</div>
@@ -108,6 +123,34 @@ export default function NavbarContent(): JSX.Element {
</>
)}
</div>
<div
className={clsx(
'col',
styles.headerRightMobile,
mobileSearch && styles.shifted,
)}
>
{localeDropdown && <NavbarItem {...localeDropdown} />}
<IconButton
className={styles.searchButton}
onClick={() => setMobileSearch(true)}
>
<IconSearch />
</IconButton>
</div>
<div
className={clsx(
styles.mobileSearchContainer,
mobileSearch && styles.visible,
)}
>
<MobileSearch
render={mobileSearch}
onCancel={() => setMobileSearch(false)}
/>
</div>
</div>
)
}
+113 -4
View File
@@ -116,16 +116,15 @@
.root > * {
width: auto;
flex: 0 0 auto !important;
flex: 0 0 auto;
padding: 0;
}
.headerLeft {
.root > div:first-child {
flex: 0 0 auto;
}
.headerMiddle {
margin-left: 20px;
.leftContainer.shifted {
transform: none;
}
@@ -138,6 +137,55 @@
.headerRight {
display: none !important;
}
.searchContainer {
display: none;
}
}
.headerRightMobile {
visibility: visible;
display: flex !important;
flex: 1 1 auto !important;
align-items: center;
justify-content: flex-end;
& > div {
display: initial;
}
& > div:first-child {
border-radius: 12px;
padding: 4px 12px;
height: 2.125rem;
a {
font-size: 0.875rem;
}
svg {
margin-left: 0.75rem;
vertical-align: middle;
}
margin-right: 0.75rem;
}
.searchButton {
background-color: #eeeef1;
padding: 7px 13px;
border-radius: 12px;
}
transition: all 0.8s, opacity 0.4s;
transform: translateX(0%);
&.shifted {
transform: translateX(-100%);
opacity: 0%;
}
}
/*
@@ -159,4 +207,65 @@ Hide color mode toggle in small viewports
.sidebarButton.desktop {
display: none;
}
.root > div:first-child,
.headerMiddle {
transition: 1s;
}
}
.root {
& > div:first-child,
.headerMiddle {
opacity: 1;
visibility: visible;
}
}
.root.activeMobileSearch {
& > div:first-child,
.headerMiddle {
opacity: 0;
visibility: hidden;
}
}
.mobileSearchContainer {
top: 0;
left: 0;
width: 100% !important;
height: 100%;
position: absolute;
background-color: var(--ifm-background-surface-color);
transform: translateX(100%);
opacity: 0;
transition: 0.6s ease-in-out;
visibility: hidden;
&.visible {
opacity: 1;
transform: translateX(0%);
visibility: visible;
}
}
.headerRightMobile {
display: none !important;
visibility: hidden;
flex-grow: 1 !important;
}
@include utils.responsive('lg', 'down') {
.headerRightMobile {
display: flex !important;
visibility: visible;
}
}
.links {
a svg {
display: none !important;
}
}
@@ -0,0 +1,32 @@
import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'
import { IconArrowLeftCircle } from '@site/src/components/Icon'
import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'
import NavbarLogo from '@theme/Navbar/Logo'
import React from 'react'
function CloseButton() {
const mobileSidebar = useNavbarMobileSidebar()
return (
<button
type="button"
className="clean-btn navbar-sidebar__close"
onClick={() => mobileSidebar.toggle()}
>
<IconArrowLeftCircle />
</button>
)
}
export default function NavbarMobileSidebarHeader(): JSX.Element {
return (
<div className="navbar-sidebar__brand">
<div
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}
>
<CloseButton />
<NavbarLogo />
</div>
<NavbarColorModeToggle className="margin-right--md" />
</div>
)
}
@@ -0,0 +1,40 @@
import React, { type ComponentProps } from 'react'
import { useThemeConfig } from '@docusaurus/theme-common'
import { useNavbarSecondaryMenu } from '@docusaurus/theme-common/internal'
import Translate from '@docusaurus/Translate'
import { ButtonWithIcon } from '@site/src/components/Button/ButtonWithIcon'
import { IconArrowLeftCircle } from '@site/src/components/Icon'
import styles from './styles.module.scss'
function SecondaryMenuBackButton(props: ComponentProps<'button'>) {
return (
<ButtonWithIcon
className={styles.backButton}
icon={<IconArrowLeftCircle></IconArrowLeftCircle>}
{...props}
>
<Translate
id="theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel"
description="The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
>
Main Menu
</Translate>
</ButtonWithIcon>
)
}
// The secondary menu slides from the right and shows contextual information
// such as the docs sidebar
export default function NavbarMobileSidebarSecondaryMenu(): JSX.Element | null {
const isPrimaryMenuEmpty = useThemeConfig().navbar.items.length === 0
const secondaryMenu = useNavbarSecondaryMenu()
return (
<>
{/* edge-case: prevent returning to the primaryMenu when it's empty */}
{!isPrimaryMenuEmpty && (
<SecondaryMenuBackButton onClick={() => secondaryMenu.hide()} />
)}
{secondaryMenu.content}
</>
)
}
@@ -0,0 +1,18 @@
.backButton {
& > div {
flex-direction: row-reverse;
color: #373738;
font-size: 0.875rem;
svg {
* {
stroke: #909091;
}
}
}
border: 1px solid #eeeef1;
padding: 12px 18px;
margin-bottom: 8px;
}
+56 -1
View File
@@ -1,3 +1,5 @@
@use '@site/src/css/utils';
.navbar-sidebar__brand {
--ifm-navbar-padding-horizontal: 1rem;
}
@@ -11,5 +13,58 @@
background: none;
}
:root {
@include utils.responsive('lg', 'down') {
.navbar {
background: var(--ifm-background-surface-color);
}
}
.overflow-hidden {
overflow: hidden !important;
}
.navbar-sidebar__item.menu,
.theme-doc-sidebar-menu {
li a,
.theme-doc-sidebar-item-category a {
padding: 0.75rem 1.125rem;
border-radius: 0.75rem;
font-size: 0.875rem;
color: #373738;
.menu__link--active {
background-color: #eeeef0;
}
}
.menu__list-item-collapsible {
border-radius: 0.75rem;
a::after {
display: none;
}
.dropdown-icon {
position: absolute;
right: 1.125rem;
}
}
li.menu__list-item {
.dropdown-icon {
transform: rotate(0deg);
transition: transform var(--ifm-transition-fast);
}
&--collapsed {
.dropdown-icon {
transform: rotate(-90deg);
}
}
}
li:not(:first-child) {
margin-top: 0.125rem;
}
}
+17 -2
View File
@@ -1,12 +1,27 @@
import React from 'react'
import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'
import type { WrapperProps } from '@docusaurus/types'
import Navbar from '@theme-original/Navbar'
import type NavbarType from '@theme/Navbar'
import type { WrapperProps } from '@docusaurus/types'
import React, { useEffect } from 'react'
import './index.scss'
type Props = WrapperProps<typeof NavbarType>
export default function NavbarWrapper(props: Props): JSX.Element {
const mobileSidebar = useNavbarMobileSidebar()
useEffect(() => {
if (mobileSidebar.shown) {
document
.querySelector('#__docusaurus')
?.classList?.add?.('overflow-hidden')
} else {
document
.querySelector('#__docusaurus')
?.classList?.remove?.('overflow-hidden')
}
}, [mobileSidebar.shown])
return (
<>
<Navbar {...props} />
@@ -1,13 +1,10 @@
import React from 'react'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import { useAlternatePageUtils } from '@docusaurus/theme-common/internal'
import { translate } from '@docusaurus/Translate'
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'
import IconLanguage from '@theme/Icon/Language'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import type { LinkLikeNavbarItemProps } from '@theme/NavbarItem'
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'
import type { Props } from '@theme/NavbarItem/LocaleDropdownNavbarItem'
import styles from './styles.module.css'
import React from 'react'
export default function LocaleDropdownNavbarItem({
mobile,
@@ -59,7 +56,7 @@ export default function LocaleDropdownNavbarItem({
<DropdownNavbarItem
{...props}
mobile={mobile}
label={<>{dropdownLabel}</>}
label={<>{currentLocale.toUpperCase()}</>}
items={items}
/>
)
@@ -1,3 +1,11 @@
@use '@site/src/css/utils';
.root {
position: relative;
}
@include utils.responsive('lg', 'down') {
.root {
position: unset;
}
}
@@ -1,5 +1,7 @@
// TODO: use ifm variables
@use '@site/src/css/utils';
.root {
width: auto;
position: relative;
@@ -72,3 +74,27 @@
background-color: var(--ifm-background-color);
}
}
@include utils.responsive('lg', 'down') {
.shortcuts {
display: none;
}
.root {
width: 100%;
}
.root input {
width: 100%;
border-radius: 12px;
background: #eeeef0;
}
.root.expanded input {
width: 100%;
}
.label {
display: none;
}
}
@@ -2,7 +2,7 @@ import { useOS } from '@site/src/lib/useOS'
import { useWindowEventListener } from '@site/src/lib/useWindowEventListener'
import clsx from 'clsx'
import React from 'react'
import {} from 'react-use'
import { useMedia } from 'react-use'
import styles from './SearchInput.module.scss'
export type SearchInputProps = Omit<
@@ -30,6 +30,7 @@ export const SearchInput: React.FC<SearchInputProps> = ({
...props
}) => {
const os = useOS()
const isMobile = useMedia('(max-width: 996px)')
const expanded = active || value?.length > 0
@@ -67,7 +68,7 @@ export const SearchInput: React.FC<SearchInputProps> = ({
ref={inputRef}
onFocus={onFocus}
{...inputProps}
placeholder={expanded ? inputProps.placeholder : ''}
placeholder={expanded || isMobile ? inputProps.placeholder : ''}
/>
<div className={styles.shortcuts}>
{active ? (
@@ -1,3 +1,5 @@
@use '@site/src/css/utils';
.root {
background-color: var(--ifm-background-surface-color);
border-radius: 1rem;
@@ -19,3 +21,11 @@
visibility: visible;
}
}
@include utils.responsive('lg', 'down') {
.root {
width: 100%;
margin-top: 0;
border-radius: 0;
}
}

Some files were not shown because too many files have changed in this diff Show More