Skip to content

Commit

Permalink
feat(avatar-group): overflow (#806)
Browse files Browse the repository at this point in the history
  • Loading branch information
321gillian authored Jan 24, 2025
1 parent fcaeac0 commit 8317321
Show file tree
Hide file tree
Showing 33 changed files with 2,667 additions and 219 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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 { groupKeyboardNavigation } from '../gux-avatar-group.service';
import { generateInitials } from '@utils/string/generate-initials';
import { getAvatarAccentClass } from 'components/beta/gux-avatar/gux-avatar.service';
import { GuxAvatarAccent } from 'components/beta/gux-avatar/gux-avatar.types';

/**
* @slot image - Avatar photo.
Expand All @@ -20,10 +22,9 @@ import { generateInitials } from '@utils/string/generate-initials';
@Component({
styleUrl: 'gux-avatar-group-item.scss',
tag: 'gux-avatar-group-item-beta',
shadow: true
shadow: { delegatesFocus: true }
})
export class GuxAvatarGroupItem {
private buttonElement: HTMLButtonElement;
private tooltip: HTMLGuxTooltipBetaElement;

@Element()
Expand All @@ -50,15 +51,6 @@ export class GuxAvatarGroupItem {
this.validatingInputs();
}

/*
* Focus button element
*/
@Method()
// eslint-disable-next-line @typescript-eslint/require-await
async guxFocus(): Promise<void> {
this.buttonElement.focus();
}

/*
* Hide tooltip
*/
Expand All @@ -80,17 +72,6 @@ export class GuxAvatarGroupItem {
return index === children.length - 1;
}

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) {
Expand All @@ -104,32 +85,32 @@ export class GuxAvatarGroupItem {

render(): JSX.Element {
return (
<button
type="button"
role="menuitem"
aria-label={this.name}
tabIndex={-1}
ref={el => (this.buttonElement = el)}
class={{
'gux-avatar': true,
[this.getAccent()]: true,
'gux-last-item': this.isLastItemInGroup()
}}
>
<slot name="image">
<span class="gux-avatar-initials" aria-hidden="true">
{generateInitials(this.name)}
</span>
</slot>
<gux-tooltip-beta
aria-hidden="true"
visual-only
placement="top"
ref={el => (this.tooltip = el)}
<Host role="menuitem">
<button
type="button"
aria-label={this.name}
tabIndex={-1}
class={{
'gux-avatar': true,
[getAvatarAccentClass(this.accent, this.name)]: true,
'gux-last-item': this.isLastItemInGroup()
}}
>
<div slot="content">{this.name}</div>
</gux-tooltip-beta>
</button>
<slot name="image">
<span class="gux-avatar-initials" aria-hidden="true">
{generateInitials(this.name)}
</span>
</slot>
<gux-tooltip-beta
aria-hidden="true"
visual-only
placement="top"
ref={el => (this.tooltip = el)}
>
<div slot="content">{this.name}</div>
</gux-tooltip-beta>
</button>
</Host>
) as JSX.Element;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@

## Methods

### `guxFocus() => Promise<void>`



#### Returns

Type: `Promise<void>`



### `hideTooltip() => Promise<void>`


Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
@use '~genesys-spark/dist/scss/focus.scss';

:host {
display: flex;
align-items: center;
inline-size: fit-content;
padding-block: var(--gse-semantic-interactive-sm-padding);
padding-inline: var(--gse-semantic-interactive-sm-padding)
var(--gse-semantic-interactive-md-padding);
}

&:focus-visible {
outline: none;
}
:host(:focus-visible) {
@include focus.gux-focus-ring;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { getClosestElement } from '@utils/dom/get-closest-element';
import { GuxAvatarGroupChild } from './gux-avatar-group.types';
import { logWarn } from '@utils/error/log-error';

export function groupKeyboardNavigation(
event: KeyboardEvent,
currentElement: Element
): void {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
event.stopPropagation();
event.preventDefault();

focusPreviousSiblingLoop(currentElement);
break;

case 'ArrowRight':
case 'ArrowDown':
event.stopPropagation();
event.preventDefault();

Expand All @@ -34,7 +40,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;
});
Expand All @@ -51,94 +57,112 @@ 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 firstFocusableElement.focus();
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 lastFocusableElement.focus();
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);

if (previousFocusableElement) {
void previousFocusableElement.guxFocus();
void previousFocusableElement.focus();
void setItemTabIndex(previousFocusableElement, 0);
} else {
focusLastSibling(currentElement);
}
}

function focusNextSiblingLoop(currentElement: Element): void {
const nextFocusableElement =
currentElement.nextElementSibling as HTMLGuxAvatarGroupItemBetaElement;
setItemTabIndex(currentElement, -1);
const groupItems = getGroupItems(currentElement);
const currentElementIndex = groupItems.findIndex(
(item: Element) => item === currentElement
);

if (nextFocusableElement) {
void nextFocusableElement.guxFocus();
void setItemTabIndex(nextFocusableElement, 0);
} else {
const nextIndex = (currentElementIndex + 1) % groupItems.length;
if (nextIndex === 0) {
focusFirstSibling(currentElement);
}
}
const nextFocusableElement = groupItems[nextIndex] as GuxAvatarGroupChild;

function getFirstFocusableElement(currentElement: Element): Element {
let firstFocusableElement = currentElement;
setItemTabIndex(currentElement, -1);

while (firstFocusableElement.previousElementSibling !== null) {
firstFocusableElement = firstFocusableElement.previousElementSibling;
if (nextFocusableElement !== null) {
void nextFocusableElement.focus();
void setItemTabIndex(nextFocusableElement, 0);
}
}

return firstFocusableElement;
function getFirstFocusableElement(currentElement: Element): Element {
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) {
const button = getGroupItemButton(element);
if (button) {
button.tabIndex = newIndex;
} else {
console.log('No button found in the gux-avatar-group-item element');
logWarn(
element as HTMLElement,
'gux-avatar-group-beta: No button found in the element'
);
}
}
function isHidden(element: Element): boolean {
return element.hasAttribute('hidden');
}
Loading

0 comments on commit 8317321

Please sign in to comment.