outsystems-ui
Version:
OutSystems UI Framework
382 lines (344 loc) • 11 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Patterns.OverflowMenu {
/**
* Class for the OverflowMenu Pattern
*
* @export
* @class OverflowMenu
* @extends {AbstractPattern<OverflowMenuConfig>}
* @implements {IOverflowMenu}
*/
export class OverflowMenu extends AbstractPattern<OverflowMenuConfig> implements IOverflowMenu {
// Store the aria-label text used on the Trigger Element
private _ariaLabelTrigger: string;
// Store the Balloon Element
private _balloonElem: HTMLElement;
// Store the Balloon Class instance
private _balloonFeature: Feature.Balloon.IBalloon;
// Store the Event Callbacks
private _eventBalloonOnToggle: GlobalCallbacks.Generic;
private _eventOnClick: GlobalCallbacks.Generic;
private _eventOnKeydown: GlobalCallbacks.Generic;
// Flag used to set the enable/disable state
private _isDisabled = false;
// Flag used to deal with onBodyClick and open api concurrency methods!
private _isOpenedByApi = false;
// Store the platform events
private _platformEventOnToggle: Callbacks.OSOnToggleEvent;
// Store the element that triggers the balloon
private _triggerElem: HTMLElement;
// Store the Balloon options to pass to the Balloon Class
public balloonOptions: Feature.Balloon.BalloonOptions;
// Store the isOpen status
public isOpen = false;
constructor(uniqueId: string, configs: JSON) {
super(uniqueId, new OverflowMenuConfig(configs));
}
// Method to handle the custom BalloonOnToggle callback
private _balloonOnToggleCallback(_args: string, e: CustomEvent): void {
// If the balloon closed is the one from this pattern, toggle the OverflowMenu
if (e.detail.balloonElem === this._balloonElem) {
this._togglePattern(e.detail.isOpen);
}
}
// Method to handle the trigger click callback
private _onClickCallback(): void {
if (this._balloonFeature.isOpen) {
this.close();
} else {
this._isOpenedByApi = false;
this.open(this._isOpenedByApi);
}
}
// Method to handle the trigger Keydown callback
private _onKeydownCallback(event: KeyboardEvent): void {
if (
!this._balloonFeature.isOpen &&
(event.key === GlobalEnum.Keycodes.ArrowDown || event.key === GlobalEnum.Keycodes.ArrowUp)
) {
this._isOpenedByApi = false;
this.open(this._isOpenedByApi, event.key);
event.preventDefault(); // Prevent scroll
}
}
// Method to call the Balloon Class
private _setBalloonFeature(): void {
this.setBalloonOptions();
this._balloonFeature = new OSFramework.OSUI.Feature.Balloon.Balloon<IOverflowMenu>(
this,
this._balloonElem,
this.balloonOptions
);
}
// Method to handle the Shape config css variable
private _setOverflowMenuShape(shape?: GlobalEnum.ShapeTypes): void {
if (shape !== undefined) {
this.configs.Shape = shape;
}
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.CssCustomProperties.Shape,
`var(--border-radius-${this.configs.Shape})`
);
}
// Method to toggle the Pattern
private _togglePattern(isOpen: boolean): void {
if (isOpen) {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.Open);
} else {
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.Open);
}
this.isOpen = isOpen;
this.setA11YProperties();
this._triggerOnToggleEvent();
}
// Method that triggers the OnToggle event
private _triggerOnToggleEvent(): void {
this.triggerPlatformEventCallback(this._platformEventOnToggle, this.isOpen);
}
/**
* Method that removes the event listeners
*
* @protected
* @memberof OverflowMenu
*/
protected removeEventListeners(): void {
this._triggerElem.removeEventListener(GlobalEnum.HTMLEvent.Click, this._eventOnClick);
this._triggerElem.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnKeydown);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
Event.DOMEvents.Listeners.Type.BalloonOnToggle,
this._eventBalloonOnToggle
);
}
/**
* Add the Accessibility Attributes values
*
* @protected
* @memberof OSFramework.Patterns.OverflowMenu.OverflowMenu
*/
protected setA11YProperties(): void {
if (this.isBuilt === false) {
Helper.A11Y.AriaHasPopup(this._triggerElem, Constants.A11YAttributes.Aria.Haspopup.value.Menu);
Helper.A11Y.AriaControls(this._triggerElem, this._balloonElem.id);
Helper.A11Y.RoleMenu(this._balloonElem);
this.setTriggerAriaLabel(Enum.AriaLabel.Trigger);
Helper.Dom.Attribute.Set(this._triggerElem, Constants.FocusTrapIgnoreAttr, true);
// Set the attr that will be used to define the default tabindex element
Helper.Dom.Attribute.Set(this._triggerElem, Constants.FocusableTabIndexDefault, Constants.EmptyString);
}
Helper.A11Y.AriaExpanded(this._triggerElem, this.isOpen.toString());
}
/**
* Set the callbacks
*
* @protected
* @memberof OSFramework.Patterns.OverflowMenu.OverflowMenu
*/
protected setCallbacks(): void {
this._eventBalloonOnToggle = this._balloonOnToggleCallback.bind(this);
this._eventOnClick = this._onClickCallback.bind(this);
this._eventOnKeydown = this._onKeydownCallback.bind(this);
}
/**
* Method to set the event listeners
*
* @protected
* @memberof OverflowMenu
*/
protected setEventListeners(): void {
this._triggerElem.addEventListener(GlobalEnum.HTMLEvent.Click, this._eventOnClick);
this._triggerElem.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnKeydown);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
Event.DOMEvents.Listeners.Type.BalloonOnToggle,
this._eventBalloonOnToggle
);
}
/**
* Update info based on htmlContent
*
* @protected
* @memberof OSFramework.Patterns.OverflowMenu.OverflowMenu
*/
protected setHtmlElements(): void {
this._triggerElem = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.Trigger);
this._balloonElem = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.Balloon);
}
/**
* Method to unset the callbacks.
*
* @protected
* @memberof OSFramework.Patterns.OverflowMenu.OverflowMenu
*/
protected unsetCallbacks(): void {
this._eventBalloonOnToggle = undefined;
this._eventOnClick = undefined;
}
/**
* Removes the local value of the variables pointing to HTML elements;
*
* @protected
* @memberof OSFramework.Patterns.OverflowMenu.OverflowMenu
*/
protected unsetHtmlElements(): void {
this._balloonElem = undefined;
this._triggerElem = undefined;
this._balloonFeature = undefined;
}
/**
* Method to build the OverflowMenu
*
* @memberof OverflowMenu
*/
public build(): void {
super.build();
this.setHtmlElements();
this.setA11YProperties();
this._setBalloonFeature();
this._setOverflowMenuShape();
this.setCallbacks();
this.setEventListeners();
this.finishBuild();
}
/**
* Method to change the value of configs/current state.
*
* @param {string} propertyName
* @param {unknown} propertyValue
* @memberof OverflowMenu
*/
public changeProperty(propertyName: string, propertyValue: unknown): void {
super.changeProperty(propertyName, propertyValue);
if (this.isBuilt) {
switch (propertyName) {
case Enum.Properties.Position:
this._balloonFeature.updatePositionOption(propertyValue as GlobalEnum.FloatingPosition);
break;
case Enum.Properties.Shape:
this._setOverflowMenuShape(propertyValue as GlobalEnum.ShapeTypes);
this._balloonFeature.setBalloonShape(propertyValue as GlobalEnum.ShapeTypes);
break;
}
}
}
/**
* Method to close the Pattern
*
* @memberof OverflowMenu
*/
public close(): void {
if (this._balloonFeature.isOpen) {
this._balloonFeature.close();
}
}
/**
* Method to disable the pattern
*
* @memberof OverflowMenu
*/
public disable(): void {
this._isDisabled = true;
this.close();
Helper.Dom.Attribute.Set(
this._triggerElem,
GlobalEnum.HTMLAttributes.Disabled,
OSFramework.OSUI.Constants.EmptyString
);
}
/**
* Method to destroy the Pattern
*
* @memberof OverflowMenu
*/
public dispose(): void {
this._balloonFeature?.dispose();
this.removeEventListeners();
this.unsetCallbacks();
this.unsetHtmlElements();
super.dispose();
}
/**
* Method to enable the pattern
*
* @memberof OverflowMenu
*/
public enable(): void {
this._isDisabled = false;
Helper.Dom.Attribute.Remove(this._triggerElem, GlobalEnum.HTMLAttributes.Disabled);
}
/**
* Method to open the Pattern
*
* @memberof OverflowMenu
*/
public open(
isOpenedByApi: boolean,
keyPressed?: GlobalEnum.Keycodes.ArrowDown | GlobalEnum.Keycodes.ArrowUp
): void {
if (this._balloonFeature.isOpen === false && this._isDisabled === false) {
this._isOpenedByApi = isOpenedByApi;
this._balloonFeature.open(this._isOpenedByApi, keyPressed);
}
}
/**
* Register the callbacks for the Pattern
*
* @param {string} eventName
* @param {GlobalCallbacks.OSGeneric} callback
@memberof OSFramework.Patterns.OverflowMenu.OverflowMenu
*/
public registerCallback(eventName: string, callback: GlobalCallbacks.OSGeneric): void {
switch (eventName) {
case Patterns.OverflowMenu.Enum.Events.OnMenuToggle:
if (this._platformEventOnToggle === undefined) {
this._platformEventOnToggle = callback;
} else {
console.warn(`The ${GlobalEnum.PatternName.OverflowMenu} already has the toggle callback set.`);
}
break;
default:
super.registerCallback(eventName, callback);
}
}
/**
* Method to set the Balloon Feature options
*
* @param {Feature.Balloon.BalloonOptions} [balloonOptions]
* @memberof OverflowMenu
*/
public setBalloonOptions(balloonOptions?: Feature.Balloon.BalloonOptions): void {
if (balloonOptions !== undefined) {
this.balloonOptions = balloonOptions;
} else {
// Set focus options to pass to the Balloon feature
const _focusOptions = {
useFocus: true,
};
this.balloonOptions = {
alignment: GlobalEnum.FloatingAlignment.Start,
allowedPlacements: [
GlobalEnum.FloatingPosition.BottomStart,
GlobalEnum.FloatingPosition.BottomEnd,
GlobalEnum.FloatingPosition.TopStart,
GlobalEnum.FloatingPosition.TopEnd,
],
anchorElem: this._triggerElem,
position: this.configs.Position,
shape: this.configs.Shape,
focusOptions: _focusOptions,
};
}
}
/**
* Method to set the aria-label text for the Trigger element
*
* @param {string} ariaLabelText
* @memberof OverflowMenu
*/
public setTriggerAriaLabel(ariaLabelText: string): void {
if (ariaLabelText !== Constants.EmptyString) {
this._ariaLabelTrigger = ariaLabelText;
Helper.A11Y.AriaLabel(this._triggerElem, this._ariaLabelTrigger);
}
}
}
}