Skip to content

Commit

Permalink
Menu Web Component [ PRIORITY 1 ] (#27906)
Browse files Browse the repository at this point in the history
* 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
brianchristopherbrady authored and radium-v committed May 6, 2024
1 parent 167a6b8 commit 318173c
Show file tree
Hide file tree
Showing 11 changed files with 758 additions and 0 deletions.
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"
}
4 changes: 4 additions & 0 deletions packages/web-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"types": "./dist/dts/label/define.d.ts",
"default": "./dist/esm/label/define.js"
},
"./menu.js": {
"types": "./dist/dts/menu/define.d.ts",
"default": "./dist/esm/menu/define.js"
},
"./menu-list.js": {
"types": "./dist/dts/menu-list/define.d.ts",
"default": "./dist/esm/menu-list/define.js"
Expand Down
1 change: 1 addition & 0 deletions packages/web-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './counter-badge/index.js';
export * from './divider/index.js';
export * from './image/index.js';
export * from './label/index.js';
export * from './menu/index.js';
export * from './menu-button/index.js';
export * from './menu-item/index.js';
export * from './menu-list/index.js';
Expand Down
145 changes: 145 additions & 0 deletions packages/web-components/src/menu/README.md
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. |
4 changes: 4 additions & 0 deletions packages/web-components/src/menu/define.ts
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);
4 changes: 4 additions & 0 deletions packages/web-components/src/menu/index.ts
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';
17 changes: 17 additions & 0 deletions packages/web-components/src/menu/menu.definition.ts
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,
});
106 changes: 106 additions & 0 deletions packages/web-components/src/menu/menu.stories.ts
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>
`);
18 changes: 18 additions & 0 deletions packages/web-components/src/menu/menu.styles.ts
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);
}
`;
26 changes: 26 additions & 0 deletions packages/web-components/src/menu/menu.template.ts
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();
Loading

0 comments on commit 318173c

Please sign in to comment.