Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EUI+] Implement site header design changes #7889

Merged
146 changes: 146 additions & 0 deletions packages/docusaurus-theme/src/components/navbar_item/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useContext } from 'react';
import clsx from 'clsx';
import { css } from '@emotion/react';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {
CommonProps,
EuiIcon,
ExclusiveUnion,
IconType,
PropsForAnchor,
PropsForButton,
useEuiMemoizedStyles,
UseEuiTheme,
} from '@elastic/eui';
import { AppThemeContext } from '../theme_context';

type SharedProps = {
icon: IconType;
showLabel?: boolean;
isMenuItem?: boolean;
} & CommonProps;

type Props = ExclusiveUnion<
PropsForAnchor<SharedProps>,
PropsForButton<SharedProps>
>;

// converted from css modules to Emotion
export const getStyles = ({ euiTheme }: UseEuiTheme) => ({
item: css`
display: flex;
align-items: center;

-webkit-tap-highlight-color: transparent;
transition: background var(--ifm-transition-fast);

&:hover {
background-color: var(--ifm-color-emphasis-200);
color: currentColor;
}
`,
navItem: css`
justify-content: center;
width: ${euiTheme.size.xl};
height: ${euiTheme.size.xl};
border-radius: 50%;
`,
menuItem: css`
justify-content: flex-start;
gap: ${euiTheme.size.s};

@media (min-width: 997px) {
justify-content: center;
width: ${euiTheme.size.xl};
height: ${euiTheme.size.xl};
border-radius: 50%;
}
`,
darkMode: css`
&:hover {
background-color: var(--ifm-color-gray-800);
color: currentColor;
}
`,
disabled: css`
cursor: not-allowed;
`,
title: css`
@media (min-width: 997px) {
display: none;
}
`,
});

// using a type guard to ensure proper typing from ExclusiveUnion
const isAnchorClick = (
onClick: Props['onClick'],
href: Props['href']
): onClick is PropsForAnchor<SharedProps>['onClick'] => href != null;

export const NavbarItem = (props: Props) => {
const {
className,
title,
icon,
onClick,
href,
target,
showLabel,
isMenuItem = true,
} = props;

const isBrowser = useIsBrowser();
const { theme } = useContext(AppThemeContext);

const isDarkMode = theme === 'dark';

const styles = useEuiMemoizedStyles(getStyles);
const cssStyles = [
styles.item,
isMenuItem ? styles.menuItem : styles.navItem,
!isBrowser && styles.disabled,
isDarkMode && styles.darkMode,
];

const content = showLabel ? (
<>
<EuiIcon type={icon} />
<span css={styles.title}>{title}</span>
</>
) : (
<EuiIcon type={icon} />
);

if (isAnchorClick(onClick, href)) {
return (
<a
href={href}
target={target ?? '_blank'}
title={title}
className={clsx('clean-btn', className)}
css={cssStyles}
onClick={onClick}
aria-label={title}
aria-live="polite"
>
{content}
</a>
);
}

return (
<button
type="button"
disabled={!isBrowser}
className={clsx('clean-btn', className)}
css={cssStyles}
onClick={onClick}
title={title}
aria-label={title}
aria-live="polite"
>
{content}
</button>
);
};
72 changes: 60 additions & 12 deletions packages/docusaurus-theme/src/theme/ColorModeToggle/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { useContext, useEffect } from 'react';
import OriginalColorModeToggle from '@theme-init/ColorModeToggle';
import React, { useCallback, useContext, useEffect } from 'react';
import { translate } from '@docusaurus/Translate';
import type { Props } from '@theme-original/ColorModeToggle';

import type ColorModeToggleType from '@theme-init/ColorModeToggle';
import type { WrapperProps } from '@docusaurus/types';
import { EuiThemeColorMode } from '@elastic/eui';

import { NavbarItem } from '../../components/navbar_item';
import { AppThemeContext } from '../../components/theme_context';

type Props = WrapperProps<typeof ColorModeToggleType>;
type WrappedProps = WrapperProps<typeof ColorModeToggleType>;

