UNPKG

outsystems-ui

Version:
564 lines (490 loc) 17.6 kB
// 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; } } }