Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion playwright/cps-accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ const components: ComponentEntry[] = [
selector: '.example-content cps-tab-group'
},
// { route: '/table', name: 'Table', selector: 'cps-table' },
// { route: '/tag', name: 'Tag', selector: 'cps-tag' },
{ route: '/tag', name: 'Tag', selector: 'cps-tag' },
{ route: '/textarea', name: 'Textarea', selector: 'cps-textarea' },
{
route: '/timepicker',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.tags-group {
gap: 32px;
margin-left: 0.5rem;
gap: 2rem;
display: flex;
flex-direction: column;
}

.sync-val-example {
.sync-val {
margin-top: 16px;
margin-top: 1rem;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
<div
[ngClass]="classesList"
[class.unselected]="!value && selectable"
[ngStyle]="{ borderColor: color }"
(click)="toggleSelected()">
<p>{{ label }}</p>
[style.borderColor]="cvtColor"
[attr.role]="selectable ? 'checkbox' : null"
[attr.aria-checked]="selectable ? value : null"
[attr.aria-disabled]="selectable && disabled ? true : null"
[attr.tabindex]="selectable ? (disabled ? -1 : 0) : null"
(click)="toggleSelected()"
(keydown.enter)="handleEnterKeydown($event)"
[class.cps-tag--pressing]="pressing"
(keydown.space)="handleSpaceKeydown($event)"
(keyup.space)="toggleSelected()">
<span>{{ label }}</span>
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use '../../../../styles/mixins' as *;

:host {
width: fit-content;
display: inline-block;
Expand All @@ -6,38 +8,44 @@
user-select: none;
.cps-tag {
font-family: 'Source Sans Pro', sans-serif;
min-height: 25px;
min-height: 1.5625rem;
align-items: center;
padding: 0 10px;
padding: 0 0.625rem;
background-color: white;
display: inline-flex;
cursor: default;

border: solid 1px;
border-left: solid 4px;
border: solid 0.0625rem;
border-left: solid 0.25rem;

&.cps-tag--selectable {
cursor: pointer;
&:not(:active):not(:disabled) {
box-shadow: 1px 3px 4px rgb(0 0 0 / 10%);
&:not(:active):not(.cps-tag--pressing) {
box-shadow: 0.0625rem 0.1875rem 0.25rem rgb(0 0 0 / 10%);
}
&:focus {
outline: none;
}
&:focus-visible {
@include focus-ring(0.25rem, 0.1875rem, 0);
}
}

&.cps-tag--disabled {
pointer-events: none;
border-color: var(--cps-color-line-dark) !important;
p {
color: var(--cps-color-text-light);
border-color: var(--cps-color-text-lightest) !important;
span {
color: var(--cps-color-text-mild);
}
}

&.unselected {
border-color: var(--cps-color-text-light) !important;
}

p {
span {
margin: 0;
font-size: 11px;
font-size: 0.6875rem;
color: var(--cps-color-text-dark);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('CpsTagComponent', () => {

fixture = TestBed.createComponent(CpsTagComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('label', 'Tag');
fixture.detectChanges();
});

Expand All @@ -21,17 +22,17 @@ describe('CpsTagComponent', () => {
});

it('should have default values', () => {
expect(component.label).toBe('');
expect(component.color).toBe('calm');
expect(component.disabled).toBe(false);
expect(component.selectable).toBe(false);
expect(component.value).toBe(false);
expect(component.pressing).toBe(false);
});

it('should render label', () => {
fixture.componentRef.setInput('label', 'Test Tag');
fixture.detectChanges();
const tag = fixture.nativeElement.querySelector('p');
const tag = fixture.nativeElement.querySelector('span');
expect(tag.textContent).toContain('Test Tag');
});

Expand Down Expand Up @@ -103,7 +104,7 @@ describe('CpsTagComponent', () => {

it('should call setClasses on ngOnChanges', () => {
const spy = jest.spyOn(component, 'setClasses');
fixture.componentRef.setInput('color', 'calm');
fixture.componentRef.setInput('selectable', true);
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
});
Expand All @@ -127,4 +128,207 @@ describe('CpsTagComponent', () => {
component.value = true;
expect(fn).toHaveBeenCalledWith(true);
});

describe('ARIA attributes', () => {
it('should set role="checkbox" when selectable', () => {
fixture.componentRef.setInput('selectable', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('role')).toBe('checkbox');
});

it('should not set role when not selectable', () => {
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('role')).toBeNull();
});

it('should set aria-checked to false when selectable and not selected', () => {
fixture.componentRef.setInput('selectable', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('aria-checked')).toBe('false');
});

it('should set aria-checked to true when selectable and selected', () => {
fixture.componentRef.setInput('selectable', true);
fixture.detectChanges();
component.selectable = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think component.selectable = true; might be redundant as we already set it here fixture.componentRef.setInput('selectable', true);

component.toggleSelected();
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('aria-checked')).toBe('true');
});

it('should not set aria-checked when not selectable', () => {
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('aria-checked')).toBeNull();
});

it('should set aria-disabled when selectable and disabled', () => {
fixture.componentRef.setInput('selectable', true);
fixture.componentRef.setInput('disabled', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('aria-disabled')).toBe('true');
});

it('should not set aria-disabled when not disabled', () => {
fixture.componentRef.setInput('selectable', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('aria-disabled')).toBeNull();
});

it('should not set aria-disabled on non-selectable tags', () => {
fixture.componentRef.setInput('disabled', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('aria-disabled')).toBeNull();
});
});

describe('tabindex', () => {
it('should set tabindex="0" when selectable and not disabled', () => {
fixture.componentRef.setInput('selectable', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('tabindex')).toBe('0');
});

it('should set tabindex="-1" when selectable and disabled', () => {
fixture.componentRef.setInput('selectable', true);
fixture.componentRef.setInput('disabled', true);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('tabindex')).toBe('-1');
});

it('should not set tabindex when not selectable', () => {
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.getAttribute('tabindex')).toBeNull();
});
});

describe('keyboard interaction', () => {
beforeEach(() => {
component.selectable = true;
});

it('should toggle on Enter keydown', () => {
const event = new KeyboardEvent('keydown', {
key: 'Enter',
bubbles: true
});
component.handleEnterKeydown(event);
expect(component.value).toBe(true);
});

it('should not toggle on repeated Enter keydown', () => {
const event = new KeyboardEvent('keydown', {
key: 'Enter',
repeat: true,
bubbles: true
});
component.handleEnterKeydown(event);
expect(component.value).toBe(false);
});

it('should prevent default on Enter keydown', () => {
const event = new KeyboardEvent('keydown', {
key: 'Enter',
bubbles: true
});
jest.spyOn(event, 'preventDefault');
component.handleEnterKeydown(event);
expect(event.preventDefault).toHaveBeenCalled();
});

it('should set pressing=true on Space keydown', () => {
const event = new KeyboardEvent('keydown', { key: ' ', bubbles: true });
component.handleSpaceKeydown(event);
expect(component.pressing).toBe(true);
});

it('should not toggle on Space keydown', () => {
const event = new KeyboardEvent('keydown', { key: ' ', bubbles: true });
component.handleSpaceKeydown(event);
expect(component.value).toBe(false);
});

it('should prevent default on Space keydown', () => {
const event = new KeyboardEvent('keydown', { key: ' ', bubbles: true });
jest.spyOn(event, 'preventDefault');
component.handleSpaceKeydown(event);
expect(event.preventDefault).toHaveBeenCalled();
});

it('should toggle and clear pressing on toggleSelected (Space keyup)', () => {
component.pressing = true;
component.toggleSelected();
expect(component.value).toBe(true);
expect(component.pressing).toBe(false);
});

it('should clear pressing even when disabled', () => {
component.disabled = true;
component.pressing = true;
component.toggleSelected();
expect(component.pressing).toBe(false);
expect(component.value).toBe(false);
});
});

describe('_logMissingLabelError', () => {
let errorFixture: ComponentFixture<CpsTagComponent>;
let errorComponent: CpsTagComponent;

beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
errorFixture = TestBed.createComponent(CpsTagComponent);
errorComponent = errorFixture.componentInstance;
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should log error on init when label is empty', () => {
errorFixture.detectChanges();
expect(console.error).toHaveBeenCalledWith(
'CpsTagComponent: the tag must have a label.'
);
});

it('should log error on init when label is whitespace only', () => {
errorFixture.componentRef.setInput('label', ' ');
errorFixture.detectChanges();
expect(console.error).toHaveBeenCalledWith(
'CpsTagComponent: the tag must have a label.'
);
});

it('should not log error when label is set', () => {
errorFixture.componentRef.setInput('label', 'Tag');
errorFixture.detectChanges();
expect(console.error).not.toHaveBeenCalled();
});

it('should log error on ngOnChanges when label becomes empty', () => {
errorFixture.componentRef.setInput('label', 'Tag');
errorFixture.detectChanges();
jest.clearAllMocks();
errorFixture.componentRef.setInput('label', '');
errorFixture.detectChanges();
expect(console.error).toHaveBeenCalledWith(
'CpsTagComponent: the tag must have a label.'
);
});

it('should not set errorComponent label', () => {
expect(errorComponent.label).toBe('');
});
});
});
Loading
Loading