outsystems-ui
Version:
OutSystems UI Framework
564 lines (490 loc) • 17.6 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Patterns.Sidebar {
/**
* Defines the interface for OutSystemsUI Patterns
*
* @export
* @class Sidebar
* @extends {AbstractPattern<SidebarConfig>}
* @implements {ISidebar}
*/
export class Sidebar extends AbstractPattern<SidebarConfig> implements ISidebar, Interface.IDragEvent {
// Hold the animateOnDrag intance, that helps transition the sidebar on drag
private _animateOnDragInstance: Behaviors.AnimateOnDrag;
// Hold the value to toggle the outside click to close behavior
private _clickOutsideToClose: boolean;
// Store if the click was outside the sidebar
private _clickedOutsideElement: boolean;
// Store the Sidebar direction
private _currentDirectionCssClass: string;
// Store the click event with bind(this)
private _eventOverlayClick: GlobalCallbacks.Generic;
// Store the mousedown event with bind(this)
private _eventOverlayMouseDown: GlobalCallbacks.Generic;
// Store the keypress event with bind(this)
private _eventSidebarKeypress: GlobalCallbacks.Generic;
// Store focus manager instance
private _focusManagerInstance: Behaviors.FocusManager;
// Store focus trap instance
private _focusTrapInstance: Behaviors.FocusTrap;
// Store gesture events instance
private _gestureEventInstance: Event.GestureEvent.DragEvent;
// Store if the pattern has gesture events added
private _hasGestureEvents: boolean;
// Stores the current status of the sidebar
private _isOpen: boolean;
// Store the parent element
private _parentSelf: HTMLElement;
// Store the platform events
private _platformEventOnToggle: Callbacks.OSOnToggleEvent;
constructor(uniqueId: string, configs: JSON) {
super(uniqueId, new SidebarConfig(configs));
this._isOpen = this.configs.StartsOpen;
this._currentDirectionCssClass = Enum.CssClass.ClassModifier + this.configs.Direction;
}
// Method to close Sidebar
private _closeSidebar(): void {
this._isOpen = false;
// Remove the A11Y states to focus trap
this._focusTrapInstance.disableForA11y();
if (this.isBuilt) {
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.IsOpen);
Helper.A11Y.TabIndexFalse(this.selfElement);
Helper.A11Y.AriaHiddenTrue(this.selfElement);
this._triggerOnToggleEvent();
this.selfElement.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventSidebarKeypress);
// Will handle the tabindex value of the elements inside pattern
Helper.A11Y.SetElementsTabIndex(this._isOpen, this._focusTrapInstance.focusableElements);
if (this._clickOutsideToClose || (this.configs.HasOverlay && this._clickOutsideToClose === undefined)) {
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
Event.DOMEvents.Listeners.Type.BodyOnMouseDown,
this._eventOverlayMouseDown
);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
Event.DOMEvents.Listeners.Type.BodyOnClick,
this._eventOverlayClick
);
}
}
this._focusManagerInstance.setFocusToStoredElement();
}
// Method to add Focus Trap to Pattern
private _handleFocusBehavior(): void {
const opts = {
focusTargetElement: this._parentSelf,
canContainOtherPatterns: true,
} as Behaviors.FocusTrapParams;
this._focusTrapInstance = new Behaviors.FocusTrap(opts);
this._focusManagerInstance = new Behaviors.FocusManager();
// Disable tabIndex to the inner focusable elements if its start closed!
if (this._isOpen === false) {
// Will handle the tabindex value of the elements inside pattern
Helper.A11Y.SetElementsTabIndex(false, this._focusTrapInstance.focusableElements);
}
}
// Method to handle the creation of the GestureEvents
private _handleGestureEvents(): void {
if (Helper.DeviceInfo.IsNative) {
// Create and save gesture event instance. Created here and not on constructor,
// as we need to pass this.selfElement, only available after super.build()
this._gestureEventInstance = new Event.GestureEvent.DragEvent(this.selfElement);
// Set event listeners and callbacks
this.setGestureEvents(
this._onGestureStart.bind(this),
this._onGestureMove.bind(this),
this._onGestureEnd.bind(this)
);
// Apply transform on an element and perform animation
this._animateOnDragInstance = new Behaviors.AnimateOnDrag(this.selfElement);
}
}
// Method to handle the start of a gesture
private _onGestureEnd(offsetX: number, offsetY: number, timeTaken: number): void {
this._animateOnDragInstance.onDragEnd(offsetX, offsetY, timeTaken, this._toggle.bind(this));
if (this.configs.HasOverlay) {
Behaviors.OverlayTransitionOnDrag.UnSet(this.selfElement);
}
}
// Method to handle the gesture move
private _onGestureMove(x: number, y: number, offsetX: number, offsetY: number, evt: TouchEvent): void {
this._animateOnDragInstance.onDragMove(offsetX, offsetY, x, y, evt);
if (this.configs.HasOverlay) {
Behaviors.OverlayTransitionOnDrag.Set(this.selfElement, x, this.configs.Direction, this.configs.Width);
}
}
// Method to handle the end of a gesture
private _onGestureStart(x: number, y: number): void {
this._animateOnDragInstance.onDragStart(
false,
this.configs.Direction,
x,
y,
this._isOpen,
this.configs.Width
);
}
// Method to open Sidebar
private _openSidebar() {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.IsOpen);
Helper.A11Y.TabIndexTrue(this.selfElement);
Helper.A11Y.AriaHiddenFalse(this.selfElement);
// Add the A11Y states to focus trap
this._focusTrapInstance.enableForA11y();
this._focusManagerInstance.storeLastFocusedElement();
if (this.isBuilt) {
//let's only change the property and trigger the OS event IF the pattern is already built.
this._isOpen = true;
this._triggerOnToggleEvent();
}
// The sidebar can be set to be open, when created (start open). And in this case, the events
// should be added, even if the pattern is not built yet.
if (this._clickOutsideToClose || (this.configs.HasOverlay && this._clickOutsideToClose === undefined)) {
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
Event.DOMEvents.Listeners.Type.BodyOnMouseDown,
this._eventOverlayMouseDown
);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
Event.DOMEvents.Listeners.Type.BodyOnClick,
this._eventOverlayClick
);
}
this.selfElement.focus();
this.selfElement.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventSidebarKeypress);
// Will handle the tabindex value of the elements inside pattern
Helper.A11Y.SetElementsTabIndex(this._isOpen, this._focusTrapInstance.focusableElements);
}
// Method to close a Sidebar with a click on the overlay
private _overlayClickCallback(_args: string, e: MouseEvent): void {
// If the sidebar is opened and the mouse down event occurred outside the sidebar, close it.
if (
this._isOpen &&
this._clickedOutsideElement &&
(e.target === this.selfElement || this._clickOutsideToClose)
) {
this.close();
}
e.stopPropagation();
}
/**
* Method to check if the mouse down event happened outside the sidebar
* This is required to cover the cases when selecting text and moving the cursor to the sidebar's overlay.
*/
private _overlayMouseDownCallback(_args: string, e: MouseEvent): void {
const targetElem = e.target as HTMLElement;
this._clickedOutsideElement = true;
if (
targetElem.closest(`${Constants.Dot}${Enum.CssClass.Header}`) ||
targetElem.closest(`${Constants.Dot}${Enum.CssClass.Content}`)
) {
// If the click was inside the side bar, then change the flag to false.
this._clickedOutsideElement = false;
}
}
// Method to remove the event listeners
private _removeEvents(): void {
this.selfElement.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventSidebarKeypress);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
Event.DOMEvents.Listeners.Type.BodyOnMouseDown,
this._eventOverlayMouseDown
);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
Event.DOMEvents.Listeners.Type.BodyOnClick,
this._eventOverlayClick
);
}
// Method to set the Sidebar opening/closing direction
private _setDirection(): void {
// Reset direction class
if (this._currentDirectionCssClass !== '') {
Helper.Dom.Styles.RemoveClass(this.selfElement, this._currentDirectionCssClass);
}
this._currentDirectionCssClass = Enum.CssClass.ClassModifier + this.configs.Direction;
Helper.Dom.Styles.AddClass(this.selfElement, this._currentDirectionCssClass);
}
// Method to sets the Sidebar overlay
private _setHasOverlay(): void {
const alreadyHasOverlayClass = Helper.Dom.Styles.ContainsClass(this.selfElement, Enum.CssClass.HasOverlay);
if (this.configs.HasOverlay && alreadyHasOverlayClass === false) {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.HasOverlay);
} else if (this.isBuilt) {
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.HasOverlay);
}
}
// Method to set the cssClasses that should be assigned to the element on it's initialization
private _setInitialCssClasses(): void {
this._setDirection();
this._setWidth();
this._setHasOverlay();
// Set IsOpen class
if (this._isOpen) {
this._openSidebar();
}
}
// Method to set the Sidebar width
private _setWidth(): void {
Helper.Dom.Styles.SetStyleAttribute(this.selfElement, Enum.CssProperty.Width, this.configs.Width);
}
// Method that will handle the tab navigation and sidebar closing on Escape
private _sidebarKeypressCallback(e: KeyboardEvent): void {
const isEscapedPressed = e.key === GlobalEnum.Keycodes.Escape;
// Close the sidebar when pressing Esc
if (isEscapedPressed) {
this.close();
}
e.stopPropagation();
}
// Method to toggle the Sidebar
private _toggle(): void {
if (this._isOpen) {
this.close();
} else {
this.open();
}
}
// Method to toggle gestures on Sidebar
private _toggleGesturesSidebar(enableSwipes: boolean): void {
if (enableSwipes && this._hasGestureEvents === false) {
if (this._gestureEventInstance === undefined) {
this._handleGestureEvents();
}
} else if (enableSwipes === false && this._hasGestureEvents) {
this.removeGestureEvents();
}
}
// Method that triggers the OnToggle event
private _triggerOnToggleEvent(): void {
this.triggerPlatformEventCallback(this._platformEventOnToggle, this._isOpen);
}
/**
* Sets the A11Y properties when the pattern is built.
*
* @protected
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
protected setA11YProperties(): void {
Helper.A11Y.RoleComplementary(this.selfElement);
Helper.A11Y.AriaHasPopupTrue(this.selfElement);
// Set the attr that will be used to define the default tabindex element
Helper.Dom.Attribute.Set(this.selfElement, Constants.FocusableTabIndexDefault, Constants.EmptyString);
if (this._isOpen) {
Helper.A11Y.TabIndexTrue(this.selfElement);
Helper.A11Y.AriaHiddenFalse(this.selfElement);
} else {
Helper.A11Y.TabIndexFalse(this.selfElement);
Helper.A11Y.AriaHiddenTrue(this.selfElement);
}
}
/**
* Sets the callbacks to be used in the pattern.
*
* @protected
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
protected setCallbacks(): void {
this._eventSidebarKeypress = this._sidebarKeypressCallback.bind(this);
this._eventOverlayClick = this._overlayClickCallback.bind(this);
this._eventOverlayMouseDown = this._overlayMouseDownCallback.bind(this);
}
/**
* Set the html references that will be used to manage the cssClasses and atribute properties
*
* @protected
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
protected setHtmlElements(): void {
this._parentSelf = Helper.Dom.GetElementById(this.widgetId);
this._setWidth();
}
/**
* Method to remove all assigned callbacks
*
* @protected
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
protected unsetCallbacks(): void {
this._removeEvents();
this._eventSidebarKeypress = undefined;
this._eventOverlayClick = undefined;
this._eventOverlayMouseDown = undefined;
}
/**
* Release references to HTML elements.
*
* @protected
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
protected unsetHtmlElements(): void {
this._parentSelf = undefined;
this._platformEventOnToggle = undefined;
}
/**
* Method to build the Sidebar
*
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public build(): void {
super.build();
this.setCallbacks();
this.setHtmlElements();
this._handleFocusBehavior();
this._setInitialCssClasses();
this.setA11YProperties();
this._handleGestureEvents();
this.finishBuild();
}
/**
* Method to change the value of configs/current state.
*
* @param {string} propertyName
* @param {unknown} propertyValue
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public changeProperty(propertyName: string, propertyValue: unknown): void {
super.changeProperty(propertyName, propertyValue);
if (this.isBuilt) {
// Check which property changed and call respective method to update it
switch (propertyName) {
case Enum.Properties.StartsOpen:
console.warn(
`Sidebar (${this.widgetId}): changes to ${Enum.Properties.StartsOpen} parameter do not affect the sidebar. Use the client actions 'SidebarOpen' and 'SidebarClose' to affect the Sidebar.`
);
break;
case Enum.Properties.Direction:
this._setDirection();
break;
case Enum.Properties.Width:
this._setWidth();
break;
case Enum.Properties.HasOverlay:
this._setHasOverlay();
break;
}
}
}
/**
* Public method to toggle the click on outside to close the sidebar.
*
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public clickOutsideToClose(closeOnOutSideClick: boolean): void {
this._clickOutsideToClose = closeOnOutSideClick;
}
/**
* Public method to close the sidebar, if it's open.
*
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public close(): void {
if (this._isOpen) {
this._closeSidebar();
}
}
/**
* Method to remove event listener and destroy sidebar instance
*
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public dispose(): void {
this.unsetCallbacks();
this.unsetHtmlElements();
// Remove focus trap events and callbacks
this._focusTrapInstance.dispose();
// Remove gesture events
if (this._hasGestureEvents) {
this.removeGestureEvents();
}
//Destroying the base of pattern
super.dispose();
}
/**
* Method that opens the sidebar.
*
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public open(): void {
if (this._isOpen === false) {
this._openSidebar();
}
}
/**
* Method to set the callbacks for the pattern
*
* @param {string} eventName
* @param {GlobalCallbacks.OSGeneric} callback
@memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public registerCallback(eventName: string, callback: GlobalCallbacks.OSGeneric): void {
switch (eventName) {
case Patterns.Sidebar.Enum.Events.OnToggle:
if (this._platformEventOnToggle === undefined) {
this._platformEventOnToggle = callback;
} else {
console.warn(`The ${GlobalEnum.PatternName.Sidebar} already has the toggle callback set.`);
}
break;
default:
super.registerCallback(eventName, callback);
}
}
/**
* Method that removes the gesture events to open/close the Sidebar on Native Apps
*
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public removeGestureEvents(): void {
if (this._gestureEventInstance !== undefined) {
this._gestureEventInstance.unsetTouchEvents();
this._hasGestureEvents = false;
// Unset the event instance of gesture events
this._gestureEventInstance = undefined;
}
}
/**
* Method that sets the gesture events to open/close the Sidebar on Native Apps
*
* @protected
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public setGestureEvents(
onGestureStartCallback: Event.GestureEvent.Callbacks.GestureStart,
onGestureMoveCallback: Event.GestureEvent.Callbacks.GestureMove,
onGestureEndCallback: Event.GestureEvent.Callbacks.GestureEnd
): void {
this._gestureEventInstance.setSwipeEvents(
onGestureStartCallback,
onGestureMoveCallback,
onGestureEndCallback
);
this._hasGestureEvents = true;
}
/**
* Method that toggle swipes on sidebar.
*
* @param {boolean} enableSwipe
* @memberof Sidebar
*/
public toggleGestures(enableSwipe: boolean): void {
this._toggleGesturesSidebar(enableSwipe);
}
/**
* Get Gesture Events Instance
*
* @readonly
* @type {Event.GestureEvent.DragEvent}
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public get gestureEventInstance(): Event.GestureEvent.DragEvent {
return this._gestureEventInstance;
}
/**
* Get if has gesture events
*
* @readonly
* @type {boolean}
* @memberof OSFramework.Patterns.Sidebar.Sidebar
*/
public get hasGestureEvents(): boolean {
return this._hasGestureEvents;
}
}
}