forked from ampproject/amphtml
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ [bento-mega-menu] Implemented bento mega menu (ampproject#38197)
* feature(bento-mega-menu): initial output from `make-extension` * feature(bento-mega-menu): implemented menu toggling * feature(bento-mega-menu): split up MegaMenu into multiple React components * feature(bento-mega-menu): improved styling of menu * feature(bento-mega-menu): added Aria attributes to open modal * feature(bento-mega-menu): ensure mask blocks background * feature(bento-mega-menu): added bento test * feature(bento-mega-menu): added DOM mappings * feature(bento-mega-menu): improved code, eliminated complex ref, by using `useEvent` * feature(bento-mega-menu): implemented a shim-based approach to bento-mode, similar to accordion * feature(bento-mega-menu): use `single:false` to pass children with multiple slots * feature(bento-mega-menu): implemented bento API by using a shim wrapper * feature(bento-mega-menu): added memo to Shim * feature(bento-mega-menu): extracted Shim * feature(bento-mega-menu): updated bento-mode CSS * feature(bento-mega-menu): use mutation observer to update state based on `expanded` props * feature(bento-mega-menu): ignore setting classes in the lightDOM * feature(bento-mega-menu): improved demo fixture * feature(bento-mega-menu): improved types * feature(bento-mega-menu): improved Shim types * feature(bento-mega-menu): removed dead code * feature(bento-mega-menu): removed dead code * feature(bento-mega-menu): removed dead code * feature(bento-mega-menu): moved useMutationObserver to common location * feature(bento-mega-menu): improved useMutationObserver docs * feature(bento-mega-menu): improved Shim docs * feature(bento-mega-menu): moved useClickOutside to common * feature(bento-mega-menu): not sure if the world is ready for `createProviderFromHook` * feature(bento-mega-menu): added simple storybook * feature(bento-mega-menu): added test coverage * feature(bento-mega-menu): fixed broken import * feature(bento-mega-menu): export an explicit API * feature(bento-mega-menu): improved bento test coverage * feature(bento-mega-menu): added detailed story * feature(bento-mega-menu): don't rename `children` as `slot` * feature(bento-mega-menu): updated jsdoc types to use TypeScript types * feature(bento-mega-menu): lint fix * feature(bento-mega-menu): added `memo` implementation * feature(bento-mega-menu): lazy-initialize the MutationObserver * feature(bento-mega-menu): removed unnecessary `type === 'attributes'` check * feature(bento-mega-menu): deep-compare `config` to ensure mutation observer doesn't disconnect * feature(bento-mega-menu): added unit tests for `memo` since it wasn't included in regular tests * feature(bento-mega-menu): added simpler implementation of `objectsEqualDeep` * feature(bento-mega-menu): added tests for `objectsEqualDeep` * feature(bento-mega-menu): disable codecov for preact code * feature(bento-mega-menu): improved types for objectsEqualDeep * feature(bento-mega-menu): generated new zindex Co-authored-by: scottrippey <[email protected]>
- Loading branch information
1 parent
df93c34
commit ed0c674
Showing
32 changed files
with
1,784 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import {PreactBaseElement} from '#preact/base-element'; | ||
|
||
import {BentoMegaMenu} from './component'; | ||
import {CSS as COMPONENT_CSS} from './component.jss'; | ||
import {BentoItem} from './component/BentoItem'; | ||
|
||
export class BaseElement extends PreactBaseElement { | ||
/** @override */ | ||
init() { | ||
super.init(); | ||
return {ItemWrapper: BentoItem}; | ||
} | ||
} | ||
|
||
/** @override */ | ||
BaseElement['Component'] = BentoMegaMenu; | ||
|
||
/** @override */ | ||
BaseElement['props'] = { | ||
'children': { | ||
selector: '*', | ||
single: false, | ||
}, | ||
}; | ||
|
||
/** @override */ | ||
BaseElement['usesShadowDom'] = true; | ||
|
||
/** @override */ | ||
BaseElement['shadowCss'] = COMPONENT_CSS; |
42 changes: 42 additions & 0 deletions
42
src/bento/components/bento-mega-menu/1.0/bento-mega-menu.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Pre-upgrade: | ||
* - display:block element | ||
* - contain:layout element | ||
*/ | ||
bento-mega-menu { | ||
background: white; | ||
position: relative; | ||
z-index: 1000; | ||
|
||
/* Before built, display the contents correctly: */ | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: 0.5em; | ||
} | ||
|
||
bento-mega-menu.i-amphtml-built { | ||
display: block; | ||
} | ||
|
||
bento-mega-menu > section > :first-child { | ||
/* Title */ | ||
cursor: pointer; | ||
} | ||
bento-mega-menu > section > :last-child { | ||
/* Content */ | ||
position: absolute; | ||
left: 0; | ||
width: 100%; | ||
background: white; | ||
opacity: 0; | ||
visibility: hidden; | ||
transition: opacity 200ms, visibility 0ms 200ms; | ||
transition-timing-function: ease-in; | ||
} | ||
bento-mega-menu > section[expanded] > :last-child { | ||
/* Content (expanded) */ | ||
opacity: 1; | ||
visibility: visible; | ||
transition-delay: 0ms; | ||
transition-timing-function: ease-out; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './component/BentoMegaMenu'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import {createUseStyles} from 'react-jss'; | ||
|
||
const mainNav = { | ||
whiteSpace: 'nowrap', | ||
background: 'white', | ||
position: 'relative', | ||
zIndex: 1000, | ||
|
||
'& > ul': { | ||
display: 'flex', | ||
listStyleType: 'none', | ||
padding: 0, | ||
margin: 0, | ||
flexWrap: 'wrap', | ||
|
||
gap: '0.5em', | ||
}, | ||
}; | ||
|
||
const title = { | ||
cursor: 'pointer', | ||
}; | ||
const content = { | ||
position: 'absolute', | ||
left: '0', | ||
width: '100vw', | ||
background: 'white', | ||
opacity: 0, | ||
visibility: 'hidden', | ||
transition: 'opacity 200ms, visibility 0s 200ms', | ||
transitionTimingFunction: 'ease-in', | ||
|
||
'&.open': { | ||
opacity: 1, | ||
visibility: 'visible', | ||
transitionDelay: 0, | ||
transitionTimingFunction: 'ease-out', | ||
}, | ||
}; | ||
|
||
const mask = { | ||
position: 'fixed', | ||
zIndex: 999, | ||
visibility: 'hidden', | ||
opacity: 0, | ||
|
||
top: 0, | ||
bottom: 0, | ||
left: 0, | ||
right: 0, | ||
|
||
background: 'black', | ||
transition: 'opacity 200ms, visibility 0s 200ms', | ||
transitionTimingFunction: 'ease-in', | ||
|
||
'&.open': { | ||
opacity: 0.5, | ||
visibility: 'visible', | ||
transitionDelay: 0, | ||
transitionTimingFunction: 'ease-out', | ||
}, | ||
}; | ||
|
||
const JSS = { | ||
mainNav, | ||
title, | ||
content, | ||
mask, | ||
}; | ||
|
||
// useStyles gets replaced for AMP builds via `babel-plugin-transform-jss`. | ||
// eslint-disable-next-line local/no-export-side-effect | ||
export const useStyles = createUseStyles(JSS); |
10 changes: 10 additions & 0 deletions
10
src/bento/components/bento-mega-menu/1.0/component.type.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** @externs */ | ||
|
||
/** @const */ | ||
var BentoMegaMenuDef = {}; | ||
|
||
/** @type {BentoMegaMenuProps} */ | ||
BentoMegaMenuDef.Props; | ||
|
||
/** @type {BentoMegaMenuApi} */ | ||
BentoMegaMenuDef.BentoMegaMenuApi = class {}; |
63 changes: 63 additions & 0 deletions
63
src/bento/components/bento-mega-menu/1.0/component/BentoItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import {Shim} from '#bento/components/bento-mega-menu/1.0/component/Shim'; | ||
|
||
import * as Preact from '#preact'; | ||
import {useLayoutEffect, useRef} from '#preact'; | ||
import {useAttributeObserver} from '#preact/hooks/useMutationObserver'; | ||
import {FC} from '#preact/types'; | ||
|
||
import {Content} from './Content'; | ||
import {Item, useMegaMenuItem} from './Item'; | ||
import {Title} from './Title'; | ||
|
||
export const BentoItem: FC = ({children}) => { | ||
return ( | ||
<Item> | ||
<SlottedDomWrapper>{children}</SlottedDomWrapper> | ||
</Item> | ||
); | ||
}; | ||
|
||
/** | ||
* Renders the slot, and uses a bunch of shims to control the slotted (light DOM) elements | ||
*/ | ||
const SlottedDomWrapper: FC = ({children}) => { | ||
const {actions, isOpen} = useMegaMenuItem(); | ||
|
||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
// Capture all the Light DOM elements: | ||
const sectionRef = useRef<HTMLElement>(null); | ||
const titleRef = useRef<HTMLElement>(null); | ||
const contentsRef = useRef<HTMLElement>(null); | ||
useLayoutEffect(() => { | ||
const slot = ref.current!.querySelector('slot')!; | ||
const section = slot.assignedElements()[0]; | ||
const {firstElementChild: header, lastElementChild: contents} = section; | ||
sectionRef.current = section as HTMLElement; | ||
contentsRef.current = contents as HTMLElement; | ||
titleRef.current = header as HTMLElement; | ||
}, []); | ||
|
||
// Watch the section's 'expanded' attribute: | ||
useAttributeObserver(sectionRef, 'expanded', (attrValue) => { | ||
const expanded = attrValue !== null; | ||
const shouldToggle = (expanded && !isOpen) || (!expanded && isOpen); | ||
if (shouldToggle) { | ||
actions.toggle(); | ||
} | ||
}); | ||
|
||
const sectionAttributes = { | ||
expanded: isOpen, | ||
}; | ||
// Render the slot, Title, and Content elements. | ||
// Render these using Shims, to control the Light DOM elements | ||
return ( | ||
<div ref={ref}> | ||
{children} | ||
<Shim elementRef={sectionRef} {...sectionAttributes} /> | ||
<Title as={Shim} elementRef={titleRef} /> | ||
<Content as={Shim} elementRef={contentsRef} /> | ||
</div> | ||
); | ||
}; |
81 changes: 81 additions & 0 deletions
81
src/bento/components/bento-mega-menu/1.0/component/BentoMegaMenu.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import objStr from 'obj-str'; | ||
|
||
import * as Preact from '#preact'; | ||
import {useImperativeHandle, useRef} from '#preact'; | ||
import {Children, forwardRef} from '#preact/compat'; | ||
import {ContainWrapper} from '#preact/component'; | ||
import {useClickOutside} from '#preact/hooks/useClickOutside'; | ||
import type {ComponentChildren, ComponentType, Ref} from '#preact/types'; | ||
|
||
import {Content} from './Content'; | ||
import {Item} from './Item'; | ||
import {Title} from './Title'; | ||
import {MegaMenuContext, useMegaMenu} from './useMegaMenu'; | ||
|
||
import {useStyles} from '../component.jss'; | ||
|
||
export type BentoMegaMenuProps = { | ||
children?: ComponentChildren; | ||
/** | ||
* Used for Bento web-component mode | ||
*/ | ||
ItemWrapper?: ComponentType; | ||
}; | ||
|
||
export type BentoMegaMenuApi = ReturnType<typeof useMegaMenu>; | ||
|
||
/** | ||
* @return {PreactDef.Renderable} | ||
*/ | ||
function BentoMegaMenuWithRef( | ||
{ItemWrapper, children, ...rest}: BentoMegaMenuProps, | ||
ref: Ref<BentoMegaMenuApi> | ||
) { | ||
const megaMenu = useMegaMenu(); | ||
|
||
const isAnyOpen = megaMenu.openId !== null; | ||
|
||
const navRef = useRef<HTMLDivElement>(null); | ||
useClickOutside(navRef, (ev: MouseEvent) => { | ||
if (megaMenu.isAnyOpen) { | ||
megaMenu.actions.closeMenu(); | ||
|
||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
} | ||
}); | ||
|
||
useImperativeHandle(ref, () => megaMenu, [megaMenu]); | ||
const classes = useStyles(); | ||
return ( | ||
<ContainWrapper {...rest}> | ||
<MegaMenuContext.Provider value={megaMenu}> | ||
<nav class={classes.mainNav} ref={navRef}> | ||
<ul> | ||
{Children.map(children, (child, index) => { | ||
if (ItemWrapper) { | ||
child = <ItemWrapper>{child}</ItemWrapper>; | ||
} | ||
return <li key={index}>{child}</li>; | ||
})} | ||
</ul> | ||
</nav> | ||
<div | ||
class={objStr({ | ||
[classes.mask]: true, | ||
'open': isAnyOpen, | ||
})} | ||
aria-hidden | ||
/> | ||
</MegaMenuContext.Provider> | ||
</ContainWrapper> | ||
); | ||
} | ||
|
||
const BentoMegaMenuFwd = forwardRef(BentoMegaMenuWithRef); | ||
const BentoMegaMenu = Object.assign(BentoMegaMenuFwd, { | ||
Title, | ||
Content, | ||
Item, | ||
}); | ||
export {BentoMegaMenu}; |
55 changes: 55 additions & 0 deletions
55
src/bento/components/bento-mega-menu/1.0/component/Content.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import objStr from 'obj-str'; | ||
|
||
import * as Preact from '#preact'; | ||
import {useLayoutEffect} from '#preact'; | ||
import {ComponentChildren} from '#preact/types'; | ||
import {propName} from '#preact/utils'; | ||
|
||
import {useMegaMenuItem} from './Item'; | ||
import {AriaAttributes, AsComponent, AsProps} from './types'; | ||
|
||
import {useStyles} from '../component.jss'; | ||
|
||
type ContentProps<TAs extends AsComponent> = AsProps<TAs> & { | ||
children?: ComponentChildren; | ||
class?: string; | ||
id?: string; | ||
}; | ||
|
||
export function Content<TAs extends AsComponent = 'div'>({ | ||
as: As = 'div', | ||
children, | ||
id: idProp, | ||
[propName('class')]: className, | ||
...rest | ||
}: ContentProps<TAs>) { | ||
const classes = useStyles(); | ||
const {actions, isOpen, itemId} = useMegaMenuItem(); | ||
|
||
// If this ID is set, pass it to the parent: | ||
useLayoutEffect(() => { | ||
if (idProp) { | ||
actions.overrideItemId(idProp); | ||
} | ||
}, [actions, idProp]); | ||
|
||
const ariaAttrs: AriaAttributes = { | ||
'aria-modal': isOpen, | ||
}; | ||
|
||
return ( | ||
<As | ||
role="dialog" | ||
id={itemId} | ||
{...ariaAttrs} | ||
{...rest} | ||
class={objStr({ | ||
[className!]: !!className, | ||
[classes.content]: true, | ||
['open']: isOpen, | ||
})} | ||
> | ||
{children} | ||
</As> | ||
); | ||
} |
Oops, something went wrong.