outsystems-ui
Version:
OutSystems UI Framework
728 lines (640 loc) • 23.3 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Patterns.AccordionItem {
/**
* Defines the interface for OutSystemsUI Patterns
*/
export class AccordionItem
extends AbstractChild<AccordionItemConfig, Accordion.IAccordion>
implements IAccordionItem
{
// Stores the HTML element of the pattern's content
private _accordionItemContentElem: HTMLElement;
// Stores the HTML element of the pattern's icon
private _accordionItemIconCustomElem: HTMLElement;
// Stores the HTML element of the pattern's icon
private _accordionItemIconElem: HTMLElement;
// Stores the HTML element of the pattern's placeholder
private _accordionItemPlaceholder: HTMLElement;
// Stores the HTML element of the pattern's title
private _accordionItemTitleElem: HTMLElement;
// Stores the HTML element of the pattern's title placeholder
private _accordionTitleFocusableChildren: HTMLElement[];
// Stores if should be aware of elements clickable inside the title
private _allowTitleEvents: boolean;
// Store the collapsed height value
private _collapsedHeight = 0;
// Store the click event with bind(this)
private _eventOnClick: GlobalCallbacks.Generic;
//Stores the transition end callback function
private _eventOnTransitionEnd: GlobalCallbacks.Generic;
//Stores the keyboard callback function
private _eventOnkeyPress: GlobalCallbacks.Generic;
// Store the expanded height value
private _expandedHeight: number;
// Stores if the element is open
private _isOpen: boolean;
// Callback function to trigger the click event on the platform
private _platformEventOnToggle: GlobalCallbacks.Generic;
constructor(uniqueId: string, configs: JSON) {
super(uniqueId, new AccordionItemConfig(configs));
this._isOpen = this.configs.StartsExpanded;
}
/**
* Getter that obtains the element that should be the clicking place to toggle the accordion.
*
* @readonly
* @private
* @type {HTMLElement}
* @memberof AccordionItem
*/
private get _elementWithEvents(): HTMLElement {
let elementWithEvent = this._accordionItemTitleElem;
if (this.configs.ToggleWithIcon) {
if (this.configs.Icon !== Enum.IconType.Custom) {
elementWithEvent = this._accordionItemIconElem;
} else {
elementWithEvent = this._accordionItemIconCustomElem;
}
}
return elementWithEvent;
}
/**
* Getter that obtains the previous element that was the clicking place. Useful to remove
* events and A11Y attributes.
*
* @readonly
* @private
* @type {HTMLElement}
* @memberof AccordionItem
*/
private get _previousElementWithEvents(): HTMLElement {
let elementWithEvent = this._accordionItemTitleElem;
if (!this.configs.ToggleWithIcon) {
if (this.configs.Icon !== Enum.IconType.Custom) {
elementWithEvent = this._accordionItemIconElem;
} else {
elementWithEvent = this._accordionItemIconCustomElem;
}
}
return elementWithEvent;
}
/**
* Method to handle the click event
*
* @private
* @param {MouseEvent} event
* @return {*} {void}
* @memberof AccordionItem
*/
private _accordionOnClickHandler(event: MouseEvent): void {
if (this._allowTitleEvents) {
if (
event?.target !== this._accordionItemTitleElem &&
event?.target !== this._accordionItemIconElem &&
event?.target !== this._accordionItemTitleElem.firstChild
) {
return;
}
}
if (this._isOpen) {
this.close();
} else {
this.open();
}
}
/**
* Method to add the event listeners
*
* @private
* @memberof AccordionItem
*/
private _addEvents(): void {
const elem = this._elementWithEvents;
elem.addEventListener(GlobalEnum.HTMLEvent.Click, this._eventOnClick);
elem.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnkeyPress);
}
/**
* Method to handle async animation
*
* @private
* @param {boolean} isExpand
* @memberof AccordionItem
*/
private _animationAsync(isExpand: boolean): void {
const finalHeight = isExpand ? this._expandedHeight : this._collapsedHeight;
// Adds is--animating class to current accordion item content to obtain the final height value
Helper.Dom.Styles.AddClass(this._accordionItemContentElem, Enum.CssClass.PatternAnimating);
if (!isExpand) {
Helper.Dom.Styles.RemoveClass(this._accordionItemContentElem, Enum.CssClass.PatternExpanded);
}
Helper.Dom.Styles.SetStyleAttribute(
this._accordionItemContentElem,
GlobalEnum.InlineStyle.Height,
finalHeight + GlobalEnum.Units.Pixel
);
this._accordionItemContentElem.addEventListener(
GlobalEnum.HTMLEvent.TransitionEnd,
this._eventOnTransitionEnd
);
if (isExpand) {
// End of animation, item is expanded
Helper.Dom.Styles.AddClass(this._accordionItemContentElem, Enum.CssClass.PatternExpanded);
this._isOpen = true;
} else {
// End of animation, item is collapsed
Helper.Dom.Styles.AddClass(this._accordionItemContentElem, Enum.CssClass.PatternCollapsed);
this._isOpen = false;
}
this.setA11YProperties();
this._onToggleCallback();
}
/**
* Method that toogles the clickable area between the whole title (default) and the icon.
*
* @private
* @memberof AccordionItem
*/
private _changeClikableTargetArea(): void {
if (this._previousElementWithEvents !== this._elementWithEvents) {
Helper.Dom.Styles.RemoveClass(this._previousElementWithEvents, Enum.CssClass.PatternClickArea);
this._removeEvents();
this._removeA11Properties();
this._setA11ToogleElement();
this._handleTabIndex();
this._addEvents();
Helper.Dom.Styles.AddClass(this._elementWithEvents, Enum.CssClass.PatternClickArea);
}
}
/**
* Method to handle the tabindex values
*
* @private
* @memberof AccordionItem
*/
private _handleTabIndex(): void {
const titleTabindexValue = this.configs.IsDisabled
? Constants.A11YAttributes.States.TabIndexHidden
: Constants.A11YAttributes.States.TabIndexShow;
const titleFocusableElementsTabindexValue =
!this.configs.IsDisabled && this._isOpen
? Constants.A11YAttributes.States.TabIndexShow
: Constants.A11YAttributes.States.TabIndexHidden;
Helper.A11Y.TabIndex(this._elementWithEvents, titleTabindexValue);
// The focusable elements inside the Accordion Title must be non-focusable unless the Accordion is expanded
for (const child of this._accordionTitleFocusableChildren) {
Helper.A11Y.TabIndex(child as HTMLElement, titleFocusableElementsTabindexValue);
}
}
/**
* Method to handle Keyboardpress event
*
* @private
* @param {KeyboardEvent} event
* @return {*} {void}
* @memberof AccordionItem
*/
private _onKeyboardPress(event: KeyboardEvent): void {
const isEscapedKey = event.key === GlobalEnum.Keycodes.Escape;
const isEnterOrSpaceKey =
event.key === GlobalEnum.Keycodes.Enter || event.key === GlobalEnum.Keycodes.Space;
if (isEscapedKey || isEnterOrSpaceKey) {
event.preventDefault();
event.stopPropagation();
} else {
return;
}
//If open, close AccordionItem
if (this._isOpen) {
this.close();
// If close, and Enter/Space pressed, open Acordion
} else if (isEnterOrSpaceKey && !this._isOpen) {
this.open();
}
}
/**
* Method to handle the keyboard interactions
*
* @private
* @memberof AccordionItem
*/
private _onToggleCallback(): void {
this.triggerPlatformEventCallback(this._platformEventOnToggle, this._isOpen);
}
private _removeA11Properties(): void {
const prevElementWithEvent = this._previousElementWithEvents;
Helper.Dom.Attribute.Remove(prevElementWithEvent, Constants.A11YAttributes.Role.AttrName);
Helper.Dom.Attribute.Remove(prevElementWithEvent, Constants.A11YAttributes.TabIndex);
}
/**
* Method to remove the event listeners
*
* @private
* @memberof AccordionItem
*/
private _removeEvents(): void {
const elem = this._previousElementWithEvents;
elem.removeEventListener(GlobalEnum.HTMLEvent.Click, this._eventOnClick);
elem.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnkeyPress);
}
private _setA11ToogleElement(): void {
const elem = this._elementWithEvents;
// Set ARIA Controls
Helper.A11Y.AriaControls(this._accordionItemTitleElem, this._accordionItemPlaceholder.id);
// Set roles
Helper.A11Y.RoleButton(elem);
}
/**
* Method to set the parent Info, if an accordion wrapper is being used
*
* @private
* @memberof AccordionItem
*/
private _setAccordionParent(): void {
// Get parent info
this.setParentInfo(
Constants.Dot + Accordion.Enum.CssClass.Pattern,
OutSystems.OSUI.Patterns.AccordionAPI.GetAccordionById,
true
);
// Notify parent about a new instance of this child has been created!
if (this.parentObject) {
this.notifyParent(Accordion.Enum.ChildNotifyActionType.Add);
}
}
/**
* Method that changes the icon's position
*
* @private
* @memberof AccordionItem
*/
private _setIconPosition(): void {
//If the page we're on is RTL, the icon's position has to change accordingly.
if (this.configs.IconPosition === GlobalEnum.Direction.Right) {
Helper.Dom.Styles.RemoveClass(this._accordionItemTitleElem, Enum.CssClass.PatternIconPositionIsLeft);
Helper.Dom.Styles.AddClass(this._accordionItemTitleElem, Enum.CssClass.PatternIconPositionIsRight);
} else {
Helper.Dom.Styles.RemoveClass(this._accordionItemTitleElem, Enum.CssClass.PatternIconPositionIsRight);
Helper.Dom.Styles.AddClass(this._accordionItemTitleElem, Enum.CssClass.PatternIconPositionIsLeft);
}
}
/**
* Method that changes the icon's type (Caret, Plus/Minus, Custom)
*
* @private
* @memberof AccordionItem
*/
private _setIconType(): void {
switch (this.configs.Icon) {
case Enum.IconType.PlusMinus:
Helper.Dom.Styles.RemoveClass(this._accordionItemIconElem, Enum.CssClass.PatternIconCaret);
Helper.Dom.Styles.RemoveClass(this._accordionItemIconCustomElem, Enum.CssClass.PatternIconCustom);
Helper.Dom.Styles.RemoveClass(this._accordionItemIconElem, Enum.CssClass.PatternIconHidden);
Helper.Dom.Styles.AddClass(this._accordionItemIconCustomElem, Enum.CssClass.PatternIconHidden);
Helper.Dom.Styles.AddClass(this._accordionItemIconElem, Enum.CssClass.PatternIconPlusMinus);
break;
case Enum.IconType.Custom:
Helper.Dom.Styles.RemoveClass(this._accordionItemIconElem, Enum.CssClass.PatternIconPlusMinus);
Helper.Dom.Styles.RemoveClass(this._accordionItemIconElem, Enum.CssClass.PatternIconCaret);
Helper.Dom.Styles.RemoveClass(this._accordionItemIconCustomElem, Enum.CssClass.PatternIconHidden);
Helper.Dom.Styles.AddClass(this._accordionItemIconElem, Enum.CssClass.PatternIconHidden);
Helper.Dom.Styles.AddClass(this._accordionItemIconCustomElem, Enum.CssClass.PatternIconCustom);
break;
default:
Helper.Dom.Styles.RemoveClass(this._accordionItemIconElem, Enum.CssClass.PatternIconPlusMinus);
Helper.Dom.Styles.RemoveClass(this._accordionItemIconCustomElem, Enum.CssClass.PatternIconCustom);
Helper.Dom.Styles.RemoveClass(this._accordionItemIconElem, Enum.CssClass.PatternIconHidden);
Helper.Dom.Styles.AddClass(this._accordionItemIconCustomElem, Enum.CssClass.PatternIconHidden);
Helper.Dom.Styles.AddClass(this._accordionItemIconElem, Enum.CssClass.PatternIconCaret);
break;
}
}
/**
* Method to handle the IsDisabled state
*
* @private
* @memberof AccordionItem
*/
private _setIsDisabledState(): void {
if (this.configs.IsDisabled) {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternDisabled);
Helper.Dom.Styles.RemoveClass(this._elementWithEvents, Enum.CssClass.PatternClickArea);
Helper.A11Y.AriaDisabledTrue(this.selfElement);
Helper.Dom.Attribute.Remove(this._elementWithEvents, Constants.FocusableTabIndexDefault);
this._removeEvents();
this.unsetCallbacks();
} else {
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.PatternDisabled);
Helper.Dom.Styles.AddClass(this._elementWithEvents, Enum.CssClass.PatternClickArea);
Helper.A11Y.AriaDisabledFalse(this.selfElement);
Helper.Dom.Attribute.Set(
this._elementWithEvents,
Constants.FocusableTabIndexDefault,
Constants.EmptyString
);
this.setCallbacks();
this._addEvents();
}
// Update tabindex values
this._handleTabIndex();
}
/**
* Method to handle the onTransitionEnd on accordion toggle animation
*
* @private
* @memberof AccordionItem
*/
private _transitionEndHandler(): void {
if (this._accordionItemContentElem) {
Helper.Dom.Styles.RemoveClass(this._accordionItemContentElem, Enum.CssClass.PatternAnimating);
Helper.Dom.Styles.SetStyleAttribute(this._accordionItemContentElem, GlobalEnum.InlineStyle.Height, '');
Helper.Dom.Styles.SetStyleAttribute(
this._accordionItemTitleElem,
GlobalEnum.InlineStyle.PointerEvents,
''
);
if (this._accordionItemContentElem.style.cssText.length === 0) {
Helper.Dom.Attribute.Remove(this._accordionItemContentElem, GlobalEnum.HTMLAttributes.Style);
}
this._accordionItemContentElem.removeEventListener(
GlobalEnum.HTMLEvent.TransitionEnd,
this._transitionEndHandler,
false
);
}
}
/**
* Method to handle Accessibility attributes
*
* @protected
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
protected setA11YProperties(): void {
// Set the static attributes on page load only
if (this.isBuilt === false) {
// Set ARIA Controls
Helper.A11Y.AriaControls(this._accordionItemTitleElem, this._accordionItemPlaceholder.id);
// Set ARIA LabelledBy
Helper.A11Y.AriaLabelledBy(this._accordionItemContentElem, this._accordionItemTitleElem.id);
// Set aria-hidden to icon
Helper.A11Y.AriaHiddenTrue(this._accordionItemIconElem);
// Set ARIA Disabled
Helper.A11Y.AriaDisabled(this._accordionItemTitleElem, this.configs.IsDisabled);
// Set roles
Helper.A11Y.RoleButton(this._accordionItemTitleElem);
Helper.A11Y.RoleRegion(this._accordionItemContentElem);
}
// Set Tabindex
this._handleTabIndex();
// Set ARIA Expanded
Helper.A11Y.AriaExpanded(this._accordionItemTitleElem, this._isOpen.toString());
// Set aria-hidden to content
Helper.A11Y.AriaHidden(this._accordionItemContentElem, (!this._isOpen).toString());
// The focusable elements inside the Accordion Title must be hidden unless the Accordion is expanded
for (const child of this._accordionTitleFocusableChildren) {
Helper.A11Y.AriaHidden(child, (!this._isOpen).toString());
}
}
/**
* Method to set the listeners and callbacks
*
* @protected
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
protected setCallbacks(): void {
this._eventOnClick = this._accordionOnClickHandler.bind(this);
this._eventOnTransitionEnd = this._transitionEndHandler.bind(this);
this._eventOnkeyPress = this._onKeyboardPress.bind(this);
}
/**
* Method to set the HTML elements of the Accordion Item
*
* @protected
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
protected setHtmlElements(): void {
this._accordionItemTitleElem = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.PatternTitle);
this._accordionItemContentElem = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.PatternContent);
this._accordionItemIconElem = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.PatternIcon);
// Getting the custom icon that is also a placeholder (ph)
this._accordionItemIconCustomElem = Helper.Dom.ClassSelector(
this.selfElement,
Enum.CssClass.PatternIcon + '.' + GlobalEnum.CssClassElements.Placeholder
);
this._accordionItemPlaceholder = this._accordionItemContentElem.firstChild as HTMLElement;
// Get all focusable elements inside Accordion Title
this._accordionTitleFocusableChildren = Helper.Dom.TagSelectorAll(
this._accordionItemTitleElem,
Constants.FocusableElems
);
}
/**
* Method to set the initial CSS Classes
*
* @protected
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
protected setInitialCssClasses(): void {
if (this._isOpen) {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternOpen);
Helper.Dom.Styles.AddClass(this._accordionItemContentElem, Enum.CssClass.PatternExpanded);
} else {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternClosed);
Helper.Dom.Styles.AddClass(this._accordionItemContentElem, Enum.CssClass.PatternCollapsed);
}
if (this.configs.ToggleWithIcon) {
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternToggleWithIcon);
}
this._setIconType();
this._setIconPosition();
}
/**
* Method to remove all assigned callbacks
*
* @protected
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
protected unsetCallbacks(): void {
this._eventOnClick = undefined;
this._eventOnTransitionEnd = undefined;
this._eventOnkeyPress = undefined;
}
/**
* Method to unset the html elements
*
* @protected
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
protected unsetHtmlElements(): void {
this._accordionItemTitleElem = undefined;
this._accordionItemContentElem = undefined;
this._accordionItemIconElem = undefined;
this._accordionItemPlaceholder = undefined;
this._accordionTitleFocusableChildren = [];
}
/**
* Method to return the isDisabled value
*
* @readonly
* @type {boolean}
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public get isDisabled(): boolean {
return this.configs.IsDisabled;
}
/**
* Method to return the IsOpen value
*
* @readonly
* @type {boolean}
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public get isOpen(): boolean {
return this._isOpen;
}
/**
* Method to prevent clicks inside thte title to open the accordion
*
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public allowTitleEvents(): void {
this._allowTitleEvents = true;
}
/**
* Method to build the AccordionItem
*
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public build(): void {
super.build();
this.setHtmlElements();
this.setInitialCssClasses();
this._setAccordionParent();
this._setIsDisabledState();
this.setA11YProperties();
this.finishBuild();
}
/**
* Method to change the value of configs/current state.
*
* @param {string} propertyName
* @param {unknown} propertyValue
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public changeProperty(propertyName: string, propertyValue: unknown): void {
super.changeProperty(propertyName, propertyValue);
if (this.isBuilt) {
switch (propertyName) {
case Enum.Properties.IsDisabled:
this._setIsDisabledState();
break;
case Enum.Properties.StartsExpanded:
console.warn(
`${GlobalEnum.PatternName.AccordionItem} (${this.widgetId}): changes to ${Enum.Properties.StartsExpanded} parameter do not affect the ${GlobalEnum.PatternName.AccordionItem}. Use the client actions 'AccordionItemExpand' and 'AccordionItemCollapse' to affect the ${GlobalEnum.PatternName.AccordionItem}.`
);
break;
case Enum.Properties.IconPosition:
this._setIconPosition();
break;
case Enum.Properties.Icon:
this._setIconType();
break;
case Enum.Properties.ToggleWithIcon:
this._changeClikableTargetArea();
break;
}
}
}
/**
* Method to close the AccordionItem
*
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public close(): void {
if (!this._isOpen) {
return;
}
Helper.Dom.Attribute.Remove(this._accordionItemContentElem, GlobalEnum.HTMLAttributes.Style);
this._expandedHeight = this._accordionItemContentElem.getBoundingClientRect().height;
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternClosed);
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.PatternOpen);
// Removes collapsed class and adds the expanded class to animate
Helper.Dom.Styles.RemoveClass(this._accordionItemContentElem, Enum.CssClass.PatternExpanded);
Helper.Dom.Styles.SetStyleAttribute(
this._accordionItemContentElem,
GlobalEnum.InlineStyle.Height,
this._expandedHeight + GlobalEnum.Units.Pixel
);
Helper.AsyncInvocation(() => {
this._animationAsync(false);
});
}
/**
* Method to remove event listener and destroy AccordionItem instance
*
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public dispose(): void {
this.unsetCallbacks();
this._removeEvents();
if (this.parentObject) {
// Notify parent about this instance will be destroyed
this.notifyParent(Accordion.Enum.ChildNotifyActionType.Removed);
}
this.unsetHtmlElements();
super.dispose();
}
/**
* Method to open the AccordionItem
*
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public open(): void {
if (this._isOpen) {
return;
}
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.PatternClosed);
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternOpen);
// While the animation is running, we don't want any clicks happening on the title
Helper.Dom.Styles.SetStyleAttribute(
this._accordionItemTitleElem,
GlobalEnum.InlineStyle.PointerEvents,
GlobalEnum.CssProperties.None
);
Helper.Dom.Styles.RemoveClass(this._accordionItemContentElem, Enum.CssClass.PatternCollapsed);
Helper.Dom.Styles.AddClass(this._accordionItemContentElem, Enum.CssClass.PatternExpanded);
Helper.Dom.Attribute.Remove(this._accordionItemTitleElem, GlobalEnum.HTMLAttributes.Style);
this._expandedHeight = this._accordionItemContentElem.getBoundingClientRect().height;
Helper.Dom.Styles.RemoveClass(this._accordionItemContentElem, Enum.CssClass.PatternExpanded);
Helper.Dom.Styles.SetStyleAttribute(
this._accordionItemContentElem,
GlobalEnum.InlineStyle.Height,
this._collapsedHeight
);
Helper.AsyncInvocation(() => {
this._animationAsync(true);
});
// Notify parent about this Item toggled
if (this.parentObject) {
this.notifyParent(Accordion.Enum.ChildNotifyActionType.Click);
}
}
/**
* Method to register a given callback event handler.
*
* @param {string} eventName
* @param {GlobalCallbacks.OSGeneric} callback
* @memberof OSFramework.Patterns.AccordionItem.AccordionItem
*/
public registerCallback(eventName: string, callback: GlobalCallbacks.OSGeneric): void {
switch (eventName) {
case Enum.Events.OnToggle:
if (this._platformEventOnToggle === undefined) {
this._platformEventOnToggle = callback;
}
break;
default:
super.registerCallback(eventName, callback);
}
}
}
}