Skip to content

WIP: feat: new Header component #3039

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

Draft
wants to merge 1 commit into
base: cass-gm-1001
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/gamut/src/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import { useRef } from 'react';

import { StyledAppBar, StyledNavBar } from './elements';
import { HeaderHeightArea } from './HeaderHeightArea';
import { HeaderProvider } from './HeaderProvider';
import { headerMobileBreakpoint } from './styles';
import { HeaderProps } from './types';
import { mapFloatingItemsToElement, mapItemsToElement } from './utilities';

export const Header: React.FC<HeaderProps> = ({ floatingItems, items }) => {
const menuContainerRef = useRef<HTMLUListElement>(null);

return (
<HeaderHeightArea
display={{ _: 'none', [headerMobileBreakpoint]: 'block' }}
>
<HeaderProvider>
<StyledAppBar aria-label="Main" as="nav">
<StyledNavBar ref={menuContainerRef}>
{mapItemsToElement(items.left, 'left')}
{mapItemsToElement(items.right, 'right')}
</StyledNavBar>
</StyledAppBar>
{mapFloatingItemsToElement(floatingItems)}
</HeaderProvider>
</HeaderHeightArea>
);
};
58 changes: 58 additions & 0 deletions packages/gamut/src/Header/HeaderHeightArea/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Box, WithChildrenProp } from '@codecademy/gamut';
import { system, transitionConcat } from '@codecademy/gamut-styles';
import { ResponsiveProp } from '@codecademy/variance';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import * as React from 'react';

import { useIsInHeaderRegion } from './useIsInHeaderRegion';

const HeaderHeightAreaBase = styled(Box)(
system.css({
borderBottom: 1,
bg: 'background',
top: 0,
zIndex: 2,
width: 1,
transition: transitionConcat(
['background-color', 'border-bottom-color'],
'fast',
'ease-in-out'
),
}),
system.states({
faded: {
bg: 'background-current',
borderColor: 'background-current',
},
})
);

export interface HeaderHeightAreaProps extends WithChildrenProp {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
as?: React.ElementType<any>;
display: ResponsiveProp<'none' | 'block'>;
ariaLabel?: string;
}

export const HeaderHeightArea: React.FC<HeaderHeightAreaProps> = ({
as,
children,
display,
ariaLabel,
}) => {
const theme = useTheme();
const isInHeaderRegion = useIsInHeaderRegion();

return (
<HeaderHeightAreaBase
as={as}
display={display}
height={theme.elements.headerHeight}
faded={isInHeaderRegion}
aria-label={ariaLabel}
>
{children}
</HeaderHeightAreaBase>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react';

export const useIsInHeaderRegion = () => {
const [isInHeaderRegion, setIsInHeaderRegion] = useState(true);

// it is not recommended to replicate this logic in other components unless absolutely necessary, as it is
// a workaround for style rehydration issues when using react-use/useWindowScroll. The reasoning behind this
// workaround is discussed here: https://github.com/Codecademy/gamut/pull/1822#discussion_r650125406
useEffect(() => {
const checkScroll = () => setIsInHeaderRegion(window?.pageYOffset === 0);
checkScroll();
document.addEventListener('scroll', checkScroll);
return () => document.removeEventListener('scroll', checkScroll);
}, []);

return isInHeaderRegion;
};
38 changes: 38 additions & 0 deletions packages/gamut/src/Header/HeaderProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from 'react';

export interface HeaderContextType {
lastOpenedDropdown: string | undefined;
setLastOpenedDropdown:
| Dispatch<SetStateAction<string | undefined>>
| undefined;
}

export const HeaderContext = createContext<HeaderContextType>({
lastOpenedDropdown: undefined,
setLastOpenedDropdown: undefined,
});

export const HeaderProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [lastOpenedDropdown, setLastOpenedDropdown] = useState<
string | undefined
>();

return (
<HeaderContext.Provider
value={{ lastOpenedDropdown, setLastOpenedDropdown }}
>
{children}
</HeaderContext.Provider>
);
};

export const useHeaderContext = () => useContext(HeaderContext);
57 changes: 57 additions & 0 deletions packages/gamut/src/Header/elements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { css } from '@codecademy/gamut-styles';
import styled from '@emotion/styled';

import { AppBar } from '../AppBar';
import { Box, BoxProps } from '../Box';

export const StyledAppBar = styled(AppBar)(
css({
boxShadow: `none`,
})
);

export const StyledNavBar = styled.ul(
css({
alignItems: 'stretch',
display: `flex`,
padding: 0,
listStyle: `none`,
margin: 0,
width: `100%`,
})
);

export const spacing = {
standard: 8,
enterprise: 12,
} as const;

export const StyledListItem = styled(Box)(
css({
display: `flex`,
justifyContent: `center`,
flexDirection: `column`,
position: `relative`,

'&:first-of-type': {
ml: { md: 0 },
},
'&:last-of-type': {
mr: { md: 0 },
},
})
);

type HeaderListItemProps = {
onBlur?: () => void;
onFocus?: () => void;
} & BoxProps;

export const HeaderListItem: React.FC<HeaderListItemProps> = ({
children,
...props
}) => (
<StyledListItem as="li" {...props}>
{children}
</StyledListItem>
);
2 changes: 2 additions & 0 deletions packages/gamut/src/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Header';
export { useHeaderContext } from './HeaderProvider';
1 change: 1 addition & 0 deletions packages/gamut/src/Header/styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const headerMobileBreakpoint = 'lg' as const;
20 changes: 20 additions & 0 deletions packages/gamut/src/Header/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { spacing } from '@codecademy/gamut-styles';

export type HeaderItem<T> = {
component: React.ComponentType<T>;
props: T;
id: string;
margin?: keyof typeof spacing;
};

export type FloatingHeaderItems = JSX.Element[];

export type FormattedHeaderItems = {
left: HeaderItem<any>[];
right: HeaderItem<any>[];
};

export type HeaderProps = {
items: FormattedHeaderItems;
floatingItems: FloatingHeaderItems;
};
25 changes: 25 additions & 0 deletions packages/gamut/src/Header/utilities.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HeaderListItem } from './elements';
import { FloatingHeaderItems, HeaderItem } from './types';

export const mapItemsToElement = <T extends HeaderItem<T>[]>(
items: T,
side: 'left' | 'right'
) => {
return items.map((item, index) => {
const { margin, component: Component, id } = item;

return (
<HeaderListItem
key={id}
mr={margin}
ml={side === 'right' && index === 0 ? 'auto' : margin}
>
<Component {...item.props} />
</HeaderListItem>
);
});
};

export const mapFloatingItemsToElement = (items: FloatingHeaderItems) => {
return items.map((item) => <>{item}</>);
};
1 change: 1 addition & 0 deletions packages/gamut/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './Flyout';
export * from './FocusTrap';
export * from './Form';
export * from './GridForm';
export * from './Header';
export * from './HiddenText';
export * from './Layout/Column';
export * from './Layout/LayoutGrid';
Expand Down
Loading