File

feature-libs/product-configurator/rulebased/components/group-menu/configurator-group-menu.component.ts

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector cx-configurator-group-menu
templateUrl ./configurator-group-menu.component.html

Index

Properties
Methods

Constructor

constructor(configCommonsService: ConfiguratorCommonsService, configuratorGroupsService: ConfiguratorGroupsService, hamburgerMenuService: HamburgerMenuService, configRouterExtractorService: ConfiguratorRouterExtractorService, configUtils: ConfiguratorStorefrontUtilsService, configGroupMenuService: ConfiguratorGroupMenuService, directionService: DirectionService)
Parameters :
Name Type Optional
configCommonsService ConfiguratorCommonsService No
configuratorGroupsService ConfiguratorGroupsService No
hamburgerMenuService HamburgerMenuService No
configRouterExtractorService ConfiguratorRouterExtractorService No
configUtils ConfiguratorStorefrontUtilsService No
configGroupMenuService ConfiguratorGroupMenuService No
directionService DirectionService No

Methods

click
click(group: Configurator.Group)
Parameters :
Name Type Optional
group Configurator.Group No
Returns : void
condenseGroups
condenseGroups(groups: Configurator.Group[])
Parameters :
Name Type Optional
groups Configurator.Group[] No
containsSelectedGroup
containsSelectedGroup(group: Configurator.Group, currentGroupId?: string)

Verifies whether the parent group contains a selected group.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Group
currentGroupId string Yes
  • Current group ID
Returns : boolean
  • 'true' if the parent group contains a selected group, otherwise 'false'
createAriaControls
createAriaControls(groupId?: string)

Generates a group ID for aria-controls.

Parameters :
Name Type Optional Description
groupId string Yes
  • group ID
Returns : string | undefined
  • generated group ID
getCondensedParentGroup
getCondensedParentGroup(parentGroup: Configurator.Group)
Parameters :
Name Type Optional
parentGroup Configurator.Group No
getConflictNumber
getConflictNumber(group: Configurator.Group)

Retrieves the number of conflicts for the current group.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Current group
Returns : string
  • number of conflicts
getGroupStatusStyles
getGroupStatusStyles(group: Configurator.Group, configuration: Configurator.Configuration)

Returns group-status style classes dependent on completeness, conflicts, visited status and configurator type.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Current group
configuration Configurator.Configuration No
  • Configuration
Returns : Observable<string>
  • true if visited and not a conflict group
Protected getParentGroup
getParentGroup(group: Configurator.Group)

Retrieves observable of parent group for a group

Parameters :
Name Type Optional
group Configurator.Group No

Parent group, undefined in case input group is already on root level

getTabIndex
getTabIndex(group: Configurator.Group, currentGroupId: string)

Retrieves the tab index depending on if the the current group is selected or the parent group contains the selected group.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Group
currentGroupId string No
  • Current group ID
Returns : number
  • tab index
hasSubGroups
hasSubGroups(group: Configurator.Group)

Verifies whether the current group has subgroups.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Current group
Returns : boolean
  • Returns 'true' if the current group has a subgroups, otherwise 'false'.
Protected isBackNavigation
isBackNavigation(event: KeyboardEvent)

Verifies whether the user navigates from a subgroup back to the main group menu.

Parameters :
Name Type Optional Description
event KeyboardEvent No
  • Keyboard event
Returns : boolean

-'true' if the user navigates back into the main group menu, otherwise 'false'.

isConflictGroupType
isConflictGroupType(groupType: Configurator.GroupType)

Verifies whether the current group is conflict one.

Parameters :
Name Type Optional Description
groupType Configurator.GroupType No
  • Group type
Returns : boolean
  • 'True' if the current group is conflict one, otherwise 'false'.
Protected isForwardsNavigation
isForwardsNavigation(event: KeyboardEvent)

Verifies whether the user navigates into a subgroup of the main group menu.

Parameters :
Name Type Optional Description
event KeyboardEvent No
  • Keyboard event
Returns : boolean

-'true' if the user navigates into the subgroup, otherwise 'false'.

isGroupSelected
isGroupSelected(groupId?: string, currentGroupId?: string)

Verifies whether the current group is selected.

Parameters :
Name Type Optional Description
groupId string Yes
  • group ID
