diff --git a/apps/e2e/indicators-storybook-e2e/src/e2e/tokens.component.cy.ts b/apps/e2e/indicators-storybook-e2e/src/e2e/tokens.component.cy.ts
index 103f87d526..25fb7a75ba 100644
--- a/apps/e2e/indicators-storybook-e2e/src/e2e/tokens.component.cy.ts
+++ b/apps/e2e/indicators-storybook-e2e/src/e2e/tokens.component.cy.ts
@@ -13,8 +13,10 @@ describe('indicators-storybook', () => {
.should('exist')
.should('be.visible')
// Capture the focus style of the first token.
- .get('sky-tokens:first-child sky-token:first-child > .sky-token')
- .click({ multiple: true })
+ .get(
+ 'sky-tokens:first-child sky-token:first-child .sky-token-btn-action'
+ )
+ .click()
.get('app-tokens')
.screenshot(`tokenscomponent-tokens--tokens-${theme}`)
.percySnapshot(`tokenscomponent-tokens--tokens-${theme}`, {
diff --git a/apps/e2e/split-view-storybook/project.json b/apps/e2e/split-view-storybook/project.json
index e1ce7e9484..c4f52533d7 100644
--- a/apps/e2e/split-view-storybook/project.json
+++ b/apps/e2e/split-view-storybook/project.json
@@ -32,7 +32,7 @@
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
- "maximumError": "4kb"
+ "maximumError": "5kb"
}
],
"fileReplacements": [
diff --git a/libs/components/indicators/src/lib/modules/tokens/fixtures/token-a11y.component.fixture.html b/libs/components/indicators/src/lib/modules/tokens/fixtures/token-a11y.component.fixture.html
new file mode 100644
index 0000000000..a17b3bfae0
--- /dev/null
+++ b/libs/components/indicators/src/lib/modules/tokens/fixtures/token-a11y.component.fixture.html
@@ -0,0 +1,13 @@
+
+
+
diff --git a/libs/components/indicators/src/lib/modules/tokens/fixtures/token-a11y.component.fixture.ts b/libs/components/indicators/src/lib/modules/tokens/fixtures/token-a11y.component.fixture.ts
new file mode 100644
index 0000000000..82d408fef6
--- /dev/null
+++ b/libs/components/indicators/src/lib/modules/tokens/fixtures/token-a11y.component.fixture.ts
@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+
+import { SkyToken } from '../types/token';
+
+@Component({
+ selector: 'sky-token-a11y-test',
+ templateUrl: './token-a11y.component.fixture.html',
+})
+export class SkyTokenA11yTestComponent {
+ public data: SkyToken<{ name: string }>[] = [
+ {
+ value: { name: 'Apple' },
+ },
+ {
+ value: { name: 'Orange' },
+ },
+ {
+ value: { name: 'Strawberry' },
+ },
+ ];
+}
diff --git a/libs/components/indicators/src/lib/modules/tokens/fixtures/tokens-fixtures.module.ts b/libs/components/indicators/src/lib/modules/tokens/fixtures/tokens-fixtures.module.ts
index 8cf7bb88ce..8d2b7a21f8 100644
--- a/libs/components/indicators/src/lib/modules/tokens/fixtures/tokens-fixtures.module.ts
+++ b/libs/components/indicators/src/lib/modules/tokens/fixtures/tokens-fixtures.module.ts
@@ -4,12 +4,21 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyTokensModule } from '../tokens.module';
+import { SkyTokenA11yTestComponent } from './token-a11y.component.fixture';
import { SkyTokenTestComponent } from './token.component.fixture';
import { SkyTokensTestComponent } from './tokens.component.fixture';
@NgModule({
- declarations: [SkyTokenTestComponent, SkyTokensTestComponent],
+ declarations: [
+ SkyTokenA11yTestComponent,
+ SkyTokenTestComponent,
+ SkyTokensTestComponent,
+ ],
imports: [CommonModule, NoopAnimationsModule, SkyTokensModule],
- exports: [SkyTokenTestComponent, SkyTokensTestComponent],
+ exports: [
+ SkyTokenA11yTestComponent,
+ SkyTokenTestComponent,
+ SkyTokensTestComponent,
+ ],
})
export class SkyTokensFixturesModule {}
diff --git a/libs/components/indicators/src/lib/modules/tokens/token.component.html b/libs/components/indicators/src/lib/modules/tokens/token.component.html
index dfcb38dbd7..eab6cf408d 100644
--- a/libs/components/indicators/src/lib/modules/tokens/token.component.html
+++ b/libs/components/indicators/src/lib/modules/tokens/token.component.html
@@ -1,39 +1,69 @@
-
-
+
+
+
+
+
diff --git a/libs/components/indicators/src/lib/modules/tokens/token.component.scss b/libs/components/indicators/src/lib/modules/tokens/token.component.scss
index b99999c261..390f100baa 100644
--- a/libs/components/indicators/src/lib/modules/tokens/token.component.scss
+++ b/libs/components/indicators/src/lib/modules/tokens/token.component.scss
@@ -4,21 +4,25 @@
.sky-token {
background-color: $sky-background-color-info-light;
border: 1px solid $sky-highlight-color-info;
- overflow: hidden;
- padding: 2px 8px;
+ padding: 0;
display: inline-block;
- user-select: none;
&:hover,
- &:focus {
+ &.sky-token-focused {
background-color: darken($sky-background-color-info-light, 10%);
border-color: darken($sky-highlight-color-info, 10%);
cursor: pointer;
}
- &:focus {
+ &.sky-token-focused {
@include mixins.sky-field-status('active');
}
+
+ &.sky-btn-disabled {
+ .sky-btn-disabled {
+ opacity: 1;
+ }
+ }
}
.sky-btn-disabled {
@@ -26,19 +30,34 @@
user-select: none;
}
-.sky-token-btn-close {
- background: transparent;
- padding: 0;
+.sky-token-btn {
+ background-color: transparent;
border: 0;
- margin-left: 2px;
+ padding: 2px 8px;
+}
+
+.sky-token-btn-action {
+ &:focus-visible {
+ outline: none;
+ }
+}
+
+.sky-token-btn-close {
+ margin-left: -2px;
opacity: 0.9;
&:hover,
- &:focus {
+ &:focus-visible {
opacity: 1;
}
}
+.sky-token-dismissible {
+ .sky-token-btn-action {
+ padding-right: 0;
+ }
+}
+
.sky-token-btn-close-icon-modern {
display: none;
}
@@ -47,19 +66,18 @@
.sky-token {
align-items: center;
display: inline-flex;
- padding: 1px 5px;
font-size: 14px;
- &.sky-btn-disabled {
- color: $sky-text-color-default;
- }
-
&:not(.sky-btn-disabled) {
@include mixins.sky-theme-modern-border($sky-background-color-info);
}
- &.sky-token-dismissable {
+ &.sky-token-dismissible {
padding-right: 1px;
+
+ .sky-token-btn-action {
+ padding-right: 0;
+ }
}
&:hover:not(:active) {
@@ -70,16 +88,31 @@
@include mixins.sky-theme-modern-border-active;
}
- &:focus:not(:active) {
+ &.sky-token-focused:not(:active) {
@include mixins.sky-theme-modern-border-focus;
}
- &:focus,
+ &.sky-token-focused,
&:hover {
background-color: $sky-background-color-info-light;
}
}
+ .sky-token-btn {
+ padding: 1px 5px;
+ border: 0;
+ box-shadow: none;
+ background-color: transparent;
+
+ &.sky-btn-disabled {
+ color: $sky-text-color-default;
+ }
+ }
+
+ .sky-token-btn-action {
+ font-size: 14px;
+ }
+
.sky-token-btn-close {
align-items: center;
border-radius: $sky-theme-modern-border-radius-md - 1;
@@ -87,6 +120,7 @@
height: 20px;
margin-left: $sky-theme-modern-margin-inline-xs;
width: 20px;
+ padding: 0;
&:hover {
@include mixins.sky-theme-modern-border-hover(transparent);
@@ -96,7 +130,7 @@
@include mixins.sky-theme-modern-border-active;
}
- &:focus {
+ &:focus-visible {
outline: none;
&:not(:active) {
diff --git a/libs/components/indicators/src/lib/modules/tokens/token.component.spec.ts b/libs/components/indicators/src/lib/modules/tokens/token.component.spec.ts
index e03ddd8ac7..7f5733d59b 100644
--- a/libs/components/indicators/src/lib/modules/tokens/token.component.spec.ts
+++ b/libs/components/indicators/src/lib/modules/tokens/token.component.spec.ts
@@ -42,6 +42,37 @@ describe('Token component', () => {
validateActive('sky-token-btn-close');
});
+ it('should emit when token focused', () => {
+ const fixture = TestBed.createComponent(SkyTokenComponent);
+ const tokenEl = fixture.nativeElement.querySelector('.sky-token');
+
+ const focusSpy = spyOn(
+ fixture.componentInstance.tokenFocus,
+ 'emit'
+ ).and.callThrough();
+
+ fixture.componentInstance.focusable = true;
+ fixture.detectChanges();
+
+ SkyAppTestUtility.fireDomEvent(tokenEl, 'focusin');
+ fixture.detectChanges();
+
+ // Ensure CSS class is added on focus.
+ expect(tokenEl).toHaveClass('sky-token-focused');
+ expect(focusSpy).toHaveBeenCalled();
+
+ SkyAppTestUtility.fireDomEvent(tokenEl, 'focusout', {
+ customEventInit: {
+ // Mock an element that is not a child of the token.
+ relatedTarget: document.createElement('div'),
+ },
+ });
+ fixture.detectChanges();
+
+ // Ensure CSS class is removed on blur.
+ expect(tokenEl).not.toHaveClass('sky-token-focused');
+ });
+
it('should use the specified ARIA label', () => {
const fixture = TestBed.createComponent(SkyTokenTestComponent);
@@ -59,4 +90,12 @@ describe('Token component', () => {
expect(btnEl.getAttribute('aria-label')).toBe('Remove item');
});
+
+ it('should not have a role by default', () => {
+ const fixture = TestBed.createComponent(SkyTokenTestComponent);
+ fixture.detectChanges();
+ expect(
+ fixture.nativeElement.querySelector('.sky-token').getAttribute('role')
+ ).toBeNull();
+ });
});
diff --git a/libs/components/indicators/src/lib/modules/tokens/token.component.ts b/libs/components/indicators/src/lib/modules/tokens/token.component.ts
index af0235ad37..e68d926984 100644
--- a/libs/components/indicators/src/lib/modules/tokens/token.component.ts
+++ b/libs/components/indicators/src/lib/modules/tokens/token.component.ts
@@ -5,8 +5,8 @@ import {
EventEmitter,
Input,
Output,
+ ViewChild,
} from '@angular/core';
-import { SkyLibResourcesService } from '@skyux/i18n';
@Component({
selector: 'sky-token',
@@ -36,13 +36,7 @@ export class SkyTokenComponent {
* @default "Remove item"
*/
@Input()
- public set ariaLabel(value: string | undefined) {
- this.#ariaLabelOrDefault = value || this.#getDefaultAriaLabel();
- }
-
- public get ariaLabel(): string {
- return this.#ariaLabelOrDefault;
- }
+ public ariaLabel: string | undefined;
/**
* Indicates whether users can remove the token from the list by selecting the close button.
@@ -67,6 +61,13 @@ export class SkyTokenComponent {
this.tabIndex = value !== false ? 0 : -1;
}
+ /**
+ * Used by the tokens component to set the appropriate role for each token.
+ * @internal
+ */
+ @Input()
+ public role: string | undefined;
+
/**
* Fires when users click the close button.
*/
@@ -79,22 +80,34 @@ export class SkyTokenComponent {
@Output()
public tokenFocus = new EventEmitter();
+ @ViewChild('actionButton', { read: ElementRef, static: true })
+ private actionButtonRef: ElementRef | undefined;
+
+ public isFocused = false;
public tokenActive = false;
public closeActive = false;
public tabIndex = 0;
- #ariaLabelOrDefault: string;
#elementRef: ElementRef;
- #resourcesSvc: SkyLibResourcesService;
#_disabled = false;
#_dismissible = true;
- constructor(elementRef: ElementRef, resourcesSvc: SkyLibResourcesService) {
+ constructor(elementRef: ElementRef) {
this.#elementRef = elementRef;
- this.#resourcesSvc = resourcesSvc;
+ }
+
+ protected onFocusIn(): void {
+ if (!this.isFocused) {
+ this.tokenFocus.emit();
+ this.isFocused = true;
+ }
+ }
- this.#ariaLabelOrDefault = this.#getDefaultAriaLabel();
+ protected onFocusOut(event: FocusEvent): void {
+ this.isFocused = this.#elementRef.nativeElement.contains(
+ event.relatedTarget
+ );
}
public dismissToken(event: Event): void {
@@ -103,7 +116,7 @@ export class SkyTokenComponent {
}
public focusElement(): void {
- this.#elementRef.nativeElement.querySelector('.sky-token').focus();
+ this.actionButtonRef?.nativeElement.focus();
}
public setTokenActive(tokenActive: boolean): void {
@@ -113,12 +126,4 @@ export class SkyTokenComponent {
public setCloseActive(closeActive: boolean): void {
this.closeActive = closeActive;
}
-
- #getDefaultAriaLabel(): string {
- // TODO: Need to implement the async `getString` method in a breaking change.
- return this.#resourcesSvc.getStringForLocale(
- { locale: 'en-US' },
- 'skyux_tokens_dismiss_button_title'
- );
- }
}
diff --git a/libs/components/indicators/src/lib/modules/tokens/tokens.component.html b/libs/components/indicators/src/lib/modules/tokens/tokens.component.html
index ba00ed63c3..d532a6360f 100644
--- a/libs/components/indicators/src/lib/modules/tokens/tokens.component.html
+++ b/libs/components/indicators/src/lib/modules/tokens/tokens.component.html
@@ -1,11 +1,11 @@
{
fixture.detectChanges();
expect(component.tokensComponent?.activeIndex).toEqual(1);
expect(document.activeElement).toEqual(
- tokenElements.item(1).querySelector('.sky-token')
+ tokenElements.item(1).querySelector('.sky-token-btn-action')
);
SkyAppTestUtility.fireDomEvent(tokenElements.item(1), 'keydown', {
@@ -47,7 +48,7 @@ describe('Tokens component', () => {
expect(component.tokensComponent?.activeIndex).toEqual(0);
expect(document.activeElement).toEqual(
- tokenElements.item(0).querySelector('.sky-token')
+ tokenElements.item(0).querySelector('.sky-token-btn-action')
);
}
@@ -58,7 +59,9 @@ describe('Tokens component', () => {
component.messageStream?.next({ type });
fixture.detectChanges();
- const tokenElements = fixture.nativeElement.querySelectorAll('.sky-token');
+ const tokenElements = fixture.nativeElement.querySelectorAll(
+ '.sky-token-btn-action'
+ );
const focusedToken = tokenElements[index] as HTMLElement;
expect(component.tokensComponent?.activeIndex).toEqual(index);
@@ -318,8 +321,9 @@ describe('Tokens component', () => {
fixture.detectChanges();
- const tokenElements =
- fixture.nativeElement.querySelectorAll('.sky-token');
+ const tokenElements = fixture.nativeElement.querySelectorAll(
+ '.sky-token-btn-action'
+ );
const lastToken = tokenElements[tokenElements.length - 1] as HTMLElement;
expect(component.tokensComponent?.activeIndex).toEqual(
@@ -464,22 +468,40 @@ describe('Tokens component', () => {
component.publishTokens();
fixture.detectChanges();
- let tokenDivs: NodeListOf =
+ let tokenButtons: NodeListOf =
component.tokensElementRef?.nativeElement.querySelectorAll(
- '.sky-token'
+ '.sky-token-btn-action'
);
- expect(tokenDivs.item(0).tabIndex).toEqual(0);
+ expect(tokenButtons.item(0).tabIndex).toEqual(0);
component.focusable = false;
fixture.detectChanges();
- tokenDivs =
+ tokenButtons =
component.tokensElementRef?.nativeElement.querySelectorAll(
'.sky-token'
);
- expect(tokenDivs.item(0).tabIndex).toEqual(-1);
+ expect(tokenButtons.item(0).tabIndex).toEqual(-1);
await expectAsync(fixture.nativeElement).toBeAccessible();
});
});
+
+ it('should be accessible', async () => {
+ const a11yFixture = TestBed.createComponent(SkyTokenA11yTestComponent);
+
+ a11yFixture.detectChanges();
+
+ await expectAsync(
+ a11yFixture.nativeElement.querySelector(
+ '[data-sky-id="non-dismissible-tokens"]'
+ )
+ ).toBeAccessible();
+
+ await expectAsync(
+ a11yFixture.nativeElement.querySelector(
+ '[data-sky-id="dismissible-tokens"]'
+ )
+ ).toBeAccessible();
+ });
});
diff --git a/libs/components/indicators/src/lib/modules/tokens/tokens.component.ts b/libs/components/indicators/src/lib/modules/tokens/tokens.component.ts
index 7c4cf9811d..cf925e2761 100644
--- a/libs/components/indicators/src/lib/modules/tokens/tokens.component.ts
+++ b/libs/components/indicators/src/lib/modules/tokens/tokens.component.ts
@@ -221,11 +221,12 @@ export class SkyTokensComponent implements OnDestroy {
constructor(changeDetector: ChangeDetectorRef) {
this.#changeDetector = changeDetector;
+ this.#initMessageStream();
// Angular calls the trackBy function without applying the component instance's scope.
// Use a fat-arrow function so the current component instance's trackWith property can
// be referenced.
- this.trackTokenFn = (_index, item) => {
+ this.trackTokenFn = (_index, item): unknown => {
if (this.trackWith) {
return item.value[this.trackWith];
}
diff --git a/libs/components/indicators/testing/src/tokens/token-harness.ts b/libs/components/indicators/testing/src/tokens/token-harness.ts
index 000c0fae9c..997f053d33 100644
--- a/libs/components/indicators/testing/src/tokens/token-harness.ts
+++ b/libs/components/indicators/testing/src/tokens/token-harness.ts
@@ -11,6 +11,8 @@ export class SkyTokenHarness extends ComponentHarness {
*/
public static hostSelector = 'sky-token';
+ #getActionButton = this.locatorFor('button.sky-token-btn-action');
+
#getDismissButton = this.locatorFor('button.sky-token-btn-close');
#getWrapper = this.locatorFor('.sky-token');
@@ -50,7 +52,7 @@ export class SkyTokenHarness extends ComponentHarness {
public async dismiss(): Promise {
if (!(await this.isDismissible())) {
throw new Error(
- 'Could not dismiss the token because it is not dismissable.'
+ 'Could not dismiss the token because it is not dismissible.'
);
}
@@ -68,20 +70,20 @@ export class SkyTokenHarness extends ComponentHarness {
* Whether the token is disabled.
*/
public async isDisabled(): Promise {
- return (await this.#getWrapper()).hasClass('sky-btn-disabled');
+ return (await this.#getWrapper()).hasClass('sky-token-disabled');
}
/**
* Whether the token is dismissible.
*/
public async isDismissible(): Promise {
- return (await this.#getWrapper()).hasClass('sky-token-dismissable');
+ return (await this.#getWrapper()).hasClass('sky-token-dismissible');
}
/**
* Whether the token is focused.
*/
public async isFocused(): Promise {
- return (await this.#getWrapper()).isFocused();
+ return (await this.#getActionButton()).isFocused();
}
}
diff --git a/libs/components/indicators/testing/src/tokens/tokens-harness.spec.ts b/libs/components/indicators/testing/src/tokens/tokens-harness.spec.ts
index 852a236cc7..ce5d3c7ce7 100644
--- a/libs/components/indicators/testing/src/tokens/tokens-harness.spec.ts
+++ b/libs/components/indicators/testing/src/tokens/tokens-harness.spec.ts
@@ -89,7 +89,7 @@ describe('Tokens harness', () => {
await expectAsync(tokens[0].isDismissible()).toBeResolvedTo(false);
await expectAsync(tokens[0].dismiss()).toBeRejectedWithError(
- 'Could not dismiss the token because it is not dismissable.'
+ 'Could not dismiss the token because it is not dismissible.'
);
});
@@ -147,7 +147,9 @@ describe('Tokens harness', () => {
await expectAsync(firstToken.isFocused()).toBeResolvedTo(false);
- fixture.nativeElement.querySelectorAll('sky-token .sky-btn')[0].focus();
+ fixture.nativeElement
+ .querySelectorAll('sky-token .sky-token-btn')[0]
+ .focus();
await expectAsync(firstToken.isFocused()).toBeResolvedTo(true);
});
diff --git a/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts b/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts
index e1e7cb9dac..5734bd9e2b 100644
--- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts
+++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts
@@ -106,14 +106,14 @@ describe('Lookup component', function () {
if (async) {
(
document.querySelectorAll(
- '#my-async-lookup .sky-lookup-tokens .sky-token'
+ '#my-async-lookup .sky-lookup-tokens .sky-token-btn-action'
)[index] as HTMLElement
).click();
} else {
(
- document.querySelectorAll('#my-lookup .sky-lookup-tokens .sky-token')[
- index
- ] as HTMLElement
+ document.querySelectorAll(
+ '#my-lookup .sky-lookup-tokens .sky-token-btn-action'
+ )[index] as HTMLElement
).click();
}
fixture.detectChanges();
@@ -301,9 +301,11 @@ describe('Lookup component', function () {
function getTokenElements(async: boolean = false): NodeListOf {
if (async) {
- return document.querySelectorAll('#my-async-lookup .sky-token');
+ return document.querySelectorAll(
+ '#my-async-lookup .sky-token-btn-action'
+ );
} else {
- return document.querySelectorAll('#my-lookup .sky-token');
+ return document.querySelectorAll('#my-lookup .sky-token-btn-action');
}
}
@@ -3486,10 +3488,9 @@ describe('Lookup component', function () {
performSearch('s', fixture);
selectSearchResult(0, fixture);
- const tokenElements = getTokenElements();
const input = getInputElement(lookupComponent);
- triggerClick(tokenElements.item(0), fixture, true);
+ clickToken(0, fixture, false);
expect(document.activeElement).not.toEqual(input);
}));
@@ -6446,10 +6447,9 @@ describe('Lookup component', function () {
performSearch('s', fixture);
selectSearchResult(0, fixture);
- const tokenElements = getTokenElements();
const input = getInputElement(lookupComponent);
- triggerClick(tokenElements.item(0), fixture, true);
+ clickToken(0, fixture, false);
expect(document.activeElement).not.toEqual(input);
}));
@@ -6537,10 +6537,9 @@ describe('Lookup component', function () {
performSearch('s', fixture);
selectSearchResult(0, fixture);
- const tokenElements = getTokenElements();
const input = getInputElement(lookupComponent);
- triggerClick(tokenElements.item(0), fixture, true);
+ clickToken(0, fixture, false);
expect(document.activeElement).not.toEqual(input);
}));
diff --git a/libs/components/select-field/src/lib/modules/select-field/select-field.component.spec.ts b/libs/components/select-field/src/lib/modules/select-field/select-field.component.spec.ts
index d3db3624f5..a584e5e591 100644
--- a/libs/components/select-field/src/lib/modules/select-field/select-field.component.spec.ts
+++ b/libs/components/select-field/src/lib/modules/select-field/select-field.component.spec.ts
@@ -97,7 +97,7 @@ describe('Select field component', () => {
function closeToken(index: number) {
const tokens = getTokens();
- tokens.item(index).querySelector('button').click();
+ tokens.item(index).querySelector('button.sky-token-btn-close').click();
tick();
fixture.detectChanges();
}