Skip to content

Feature/bao toggle #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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: 2 additions & 0 deletions projects/angular-ui/src/lib/bao.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { BaoModalModule } from './modal/module';
import { BaoHyperlinkModule } from './hyperlink';
import { BaoDropdownMenuModule } from './dropdown-menu';
import { BaoFileModule } from './file/module';
import { BaoToggleModule } from './toggle';
import { BaoSnackBarModule } from './snack-bar/module';

@NgModule({
Expand Down Expand Up @@ -52,6 +53,7 @@ import { BaoSnackBarModule } from './snack-bar/module';
BaoHyperlinkModule,
BaoDropdownMenuModule,
BaoFileModule,
BaoToggleModule,
BaoSnackBarModule
// TODO: reactivate once component does not depend on global css BaoBadgeModule,
]
Expand Down
7 changes: 7 additions & 0 deletions projects/angular-ui/src/lib/toggle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
export * from './module';
export * from './toggle.component';
19 changes: 19 additions & 0 deletions projects/angular-ui/src/lib/toggle/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
import { ObserversModule } from '@angular/cdk/observers';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BaoCommonComponentsModule } from '../common-components/module';
import { BaoToggleComponent } from './toggle.component';

const TOGGLE_DIRECTIVES = [BaoToggleComponent];

@NgModule({
imports: [CommonModule, BaoCommonComponentsModule, ObserversModule],
declarations: TOGGLE_DIRECTIVES,
exports: TOGGLE_DIRECTIVES
})
export class BaoToggleModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/

import { Component } from '@angular/core';

@Component({
template: `
<bao-toggle id="id01" name="name01" [checked]="checked" [hiddenLabel]="hiddenLabel" [disabled]="disabled" [aria-label]="ariaLabel">
Label
</bao-toggle>
`
})
export class TestToggleHostComponent {
checked: boolean;
disabled: boolean;
hiddenLabel: boolean;
isFocus: boolean;
ariaLabel: string;
}
9 changes: 9 additions & 0 deletions projects/angular-ui/src/lib/toggle/toggle.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<button #button [id]="buttonId" type="button" role="switch" name="name" [attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledby" [attr.aria-checked]="checked" (change)="onInteractionEvent($event)"
(click)="onButtonClick($event)"></button>
<label [for]="buttonId" class="bao-toggle-container">
<div class="bao-toggle-label" [id]="ariaLabelledby">
<ng-content></ng-content>
</div>
<div class="bao-toggle-switch"></div>
</label>
144 changes: 144 additions & 0 deletions projects/angular-ui/src/lib/toggle/toggle.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
@import "../core/colors";