currentGroupId string Yes
  • Current group ID
Returns : boolean
  • 'true' if the current group is selected, otherwise 'false'
isGroupVisited
isGroupVisited(group: Configurator.Group, configuration: Configurator.Configuration)

Returns true if group has been visited and if the group is not a conflict group.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Current group
configuration Configurator.Configuration No
  • Configuration
Returns : Observable<boolean>
  • true if visited and not a conflict group
Protected isLTRDirection
isLTRDirection()
Returns : boolean
Protected isRTLDirection
isRTLDirection()
Returns : boolean
navigateUp
navigateUp()
Returns : void
setFocusForMainMenu
setFocusForMainMenu(currentGroupId?: string)

Persists the keyboard focus state for the given key from the main group menu by back navigation.

Parameters :
Name Type Optional Description
currentGroupId string Yes
  • Current group ID
Returns : void
setFocusForSubGroup
setFocusForSubGroup(group: Configurator.Group, currentGroupId?: string)

Persists the keyboard focus state for the given key from the subgroup menu by forwards navigation.

Parameters :
Name Type Optional Description
group Configurator.Group No
  • Group
currentGroupId string Yes
  • Current group ID
Returns : void
switchGroupOnArrowPress
switchGroupOnArrowPress(event: KeyboardEvent, groupIndex: number, targetGroup: Configurator.Group, currentGroup: Configurator.Group)

Switches the group on pressing an arrow key.

Parameters :
Name Type Optional Description
event KeyboardEvent No
  • Keyboard event
groupIndex number No
  • Group index
targetGroup Configurator.Group No
  • Target group
currentGroup Configurator.Group No
  • Current group
Returns : void

Properties

