diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/example.html b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/example.html
index 70360433e..27464f190 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/example.html
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/example.html
@@ -1,53 +1,396 @@
gux-avatar-group-beta
+Summary
+A group that can contain gux-avatar-group-item-beta tags. A tooltip
+will be displayed for each group item on focus or hover.
+
-
+
+
+
+Properties
+quantity
+The quantity property indicates the number of group items that will be displayed
+outside of the overflow menu. The minimum is 1 and the maximum is 7 (7 is also
+the default amount).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.tsx b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.tsx
index 2cd06548a..bd8cc959b 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.tsx
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.tsx
@@ -5,11 +5,12 @@ import {
Prop,
Element,
Listen,
- Method
+ Method,
+ Host
} from '@stencil/core';
import { trackComponent } from '@utils/tracking/usage';
import { logWarn } from '@utils/error/log-error';
-import { GuxAvatarAccent } from './gux-avatar-group-item.types';
+import { GuxAvatarAccent } from '../gux-avatar-group.types';
import { groupKeyboardNavigation } from '../gux-avatar-group.service';
import { generateInitials } from '@utils/string/generate-initials';
@@ -104,32 +105,33 @@ export class GuxAvatarGroupItem {
render(): JSX.Element {
return (
-
+
+
+ {generateInitials(this.name)}
+
+
+ (this.tooltip = el)}
+ >
+ {this.name}
+
+
+
) as JSX.Element;
}
}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.scss b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.scss
index b3ef49217..199444b41 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.scss
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.scss
@@ -10,3 +10,7 @@
outline: none;
}
}
+
+::slotted(gux-avatar-group-item-beta.gux-hidden) {
+ display: none;
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.service.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.service.ts
index 5c301e7db..b73bf75ef 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.service.ts
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.service.ts
@@ -1,3 +1,6 @@
+import { getClosestElement } from '@utils/dom/get-closest-element';
+import { GuxAvatarGroupChild } from './gux-avatar-group.types';
+
export function groupKeyboardNavigation(
event: KeyboardEvent,
currentElement: Element
@@ -34,7 +37,7 @@ export function groupKeyboardNavigation(
}
export function resetFocusableSibling(element: Element) {
- const focusableSibling = getSiblings(element).find((sibling: Element) => {
+ const focusableSibling = getGroupItems(element).find((sibling: Element) => {
const button = getGroupItemButton(sibling);
return button && button.tabIndex !== -1;
});
@@ -51,30 +54,37 @@ export function setFocusTarget(element: Element): void {
function focusFirstSibling(currentElement: Element): void {
const firstFocusableElement = getFirstFocusableElement(
currentElement
- ) as HTMLGuxAvatarGroupItemBetaElement;
+ ) as GuxAvatarGroupChild;
if (firstFocusableElement) {
void firstFocusableElement.guxFocus();
- void setItemTabIndex(firstFocusableElement, 0);
void resetFocusableSibling(firstFocusableElement);
+ void setItemTabIndex(firstFocusableElement, 0);
}
}
function focusLastSibling(currentElement: Element): void {
const lastFocusableElement = getLastFocusableElement(
currentElement
- ) as HTMLGuxAvatarGroupItemBetaElement;
+ ) as GuxAvatarGroupChild;
if (lastFocusableElement) {
void lastFocusableElement.guxFocus();
- void setItemTabIndex(lastFocusableElement, 0);
void resetFocusableSibling(lastFocusableElement);
+ void setItemTabIndex(lastFocusableElement, 0);
}
}
function focusPreviousSiblingLoop(currentElement: Element): void {
- const previousFocusableElement =
- currentElement.previousElementSibling as HTMLGuxAvatarGroupItemBetaElement;
+ const groupItems = getGroupItems(currentElement);
+ const currentElementIndex = groupItems.findIndex(
+ (item: Element) => item === currentElement
+ );
+
+ const previousIndex = (currentElementIndex - 1) % groupItems.length;
+ const previousFocusableElement = groupItems[
+ previousIndex
+ ] as GuxAvatarGroupChild;
setItemTabIndex(currentElement, -1);
@@ -87,51 +97,56 @@ function focusPreviousSiblingLoop(currentElement: Element): void {
}
function focusNextSiblingLoop(currentElement: Element): void {
- const nextFocusableElement =
- currentElement.nextElementSibling as HTMLGuxAvatarGroupItemBetaElement;
+ const groupItems = getGroupItems(currentElement);
+ const currentElementIndex = groupItems.findIndex(
+ (item: Element) => item === currentElement
+ );
+
+ const nextIndex = (currentElementIndex + 1) % groupItems.length;
+ if (nextIndex === 0) {
+ focusFirstSibling(currentElement);
+ }
+ const nextFocusableElement = groupItems[nextIndex] as GuxAvatarGroupChild;
+
setItemTabIndex(currentElement, -1);
- if (nextFocusableElement) {
+ if (nextFocusableElement !== null) {
void nextFocusableElement.guxFocus();
void setItemTabIndex(nextFocusableElement, 0);
- } else {
- focusFirstSibling(currentElement);
}
}
function getFirstFocusableElement(currentElement: Element): Element {
- let firstFocusableElement = currentElement;
-
- while (firstFocusableElement.previousElementSibling !== null) {
- firstFocusableElement = firstFocusableElement.previousElementSibling;
- }
-
- return firstFocusableElement;
+ return getGroupItems(currentElement)[0];
}
function getLastFocusableElement(currentElement: Element): Element {
- let lastFocusableElement = currentElement;
-
- while (lastFocusableElement.nextElementSibling !== null) {
- lastFocusableElement = lastFocusableElement.nextElementSibling;
- }
-
- return lastFocusableElement;
+ const groupItems = getGroupItems(currentElement);
+ return groupItems[groupItems.length - 1];
}
function getGroupItemButton(element: Element): HTMLButtonElement {
return element.shadowRoot?.querySelector('button') as HTMLButtonElement;
}
-function getSiblings(element: Element): Element[] {
- const siblings = Array.from(element.parentElement.children);
-
- // Early return for performance when there are no siblings
- if (siblings.length <= 1) {
- return [];
+function getGroupItems(element: Element): Element[] {
+ const group = getClosestElement(
+ 'gux-avatar-group-beta',
+ element as HTMLElement
+ ) as Element;
+
+ const slottedItems = Array.from(
+ group.querySelectorAll('gux-avatar-group-item-beta')
+ ).filter(child => !isHidden(child)) as GuxAvatarGroupChild[];
+
+ const overflow = group.shadowRoot.querySelector(
+ 'gux-avatar-overflow-beta'
+ ) as HTMLGuxAvatarOverflowBetaElement;
+ if (overflow) {
+ slottedItems.push(overflow);
}
- return siblings.filter(child => child !== element);
+ return slottedItems as GuxAvatarGroupChild[];
}
function setItemTabIndex(element: Element, newIndex: number) {
@@ -139,6 +154,9 @@ function setItemTabIndex(element: Element, newIndex: number) {
if (button) {
button.tabIndex = newIndex;
} else {
- console.log('No button found in the gux-avatar-group-item element');
+ console.log('No button found in the element');
}
}
+function isHidden(element: Element): boolean {
+ return element.classList.contains('gux-hidden');
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.tsx b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.tsx
index 2f5d681e3..2ae597331 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.tsx
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.tsx
@@ -1,10 +1,22 @@
-import { Component, h, JSX, Element, Host, Listen } from '@stencil/core';
+import { Component, h, JSX, Element, Host, Listen, Prop } from '@stencil/core';
import { trackComponent } from '@utils/tracking/usage';
import { logWarn } from '@utils/error/log-error';
import {
setFocusTarget,
resetFocusableSibling
} from './gux-avatar-group.service';
+import {
+ GuxAvatarAccent,
+ GuxAvatarGroupQuantity
+} from './gux-avatar-group.types';
+
+interface ProcessedItem {
+ img?: HTMLImageElement | null;
+ name: string;
+ accent: GuxAvatarAccent;
+ isOverflow: boolean;
+ groupItem: HTMLGuxAvatarGroupItemBetaElement;
+}
/**
* @slot - slot for gux-avatar-group-item components
@@ -15,43 +27,75 @@ import {
shadow: { delegatesFocus: true }
})
export class GuxAvatarGroup {
+ private processedGroupItems: ProcessedItem[] = [];
+
@Element()
root: HTMLElement;
+ @Prop()
+ quantity: GuxAvatarGroupQuantity = 7;
+
async componentWillLoad(): Promise {
trackComponent(this.root);
- if (this.root.querySelectorAll('gux-avatar-group-item-beta').length === 0) {
- logWarn(
- this.root,
- 'gux-avatar-group-beta: No gux-avatar-group-item-beta tags found in slot. Please add gux-avatar-group-item-beta tags to the slot.'
- );
- }
}
componentDidLoad(): void {
this.setInitialFocusTarget();
}
- private setInitialFocusTarget(): void {
- const groupItems = Array.from(
+ componentWillRender(): void {
+ this.processGroupItems();
+ this.hideOverflowGroupItems();
+ }
+
+ @Listen('mouseover')
+ onMouseOver(event: MouseEvent): void {
+ this.hideCurrentTooltip(event);
+ }
+
+ private getGroupItems(): HTMLGuxAvatarGroupItemBetaElement[] {
+ return Array.from(
this.root.children
) as HTMLGuxAvatarGroupItemBetaElement[];
+ }
+
+ private setInitialFocusTarget(): void {
+ const groupItems = this.getGroupItems();
+
if (groupItems.length > 0) {
const firstGroupItem = groupItems[0] as Element;
setFocusTarget(firstGroupItem);
}
}
- @Listen('mouseover')
- onMouseOver(event: MouseEvent): void {
- this.hideCurrentTooltip(event);
+ private processGroupItems(): void {
+ const groupItems = this.getGroupItems();
+ this.validateChildElements(groupItems);
+
+ this.processedGroupItems = groupItems.map((item, index) => {
+ return {
+ img: item.querySelector('img') ?? null,
+ name: item?.name ?? '',
+ accent: item?.accent ?? null,
+ isOverflow: index >= this.quantity,
+ groupItem: item
+ } as ProcessedItem;
+ });
+ }
+
+ private hideOverflowGroupItems(): void {
+ const overflowItems = this.processedGroupItems.filter(
+ item => item.isOverflow
+ );
+
+ overflowItems.forEach(item => {
+ item.groupItem.classList.add('gux-hidden');
+ });
}
private hideCurrentTooltip(event: Event) {
const target = event.target as HTMLGuxAvatarGroupItemBetaElement;
- const groupItems = Array.from(
- this.root.children
- ) as HTMLGuxAvatarGroupItemBetaElement[];
+ const groupItems = this.getGroupItems();
const focusedChild = groupItems.find(child =>
child.matches(':focus-within')
@@ -64,14 +108,72 @@ export class GuxAvatarGroup {
private handleClick(event: MouseEvent) {
const clickedElement = event.target as HTMLGuxAvatarGroupItemBetaElement;
- resetFocusableSibling(clickedElement);
- setFocusTarget(clickedElement);
+
+ if (
+ clickedElement.tagName === 'GUX-AVATAR-GROUP-ITEM-BETA' &&
+ !clickedElement.classList.contains('gux-hidden')
+ ) {
+ resetFocusableSibling(clickedElement);
+ setFocusTarget(clickedElement);
+ }
+ }
+
+ private validateChildElements(childElements: HTMLElement[]) {
+ if (childElements.length === 0) {
+ logWarn(
+ this.root,
+ 'gux-avatar-group-beta: No child elements detected. Please add some gux-avatar-item-beta tags to slot'
+ );
+ }
+
+ const validTagNames = ['GUX-AVATAR-GROUP-ITEM-BETA'];
+ const invalidElements = childElements.some(
+ el => !validTagNames.includes(el.tagName)
+ );
+
+ if (invalidElements) {
+ logWarn(
+ this.root,
+ 'gux-avatar-group-beta: Invalid child element detected. All child elements must be either buttons, anchor tags or gux-avatar-beta components'
+ );
+ }
+ }
+
+ private renderOverflowMenu(): JSX.Element | null {
+ if (this.root.children.length > this.quantity) {
+ const overflowItems = this.processedGroupItems.filter(
+ item => item.isOverflow
+ );
+
+ return (
+
+ {
+ overflowItems.map(item => {
+ return (
+ item.groupItem.click()}
+ >
+ {item.img ? (
+
+ ) : null}
+
+ );
+ }) as JSX.Element[]
+ }
+
+ ) as JSX.Element;
+ } else {
+ return null;
+ }
}
render(): JSX.Element {
return (
+ {this.renderOverflowMenu()}
) as JSX.Element;
}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.types.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.types.ts
similarity index 50%
rename from packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.types.ts
rename to packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.types.ts
index 9e6640f4b..5e0dde8c6 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group-item/gux-avatar-group-item.types.ts
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-group.types.ts
@@ -15,3 +15,9 @@ export type GuxAvatarAccent =
| '10'
| '11'
| '12';
+
+export type GuxAvatarGroupQuantity = 1 | 2 | 3 | 4 | 5 | 6 | 7;
+
+export type GuxAvatarGroupChild =
+ | HTMLGuxAvatarGroupItemBetaElement
+ | HTMLGuxAvatarOverflowBetaElement;
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/gux-avatar-overflow-item.scss b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/gux-avatar-overflow-item.scss
new file mode 100644
index 000000000..24e6fcdcc
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/gux-avatar-overflow-item.scss
@@ -0,0 +1,139 @@
+@use '~genesys-spark/dist/scss/focus.scss';
+
+::slotted(img) {
+ inline-size: 100%;
+ block-size: 100%;
+ object-fit: cover;
+}
+
+button {
+ all: unset;
+ box-sizing: border-box;
+ display: flex;
+ gap: var(--gse-ui-menu-option-gap);
+ align-items: center;
+ inline-size: 100%;
+ min-block-size: var(--gse-ui-menu-option-height);
+ padding: var(--gse-ui-menu-option-padding);
+ font-family: var(--gse-ui-menu-option-label-default-text-fontFamily);
+ font-size: var(--gse-ui-menu-option-label-default-text-fontSize);
+ font-weight: var(--gse-ui-menu-option-label-default-text-fontWeight);
+ line-height: var(--gse-ui-menu-option-label-default-text-lineHeight);
+ color: var(--gse-ui-menu-option-label-foregroundColor);
+ word-wrap: break-word;
+ cursor: pointer;
+ background-color: var(--gse-ui-menu-option-default-backgroundColor);
+ border: none;
+ outline: none;
+ outline-offset: calc(var(--gse-ui-menu-option-focus-border-width) * -1);
+
+ &:focus-visible {
+ border-radius: var(--gse-semantic-focusOutline-sm-borderRadius);
+ outline: var(--gse-ui-menu-option-focus-border-width)
+ var(--gse-ui-menu-option-focus-border-style)
+ var(--gse-ui-menu-option-focus-border-color);
+ }
+
+ &:hover {
+ background: var(--gse-ui-menu-option-hover-backgroundColor);
+ }
+
+ &:active {
+ font-family: var(--gse-ui-menu-option-label-active-text-fontFamily);
+ font-size: var(--gse-ui-menu-option-label-active-text-fontSize);
+ font-weight: var(--gse-ui-menu-option-label-active-text-fontWeight);
+ line-height: var(--gse-ui-menu-option-label-active-text-lineHeight);
+ background: var(--gse-ui-menu-option-selected-backgroundColor);
+ }
+
+ .gux-avatar {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ inline-size: var(--gse-ui-avatar-xsmall-content-size);
+ block-size: var(--gse-ui-avatar-xsmall-content-size);
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ color: var(--gse-ui-avatar-media-initialsForeground-inverse);
+ cursor: pointer;
+ background: none;
+ background-color: var(--gse-ui-avatar-media-initialsBackground-default);
+ border: none;
+ border-radius: 50%;
+
+ .gux-avatar-initials {
+ font-family: var(--gse-ui-avatar-small-initials-fontFamily);
+ font-size: var(--gse-ui-avatar-xsmall-initials-fontSize);
+ font-weight: var(--gse-ui-avatar-xsmall-initials-fontWeight);
+ line-height: var(--gse-ui-avatar-small-initials-lineHeight);
+ text-transform: uppercase;
+ }
+
+ &.gux-accent-1 {
+ color: var(--gse-ui-avatar-media-initialsForeground-inverse);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent1);
+ }
+
+ &.gux-accent-2 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent2);
+ }
+
+ &.gux-accent-3 {
+ color: var(--gse-ui-avatar-media-initialsForeground-inverse);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent3);
+ }
+
+ &.gux-accent-4 {
+ color: var(--gse-ui-avatar-media-initialsForeground-inverse);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent4);
+ }
+
+ &.gux-accent-5 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent5);
+ }
+
+ &.gux-accent-6 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent6);
+ }
+
+ &.gux-accent-7 {
+ color: var(--gse-ui-avatar-media-initialsForeground-inverse);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent7);
+ }
+
+ &.gux-accent-8 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent8);
+ }
+
+ &.gux-accent-9 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent9);
+ }
+
+ &.gux-accent-10 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent10);
+ }
+
+ &.gux-accent-11 {
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent11);
+ }
+
+ &.gux-accent-12 {
+ color: var(--gse-ui-avatar-media-initialsForeground-inverse);
+ background-color: var(--gse-ui-avatar-media-initialsBackground-accent12);
+ }
+
+ &.gux-accent-inherit {
+ color: inherit;
+ background-color: inherit;
+ }
+ }
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/gux-avatar-overflow-item.tsx b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/gux-avatar-overflow-item.tsx
new file mode 100644
index 000000000..049684071
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/gux-avatar-overflow-item.tsx
@@ -0,0 +1,111 @@
+import {
+ Component,
+ h,
+ JSX,
+ Prop,
+ Element,
+ Method,
+ Listen,
+ Host
+} from '@stencil/core';
+import { trackComponent } from '@utils/tracking/usage';
+import { logWarn } from '@utils/error/log-error';
+import { GuxAvatarAccent } from '../../gux-avatar-group.types';
+import { generateInitials } from '@utils/string/generate-initials';
+import { overflowNavigation } from '../gux-avatar-overflow.service';
+
+/**
+ * @slot image - Avatar photo.
+ */
+
+@Component({
+ styleUrl: 'gux-avatar-overflow-item.scss',
+ tag: 'gux-avatar-overflow-item-beta',
+ shadow: true
+})
+export class GuxAvatarOverflowItem {
+ private buttonElement: HTMLButtonElement;
+
+ @Element()
+ root: HTMLElement;
+
+ @Prop()
+ name: string;
+
+ /**
+ * Manually sets avatar accent
+ */
+ @Prop()
+ accent: GuxAvatarAccent = 'auto';
+
+ async componentWillLoad(): Promise {
+ trackComponent(this.root);
+ }
+
+ componentDidLoad() {
+ this.validatingInputs();
+ }
+
+ @Listen('keydown')
+ onKeydown(event: KeyboardEvent): void {
+ overflowNavigation(event, this.root);
+ }
+
+ /*
+ * Focus button element
+ */
+ @Method()
+ // eslint-disable-next-line @typescript-eslint/require-await
+ async guxFocus(): Promise {
+ this.buttonElement.focus();
+ }
+
+ private getAccent(): string {
+ if (this.accent !== 'auto') {
+ return `gux-accent-${this.accent}`;
+ }
+ const hashedName = this.name
+ ?.split('')
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
+ const hashedNameAccent = (hashedName % 12).toString();
+ return `gux-accent-${hashedNameAccent}`;
+ }
+
+ private validatingInputs(): void {
+ const avatarImage = this.root.querySelector('img');
+ if (!this.name) {
+ logWarn(this.root, 'Name prop is required');
+ }
+
+ if (avatarImage && !avatarImage.getAttribute('alt')) {
+ logWarn(this.root, 'Alt attribute is required for slotted image.');
+ }
+ }
+
+ render(): JSX.Element {
+ return (
+
+ (this.buttonElement = el)}
+ >
+
+
+
+ {generateInitials(this.name)}
+
+
+
+ {this.name}
+
+
+ ) as JSX.Element;
+ }
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/readme.md b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/readme.md
new file mode 100644
index 000000000..1f5dfd14e
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/readme.md
@@ -0,0 +1,51 @@
+# gux-avatar-overflow-item-beta
+
+
+
+
+
+
+## Properties
+
+| Property | Attribute | Description | Type | Default |
+| -------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `accent` | `accent` | Manually sets avatar accent | `"0" \| "1" \| "10" \| "11" \| "12" \| "2" \| "3" \| "4" \| "5" \| "6" \| "7" \| "8" \| "9" \| "auto" \| "default" \| "inherit"` | `'auto'` |
+| `name` | `name` | | `string` | `undefined` |
+
+
+## Methods
+
+### `guxFocus() => Promise`
+
+
+
+#### Returns
+
+Type: `Promise`
+
+
+
+
+## Slots
+
+| Slot | Description |
+| --------- | ------------- |
+| `"image"` | Avatar photo. |
+
+
+## Dependencies
+
+### Used by
+
+ - [gux-avatar-group-beta](../..)
+
+### Graph
+```mermaid
+graph TD;
+ gux-avatar-group-beta --> gux-avatar-overflow-item-beta
+ style gux-avatar-overflow-item-beta fill:#f9f,stroke:#333,stroke-width:4px
+```
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/__snapshots__/gux-avatar-overflow-item.e2e.ts.snap b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/__snapshots__/gux-avatar-overflow-item.e2e.ts.snap
new file mode 100644
index 000000000..d60fd153a
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/__snapshots__/gux-avatar-overflow-item.e2e.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gux-avatar-overflow-item-beta #render renders with name 1`] = `"JDJohn Doe"`;
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/__snapshots__/gux-avatar-overflow-item.spec.ts.snap b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/__snapshots__/gux-avatar-overflow-item.spec.ts.snap
new file mode 100644
index 000000000..396919189
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/__snapshots__/gux-avatar-overflow-item.spec.ts.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gux-avatar-overflow-item-beta Render should render with name 1`] = `
+
+
+
+
+
+
+ JD
+
+
+
+
+ John Doe
+
+
+
+
+`;
+
+exports[`gux-avatar-overflow-item-beta Render should render with slotted image 1`] = `
+
+
+
+
+
+
+ JD
+
+
+
+
+ John Doe
+
+
+
+
+
+`;
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/gux-avatar-overflow-item.e2e.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/gux-avatar-overflow-item.e2e.ts
new file mode 100644
index 000000000..6dad4ccf4
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/gux-avatar-overflow-item.e2e.ts
@@ -0,0 +1,95 @@
+import { E2EPage } from '@stencil/core/testing';
+import {
+ newSparkE2EPage,
+ a11yCheck
+} from '../../../../../../test/e2eTestUtils';
+
+describe('gux-avatar-overflow-item-beta', () => {
+ describe('#render', () => {
+ it('renders with name', async () => {
+ const html = `
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const element = await page.find('gux-avatar-overflow-item-beta');
+
+ await a11yCheck(page);
+ expect(element.outerHTML).toMatchSnapshot();
+ });
+ });
+
+ describe('#interactions', () => {
+ it('should focus on button when guxFocus method is called', async () => {
+ const html = `
+
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const element = await page.find('gux-avatar-overflow-item-beta');
+
+ expect(document.activeElement).toBeFalsy();
+
+ await element.callMethod('guxFocus');
+ await page.waitForChanges();
+
+ expect(await page.evaluate(() => document.activeElement.tagName)).toBe(
+ 'GUX-AVATAR-OVERFLOW-ITEM-BETA'
+ );
+ });
+
+ it('should handle keyboard navigation', async () => {
+ const getActiveElementLabel = async (page: E2EPage): Promise => {
+ return page.evaluate(() => {
+ const activeElement =
+ document.activeElement?.shadowRoot?.querySelector(
+ 'button'
+ ) as HTMLElement;
+ return activeElement.getAttribute('aria-label');
+ });
+ };
+
+ const html = `
+
+
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const firstButton = await page.find('pierce/button');
+
+ await firstButton.focus();
+ await page.keyboard.press('ArrowDown');
+ await page.waitForChanges();
+
+ expect(await getActiveElementLabel(page)).toBe('Jane Smith');
+
+ await page.keyboard.press('ArrowDown');
+ await page.waitForChanges();
+
+ expect(await getActiveElementLabel(page)).toBe('John Doe');
+
+ await page.keyboard.press('ArrowUp');
+ await page.waitForChanges();
+
+ expect(await getActiveElementLabel(page)).toBe('Jane Smith');
+
+ await page.keyboard.press('Home');
+ await page.waitForChanges();
+
+ expect(await getActiveElementLabel(page)).toBe('John Doe');
+
+ await page.keyboard.press('End');
+ await page.waitForChanges();
+
+ expect(await getActiveElementLabel(page)).toBe('Jane Smith');
+ });
+ });
+});
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/gux-avatar-overflow-item.spec.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/gux-avatar-overflow-item.spec.ts
new file mode 100644
index 000000000..05f18931a
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow-item/tests/gux-avatar-overflow-item.spec.ts
@@ -0,0 +1,118 @@
+import { newSpecPage } from '@stencil/core/testing';
+import { GuxAvatarOverflowItem } from '../gux-avatar-overflow-item';
+
+const components = [GuxAvatarOverflowItem];
+describe('gux-avatar-overflow-item-beta', () => {
+ let component: GuxAvatarOverflowItem;
+
+ beforeEach(async () => {
+ const page = await newSpecPage({
+ components,
+ html: ``
+ });
+ component = page.rootInstance;
+ });
+
+ it('should build', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('validatingInputs', () => {
+ it('should log warning when name is not provided', async () => {
+ const logWarnSpy = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => {});
+ await newSpecPage({
+ components,
+ html: ``
+ });
+
+ expect(logWarnSpy).toHaveBeenCalled();
+ logWarnSpy.mockRestore();
+ });
+
+ it('should log warning when image has no alt attribute', async () => {
+ const logWarnSpy = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => {});
+ await newSpecPage({
+ components,
+ html: `
+
+
+
+ `
+ });
+
+ expect(logWarnSpy).toHaveBeenCalled();
+ logWarnSpy.mockRestore();
+ });
+ });
+
+ describe('guxFocus', () => {
+ it('should focus the button element', async () => {
+ const page = await newSpecPage({
+ components,
+ html: ``
+ });
+ const instance = page.rootInstance;
+ const focusSpy = jest.spyOn(instance.buttonElement, 'focus');
+
+ await instance.guxFocus();
+ expect(focusSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Render', () => {
+ it('should render with name', async () => {
+ const page = await newSpecPage({
+ components,
+ html: ``
+ });
+
+ expect(page.root).toMatchSnapshot();
+ });
+
+ it('should render with slotted image', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+
+
+ `
+ });
+
+ expect(page.root).toMatchSnapshot();
+ });
+
+ it('should render with initials when no image is provided', async () => {
+ const page = await newSpecPage({
+ components,
+ html: ``
+ });
+
+ const initialsElement = page.root.shadowRoot.querySelector(
+ '.gux-avatar-initials'
+ );
+ expect(initialsElement.textContent).toBe('JD');
+ });
+ });
+
+ describe('Events', () => {
+ it('should handle keydown event', async () => {
+ const page = await newSpecPage({
+ components,
+ html: ``
+ });
+ const instance = page.rootInstance;
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
+
+ // Mock overflowNavigation function
+ const overflowNavigationSpy = jest.fn();
+ instance.onKeydown(event);
+
+ expect(overflowNavigationSpy).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.scss b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.scss
new file mode 100644
index 000000000..330afa9c6
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.scss
@@ -0,0 +1,76 @@
+@use '~genesys-spark/dist/scss/focus.scss';
+@use '~genesys-spark/dist/scss/mixins.scss';
+
+:host {
+ display: block;
+ margin-inline-end: var(--gse-ui-avatar-groupSet-gap);
+}
+
+.gux-avatar-overflow {
+ position: relative;
+ box-sizing: border-box;
+ inline-size: 100%;
+ padding: 0;
+ margin: 0;
+ line-height: 0px;
+ cursor: pointer;
+ background: none;
+ border: none;
+ border-radius: 50%;
+
+ &:focus-visible {
+ @include focus.gux-focus-ring;
+ }
+
+ &:hover,
+ &:focus {
+ z-index: var(--gse-semantic-zIndex-showFocus);
+ }
+
+ .gux-avatar-overflow-wrapper {
+ position: relative;
+ display: flex;
+
+ .gux-avatar-overflow-content {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ inline-size: var(--gse-ui-avatar-small-content-size);
+ block-size: var(--gse-ui-avatar-small-content-size);
+ overflow: hidden;
+ font-family: var(--gse-ui-avatar-small-initials-fontFamily);
+ font-size: var(--gse-ui-avatar-small-initials-fontSize);
+ font-weight: var(--gse-ui-avatar-small-initials-fontWeight);
+ line-height: var(--gse-core-lineHeight-matchFontSize);
+ color: var(--gse-ui-avatar-media-initialsForeground-default);
+ background-color: var(
+ --gse-ui-avatar-media-initialsBackground-overflowCount
+ );
+ border-radius: 50%;
+ }
+ }
+}
+
+.gux-menu-wrapper {
+ position: fixed;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ z-index: var(--gse-semantic-zIndex-popup);
+ flex-direction: column;
+ block-size: 100px; // TODO: Replace with token COMUI-3374
+ padding: var(--gse-ui-menu-padding);
+ margin: 0;
+ overflow-y: scroll;
+ visibility: hidden;
+ background-color: var(--gse-ui-menu-backgroundColor);
+ border: none;
+ // TODO: Replace with tokens COMUI-3374
+ border: 1px solid #c6c8ce;
+ border-radius: var(--gse-ui-menu-borderRadius);
+ box-shadow: 0 2px 4px 1px rgb(35 57 92 / 10%);
+
+ &.gux-shown {
+ visibility: visible;
+ }
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.service.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.service.ts
new file mode 100644
index 000000000..59c2bc7a6
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.service.ts
@@ -0,0 +1,110 @@
+export const hideDelay = 250;
+
+export function overflowNavigation(
+ event: KeyboardEvent,
+ currentElement: Element
+): void {
+ switch (event.key) {
+ case 'ArrowUp':
+ event.stopPropagation();
+ event.preventDefault();
+
+ focusPreviousSiblingLoop(currentElement);
+ break;
+
+ case 'ArrowDown':
+ event.stopPropagation();
+ event.preventDefault();
+
+ focusNextSiblingLoop(currentElement);
+ break;
+
+ case 'ArrowRight':
+ event.preventDefault();
+ event.stopPropagation();
+
+ break;
+
+ case 'ArrowLeft':
+ event.preventDefault();
+ event.stopPropagation();
+
+ break;
+
+ case 'Home':
+ event.stopPropagation();
+ event.preventDefault();
+
+ focusFirstSibling(currentElement);
+ break;
+
+ case 'End':
+ event.stopPropagation();
+ event.preventDefault();
+
+ focusLastSibling(currentElement);
+ break;
+ }
+}
+
+function focusFirstSibling(currentElement: Element): void {
+ const firstFocusableElement = getFirstFocusableElement(
+ currentElement
+ ) as HTMLGuxAvatarOverflowItemBetaElement;
+
+ if (firstFocusableElement) {
+ void firstFocusableElement.guxFocus();
+ }
+}
+
+function focusLastSibling(currentElement: Element): void {
+ const lastFocusableElement = getLastFocusableElement(
+ currentElement
+ ) as HTMLGuxAvatarOverflowItemBetaElement;
+
+ if (lastFocusableElement) {
+ void lastFocusableElement.guxFocus();
+ }
+}
+
+function focusPreviousSiblingLoop(currentElement: Element): void {
+ const previousFocusableElement =
+ currentElement.previousElementSibling as HTMLGuxAvatarOverflowItemBetaElement;
+
+ if (previousFocusableElement) {
+ void previousFocusableElement.guxFocus();
+ } else {
+ focusLastSibling(currentElement);
+ }
+}
+
+function focusNextSiblingLoop(currentElement: Element): void {
+ const nextFocusableElement =
+ currentElement.nextElementSibling as HTMLGuxAvatarOverflowItemBetaElement;
+
+ if (nextFocusableElement) {
+ void nextFocusableElement.guxFocus();
+ } else {
+ focusFirstSibling(currentElement);
+ }
+}
+
+function getFirstFocusableElement(currentElement: Element): Element {
+ let firstFocusableElement = currentElement;
+
+ while (firstFocusableElement.previousElementSibling !== null) {
+ firstFocusableElement = firstFocusableElement.previousElementSibling;
+ }
+
+ return firstFocusableElement;
+}
+
+function getLastFocusableElement(currentElement: Element): Element {
+ let lastFocusableElement = currentElement;
+
+ while (lastFocusableElement.nextElementSibling !== null) {
+ lastFocusableElement = lastFocusableElement.nextElementSibling;
+ }
+
+ return lastFocusableElement;
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.tsx b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.tsx
new file mode 100644
index 000000000..f5b8db2d4
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/gux-avatar-overflow.tsx
@@ -0,0 +1,236 @@
+import {
+ Component,
+ h,
+ JSX,
+ Element,
+ State,
+ Method,
+ Host,
+ Listen
+} from '@stencil/core';
+
+import { autoUpdate, computePosition, offset } from '@floating-ui/dom';
+
+import { OnClickOutside } from '@utils/decorator/on-click-outside';
+import { trackComponent } from '@utils/tracking/usage';
+import { logWarn } from '@utils/error/log-error';
+import { groupKeyboardNavigation } from '../gux-avatar-group.service';
+import { afterNextRenderTimeout } from '@utils/dom/after-next-render';
+
+/**
+ * @slot - a number of gux-avatar-overflow-items
+ */
+@Component({
+ styleUrl: 'gux-avatar-overflow.scss',
+ tag: 'gux-avatar-overflow-beta',
+ shadow: true
+})
+export class GuxAvatarOverflow {
+ private overflowButtonElement: HTMLButtonElement;
+ private menuElement: HTMLDivElement;
+ private cleanupUpdatePosition: ReturnType;
+ private hideDelayTimeout: ReturnType;
+ private focusDelayTimeout: ReturnType;
+ private delayTime: number = 250;
+
+ @Element() root: HTMLElement;
+
+ @State() count: number = 0;
+
+ @State() expanded: boolean = false;
+
+ async componentWillLoad(): Promise {
+ trackComponent(this.root);
+ }
+
+ componentDidLoad(): void {
+ if (this.expanded) {
+ this.runUpdatePosition();
+ }
+ }
+
+ componentDidUpdate(): void {
+ if (this.expanded) {
+ this.runUpdatePosition();
+ } else if (this.cleanupUpdatePosition) {
+ this.cleanupUpdatePosition();
+ }
+ }
+
+ disconnectedCallback(): void {
+ clearTimeout(this.focusDelayTimeout);
+ clearTimeout(this.hideDelayTimeout);
+ if (this.cleanupUpdatePosition) {
+ this.cleanupUpdatePosition();
+ }
+ }
+
+ @OnClickOutside({ triggerEvents: 'mousedown' })
+ onClickOutside(): void {
+ this.expanded = false;
+ }
+
+ @Listen('keydown')
+ onKeydown(event: KeyboardEvent): void {
+ groupKeyboardNavigation(event, this.root);
+
+ switch (event.key) {
+ case 'Escape': {
+ this.hide();
+
+ const target = event.target as Element;
+ if (target.tagName === 'GUX-AVATAR-OVERFLOW-ITEM-BETA') {
+ this.focusDelayTimeout = afterNextRenderTimeout(() => {
+ this.guxFocus();
+ });
+ }
+ break;
+ }
+ case 'Tab':
+ this.hide();
+ break;
+ }
+ }
+
+ @Listen('click')
+ onClick(e: MouseEvent): void {
+ e.stopPropagation();
+
+ const target = e.target as HTMLElement;
+ if (target.tagName === 'GUX-AVATAR-OVERFLOW-ITEM-BETA') {
+ // Reset scroll on menu when clicked to avoid scroll jump when reopened
+ this.menuElement.scrollTop = 0;
+ this.expanded = false;
+ }
+ }
+
+ @Method()
+ // eslint-disable-next-line @typescript-eslint/require-await
+ async guxFocus(): Promise {
+ this.overflowButtonElement?.focus();
+ }
+
+ @Method()
+ // eslint-disable-next-line @typescript-eslint/require-await
+ async guxClose(): Promise {
+ this.hide();
+ }
+
+ private runUpdatePosition(): void {
+ if (this.root.isConnected) {
+ this.cleanupUpdatePosition = autoUpdate(
+ this.overflowButtonElement,
+ this.menuElement,
+ () => this.updatePosition(),
+ {
+ ancestorScroll: true,
+ elementResize: true,
+ animationFrame: true,
+ ancestorResize: true
+ }
+ );
+ } else {
+ this.disconnectedCallback();
+ }
+ }
+
+ private updatePosition(): void {
+ if (this.root) {
+ void computePosition(this.overflowButtonElement, this.menuElement, {
+ placement: 'bottom-start',
+ strategy: 'fixed',
+ middleware: [
+ offset({
+ mainAxis: 4,
+ crossAxis: 4
+ })
+ ]
+ }).then(({ x, y }) => {
+ Object.assign(this.menuElement.style, {
+ left: `${x}px`,
+ top: `${y}px`
+ });
+ });
+ }
+ }
+
+ private getCount() {
+ const menuItems = Array.from(
+ this.root.children
+ ) as HTMLGuxAvatarOverflowItemBetaElement[];
+ if (
+ menuItems.some(item => item.tagName !== 'GUX-AVATAR-OVERFLOW-ITEM-BETA')
+ ) {
+ logWarn(
+ this.root,
+ 'Only gux-avatar-overflow-item-beta elements are allowed as children.'
+ );
+ }
+ if (menuItems) {
+ return menuItems.length;
+ }
+ }
+
+ private toggleOverflowMenu(): void {
+ if (!this.expanded) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+
+ private show(): void {
+ clearTimeout(this.hideDelayTimeout);
+ this.expanded = true;
+ this.hideDelayTimeout = afterNextRenderTimeout(() => {
+ this.focusOnMenu();
+ });
+ }
+
+ private hide(): void {
+ if (this.expanded) {
+ this.hideDelayTimeout = setTimeout(() => {
+ this.expanded = false;
+ }, this.delayTime);
+ }
+ }
+
+ private focusOnMenu(): void {
+ const overflowItems = Array.from(this.root.children);
+
+ const nextFocusableElement =
+ overflowItems[0] as HTMLGuxAvatarOverflowItemBetaElement;
+
+ void nextFocusableElement.guxFocus();
+ }
+
+ render(): JSX.Element {
+ return (
+
+ (this.overflowButtonElement = el)}
+ onClick={() => this.toggleOverflowMenu()}
+ tabIndex={-1}
+ aria-haspopup="true"
+ aria-expanded={this.expanded.toString()}
+ >
+
+ +{this.getCount()}
+
+
+ (this.menuElement = el)}
+ >
+
+
+
+ ) as JSX.Element;
+ }
+}
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/readme.md b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/readme.md
new file mode 100644
index 000000000..897646a57
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/readme.md
@@ -0,0 +1,53 @@
+# gux-avatar-overflow-beta
+
+
+
+
+
+
+## Methods
+
+### `guxClose() => Promise`
+
+
+
+#### Returns
+
+Type: `Promise`
+
+
+
+### `guxFocus() => Promise`
+
+
+
+#### Returns
+
+Type: `Promise`
+
+
+
+
+## Slots
+
+| Slot | Description |
+| ---- | ------------------------------------- |
+| | a number of gux-avatar-overflow-items |
+
+
+## Dependencies
+
+### Used by
+
+ - [gux-avatar-group-beta](..)
+
+### Graph
+```mermaid
+graph TD;
+ gux-avatar-group-beta --> gux-avatar-overflow-beta
+ style gux-avatar-overflow-beta fill:#f9f,stroke:#333,stroke-width:4px
+```
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/__snapshots__/gux-avatar-overflow.e2e.ts.snap b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/__snapshots__/gux-avatar-overflow.e2e.ts.snap
new file mode 100644
index 000000000..6330427e8
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/__snapshots__/gux-avatar-overflow.e2e.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gux-avatar-overflow #render should render component as expected 1`] = `" +3 "`;
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/__snapshots__/gux-avatar-overflow.spec.ts.snap b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/__snapshots__/gux-avatar-overflow.spec.ts.snap
new file mode 100644
index 000000000..4a25154cd
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/__snapshots__/gux-avatar-overflow.spec.ts.snap
@@ -0,0 +1,231 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gux-avatar-overflow-beta #render should render 0 avatars 1`] = `
+
+
+
+
+
+ +0
+
+
+
+
+
+
+`;
+
+exports[`gux-avatar-overflow-beta #render should render 1 avatar 1`] = `
+
+
+
+
+
+ +1
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`gux-avatar-overflow-beta #render should render 2 avatars 1`] = `
+
+
+
+
+
+ +2
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`gux-avatar-overflow-beta #render should render 3 avatars 1`] = `
+
+
+
+
+
+ +3
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`gux-avatar-overflow-beta should build 1`] = `
+
+
+
+
+
+ +3
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/gux-avatar-overflow.e2e.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/gux-avatar-overflow.e2e.ts
new file mode 100644
index 000000000..05c372793
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/gux-avatar-overflow.e2e.ts
@@ -0,0 +1,135 @@
+import { newSparkE2EPage, a11yCheck } from '../../../../../test/e2eTestUtils';
+
+describe('gux-avatar-overflow', () => {
+ describe('#render', () => {
+ it('should render component as expected', async () => {
+ const html = `
+
+
+
+
+
+
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const element = await page.find('gux-avatar-overflow-beta');
+
+ await a11yCheck(page);
+ expect(element.outerHTML).toMatchSnapshot();
+ });
+ });
+
+ describe('interactions', () => {
+ it('should open and close menu on button click', async () => {
+ const html = `
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const button = await page.find('pierce/button');
+
+ expect(await button.getAttribute('aria-expanded')).toBe('false');
+
+ await button.click();
+ await page.waitForChanges();
+ await page.waitForTimeout(500);
+ expect(await button.getAttribute('aria-expanded')).toBe('true');
+
+ await button.click();
+ await page.waitForTimeout(500);
+ await page.waitForChanges();
+ expect(await button.getAttribute('aria-expanded')).toBe('false');
+ });
+
+ it('should focus on first item when menu opens', async () => {
+ const html = `
+
+
+
+
+
+
+
+
+ `;
+
+ const page = await newSparkE2EPage({ html });
+ const button = await page.find('pierce/button');
+
+ const activeElementNameBefore = await page.evaluate(() => {
+ const activeElement =
+ document.activeElement as HTMLGuxAvatarOverflowItemBetaElement;
+ return activeElement.name;
+ });
+ expect(activeElementNameBefore).not.toBe('John Smith');
+
+ await button.click();
+ await page.waitForChanges();
+
+ const activeElementNameAfter = await page.evaluate(() => {
+ const activeElement =
+ document.activeElement as HTMLGuxAvatarOverflowItemBetaElement;
+ return activeElement.name;
+ });
+
+ expect(activeElementNameAfter).toBe('John Smith');
+ });
+
+ it('should handle close on escape key', async () => {
+ const html = `
+
+
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const button = await page.find('pierce/button');
+
+ // Open menu
+ await button.click();
+ await page.waitForTimeout(500);
+ await page.waitForChanges();
+ expect(await button.getAttribute('aria-haspopup')).toBe('true');
+ expect(await button.getAttribute('aria-expanded')).toBe('true');
+
+ // Press Escape to close menu
+ await page.keyboard.press('Escape');
+ await page.waitForTimeout(500);
+ await page.waitForChanges();
+ expect(await button.getAttribute('aria-expanded')).toBe('false');
+ });
+
+ it('should handle close on tab key', async () => {
+ const html = `
+
+
+
+
+
+
+ `;
+ const page = await newSparkE2EPage({ html });
+ const button = await page.find('pierce/button');
+
+ // Open menu
+ await button.click();
+ await page.waitForTimeout(500);
+ await page.waitForChanges();
+ expect(await button.getAttribute('aria-expanded')).toBe('true');
+
+ // Press Tab to close menu
+ await page.keyboard.press('Tab');
+ await page.waitForTimeout(500);
+ await page.waitForChanges();
+ expect(await button.getAttribute('aria-expanded')).toBe('false');
+ });
+ });
+});
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/gux-avatar-overflow.spec.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/gux-avatar-overflow.spec.ts
new file mode 100644
index 000000000..3e16b3da8
--- /dev/null
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/gux-avatar-overflow/tests/gux-avatar-overflow.spec.ts
@@ -0,0 +1,299 @@
+jest.mock('../../../../../utils/error/log-error.ts', () => ({
+ __esModule: true,
+ logWarn: jest.fn()
+}));
+
+import { newSpecPage } from '@stencil/core/testing';
+import { GuxAvatarOverflow } from '../gux-avatar-overflow';
+import { GuxAvatarOverflowItem } from '../gux-avatar-overflow-item/gux-avatar-overflow-item';
+
+const components = [GuxAvatarOverflow, GuxAvatarOverflowItem];
+
+describe('gux-avatar-overflow-beta', () => {
+ beforeAll(() => {
+ global.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ };
+ });
+
+ afterAll(() => {
+ delete global.ResizeObserver;
+ jest.clearAllTimers();
+ });
+
+ it('should build', async () => {
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const page = await newSpecPage({ components, html, language: 'en' });
+
+ expect(page.rootInstance).toBeInstanceOf(GuxAvatarOverflow);
+ expect(page.root).toMatchSnapshot();
+ });
+
+ describe('#render', () => {
+ [
+ {
+ description: 'should render 3 avatars',
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ expectedAvatarCount: 3
+ },
+ {
+ description: 'should render 2 avatars',
+ html: `
+
+
+
+
+
+
+
+
+ `,
+ expectedAvatarCount: 2
+ },
+ {
+ description: 'should render 1 avatar',
+ html: `
+
+
+
+
+
+ `,
+ expectedAvatarCount: 1
+ },
+ {
+ description: 'should render 0 avatars',
+ html: `
+
+
+ `,
+ expectedAvatarCount: 0
+ }
+ ].forEach(({ description, html, expectedAvatarCount }) => {
+ it(description, async () => {
+ const page = await newSpecPage({ components, html, language: 'en' });
+
+ const avatarOverflowItems = page.root.querySelectorAll(
+ 'gux-avatar-overflow-item-beta'
+ );
+ expect(avatarOverflowItems.length).toBe(expectedAvatarCount);
+ expect(page.root).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('interactions', () => {
+ beforeEach(() => {
+ jest.useFakeTimers({ legacyFakeTimers: true });
+ });
+
+ it('should toggle menu on button click', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+ Item 1
+
+ `
+ });
+
+ const component = page.rootInstance;
+ const button = page.root.shadowRoot.querySelector('button');
+
+ expect(component.expanded).toBe(false);
+
+ button.click();
+ await page.waitForChanges();
+ expect(component.expanded).toBe(true);
+
+ button.click();
+ jest.advanceTimersByTime(300);
+ await page.waitForChanges();
+ expect(component.expanded).toBe(false);
+ });
+
+ it('should close menu on Escape key', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+ Item 1
+
+ `
+ });
+
+ const component = page.rootInstance;
+ component.expanded = true;
+ await page.waitForChanges();
+
+ page.root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+ jest.advanceTimersByTime(300);
+ await page.waitForChanges();
+ expect(component.expanded).toBe(false);
+ });
+
+ it('should close menu on Tab key', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+ Item 1
+
+ `
+ });
+
+ const component = page.rootInstance;
+ component.expanded = true;
+ await page.waitForChanges();
+
+ page.root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
+ jest.advanceTimersByTime(300);
+ await page.waitForChanges();
+ expect(component.expanded).toBe(false);
+ });
+
+ it('should close menu on Tab key', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+ Item 1
+
+ `
+ });
+
+ const component = page.rootInstance;
+ component.expanded = true;
+ await page.waitForChanges();
+
+ page.root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
+ jest.advanceTimersByTime(300);
+ await page.waitForChanges();
+ expect(component.expanded).toBe(false);
+ });
+
+ it('should close menu on click of a slotted overflow-item', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+ Item 1
+
+ `
+ });
+
+ const component = page.rootInstance;
+ component.expanded = true;
+ await page.waitForChanges();
+
+ const overflowItem = page.root.querySelector(
+ 'gux-avatar-overflow-item-beta'
+ );
+ overflowItem.click();
+ jest.advanceTimersByTime(300);
+ await page.waitForChanges();
+ expect(component.expanded).toBe(false);
+ });
+
+ it('should navigate between menu items using keyboard', async () => {
+ const page = await newSpecPage({
+ components,
+ html: `
+
+ Item 1
+ Item 2
+
+ `
+ });
+
+ const component = page.rootInstance;
+ component.expanded = true;
+ await page.waitForChanges();
+
+ const items = page.root.querySelectorAll('gux-avatar-overflow-item-beta');
+ const firstItem = items[0] as HTMLGuxAvatarOverflowItemBetaElement;
+ const secondItem = items[1] as HTMLGuxAvatarOverflowItemBetaElement;
+
+ const firstItemMockFocus = jest.fn();
+ const secondItemMockFocus = jest.fn();
+
+ Object.defineProperty(firstItem, 'guxFocus', {
+ value: firstItemMockFocus,
+ writable: true
+ });
+
+ Object.defineProperty(secondItem, 'guxFocus', {
+ value: secondItemMockFocus,
+ writable: true
+ });
+
+ // Press down arrow key - should focus second item
+ firstItem.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ );
+ await page.waitForChanges();
+ expect(secondItemMockFocus).toHaveBeenCalled();
+
+ // Press up arrow key - should focus first item again
+ secondItem.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
+ );
+ await page.waitForChanges();
+ expect(firstItemMockFocus).toHaveBeenCalledTimes(1);
+
+ // press up arrow on first item should focus last item
+ firstItem.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
+ );
+ await page.waitForChanges();
+ expect(secondItemMockFocus).toHaveBeenCalledTimes(2);
+
+ // press down arrow on last item should focus first item
+ secondItem.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ );
+ await page.waitForChanges();
+ expect(firstItemMockFocus).toHaveBeenCalledTimes(2);
+
+ // press end key on first item should focus last item
+ firstItem.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'End', bubbles: true })
+ );
+ await page.waitForChanges();
+ expect(secondItemMockFocus).toHaveBeenCalledTimes(3);
+
+ // press home key on last item should focus first item
+ secondItem.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Home', bubbles: true })
+ );
+ await page.waitForChanges();
+ expect(firstItemMockFocus).toHaveBeenCalledTimes(3);
+ });
+ });
+});
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/readme.md b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/readme.md
index 9c40ad6a2..e1c7764e4 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/readme.md
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/readme.md
@@ -5,6 +5,13 @@
+## Properties
+
+| Property | Attribute | Description | Type | Default |
+| ---------- | ---------- | ----------- | --------------------------------- | ------- |
+| `quantity` | `quantity` | | `1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` | `7` |
+
+
## Slots
| Slot | Description |
@@ -12,6 +19,21 @@
| | slot for gux-avatar-group-item components |
+## Dependencies
+
+### Depends on
+
+- [gux-avatar-overflow-beta](gux-avatar-overflow)
+- [gux-avatar-overflow-item-beta](./gux-avatar-overflow/gux-avatar-overflow-item)
+
+### Graph
+```mermaid
+graph TD;
+ gux-avatar-group-beta --> gux-avatar-overflow-beta
+ gux-avatar-group-beta --> gux-avatar-overflow-item-beta
+ style gux-avatar-group-beta fill:#f9f,stroke:#333,stroke-width:4px
+```
+
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.e2e.ts.snap b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.e2e.ts.snap
index d93e9ad93..6b7f170cb 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.e2e.ts.snap
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.e2e.ts.snap
@@ -1,3 +1,296 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`gux-avatar-group #render should render component as expected 1`] = `" "`;
+exports[`gux-avatar-group #interaction should handles tabindex on navigation as expected with overflow 1`] = `
+Page {
+ "_e2eClose": [Function],
+ "_e2eElements": [
+
+
+
+
+
+
+
+
+
+
+,
+
+
+
+
+
+ CD
+
+
+
+
+ Conor Darcy
+
+
+
+
+,
+
+
+
+ CD
+
+
+
+
+ Conor Darcy
+
+
+,
+
+
+
+
+
+ EF
+
+
+
+
+ Elliot Fitzgerald
+
+
+
+
+,
+
+
+
+ EF
+
+
+
+
+ Elliot Fitzgerald
+
+
+,
+
+
+
+
+
+ +1
+
+
+
+
+
+
+,
+
+
+
+ +1
+
+
+,
+ ],
+ "_e2eEventIds": 0,
+ "_e2eEvents": Map {},
+ "_e2eGoto": [Function],
+ "close": [Function],
+ "compareScreenshot": [Function],
+ "debugger": [Function],
+ "emitter": {
+ "all": Map {
+ "console" => [
+ [Function],
+ ],
+ "pageerror" => [
+ [Function],
+ ],
+ "requestfailed" => [
+ [Function],
+ ],
+ "request" => [
+ [Function],
+ ],
+ },
+ "emit": [Function],
+ "off": [Function],
+ "on": [Function],
+ },
+ "eventsMap": Map {
+ "console" => [
+ [Function],
+ ],
+ "pageerror" => [
+ [Function],
+ ],
+ "requestfailed" => [
+ [Function],
+ ],
+ "request" => [
+ [Function],
+ ],
+ },
+ "find": [Function],
+ "findAll": [Function],
+ "getDiagnostics": [Function],
+ "goto": [Function],
+ "setContent": [Function],
+ "spyOnEvent": [Function],
+ "waitForChanges": [Function],
+ "waitForEvent": [Function],
+}
+`;
+
+exports[`gux-avatar-group #interaction should handles tabindex on navigation as expected without overflow 1`] = `
+Page {
+ "_e2eClose": [Function],
+ "_e2eElements": [
+
+
+
+
+
+
+
+,
+
+
+
+
+
+ CD
+
+
+
+
+ Conor Darcy
+
+
+
+
+,
+
+
+
+ CD
+
+
+
+
+ Conor Darcy
+
+
+,
+
+
+
+
+
+ EF
+
+
+
+
+ Elliot Fitzgerald
+
+
+
+
+,
+
+
+
+ EF
+
+
+
+
+ Elliot Fitzgerald
+
+
+,
+
+
+
+
+
+ GH
+
+
+
+
+ Greg Hayes
+
+
+
+
+,
+
+
+
+ GH
+
+
+
+
+ Greg Hayes
+
+
+,
+ ],
+ "_e2eEventIds": 0,
+ "_e2eEvents": Map {},
+ "_e2eGoto": [Function],
+ "close": [Function],
+ "compareScreenshot": [Function],
+ "debugger": [Function],
+ "emitter": {
+ "all": Map {
+ "console" => [
+ [Function],
+ ],
+ "pageerror" => [
+ [Function],
+ ],
+ "requestfailed" => [
+ [Function],
+ ],
+ "request" => [
+ [Function],
+ ],
+ },
+ "emit": [Function],
+ "off": [Function],
+ "on": [Function],
+ },
+ "eventsMap": Map {
+ "console" => [
+ [Function],
+ ],
+ "pageerror" => [
+ [Function],
+ ],
+ "requestfailed" => [
+ [Function],
+ ],
+ "request" => [
+ [Function],
+ ],
+ },
+ "find": [Function],
+ "findAll": [Function],
+ "getDiagnostics": [Function],
+ "goto": [Function],
+ "setContent": [Function],
+ "spyOnEvent": [Function],
+ "waitForChanges": [Function],
+ "waitForEvent": [Function],
+}
+`;
+
+exports[`gux-avatar-group #render should render component as expected 1`] = `" "`;
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.spec.ts.snap b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.spec.ts.snap
index ac12ac8fc..186cb62cf 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.spec.ts.snap
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/__snapshots__/gux-avatar-group.spec.ts.snap
@@ -4,10 +4,17 @@ exports[`gux-avatar-group render 1`] = `
+
+
+
+
+
+
+
-
+
-
+
CD
@@ -21,9 +28,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
EF
@@ -37,9 +44,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
GH
@@ -53,9 +60,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
IJ
@@ -69,9 +76,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
JK
@@ -85,9 +92,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
LM
@@ -101,9 +108,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
NO
@@ -117,9 +124,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
PQ
@@ -133,9 +140,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
RS
@@ -149,9 +156,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
TU
@@ -165,9 +172,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
VW
@@ -181,9 +188,9 @@ exports[`gux-avatar-group render 1`] = `
-
+
-
+
XY
diff --git a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/gux-avatar-group.e2e.ts b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/gux-avatar-group.e2e.ts
index 53b50e9d3..75910313c 100644
--- a/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/gux-avatar-group.e2e.ts
+++ b/packages/genesys-spark-components/src/components/beta/gux-avatar-group/tests/gux-avatar-group.e2e.ts
@@ -129,16 +129,17 @@ describe('gux-avatar-group', () => {
});
describe('#interaction', () => {
- it(`should handles tabindex on navigation correctly`, async () => {
+ it(`should handles tabindex on navigation as expected without overflow`, async () => {
const html = `
-
-
-
- `;
+
+
+
+
+ `;
const page = await newNonrandomE2EPage({ html });
const element = await page.find('gux-avatar-group-beta');
const firstAvatarGroupItem = await page.find(
@@ -154,76 +155,137 @@ describe('gux-avatar-group', () => {
'gux-avatar-group-item-beta[name="Greg Hayes"]'
);
const lastItemButton = await lastAvatarGroupItem.find('pierce/button');
-
await element.press('Tab');
await page.waitForChanges();
-
+ expect(page).toMatchSnapshot();
// correct initially
expect(firstItemButton.getAttribute('tabindex')).toEqual('0');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('-1');
-
// navigate to next group item
await element.press('ArrowRight');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
expect(secondItemButton.getAttribute('tabindex')).toEqual('0');
expect(lastItemButton.getAttribute('tabindex')).toEqual('-1');
-
// navigate to last group item
await element.press('ArrowRight');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('0');
-
// navigate to first group item going right
await element.press('ArrowRight');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('0');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('-1');
-
// navigate to last group item going left
await element.press('ArrowLeft');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('0');
-
await element.press('Home');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('0');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('-1');
-
await element.press('End');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('0');
-
// navigate out of group, keeps correct tab index
await element.press('Tab');
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
expect(lastItemButton.getAttribute('tabindex')).toEqual('0');
-
// click on second item
await secondAvatarGroupItem.click();
await page.waitForChanges();
-
expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
expect(secondItemButton.getAttribute('tabindex')).toEqual('0');
expect(lastItemButton.getAttribute('tabindex')).toEqual('-1');
});
+ it(`should handles tabindex on navigation as expected with overflow`, async () => {
+ const html = `
+
+
+
+
+ `;
+ const page = await newNonrandomE2EPage({ html });
+ const element = await page.find('gux-avatar-group-beta');
+ const firstAvatarGroupItem = await page.find(
+ 'gux-avatar-group-item-beta[name="Conor Darcy"]'
+ );
+ const firstItemButton = await firstAvatarGroupItem.find('pierce/button');
+ const secondAvatarGroupItem = await page.find(
+ 'gux-avatar-group-item-beta[name="Elliot Fitzgerald"]'
+ );
+ const secondItemButton =
+ await secondAvatarGroupItem.find('pierce/button');
+ await element.press('Tab');
+ await page.waitForChanges();
+ const overflow = await page.find('pierce/gux-avatar-overflow-beta');
+ const overflowButton = await overflow.find('pierce/button');
+ expect(page).toMatchSnapshot();
+ // correct initially
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('0');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('-1');
+ // navigate to next group item
+ await element.press('ArrowRight');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('0');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('-1');
+ // navigate to last group item
+ await element.press('ArrowRight');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('0');
+ // navigate to first group item going right
+ await element.press('ArrowRight');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('0');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('-1');
+ // navigate to last group item going left
+ await element.press('ArrowLeft');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('0');
+ await element.press('Home');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('0');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('-1');
+ await element.press('End');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('0');
+ // navigate out of group, keeps correct tab index
+ await element.press('Tab');
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('0');
+ // click on overflow item
+ await overflow.click();
+ await page.waitForChanges();
+ expect(firstItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(secondItemButton.getAttribute('tabindex')).toEqual('-1');
+ expect(overflowButton.getAttribute('tabindex')).toEqual('0');
+ });
});
});
diff --git a/packages/genesys-spark-components/src/components/stable/gux-toggle/readme.md b/packages/genesys-spark-components/src/components/stable/gux-toggle/readme.md
index c2175d471..627255c71 100644
--- a/packages/genesys-spark-components/src/components/stable/gux-toggle/readme.md
+++ b/packages/genesys-spark-components/src/components/stable/gux-toggle/readme.md
@@ -19,6 +19,7 @@ A check event is triggered when the state of the component changed.
| `disabled` | `disabled` | | `boolean` | `false` |
| `displayInline` | `display-inline` | | `boolean` | `false` |
| `errorMessage` | `error-message` | | `string` | `undefined` |
+| `label` | `label` | | `string` | `undefined` |
| `labelPosition` | `label-position` | | `"left" \| "right"` | `'right'` |
| `loading` | `loading` | | `boolean` | `false` |
| `uncheckedLabel` | `unchecked-label` | | `string` | `undefined` |