.bao-toggle {
padding-left: 0;
display: block;
font-size: 1rem;
line-height: 1.5rem;
min-height: 1.5rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
position: relative;
z-index: 1;

button {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}

.bao-toggle-switch {
padding-right: 3.25rem;
color: $ground-reversed;
font-weight: 400;
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 0;
position: relative;
vertical-align: top;
&::before {
display: block;
position: absolute;
box-sizing: border-box;
content: "";
top: calc(50% - (#{0.75rem}/ 2));
right: 0;
left: auto;
width: 2rem;
height: 0.75rem;
margin: 0 0.25rem;
background-color: $highlight-dark;
border: none;
border-radius: 0.5rem;
}

&::after {
display: block;
position: absolute;
box-sizing: border-box;
content: "";
top: calc(50% - 0.6rem);
right: 1.25rem;
left: auto;
width: 1.25rem;
height: 1.25rem;
margin: 0;
background-color: $white;
border: $neutral-stroke 1px solid;
border-radius: 50%;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.15s ease-in-out;
}
}

.bao-toggle-container {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
justify-content: space-between;
align-items: center;
}

&.bao-toggle-switch-checked {
.bao-toggle-switch {
&::before {
background-color: $highlight-dark;
}
&::after {
background-color: $action;
border-color: $action;
transition: transform 0.15s ease-in-out;
transform: translateX(1.2rem);
}
}
}

&.bao-toggle-switch-disabled {
.bao-toggle-switch {
&::before {
background-color: $neutral-stroke;
border-color: $neutral-stroke;
cursor: not-allowed;
}
&::after {
background-color: $underground-2;
border-color: $underground-2;
cursor: not-allowed;
}
}
}

&.bao-toggle-switch-hidden-label {
.bao-toggle-switch {
&::before {
top: calc(50% + (#{0.65rem}/ 2));
}
&::after {
top: calc(50% + 0.075rem);
}
}
}

&.bao-toggle-switch-focus {
.bao-toggle-switch {
&::after {
box-shadow: 0 0 0 2px white, 0 0 0 4px #0079C4;
}
}
}

&.bao-toggle-label-disabled {
.bao-toggle-label {
color: $underground-2;
cursor: not-allowed;
}
}

&.bao-toggle-label-hidden {
.bao-toggle-label {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
}
}
139 changes: 139 additions & 0 deletions projects/angular-ui/src/lib/toggle/toggle.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { BaoToggleComponent } from './index';
import { TestToggleHostComponent } from './tests/toggle.hostcomponent.spec';

describe('BaoToggleComponent', () => {
let testComponent: TestToggleHostComponent;
let fixture: ComponentFixture<TestToggleHostComponent>;
let toggleDebugElement: DebugElement;

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [BaoToggleComponent, TestToggleHostComponent]
});

return TestBed.compileComponents();
})
);
describe('CLASSES', () => {
beforeEach(() => {
fixture = TestBed.createComponent(TestToggleHostComponent);
testComponent = fixture.componentInstance;
fixture.detectChanges();
toggleDebugElement = fixture.debugElement.query(By.css('.bao-toggle'));
});
it('should apply global class', () => {
// Default class
expect(
toggleDebugElement.nativeNode.classList.contains('bao-toggle')
).toBe(true);
});
it('should apply class related to checked attribute', () => {
testComponent.checked = true;
fixture.detectChanges();
expect(
toggleDebugElement.nativeNode.classList.contains(
'bao-toggle-switch-checked'
)
).toBe(true);
});
it('should apply classes related to disabled attribute', () => {
testComponent.disabled = true;
fixture.detectChanges();
expect(
toggleDebugElement.nativeNode.classList.contains(
'bao-toggle-switch-disabled'
)
)
.withContext('disabled switch')
.toBe(true);
expect(
toggleDebugElement.nativeNode.classList.contains(
'bao-toggle-label-disabled'
)
)
.withContext('disabled label')
.toBe(true);
});
it('should apply classes related to hiddenLabel attribute', () => {
testComponent.hiddenLabel = true;
fixture.detectChanges();
expect(
toggleDebugElement.nativeNode.classList.contains(
'bao-toggle-label-hidden'
)
)
.withContext('hidden label')
.toBe(true);
expect(
toggleDebugElement.nativeNode.classList.contains(
'bao-toggle-switch-hidden-label'
)
)
.withContext('hidden label switch styling')
.toBe(true);
});
it('should apply class related to focus', () => {
const focusesElement =
fixture.nativeElement.querySelector('#id01-button');
focusesElement.dispatchEvent(new Event('focus'));
fixture.detectChanges();
expect(
toggleDebugElement.nativeNode.classList.contains(
'bao-toggle-switch-focus'
)
).toBe(true);
});
});
describe("ARIA", () => {
beforeEach(() => {
fixture = TestBed.createComponent(TestToggleHostComponent);
testComponent = fixture.componentInstance;
fixture.detectChanges();
toggleDebugElement = fixture.debugElement.query(By.css('.bao-toggle'));
});
it('should apply aria-label', () => {
const ariaLabel = 'test aria-label';
testComponent.ariaLabel = ariaLabel;
fixture.detectChanges();
expect(
toggleDebugElement.childNodes[0].nativeNode.getAttribute('aria-label')
).toBe(ariaLabel);
});
it('should apply not apply aria-label', () => {
fixture.detectChanges();
expect(
toggleDebugElement.childNodes[0].nativeNode.getAttribute('aria-label')
).toBeNull();
});
it('should apply aria-labelleby', () => {
fixture.detectChanges();
expect(
toggleDebugElement.childNodes[0].nativeNode.getAttribute(
'aria-labelledby'
)
).toContain(`id01-arialabelledby`);
});
it('should apply aria-checked to false', () => {
fixture.detectChanges();
expect(
toggleDebugElement.childNodes[0].nativeNode.getAttribute('aria-checked')
).toBeNull();
});
it('should apply aria-checked to true', () => {
testComponent.checked = true;
fixture.detectChanges();
expect(
toggleDebugElement.childNodes[0].nativeNode.getAttribute('aria-checked')
).toBe('true');
});
});
});
Loading