export default function ColorModeToggle({
// Additional wrapper to connect Docusaurus color mode with eui theme
function ColorModeToggle({
value,
onChange,
...rest
}: Props): JSX.Element {
}: WrappedProps): JSX.Element {
const { theme, changeTheme } = useContext(AppThemeContext);

useEffect(() => {
Expand All @@ -25,12 +29,56 @@ export default function ColorModeToggle({
};

return (
<>
<OriginalColorModeToggle
value={theme}
onChange={handleOnChange}
{...rest}
/>
</>
<OriginalColorModeToggle
value={theme}
onChange={handleOnChange}
{...rest}
/>
);
}

function OriginalColorModeToggle({
className,
value,
onChange,
}: Props): JSX.Element {
const isDarkMode = value === 'dark';

const title = translate(
{
message: 'Switch between dark and light mode (currently {mode})',
id: 'theme.colorToggle.ariaLabel',
description: 'The ARIA label for the navbar color mode toggle',
},
{
mode:
value === 'dark'
? translate({
message: 'dark mode',
id: 'theme.colorToggle.ariaLabel.mode.dark',
description: 'The name for the dark color mode',
})
: translate({
message: 'light mode',
id: 'theme.colorToggle.ariaLabel.mode.light',
description: 'The name for the light color mode',
}),
}
);

const handleOnChange = useCallback(() => {
onChange(value === 'dark' ? 'light' : 'dark');
}, [value, onChange]);

return (
<NavbarItem
className={className}
title={title}
icon={isDarkMode ? 'sun' : 'moon'}
isMenuItem={false}
onClick={handleOnChange}
/>
);
}

export default React.memo(ColorModeToggle);
137 changes: 137 additions & 0 deletions packages/docusaurus-theme/src/theme/Logo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useContext } from 'react';
import { css } from '@emotion/react';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {
useColorMode,
useThemeConfig,
type NavbarLogo,
} from '@docusaurus/theme-common';
import type { Props } from '@theme-original/Logo';
import {
EuiImage,
euiTextTruncate,
useEuiMemoizedStyles,
UseEuiTheme,
} from '@elastic/eui';
import { AppThemeContext } from '../../components/theme_context';

const getStyles = ({ euiTheme }: UseEuiTheme) => ({
wrapper: css`
${euiTextTruncate()}
// create space to prevent focus outline from being cut off
padding: ${euiTheme.size.xs};

@media (min-width: 997px) {
border-right: ${euiTheme.border.thin};
}

.navbar__brand {
display: flex;
align-items: center;

margin-inline-end: ${euiTheme.size.m};

@media (min-width: 997px) {
margin-inline-end: ${euiTheme.size.l};
}
}

.navbar__logo {
height: 100%;
}
`,
imageWrapper: css`
margin-inline-end: ${euiTheme.size.m};
`,
image: css`
position: relative;
block-size: ${euiTheme.size.l};
inline-size: ${euiTheme.size.l};
margin: 0;
`,
});

function LogoThemedImage({
logo,
alt,
imageClassName,
}: {
logo: NavbarLogo;
alt: string;
imageClassName?: string;
}) {
const { theme } = useContext(AppThemeContext);
const isDarkMode = theme === 'dark';

const styles = useEuiMemoizedStyles(getStyles);

const src = isDarkMode
? useBaseUrl(logo.srcDark || logo.src)
: useBaseUrl(logo.src);

const themedImage = (
<EuiImage
src={src}
size="fullWidth"
alt={alt}
className={logo.className}
wrapperProps={{
style: logo.style,
css: styles.image,
}}
/>
);

// Is this extra div really necessary?
// introduced in https://github.com/facebook/docusaurus/pull/5666
return imageClassName ? (
<div className={imageClassName} css={styles.imageWrapper}>
{themedImage}
</div>
) : (
themedImage
);
}

export default function Logo(props: Props): JSX.Element {
const {
siteConfig: { title },
} = useDocusaurusContext();
const {
navbar: { title: navbarTitle, logo },
} = useThemeConfig();

const { imageClassName, titleClassName, ...propsRest } = props;
const logoLink = useBaseUrl(logo?.href || '/');

const styles = useEuiMemoizedStyles(getStyles);

// If visible title is shown, fallback alt text should be
// an empty string to mark the logo as decorative.
const fallbackAlt = navbarTitle ? '' : title;

// Use logo alt text if provided (including empty string),
// and provide a sensible fallback otherwise.
const alt = logo?.alt ?? fallbackAlt;

return (
<div css={styles.wrapper}>
<Link
to={logoLink}
{...propsRest}
{...(logo?.target && { target: logo.target })}
>
{logo && (
<LogoThemedImage
logo={logo}
alt={alt}
imageClassName={imageClassName}
/>
)}
{navbarTitle != null && <b className={titleClassName}>{navbarTitle}</b>}
</Link>
</div>
);
}
Loading
Loading