COMPLETE
Type : string
Default value : ' COMPLETE'
configuration$
Type : Observable<Configurator.Configuration>
Default value : this.routerData$.pipe( switchMap((routerData) => this.configCommonsService .getConfiguration(routerData.owner) .pipe( map((configuration) => ({ routerData, configuration })), //We need to ensure that the navigation to conflict groups or //groups with mandatory attributes already has taken place, as this happens //in an onInit of another component. //otherwise we risk that this component is completely initialized too early, //in dev mode resulting in ExpressionChangedAfterItHasBeenCheckedError filter( (cont) => (cont.configuration.complete && cont.configuration.consistent) || cont.configuration.interactionState.issueNavigationDone || !cont.routerData.resolveIssues ) ) .pipe(map((cont) => cont.configuration)) ) )
currentGroup$
Type : Observable<Configurator.Group>
Default value : this.routerData$.pipe( switchMap((routerData) => this.configuratorGroupsService.getCurrentGroup(routerData.owner) ) )
displayedGroups$
Type : Observable<Configurator.Group[]>
Default value : this.displayedParentGroup$.pipe( switchMap((parentGroup) => { return this.configuration$.pipe( map((configuration) => { if (parentGroup) { return this.condenseGroups(parentGroup.subGroups); } else { return this.condenseGroups(configuration.groups); } }) ); }) )
displayedParentGroup$
Type : Observable<Configurator.Group | undefined>
Default value : this.configuration$.pipe( switchMap((configuration) => this.configuratorGroupsService.getMenuParentGroup(configuration.owner) ), switchMap((parentGroup) => { return parentGroup ? this.getCondensedParentGroup(parentGroup) : of(parentGroup); }) )

Current parent group. Undefined for top level groups

ERROR
Type : string
Default value : ' ERROR'
groups
Type : QueryList<ElementRef<HTMLElement>>
Decorators :
@ViewChildren('groupItem')
iconTypes
Default value : ICON_TYPE
routerData$
Type : Observable<ConfiguratorRouter.Data>
Default value : this.configRouterExtractorService.extractRouterData()
WARNING
Type : string
Default value : ' WARNING'
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  QueryList,
  ViewChildren,
} from '@angular/core';
import {
  ConfiguratorRouter,
  ConfiguratorRouterExtractorService,
} from '@spartacus/product-configurator/common';
import {
  DirectionMode,
  DirectionService,
  HamburgerMenuService,
  ICON_TYPE,
} from '@spartacus/storefront';
import { Observable, of } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service';
import { ConfiguratorGroupsService } from '../../core/facade/configurator-groups.service';
import { Configurator } from '../../core/model/configurator.model';
import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service';
import { ConfiguratorGroupMenuService } from './configurator-group-menu.component.service';

@Component({
  selector: 'cx-configurator-group-menu',
  templateUrl: './configurator-group-menu.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfiguratorGroupMenuComponent {
  @ViewChildren('groupItem') groups: QueryList<ElementRef<HTMLElement>>;

  routerData$: Observable<ConfiguratorRouter.Data> =
    this.configRouterExtractorService.extractRouterData();

  configuration$: Observable<Configurator.Configuration> =
    this.routerData$.pipe(
      switchMap((routerData) =>
        this.configCommonsService
          .getConfiguration(routerData.owner)
          .pipe(
            map((configuration) => ({ routerData, configuration })),
            //We need to ensure that the navigation to conflict groups or
            //groups with mandatory attributes already has taken place, as this happens
            //in an onInit of another component.
            //otherwise we risk that this component is completely initialized too early,
            //in dev mode resulting in ExpressionChangedAfterItHasBeenCheckedError
            filter(
              (cont) =>
                (cont.configuration.complete &&
                  cont.configuration.consistent) ||
                cont.configuration.interactionState.issueNavigationDone ||
                !cont.routerData.resolveIssues
            )
          )

          .pipe(map((cont) => cont.configuration))
      )
    );

  currentGroup$: Observable<Configurator.Group> = this.routerData$.pipe(
    switchMap((routerData) =>
      this.configuratorGroupsService.getCurrentGroup(routerData.owner)
    )
  );
  /**
   * Current parent group. Undefined for top level groups
   */
  displayedParentGroup$: Observable<Configurator.Group | undefined> =
    this.configuration$.pipe(
      switchMap((configuration) =>
        this.configuratorGroupsService.getMenuParentGroup(configuration.owner)
      ),
      switchMap((parentGroup) => {
        return parentGroup
          ? this.getCondensedParentGroup(parentGroup)
          : of(parentGroup);
      })
    );

  displayedGroups$: Observable<Configurator.Group[]> =
    this.displayedParentGroup$.pipe(
      switchMap((parentGroup) => {
        return this.configuration$.pipe(
          map((configuration) => {
            if (parentGroup) {
              return this.condenseGroups(parentGroup.subGroups);
            } else {
              return this.condenseGroups(configuration.groups);
            }
          })
        );
      })
    );

  iconTypes = ICON_TYPE;
  ERROR = ' ERROR';
  COMPLETE = ' COMPLETE';
  WARNING = ' WARNING';

  constructor(
    protected configCommonsService: ConfiguratorCommonsService,
    protected configuratorGroupsService: ConfiguratorGroupsService,
    protected hamburgerMenuService: HamburgerMenuService,
    protected configRouterExtractorService: ConfiguratorRouterExtractorService,
    protected configUtils: ConfiguratorStorefrontUtilsService,
    protected configGroupMenuService: ConfiguratorGroupMenuService,
    protected directionService: DirectionService
  ) {}

  click(group: Configurator.Group): void {
    this.configuration$.pipe(take(1)).subscribe((configuration) => {
      if (configuration.interactionState.currentGroup === group.id) {
        return;
      }
      if (!this.configuratorGroupsService.hasSubGroups(group)) {
        this.configuratorGroupsService.navigateToGroup(configuration, group.id);
        this.hamburgerMenuService.toggle(true);

        this.configUtils.scrollToConfigurationElement(
          '.VariantConfigurationTemplate, .CpqConfigurationTemplate'
        );
      } else {
        this.configuratorGroupsService.setMenuParentGroup(
          configuration.owner,
          group.id
        );
      }
    });
  }

  navigateUp(): void {
    this.displayedParentGroup$
      .pipe(take(1))
      .subscribe((displayedParentGroup) => {
        //we only navigate up if we are not on a sub level group
        if (displayedParentGroup) {
          const grandParentGroup$ = this.getParentGroup(displayedParentGroup);
          this.configuration$.pipe(take(1)).subscribe((configuration) => {
            grandParentGroup$.pipe(take(1)).subscribe((grandParentGroup) => {
              this.configuratorGroupsService.setMenuParentGroup(
                configuration.owner,
                grandParentGroup ? grandParentGroup.id : undefined
              );
            });
          });
        }
      });
  }

  /**
   * Retrieves the number of conflicts for the current group.
   *
   * @param {Configurator.Group} group - Current group
   * @return {string} - number of conflicts
   */
  getConflictNumber(group: Configurator.Group): string {
    if (group.groupType === Configurator.GroupType.CONFLICT_HEADER_GROUP) {
      return '(' + group.subGroups.length + ')';
    }
    return '';
  }

  /**
   * Verifies whether the current group has subgroups.
   *
   * @param {Configurator.Group} group - Current group
   * @return {boolean} - Returns 'true' if the current group has a subgroups, otherwise 'false'.
   */
  hasSubGroups(group: Configurator.Group): boolean {
    return this.configuratorGroupsService.hasSubGroups(group);
  }

  /**
   * Retrieves observable of parent group for a group
   * @param group
   * @returns Parent group, undefined in case input group is already on root level
   */
  protected getParentGroup(
    group: Configurator.Group
  ): Observable<Configurator.Group | undefined> {
    return this.configuration$.pipe(
      map((configuration) =>
        this.configuratorGroupsService.getParentGroup(
          configuration.groups,
          group
        )
      )
    );
  }

  getCondensedParentGroup(
    parentGroup: Configurator.Group
  ): Observable<Configurator.Group | undefined> {
    if (
      parentGroup &&
      parentGroup.subGroups &&
      parentGroup.subGroups.length === 1 &&
      parentGroup.groupType !== Configurator.GroupType.CONFLICT_HEADER_GROUP
    ) {
      return this.getParentGroup(parentGroup).pipe(
        switchMap((group) => {
          return group ? this.getCondensedParentGroup(group) : of(group);
        })
      );
    } else {
      return of(parentGroup);
    }
  }

  condenseGroups(groups: Configurator.Group[]): Configurator.Group[] {
    return groups.flatMap((group) => {
      if (
        group.subGroups.length === 1 &&
        group.groupType !== Configurator.GroupType.CONFLICT_HEADER_GROUP
      ) {
        return this.condenseGroups(group.subGroups);
      } else {
        return group;
      }
    });
  }

  /**
   * Returns true if group has been visited and if the group is not a conflict group.
   *
   * @param {Configurator.Group} group - Current group
   * @param {Configurator.Configuration} configuration - Configuration
   * @return {Observable<boolean>} - true if visited and not a conflict group
   */
  isGroupVisited(
    group: Configurator.Group,
    configuration: Configurator.Configuration
  ): Observable<boolean> {
    return this.configuratorGroupsService
      .isGroupVisited(configuration.owner, group.id)
      .pipe(
        map(
          (isVisited) =>
            isVisited &&
            !this.isConflictGroupType(
              group.groupType ?? Configurator.GroupType.ATTRIBUTE_GROUP
            )
        ),
        take(1)
      );
  }

  /**
   * Verifies whether the current group is conflict one.
   *
   * @param {Configurator.GroupType} groupType - Group type
   * @return {boolean} - 'True' if the current group is conflict one, otherwise 'false'.
   */
  isConflictGroupType(groupType: Configurator.GroupType): boolean {
    return this.configuratorGroupsService.isConflictGroupType(groupType);
  }

  /**
   * Returns group-status style classes dependent on completeness, conflicts, visited status and configurator type.
   *
   * @param {Configurator.Group} group - Current group
   * @param {Configurator.Configuration} configuration - Configuration
   * @return {Observable<boolean>} - true if visited and not a conflict group
   */
  getGroupStatusStyles(
    group: Configurator.Group,
    configuration: Configurator.Configuration
  ): Observable<string> {
    return this.isGroupVisited(group, configuration).pipe(
      map((isVisited) => {
        const CLOUDCPQ_CONFIGURATOR_TYPE = 'CLOUDCPQCONFIGURATOR';
        let groupStatusStyle: string = 'cx-menu-item';
        if (
          configuration.owner.configuratorType !== CLOUDCPQ_CONFIGURATOR_TYPE &&
          !group.consistent
        ) {
          groupStatusStyle = groupStatusStyle + this.WARNING;
        }
        if (
          configuration.owner.configuratorType !== CLOUDCPQ_CONFIGURATOR_TYPE &&
          group.complete &&
          group.consistent &&
          isVisited
        ) {
          groupStatusStyle = groupStatusStyle + this.COMPLETE;
        }
        if (!group.complete && isVisited) {
          groupStatusStyle = groupStatusStyle + this.ERROR;
        }
        return groupStatusStyle;
      })
    );
  }

  protected isLTRDirection(): boolean {
    return this.directionService.getDirection() === DirectionMode.LTR;
  }

  protected isRTLDirection(): boolean {
    return this.directionService.getDirection() === DirectionMode.RTL;
  }

  /**
   * Verifies whether the user navigates into a subgroup of the main group menu.
   *
   * @param {KeyboardEvent} event - Keyboard event
   * @returns {boolean} -'true' if the user navigates into the subgroup, otherwise 'false'.
   * @protected
   */
  protected isForwardsNavigation(event: KeyboardEvent): boolean {
    return (
      (event.code === 'ArrowRight' && this.isLTRDirection()) ||
      (event.code === 'ArrowLeft' && this.isRTLDirection())
    );
  }

  /**
   * Verifies whether the user navigates from a subgroup back to the main group menu.
   *
   * @param {KeyboardEvent} event - Keyboard event
   * @returns {boolean} -'true' if the user navigates back into the main group menu, otherwise 'false'.
   * @protected
   */
  protected isBackNavigation(event: KeyboardEvent): boolean {
    return (
      (event.code === 'ArrowLeft' && this.isLTRDirection()) ||
      (event.code === 'ArrowRight' && this.isRTLDirection())
    );
  }

  /**
   * Switches the group on pressing an arrow key.
   *
   * @param {KeyboardEvent} event - Keyboard event
   * @param {string} groupIndex - Group index
   * @param {Configurator.Group} targetGroup - Target group
   * @param {Configurator.Group} currentGroup - Current group
   */
  switchGroupOnArrowPress(
    event: KeyboardEvent,
    groupIndex: number,
    targetGroup: Configurator.Group,
    currentGroup: Configurator.Group
  ): void {
    if (event.code === 'ArrowUp' || event.code === 'ArrowDown') {
      this.configGroupMenuService.switchGroupOnArrowPress(
        event,
        groupIndex,
        this.groups
      );
    } else if (this.isForwardsNavigation(event)) {
      if (targetGroup && this.hasSubGroups(targetGroup)) {
        this.click(targetGroup);
        this.setFocusForSubGroup(targetGroup, currentGroup.id);
      }
    } else if (this.isBackNavigation(event)) {
      if (this.configGroupMenuService.isBackBtnFocused(this.groups)) {
        this.navigateUp();
        this.setFocusForMainMenu(currentGroup.id);
      }
    }
  }

  /**
   * Persists the keyboard focus state for the given key
   * from the main group menu by back navigation.
   *
   * @param {string} currentGroupId - Current group ID
   */
  setFocusForMainMenu(currentGroupId?: string): void {
    let key: string | undefined = currentGroupId;
    this.configuration$.pipe(take(1)).subscribe((configuration) => {
      configuration.groups?.forEach((group) => {
        if (
          group.subGroups?.length !== 1 &&
          (this.isGroupSelected(group.id, currentGroupId) ||
            this.containsSelectedGroup(group, currentGroupId))
        ) {
          key = group.id;
        }
      });
    });
    this.configUtils.setFocus(key);
  }

  /**
   * Persists the keyboard focus state for the given key
   * from the subgroup menu by forwards navigation.
   *
   * @param {Configurator.Group} group - Group
   * @param {string} currentGroupId - Current group ID
   */
  setFocusForSubGroup(
    group: Configurator.Group,
    currentGroupId?: string
  ): void {
    let key: string | undefined = 'cx-menu-back';
    if (this.containsSelectedGroup(group, currentGroupId)) {
      key = currentGroupId;
    }
    this.configUtils.setFocus(key);
  }

  /**
   * Verifies whether the parent group contains a selected group.
   *
   * @param {Configurator.Group} group - Group
   * @param {string} currentGroupId - Current group ID
   * @returns {boolean} - 'true' if the parent group contains a selected group, otherwise 'false'
   */
  containsSelectedGroup(
    group: Configurator.Group,
    currentGroupId?: string
  ): boolean {
    let isCurrentGroupFound = false;
    group.subGroups?.forEach((subGroup) => {
      if (this.isGroupSelected(subGroup.id, currentGroupId)) {
        isCurrentGroupFound = true;
      }
    });
    return isCurrentGroupFound;
  }

  /**
   * Retrieves the tab index depending on if the the current group is selected
   * or the parent group contains the selected group.
   *
   * @param {Configurator.Group} group - Group
   * @param {string} currentGroupId - Current group ID
   * @returns {number} - tab index
   */
  getTabIndex(group: Configurator.Group, currentGroupId: string): number {
    if (
      !this.isGroupSelected(group.id, currentGroupId) &&
      !this.containsSelectedGroup(group, currentGroupId)
    ) {
      return -1;
    } else {
      return 0;
    }
  }

  /**
   * Verifies whether the current group is selected.
   *
   * @param {string} groupId - group ID
   * @param {string} currentGroupId - Current group ID
   * @returns {boolean} - 'true' if the current group is selected, otherwise 'false'
   */
  isGroupSelected(groupId?: string, currentGroupId?: string): boolean {
    return groupId === currentGroupId;
  }

  /**
   * Generates a group ID for aria-controls.
   *
   * @param {string} groupId - group ID
   * @returns {string | undefined} - generated group ID
   */
  createAriaControls(groupId?: string): string | undefined {
    return this.configUtils.createGroupId(groupId);
  }
}
<ng-container *ngIf="configuration$ | async as configuration">
  <div class="cx-group-menu" role="tablist">
    <ng-container *ngIf="displayedGroups$ | async as groups">
      <ng-container *ngIf="currentGroup$ | async as currentGroup">
        <ng-container *ngFor="let group of groups; let groupIndex = index">
          <ng-container *ngIf="displayedParentGroup$ | async as parentGroup">
            <button
              *ngIf="parentGroup !== null && groupIndex === 0"
              #groupItem
              class="cx-menu-back"
              role="tab"
              [attr.aria-selected]="false"
              [cxFocus]="{ key: 'cx-menu-back' }"
              (click)="navigateUp()"
              (keydown)="
                switchGroupOnArrowPress($event, groupIndex, group, currentGroup)
              "
            >
              <cx-icon [type]="iconTypes.CARET_LEFT"></cx-icon>
              {{ 'configurator.button.back' | cxTranslate }}
            </button>
          </ng-container>
          <button
            #groupItem
            id="{{ group.id }}"
            ngClass="{{ getGroupStatusStyles(group, configuration) | async }}"
            role="tab"
            [class.DISABLED]="!group.configurable"
            [class.cx-menu-conflict]="isConflictGroupType(group.groupType)"
            [class.active]="isGroupSelected(group.id, currentGroup.id)"
            [class.disable]="!group.configurable"
            [attr.aria-selected]="isGroupSelected(group.id, currentGroup.id)"
            [attr.aria-controls]="createAriaControls(group.id)"
            [cxFocus]="{
              key: group.id
            }"
            (click)="click(group)"
            [tabindex]="getTabIndex(group, currentGroup.id)"
            (keydown)="
              switchGroupOnArrowPress($event, groupIndex, group, currentGroup)
            "
          >
            <span title="{{ group.description }}">{{ group.description }}</span>
            <div class="groupIndicators">
              <div class="conflictNumberIndicator">
                {{ getConflictNumber(group) }}
              </div>
              <div class="groupStatusIndicator">
                <cx-icon class="WARNING" [type]="iconTypes.WARNING"></cx-icon>
              </div>
              <div class="groupStatusIndicator">
                <cx-icon class="ERROR" [type]="iconTypes.ERROR"></cx-icon>
                <cx-icon class="COMPLETE" [type]="iconTypes.SUCCESS"></cx-icon>
              </div>
              <div class="subGroupIndicator">
                <cx-icon
                  *ngIf="hasSubGroups(group)"
                  [type]="iconTypes.CARET_RIGHT"
                ></cx-icon>
              </div>
            </div>
          </button>
        </ng-container>
      </ng-container>
    </ng-container>
  </div>
</ng-container>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""