Skip to content
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

Menu button updates for table #986

Merged
merged 19 commits into from
Jan 26, 2023
Merged
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core';
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
import type { MenuButton } from '@ni/nimble-components/dist/esm/menu-button';
import type { ButtonAppearance } from '@ni/nimble-components/dist/esm/menu-button/types';
import type { ButtonAppearance, MenuButtonToggleEventDetail } from '@ni/nimble-components/dist/esm/menu-button/types';
import { BooleanValueOrAttribute, toBooleanProperty } from '../utilities/template-value-helpers';

export type { MenuButton };
export type { MenuButtonToggleEventDetail };

/**
* Directive to provide Angular integration for the menu button.
Expand Down Expand Up @@ -46,14 +47,5 @@ export class NimbleMenuButtonDirective {
this.renderer.setProperty(this.elementRef.nativeElement, 'open', toBooleanProperty(value));
}

@Output() public openChange = new EventEmitter<boolean>();
mollykreis marked this conversation as resolved.
Show resolved Hide resolved

public constructor(private readonly renderer: Renderer2, private readonly elementRef: ElementRef<MenuButton>) {}

@HostListener('open-change', ['$event'])
public onOpenChange($event: Event): void {
if ($event.target === this.elementRef.nativeElement) {
this.openChange.emit(this.open);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "Add 'beforetoggle' event on menu button and rename 'open-change' event to 'toggle'",
"packageName": "@ni/nimble-angular",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "Add 'beforetoggle' event on menu button and rename 'open-change' event to 'toggle'",
"packageName": "@ni/nimble-blazor",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "Add 'beforetoggle' event on menu button and rename 'open-change' event to 'toggle'.\nUpdate menu button to work when the slotted menu is nested within additional slots.",
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
@inherits ComponentBase
<nimble-menu-button
open="@BindConverter.FormatValue(Open)"
@onnimblemenubuttonopenchange="(__value) => UpdateOpen(__value.Open)"
@onnimblemenubuttontoggle="(__value) => HandleToggle(__value)"
@onnimblemenubuttonbeforetoggle="(__value) => HandleBeforeToggle(__value)"
appearance="@Appearance.ToAttributeValue()"
position="@Position.ToAttributeValue()"
disabled="@Disabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,31 @@ public partial class NimbleMenuButton : ComponentBase
/// Gets or sets a callback that's invoked when 'open' changes
/// </summary>
[Parameter]
public EventCallback<bool?> OpenChanged { get; set; }
public EventCallback<MenuButtonToggleEventArgs> Toggle { get; set; }

/// <summary>
/// Gets or sets a callback that's invoked before the 'open' state of the menu button changes
/// </summary>
[Parameter]
public EventCallback<MenuButtonToggleEventArgs> BeforeToggle { get; set; }

/// <summary>
/// Called when 'open' changes on the web component.
/// </summary>
/// <param name="value">New value of open</param>
protected async void UpdateOpen(bool? value)
/// <param name="eventArgs">The state of the menu button</param>
protected async void HandleToggle(MenuButtonToggleEventArgs eventArgs)
{
Open = eventArgs.NewState;
await Toggle.InvokeAsync(eventArgs);
}

/// <summary>
/// Called when the 'beforetoggle' event is fired on the web component
/// </summary>
/// <param name="eventArgs">The state of the menu button</param>
protected async void HandleBeforeToggle(MenuButtonToggleEventArgs eventArgs)
{
Open = value;
await OpenChanged.InvokeAsync(value);
await BeforeToggle.InvokeAsync(eventArgs);
}

/// <summary>
Expand Down
8 changes: 5 additions & 3 deletions packages/nimble-blazor/NimbleBlazor/EventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public class CheckboxChangeEventArgs : EventArgs
public bool Checked { get; set; }
}

public class MenuButtonOpenChangeEventArgs : EventArgs
public class MenuButtonToggleEventArgs : EventArgs
{
public bool Open { get; set; }
public bool NewState { get; set; }
public bool OldState { get; set; }
}

[EventHandler("onnimbletabsactiveidchange", typeof(TabsChangeEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onnimblecheckedchange", typeof(CheckboxChangeEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onnimblemenubuttonopenchange", typeof(MenuButtonOpenChangeEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onnimblemenubuttontoggle", typeof(MenuButtonToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
[EventHandler("onnimblemenubuttonbeforetoggle", typeof(MenuButtonToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
public static class EventHandlers
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,22 @@ export function afterStarted(Blazor) {
}
});
// Used by NimbleMenuButton.razor
Blazor.registerCustomEventType('nimblemenubuttonopenchange', {
browserEventName: 'open-change',
Blazor.registerCustomEventType('nimblemenubuttontoggle', {
browserEventName: 'toggle',
createEventArgs: event => {
return {
open: event.target.open
newState: event.detail.newState,
oldState: event.detail.oldState
};
}
});
// Used by NimbleMenuButton.razor
Blazor.registerCustomEventType('nimblemenubuttonbeforetoggle', {
browserEventName: 'beforetoggle',
createEventArgs: event => {
return {
newState: event.detail.newState,
oldState: event.detail.oldState
};
}
});
Expand Down
86 changes: 71 additions & 15 deletions packages/nimble-components/src/menu-button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ButtonAppearance } from '../button/types';
import type { ToggleButton } from '../toggle-button';
import { styles } from './styles';
import { template } from './template';
import { MenuButtonPosition } from './types';
import { MenuButtonToggleEventDetail, MenuButtonPosition } from './types';
import type { ButtonPattern } from '../patterns/button/types';
import type { AnchoredRegion } from '../anchored-region';

Expand Down Expand Up @@ -108,7 +108,11 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
if (!this.open) {
// Only fire an event here if the menu is changing to being closed. Otherwise,
// wait until the menu is actually opened before firing the event.
this.$emit('open-change');
const eventDetail: MenuButtonToggleEventDetail = {
oldState: true,
newState: false
};
this.$emit('toggle', eventDetail);
}
}

Expand All @@ -120,7 +124,11 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
this.focusMenu();
}

this.$emit('open-change');
const eventDetail: MenuButtonToggleEventDetail = {
oldState: false,
newState: true
};
this.$emit('toggle', eventDetail);
}

public focusoutHandler(e: FocusEvent): boolean {
Expand All @@ -129,18 +137,21 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
}

const focusTarget = e.relatedTarget as HTMLElement;
if (!this.contains(focusTarget)) {
this.open = false;
if (
!this.contains(focusTarget)
&& !this.getMenu()?.contains(focusTarget)
) {
this.setOpen(false);
return false;
}

return true;
}

public toggleButtonCheckedChangeHandler(e: Event): boolean {
this.open = this.toggleButton!.checked;
this.setOpen(this.toggleButton!.checked);
// Don't bubble the 'change' event from the toggle button because
// the menu button has its own 'open-change' event.
// the menu button has its own 'toggle' event.
e.stopPropagation();
return false;
}
Expand All @@ -149,10 +160,10 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
switch (e.key) {
case keyArrowUp:
this.focusLastItemWhenOpened = true;
this.open = true;
this.setOpen(true);
return false;
case keyArrowDown:
this.open = true;
this.setOpen(true);
return false;
default:
return true;
Expand All @@ -162,32 +173,77 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
public menuKeyDownHandler(e: KeyboardEvent): boolean {
switch (e.key) {
case keyEscape:
this.open = false;
this.setOpen(false);
this.toggleButton!.focus();
return false;
default:
return true;
}
}

private get menu(): HTMLElement | undefined {
return this.slottedMenus?.length ? this.slottedMenus[0] : undefined;
private setOpen(newValue: boolean): void {
if (this.open === newValue) {
return;
}

const eventDetail: MenuButtonToggleEventDetail = {
oldState: this.open,
newState: newValue
};
this.$emit('beforetoggle', eventDetail);

this.open = newValue;
}

private getMenu(): HTMLElement | undefined {
// Get the menu that is slotted within the menu-button, taking into account
// that it may be nested within multiple 'slot' elements, such as when used
// within a table.
if (!this.slottedMenus?.length) {
return undefined;
}

let currentItem: HTMLElement | undefined = this.slottedMenus[0];
while (currentItem) {
if (currentItem.getAttribute('role') === 'menu') {
return currentItem;
}

if (this.isSlotElement(currentItem)) {
const firstNode = currentItem.assignedNodes()[0];
if (firstNode instanceof HTMLElement) {
currentItem = firstNode;
} else {
currentItem = undefined;
}
} else {
return undefined;
}
}

return undefined;
}

private isSlotElement(
element: HTMLElement | undefined
): element is HTMLSlotElement {
return element?.nodeName === 'SLOT';
}

private focusMenu(): void {
this.menu?.focus();
this.getMenu()?.focus();
}

private focusLastMenuItem(): void {
const menuItems = this.menu?.querySelectorAll('[role=menuitem]');
const menuItems = this.getMenu()?.querySelectorAll('[role=menuitem]');
if (menuItems?.length) {
const lastMenuItem = menuItems[menuItems.length - 1] as HTMLElement;
lastMenuItem.focus();
}
}

private readonly menuChangeHandler = (): void => {
this.open = false;
this.setOpen(false);
this.toggleButton!.focus();
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ _Methods_

_Events_

- `open-change` (event) - event for when the opened state has changed
- `beforetoggle` (event) - event fired before the opened state has changed. The event detail contains:
- `newState` - boolean - The value of `open` on the menu button that the element is transitioning in to.
- `oldState` - boolean - The value of `open` on the menu button that the element is transitioning out of.
- `toggle` (event) - event for when the opened state has changed
- `newState` - boolean - The value of `open` on the menu button that the element transitioned in to.
- `oldState` - boolean - The value of `open` on the menu button that the element transitioned out of.

_CSS Classes and CSS Custom Properties that affect the component_

Expand Down
4 changes: 2 additions & 2 deletions packages/nimble-components/src/menu-button/template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { elements, html, ref, slotted, when } from '@microsoft/fast-element';
import { html, ref, slotted, when } from '@microsoft/fast-element';
import { DesignSystem } from '@microsoft/fast-foundation';
import type { MenuButton } from '.';
import { ToggleButton } from '../toggle-button';
Expand Down Expand Up @@ -43,7 +43,7 @@ export const template = html<MenuButton>`
${ref('region')}
>
<span part="menu">
<slot name="menu" ${slotted({ property: 'slottedMenus', filter: elements('[role=menu]') })}></slot>
<slot name="menu" ${slotted({ property: 'slottedMenus' })}></slot>
</span>
</${DesignSystem.tagFor(AnchoredRegion)}>
`
Expand Down
Loading