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 9 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 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, MenuButtonBeforeToggleEventDetail } from '@ni/nimble-components/dist/esm/menu-button/types';
import { BooleanValueOrAttribute, toBooleanProperty } from '../utilities/template-value-helpers';

export type { MenuButton };
export type { MenuButtonBeforeToggleEventDetail };

/**
* Directive to provide Angular integration for the menu button.
Expand Down Expand Up @@ -48,6 +49,8 @@ export class NimbleMenuButtonDirective {

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

@Output() public beforeToggle = new EventEmitter<MenuButtonBeforeToggleEventDetail>();

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

@HostListener('open-change', ['$event'])
Expand All @@ -56,4 +59,9 @@ export class NimbleMenuButtonDirective {
this.openChange.emit(this.open);
}
}

@HostListener('beforetoggle', ['$event'])
public onBeforeToggle($event: CustomEvent): void {
this.beforeToggle.emit($event.detail as MenuButtonBeforeToggleEventDetail);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add beforetoggle event on menu button",
"packageName": "@ni/nimble-angular",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add beforetoggle event on menu button",
"packageName": "@ni/nimble-blazor",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add beforetoggle event on menu button and update 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 @@ -3,6 +3,7 @@
<nimble-menu-button
open="@BindConverter.FormatValue(Open)"
@onnimblemenubuttonopenchange="(__value) => UpdateOpen(__value.Open)"
@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 @@ -52,6 +52,12 @@ public partial class NimbleMenuButton : ComponentBase
[Parameter]
public EventCallback<bool?> OpenChanged { get; set; }

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

/// <summary>
/// Called when 'open' changes on the web component.
/// </summary>
Expand All @@ -62,6 +68,15 @@ protected async void UpdateOpen(bool? value)
await OpenChanged.InvokeAsync(value);
}

/// <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(MenuButtonBeforeToggleEventArgs eventArgs)
{
await BeforeToggle.InvokeAsync(eventArgs);
}

/// <summary>
/// Gets or sets the additional attributes on the <see cref="NimbleMenuButton"/>.
/// </summary>
Expand Down
9 changes: 8 additions & 1 deletion packages/nimble-blazor/NimbleBlazor/EventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ public class MenuButtonOpenChangeEventArgs : EventArgs
public bool Open { get; set; }
}

public class MenuButtonBeforeToggleEventArgs : EventArgs
{
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("onnimblemenubuttonopenchange", typeof(MenuButtonOpenChangeEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
[EventHandler("onnimblemenubuttonbeforetoggle", typeof(MenuButtonBeforeToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
public static class EventHandlers
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export function afterStarted(Blazor) {
};
}
});
// Used by NimbleMenuButton.razor
Blazor.registerCustomEventType('nimblemenubuttonbeforetoggle', {
browserEventName: 'beforetoggle',
createEventArgs: event => {
return {
newState: event.detail.newState,
oldState: event.detail.oldState
};
}
});
}

window.NimbleBlazor = {
Expand Down
54 changes: 45 additions & 9 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 { MenuButtonBeforeToggleEventDetail, MenuButtonPosition } from './types';
import type { ButtonPattern } from '../patterns/button/types';
import type { AnchoredRegion } from '../anchored-region';

Expand Down Expand Up @@ -129,16 +129,16 @@ 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.menu?.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.
e.stopPropagation();
Expand All @@ -149,10 +149,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,16 +162,52 @@ 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 setOpen(newValue: boolean): void {
if (this.open === newValue) {
return;
}

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

this.open = newValue;
}

private get menu(): HTMLElement | undefined {
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
return this.slottedMenus?.length ? this.slottedMenus[0] : 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 = this.slottedMenus[0];
while (currentItem) {
if (currentItem.getAttribute('role') === 'menu') {
return currentItem;
}

if (currentItem?.nodeName === 'SLOT') {
currentItem = (
currentItem as HTMLSlotElement
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
).assignedNodes()[0] as HTMLElement;
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
} else {
return undefined;
}
}

return undefined;
}

private focusMenu(): void {
Expand All @@ -187,7 +223,7 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
}

private readonly menuChangeHandler = (): void => {
this.open = false;
this.setOpen(false);
this.toggleButton!.focus();
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/nimble-components/src/menu-button/specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ _Methods_

_Events_

- `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.
- `open-change` (event) - event for when the opened state has changed
mollykreis marked this conversation as resolved.
Show resolved Hide resolved

_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