-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Menu Web Component [ PRIORITY 1 ] (#27906)
* radio init * styles radio * reverts branch * input spec init * cleans up spec * formatting * updates component name to text input * updates component name in spec * menu: menu init * menu: updates README * menu: updates template, styles * menu: updates menu and stories * menu: removes dead code * menu: removes folder and file * yarn change * menu: removes tab index on template slot * menu changes based on feedback * menu: updates stories * menu, emits event on expanded change * menu: updatews attr/property name to align with fluent ( expand --> open ) * menu: cleans up code * reverts dead file * menu: removes block styling * enhances cleanup of autoUpdate, conditionally remove mouseover event listener * Add defaultPrevented check to handleTriggerKeydown function * menu: adds open/close features * menu: fixes spelling error in readme * menu: adds part selector to positioningRegion * menu: adds z-index css variable, updates docs accordingly * menu: adds attr changed callback for closeOnScroll to manage event listeners * menu: adds exports
- Loading branch information
1 parent
167a6b8
commit 318173c
Showing
11 changed files
with
758 additions
and
0 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
change/@fluentui-web-components-efa30982-7506-4a0d-80e5-e9ca22911289.json
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,7 @@ | ||
{ | ||
"type": "prerelease", | ||
"comment": "feat(menu): adds menu web component", | ||
"packageName": "@fluentui/web-components", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
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,145 @@ | ||
# Menu | ||
|
||
> A Menu component for handling menus and menu items in a user interface. | ||
<br /> | ||
|
||
## **Design Spec** | ||
|
||
There is no design spec for the `Menu` component as the `Menu` has no visual styles. The design spec for the `MenuList` can be found at [Fluent MenuList Spec](https://www.figma.com/file/jFWrkFq61GDdOhPlsz6AtX/Menu?type=design&node-id=2-39&mode=design&t=RQguhK8xTpmR2MFe-0) | ||
|
||
<br /> | ||
|
||
## **Engineering Spec** | ||
|
||
<br /> | ||
|
||
The Menu component is responsible for managing menus and their associated menu items. It handles the open/close functionality, focus management, and positioning strategy for showing the menu items. | ||
|
||
<br /> | ||
|
||
### Use Case | ||
|
||
Creating a menu component that can be used to display a list of options or actions. | ||
|
||
<br /> | ||
|
||
## Class: `Menu` | ||
|
||
<br /> | ||
|
||
### **Variables** | ||
|
||
<br /> | ||
|
||
### **Fields** | ||
|
||
| Name | Privacy | Type | Default | Description | | ||
| -------------------- | ------- | --------------- | ------- | -------------------------------------------------------------------------------------- | | ||
| `menu` | public | `HTMLElement[]` | | The menu element(s) to be displayed | | ||
| `trigger` | public | `HTMLElement[]` | | The trigger element(s) for opening/closing menu | | ||
| `open` | public | `boolean` | `false` | Specifies if the menu is open or closed | | ||
| `menuContainer` | public | `HTMLElement` | | The container element for the menu items | | ||
| `openOnHover` | public | `boolean` | `false` | Sets whether the menu opens on hover of menu trigger | | ||
| `openOnContext` | public | `boolean` | `false` | Opens the menu on right click (context menu), removes all other menu open interactions | | ||
| `closeOnScroll` | public | `boolean` | `false` | Close when scroll outside of it | | ||
| `persistOnItemClick` | public | `boolean` | `false` | Determines if the menu open state should persis on click of menu item | | ||
|
||
<br /> | ||
|
||
### **Methods** | ||
|
||
| Name | Privacy | Description | Parameters | Return | | ||
| --------------------------- | --------- | ------------------------------------------------------------------------------------------ | -------------------------------------- | ------ | | ||
| `setComponent` | public | ets the trigger and menu list elements and adds event listeners. | | void | | ||
| `setPositioning` | protected | Calculates and applies the positioning of the menu list based on available viewport space. | | void | | ||
| `toggleMenu` | public | Toggles the open state of the menu. | | void | | ||
| `closeMenu` | public | Closes the menu. | | void | | ||
| `openMenu` | public | Opens the menu. | `e?: Event` | void | | ||
| `focusMenuList` | public | Focuses on the menu list. | | void | | ||
| `focusTrigger` | public | Focuses on the menu trigger. | | void | | ||
| `openChanged` | public | Called whenever the open state changes. Emits `onOpenChange` event. | `newValue: boolean, oldValue: boolean` | void | | ||
| `openOnHoverChanged` | public | Called whenever the 'openOnHover' property changes. | `newValue: boolean, oldValue: boolean` | void | | ||
| `persistOnItemClickChanged` | public | Called whenever the 'persisitOnItem' property changes. | `newValue: boolean, oldValue: boolean` | void | | ||
| `openOnContextChanged` | public | Called whenever the 'openOnContext' property changes. | `newValue: boolean, oldValue: boolean` | void | | ||
| `handleMenuKeydown` | public | Handles keyboard interaction for the menu. | `e: KeyboardEvent` | void | | ||
| `handleTriggerKeydown` | public | Handles keyboard interaction for the menu trigger. | `e: KeyboardEvent` | void | | ||
|
||
<br /> | ||
|
||
### **Events** | ||
|
||
| Name | Type | Description | | ||
| -------------- | ---- | ----------------------------------------------------------- | | ||
| `onOpenChange` | | emits custom `onOpenChange` event when opened state changes | | ||
|
||
<br /> | ||
|
||
### **Attributes** | ||
|
||
| Name | Field | | ||
| ----------------------- | ------------------ | | ||
| `open` | open | | ||
| `open-on-hover` | openOnHover | | ||
| `open-on-context` | openOnContext | | ||
| `close-on-scroll` | closeOnScroll | | ||
| `persist-on-item-click` | persistOnItemClick | | ||
|
||
<br /> | ||
|
||
### **Slots** | ||
|
||
| Name | Description | | ||
| --------- | -------------------------------- | | ||
| `trigger` | The trigger element for the menu | | ||
| | The menulist element | | ||
|
||
<br /> | ||
|
||
### **CSS Variables** | ||
|
||
| Name | Description | | ||
| -------------- | ------------------------------- | | ||
| `z-index-menu` | Used to set z-index of the Menu | | ||
|
||
<br /> | ||
|
||
### **Template** | ||
|
||
```html | ||
<slot name="trigger" ${slotted({ property: 'trigger', filter: elements() })}></slot> | ||
<span class="menu-list-container" ${ref('menuListContainer')} ?hidden="${(x) => !x.open}"> | ||
<slot ${slotted({ property: 'menu', filter: elements() })}></slot> | ||
</span> | ||
``` | ||
|
||
## **Accessibility** | ||
|
||
**WAI-ARIA Roles, States, and Properties** | ||
<br /> | ||
|
||
- aria-haspopup | ||
- aria-expanded | ||
|
||
<hr /> | ||
|
||
## **Preparation** | ||
|
||
### **Fluent Web Component v3 v.s Fluent React 9** | ||
|
||
<br /> | ||
|
||
**Component and Slot Mapping** | ||
|
||
| Fluent UI React 9 | Fluent Web Components 3 | | ||
| ----------------- | ----------------------- | | ||
| `<Menu>` | `<fluent-menu>` | | ||
| `<MenuList>` | `<fluent-menu-list>` | | ||
| `<MenuItem>` | `<fluent-menu-item>` | | ||
|
||
<br /> | ||
|
||
| Fluent UI React 9 | Fluent Web Components | Description of difference | | ||
| ----------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `hasIcons` | | React implementation requires user to pass the `hasIcons` to align menu items with icons. The web components implementation aligns content by default. | | ||
| `hasCheckmarks` | | React implementation requires user to pass the `hasCheckmarks` to align menu items with checkmarks. The web components implementation aligns content by default. | |
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,4 @@ | ||
import { FluentDesignSystem } from '../fluent-design-system.js'; | ||
import { definition } from './menu.definition.js'; | ||
|
||
definition.define(FluentDesignSystem.registry); |
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,4 @@ | ||
export * from './menu.js'; | ||
export { template as MenuTemplate } from './menu.template.js'; | ||
export { styles as MenuStyles } from './menu.styles.js'; | ||
export { definition as MenuDefinition } from './menu.definition.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,17 @@ | ||
import { FluentDesignSystem } from '../fluent-design-system.js'; | ||
import { Menu } from './menu.js'; | ||
import { styles } from './menu.styles.js'; | ||
import { template } from './menu.template.js'; | ||
|
||
/** | ||
* The Fluent Menu Element. | ||
* | ||
* @public | ||
* @remarks | ||
* HTML Element: <fluent-menu> | ||
*/ | ||
export const definition = Menu.compose({ | ||
name: `${FluentDesignSystem.prefix}-menu`, | ||
template, | ||
styles, | ||
}); |
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,106 @@ | ||
import { html } from '@microsoft/fast-element'; | ||
import type { Args, Meta } from '@storybook/html'; | ||
import { renderComponent } from '../helpers.stories.js'; | ||
import type { Menu as FluentMenu } from './menu.js'; | ||
import './define.js'; | ||
|
||
type MenuStoryArgs = Args & FluentMenu; | ||
type MenuStoryMeta = Meta<MenuStoryArgs>; | ||
|
||
const storyTemplate = html<MenuStoryArgs>` | ||
<style> | ||
.container { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
} | ||
</style> | ||
<fluent-menu | ||
?open-on-hover="${x => x.openOnHover}" | ||
?open-on-context="${x => x.openOnContext}" | ||
?close-on-scroll="${x => x.closeOnScroll}" | ||
?persist-on-item-click="${x => x.persistOnItemClick}" | ||
> | ||
<fluent-menu-button aria-label="Toggle Menu" appearance="primary" slot="trigger">Toggle Menu</fluent-menu-button> | ||
<fluent-menu-list> | ||
<fluent-menu-item>Menu item 1</fluent-menu-item> | ||
<fluent-menu-item>Menu item 2</fluent-menu-item> | ||
<fluent-menu-item>Menu item 3</fluent-menu-item> | ||
<fluent-menu-item>Menu item 4</fluent-menu-item> | ||
</fluent-menu-list> | ||
</fluent-menu> | ||
`; | ||
|
||
export default { | ||
title: 'Components/Menu', | ||
args: { | ||
openOnHover: false, | ||
openOnContext: false, | ||
closeOnScroll: false, | ||
persistOnItemClick: false, | ||
}, | ||
argTypes: { | ||
openOnHover: { | ||
description: 'Sets whether menu opens on hover', | ||
table: { | ||
defaultValue: { summary: false }, | ||
}, | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
openOnContext: { | ||
description: 'Opens the menu on right click (context menu), removes all other menu open interactions', | ||
table: { | ||
defaultValue: { summary: false }, | ||
}, | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
closeOnScroll: { | ||
description: 'Close when scroll outside of it', | ||
table: { | ||
defaultValue: { summary: false }, | ||
}, | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
persistOnItemClick: { | ||
description: 'Prevents the menu from closing when an item is clicked', | ||
table: { | ||
defaultValue: { summary: false }, | ||
}, | ||
control: 'boolean', | ||
defaultValue: false, | ||
}, | ||
}, | ||
} as MenuStoryMeta; | ||
|
||
export const Menu = renderComponent(storyTemplate).bind({}); | ||
|
||
export const MenuOpenOnHover = renderComponent(html<MenuStoryArgs>` | ||
<div class="container"> | ||
<fluent-menu open-on-hover> | ||
<fluent-menu-button aria-label="Toggle Menu"" appearance="primary" slot="trigger">Toggle Menu</fluent-menu-button> | ||
<fluent-menu-list> | ||
<fluent-menu-item>Menu item 1</fluent-menu-item> | ||
<fluent-menu-item>Menu item 2</fluent-menu-item> | ||
<fluent-menu-item>Menu item 3</fluent-menu-item> | ||
<fluent-menu-item>Menu item 4</fluent-menu-item> | ||
</fluent-menu-list> | ||
</fluent-menu> | ||
</div> | ||
`); | ||
|
||
export const MenuOpenOnContext = renderComponent(html<MenuStoryArgs>` | ||
<div class="container"> | ||
<fluent-menu open-on-context> | ||
<fluent-menu-button aria-label="Toggle Menu"" appearance="primary" slot="trigger">Toggle Menu</fluent-menu-button> | ||
<fluent-menu-list> | ||
<fluent-menu-item>Menu item 1</fluent-menu-item> | ||
<fluent-menu-item>Menu item 2</fluent-menu-item> | ||
<fluent-menu-item>Menu item 3</fluent-menu-item> | ||
<fluent-menu-item>Menu item 4</fluent-menu-item> | ||
</fluent-menu-list> | ||
</fluent-menu> | ||
</div> | ||
`); |
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,18 @@ | ||
import { css } from '@microsoft/fast-element'; | ||
import {} from '../theme/design-tokens.js'; | ||
|
||
/** Menu styles | ||
* @public | ||
*/ | ||
export const styles = css` | ||
:host { | ||
position: relative; | ||
z-index: var(--z-index-menu, 1); | ||
} | ||
.positioning-container { | ||
position: fixed; | ||
top: 0; | ||
left: 0; | ||
transform: translate(0, 0); | ||
} | ||
`; |
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,26 @@ | ||
import { elements, ElementViewTemplate, html, ref, slotted } from '@microsoft/fast-element'; | ||
import type { Menu } from './menu.js'; | ||
|
||
export function menuTemplate<T extends Menu>(): ElementViewTemplate<T> { | ||
return html<T>` | ||
<template | ||
?open-on-hover="${x => x.openOnHover}" | ||
?open-on-context="${x => x.openOnContext}" | ||
?close-on-scroll="${x => x.closeOnScroll}" | ||
?persist-on-item-click="${x => x.persistOnItemClick}" | ||
@keydown="${(x, c) => x.handleMenuKeydown(c.event as KeyboardEvent)}" | ||
> | ||
<slot name="trigger" ${slotted({ property: 'slottedTriggers', filter: elements() })}></slot> | ||
<span | ||
${ref('positioningContainer')} | ||
part="positioning-container" | ||
class="positioning-container" | ||
?hidden="${x => !x.open}" | ||
> | ||
<slot ${slotted({ property: 'slottedMenuList', filter: elements() })}></slot> | ||
</span> | ||
</template> | ||
`; | ||
} | ||
|
||
export const template: ElementViewTemplate<Menu> = menuTemplate(); |
Oops, something went wrong.