Skip to content

Commit

Permalink
chore(menu, menu-item): decouple from FAST Foundation (VIV-2021) (#2026)
Browse files Browse the repository at this point in the history
* Decouple menu from FAST Foundation

* Fix linter warning
  • Loading branch information
RichardHelm authored Nov 25, 2024
1 parent 7aa8179 commit c9b67e3
Show file tree
Hide file tree
Showing 4 changed files with 710 additions and 79 deletions.
8 changes: 8 additions & 0 deletions libs/components/src/lib/menu-item/menu-item.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ describe('vwc-menu-item', () => {
expect(changeSpy).toHaveBeenCalled();
});

it('should not fire "change" event on click when disabled', async () => {
element.disabled = true;

element.click();

expect(changeSpy).not.toHaveBeenCalled();
});

it('should fire "change" event on spacebar press', async () => {
pressKey(keySpace);

Expand Down
213 changes: 203 additions & 10 deletions libs/components/src/lib/menu-item/menu-item.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { attr, observable } from '@microsoft/fast-element';
import { attr, DOM, observable } from '@microsoft/fast-element';
import {
AnchoredRegion,
applyMixins,
MenuItem as FastMenuItem,
MenuItemRole as FastMenuItemRole,
FoundationElement,
getDirection,
} from '@microsoft/fast-foundation';
import { keyEnter, keySpace } from '@microsoft/fast-web-utilities';
import { Direction, keyEnter, keySpace } from '@microsoft/fast-web-utilities';
import {
keyArrowLeft,
keyArrowRight,
Expand All @@ -18,10 +20,13 @@ export const MenuItemRole = {
presentation: 'presentation',
} as const;

export type MenuItemRole = typeof MenuItemRole[keyof typeof MenuItemRole];

export enum CheckAppearance {
Normal = 'normal',
TickOnly = 'tick-only',
}

/**
* Types of fab connotation.
*
Expand All @@ -42,7 +47,194 @@ export type MenuItemConnotation = Extract<
* @event {CustomEvent<undefined>} change - Fires a custom 'change' event when a non-submenu item with a role of `menuitemcheckbox`, `menuitemradio`, or `menuitem` is invoked
* @vueModel modelValue checked change `(event.target as HTMLInputElement).checked`
*/
export class MenuItem extends FastMenuItem {
export class MenuItem extends FoundationElement {
/**
* The disabled state of the element.
*
* @public
* @remarks
* HTML Attribute: disabled
*/
@attr({ mode: 'boolean' })
disabled!: boolean;

/**
* The expanded state of the element.
*
* @public
* @remarks
* HTML Attribute: expanded
*/
@attr({ mode: 'boolean' })
expanded!: boolean;

/**
* @internal
*/
expandedChanged() {
if (this.$fastController.isConnected) {
if (this.submenu === undefined) {
return;
}
if (this.expanded === false) {
(this.submenu as Menu).collapseExpandedItem();
} else {
this.currentDirection = getDirection(this);
}
this.$emit('expanded-change', this, { bubbles: false });
}
}

/**
* The role of the element.
*
* @public
* @remarks
* HTML Attribute: role
*/
@attr
// eslint-disable-next-line @nrwl/nx/workspace/no-attribute-default-value
override role: MenuItemRole = MenuItemRole.menuitem;

/**
* The checked value of the element.
*
* @public
* @remarks
* HTML Attribute: checked
*/
@attr({ mode: 'boolean' })
checked!: boolean;

/**
* @internal
*/
checkedChanged() {
if (this.$fastController.isConnected) {
this.$emit('change');
}
}

/**
* reference to the anchored region
*
* @internal
*/
@observable
submenuRegion!: AnchoredRegion;

/**
* @internal
*/
@observable
hasSubmenu = false;

/**
* Track current direction to pass to the anchored region
*
* @internal
*/
@observable
currentDirection: Direction = Direction.ltr;

/**
* @internal
*/
@observable
submenu: Element | undefined;

private observer: MutationObserver | undefined;

/**
* @internal
*/
override connectedCallback(): void {
super.connectedCallback();
DOM.queueUpdate(() => {
this.updateSubmenu();
});

this.observer = new MutationObserver(this.updateSubmenu);
}

/**
* @internal
*/
override disconnectedCallback(): void {
super.disconnectedCallback();
this.submenu = undefined;
if (this.observer !== undefined) {
this.observer.disconnect();
this.observer = undefined;
}
}

/**
* @internal
*/
handleMenuItemClick = (e: MouseEvent): boolean => {
if (e.defaultPrevented || this.disabled) {
return false;
}

this.invoke();
return false;
};

/**
* @internal
*/
handleMouseOver = (_: MouseEvent): boolean => {
if (this.disabled || !this.hasSubmenu || this.expanded) {
return false;
}

this.expanded = true;

return false;
};

/**
* @internal
*/
handleMouseOut = (_: MouseEvent): boolean => {
if (!this.expanded || this.contains(document.activeElement)) {
return false;
}

this.expanded = false;

return false;
};

private invoke = () => {
if (this.disabled) {
return;
}

switch (this.role) {
case MenuItemRole.menuitemcheckbox:
this.checked = !this.checked;
break;

case MenuItemRole.menuitem:
// update submenu
this.updateSubmenu();
if (this.hasSubmenu) {
this.expanded = true;
} else {
this.$emit('change');
}
break;

case MenuItemRole.menuitemradio:
if (!this.checked) {
this.checked = true;
}
break;
}
};

/**
* Indicates the menu item's text.
*
Expand Down Expand Up @@ -110,12 +302,10 @@ export class MenuItem extends FastMenuItem {

constructor() {
super();
(this as any).updateSubmenu = () => this.#updateSubmenu();
this.addEventListener('expanded-change', this.#expandedChange);
(this as any).handleMenuItemKeyDown = this.#handleMenuItemKeyDown;
}

#updateSubmenu() {
private updateSubmenu() {
for (const submenu of this.#submenuArray) {
this.submenu = submenu as Menu;
(this.submenu as Menu).anchor = this as MenuItem;
Expand All @@ -137,24 +327,27 @@ export class MenuItem extends FastMenuItem {
}
}

#handleMenuItemKeyDown = (e: KeyboardEvent): boolean => {
/**
* @internal
*/
handleMenuItemKeyDown = (e: KeyboardEvent): boolean => {
if (e.defaultPrevented) {
return false;
}

switch (e.key) {
case keyEnter:
case keySpace:
(this as any).invoke();
this.invoke();
if (!this.disabled) {
this.#emitSyntheticClick();
}
return false;

case keyArrowRight:
//open/focus on submenu
(this as any).expandAndFocus();
if (this.hasSubmenu) {
this.expanded = true;
this.#emitSyntheticClick();
}
return false;
Expand Down
Loading

0 comments on commit c9b67e3

Please sign in to comment.