From a4519cbd081ec63a8fd75ca4e82463218c51a10a Mon Sep 17 00:00:00 2001 From: Jean-francois Gamache Date: Mon, 20 Feb 2023 16:28:33 -0500 Subject: [PATCH 1/6] creation --- projects/angular-ui/src/lib/bao.module.ts | 4 +- projects/angular-ui/src/lib/toggle/index.ts | 7 + projects/angular-ui/src/lib/toggle/module.ts | 19 + .../src/lib/toggle/temp-fromBOA4.scss | 93 +++++ .../src/lib/toggle/toggle.component.html | 11 + .../src/lib/toggle/toggle.component.scss | 96 +++++ .../src/lib/toggle/toggle.component.ts | 360 ++++++++++++++++++ projects/angular-ui/src/public-api.ts | 1 + .../src/stories/Checkbox/Checkbox.stories.ts | 2 +- .../src/stories/Toggle/Toggle.stories.ts | 191 ++++++++++ 10 files changed, 782 insertions(+), 2 deletions(-) create mode 100644 projects/angular-ui/src/lib/toggle/index.ts create mode 100644 projects/angular-ui/src/lib/toggle/module.ts create mode 100644 projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.html create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.scss create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.ts create mode 100644 projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts diff --git a/projects/angular-ui/src/lib/bao.module.ts b/projects/angular-ui/src/lib/bao.module.ts index b9595871..ff366c48 100644 --- a/projects/angular-ui/src/lib/bao.module.ts +++ b/projects/angular-ui/src/lib/bao.module.ts @@ -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'; @NgModule({ imports: [ @@ -50,7 +51,8 @@ import { BaoFileModule } from './file/module'; BaoModalModule, BaoHyperlinkModule, BaoDropdownMenuModule, - BaoFileModule + BaoFileModule, + BaoToggleModule, // TODO: reactivate once component does not depend on global css BaoBadgeModule, // TODO: reactivate once component does not depend on global css BaoSnackBarModule, ] diff --git a/projects/angular-ui/src/lib/toggle/index.ts b/projects/angular-ui/src/lib/toggle/index.ts new file mode 100644 index 00000000..eb08c0f4 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/index.ts @@ -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'; diff --git a/projects/angular-ui/src/lib/toggle/module.ts b/projects/angular-ui/src/lib/toggle/module.ts new file mode 100644 index 00000000..6478474e --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/module.ts @@ -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 {} diff --git a/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss b/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss new file mode 100644 index 00000000..7a5f9209 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss @@ -0,0 +1,93 @@ +$action: #097d6c; +$custom-switch-width: 2.5rem; +$custom-control-gutter: 0.75rem; +$highlight-dark: #d0f3ee; +$neutral-stroke: #ced4da; +$neutral-primary-reversed: #ffffff; +$neutral-primary-reverse: $neutral-primary-reversed; +$neutral-underground-2: #dee2e6; +$white: $neutral-primary-reversed; +$custom-switch-indicator-bg: $highlight-dark; +$custom-switch-indicator-border-radius: 0.5rem; +$custom-switch-indicator-size: 1.25rem; + +$custom-switch-indicator-bg: $highlight-dark; +$custom-switch-indicator-bg-disabled: $neutral-stroke; +$custom-switch-indicator-handle: $white; +$custom-switch-indicator-handle-disabled: $neutral-underground-2; +$custom-switch-indicator-checked-handle: $action; +$shadow-moderate: 0px 2px 8px rgba(0, 0, 0, 0.1); + +// switches +// +// Tweak a few things for switches +.custom-switch { + padding-left: 0; + + .custom-control-label { + width: 100%; + padding-right: $custom-switch-width + $custom-control-gutter; + &::before { + // Center vertically + top: calc(50% - (#{0.75rem}/ 2)); + right: 0; + left: auto; + width: 2rem; + height: 0.75rem; + margin: 0 0.25rem; + background-color: $custom-switch-indicator-bg; + border: none; + border-radius: $custom-switch-indicator-border-radius; + } + + &::after { + // Center vertically + top: calc(50% - (#{$custom-switch-indicator-size}/ 2)); + right: ($custom-switch-width/2); + left: auto; + width: $custom-switch-indicator-size; + height: $custom-switch-indicator-size; + margin: 0; + background-color: $custom-switch-indicator-handle; + border: 1px solid $action; + border-radius: 50%; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + @include transition( + transform 0.15s ease-in-out, + $custom-forms-transition + ); + } + } + + .custom-control-input:checked ~ .custom-control-label { + &::before { + background-color: $custom-switch-indicator-bg; + } + &::after { + background-color: $custom-switch-indicator-checked-handle; + border-color: $custom-switch-indicator-checked-handle; + } + } + + .custom-control-input:disabled { + ~ .custom-control-label::before { + cursor: not-allowed; + } + // Inactive disabled state + ~ .custom-control-label::after { + background-color: $custom-switch-indicator-handle-disabled; + border-color: $custom-switch-indicator-handle-disabled; + cursor: not-allowed; + } + // Active disabled background + &:checked ~ .custom-control-label::before { + cursor: not-allowed; + } + // Active disabled handle state + &:checked ~ .custom-control-label::after { + background-color: $custom-switch-indicator-handle-disabled; + border-color: $custom-switch-indicator-handle-disabled; + cursor: not-allowed; + } + } +} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.html b/projects/angular-ui/src/lib/toggle/toggle.component.html new file mode 100644 index 00000000..b7b1f923 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.html @@ -0,0 +1,11 @@ +
+ +
+ + + +
+
\ No newline at end of file diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.scss b/projects/angular-ui/src/lib/toggle/toggle.component.scss new file mode 100644 index 00000000..d8351400 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.scss @@ -0,0 +1,96 @@ +@import '../core/colors'; + +.bao-toggle { + padding-left: 0; + display: block; + font-size: 1rem; + position: relative; + z-index: 1; + + .bao-toggle-label { + width: 100%; + 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.625rem); + 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 .15s ease-in-out; + } + } + + .bao-toggle-button { + display: none; + } + + .bao-toggle-label-checked { + &::before { + background-color: $highlight-dark; + } + &::after { + background-color: $action; + border-color: $action; + transition: transform .15s ease-in-out; + transform: translateX(1.2rem); + } + } + + .bao-toggle-label-disabled { + &::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-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; + // } + } +} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.ts b/projects/angular-ui/src/lib/toggle/toggle.component.ts new file mode 100644 index 00000000..b5313802 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.ts @@ -0,0 +1,360 @@ +/* + * 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 { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +/** + * Toggle state for aria-checked property + */ +export const enum eToggleAriaState { + ON = 'on', + OFF = 'off' +} + +/** + * Unique ID for each toggle counter + */ +let toggleNextUniqueId = 0; + +@Component({ + selector: 'bao-toggle, [bao-toggle]', + templateUrl: 'toggle.component.html', + styleUrls: ['./toggle.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, // TODO JFG No form control ????? + useExisting: forwardRef(() => BaoToggleComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BaoToggleComponent + implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy +{ + /** + * The toggle ID. It is set dynamically with an unique ID by default + */ + @Input() public id: string; + + /** + * The name property of the toggle + */ + @Input() public name?: string; + + /** + * The aria-label for web accessibility + */ + @Input('aria-label') public ariaLabel?: string; + + // // // /** + // // // * The visible state of the label + // // // */ + // // // @Input() public hiddenLabel = false; + + /** + * Emitted object on change event + */ + @Output() public readonly change: EventEmitter = + new EventEmitter(); + + // // // /** + // // // * Inderminate value of the toggle whenever + // // // */ + // // // @Output() public readonly indeterminateChange: EventEmitter = + // // // new EventEmitter(); + + /** + * Reference to the button html element + */ + @ViewChild('button', { static: false }) + private buttonElement: ElementRef; + + /** + * The aria-describedby id for web accessibilty + */ + public ariaDescribedby?: string; + + /** + * The aria-labeledby id for web accessibilty + */ + public ariaLabelledby?: string; + + /** + * The ID of the button html element + */ + public buttonID: string; + + private _disabled = false; + private _checked = false; + private _hiddenLabel = false; + private _uniqueId = `bao-toggle-${++toggleNextUniqueId}`; + // // // private _required: boolean; + + constructor( + private elementRef: ElementRef, + private cdr: ChangeDetectorRef, + private focusMonitor: FocusMonitor + ) { + if (!this.id) { + this.id = this._uniqueId; + } + } + + /** + * Whether the toggle is checked. Default value : false + */ + @Input() + get checked(): boolean { + console.log('checked GET - this._checked ===', this._checked); + return this._checked; + } + + /** + * Whether the toggle is disabled. Default value : false + */ + @Input() + get disabled() { + console.log('disabled GET - this._disabled ===', this._disabled); + return this._disabled; + } + + /** + * Whether the toggle label is visible. Default value : false + */ + @Input() + get hiddenLabel() { + console.log('hiddenLabel GET - this._hiddenLabel ===', this._hiddenLabel); + return this._hiddenLabel; + } + + // // // /** + // // // * Whether the toggle is required. Default value : false + // // // */ + // // // @Input() + // // // get required(): boolean { + // // // return this._required; + // // // } + + get nativeElement(): HTMLElement { + return this.elementRef.nativeElement; + } + + set checked(value: boolean) { + console.log('checked SET - value ===', value); + if (value !== this.checked) { + console.log('checked SET change - value ===', value); + this._checked = value; + this.cdr.markForCheck(); + } + } + + set disabled(value: boolean) { + console.log('disabled SET - value ===', value); + if (value !== this.disabled) { + console.log('disabled SET change - value ===', value); + this._disabled = value; + this.cdr.markForCheck(); + } + } + + set hiddenLabel(value: boolean) { + console.log('hiddenLabel SET - value ===', value); + if (value !== this.hiddenLabel) { + console.log('hiddenLabel SET change - value ===', value); + this._hiddenLabel = value; + this.cdr.markForCheck(); + } + } + + // // // set required(value: boolean) { + // // // this._required = value; + // // // } + + public ngOnInit() { + // Set all unique ids for the html elements + this.buttonID = `${this.id}-button`; + this.ariaLabelledby = `${this.id}-arialabelledby`; + } + + public ngAfterViewInit() { + this.focusMonitor.monitor(this.elementRef, true).subscribe(focusOrigin => { + if (!focusOrigin) { + // When a focused element becomes disabled, the browser *immediately* fires a blur event. + // Angular does not expect events to be raised during change detection, so any state change + // (such as a form control's 'ng-touched') will cause a changed-after-checked error. + // See https://github.com/angular/angular/issues/17793. To work around this, we defer + // telling the form control it has been touched until the next tick. + Promise.resolve() + .then(() => { + this.onTouch(); + this.cdr.markForCheck(); + }) + .catch(() => undefined); + } + }); + + // // // this.setAriaDescribedByToDescription(); + // this.syncIndeterminate(this.indeterminate); // TODO JFG ??? See function + } + + public ngOnDestroy() { + this.focusMonitor.stopMonitoring(this.elementRef); + } + + /** + * Implement ControlValueAccessor + */ + public writeValue(value: any) { // TODO JFG No form control ????? + this.checked = !!value; + } + + /** + * Implement ControlValueAccessor + */ + public registerOnChange(fn: (value: any) => void) { // TODO JFG No form control ????? + this.onModelChange = fn; + } + + /** + * Implement ControlValueAccessor + */ + public registerOnTouched(fn: any) { // TODO JFG No form control ????? + this.onTouch = fn; + } + + /** + * Implement ControlValueAccessor + */ + public setDisabledState(isDisabled: boolean) { // TODO JFG No form control ????? + this.disabled = isDisabled; + } + + public focus(origin?: FocusOrigin, options?: FocusOptions): void { + if (origin) { + this.focusMonitor.focusVia(this.buttonElement, origin, options); + } else { + this.buttonElement.nativeElement.focus(options); + } + } + + /** + * Whenever there is change on the button html element + */ + public onInteractionEvent(event: Event) { + // We always have to stop propagation on the change event. + // Otherwise the change event, from the button element, will bubble up and + // emit its event object to the `change` output. + event.stopPropagation(); + } + + /** + * Whenever there is click event triggered on the toggle + */ + public onButtonClick(event: Event) { + event.stopPropagation(); + this.toggle(); + this.emitChangeEvent(); + } + + /** + * Get the value for the aria-checked property (web accessibility) + */ + public getAriaState(): eToggleAriaState { + if (this.checked) { + return eToggleAriaState.ON; + } + return eToggleAriaState.OFF; + } + + /** + * Emit new values whenever the toggle object has change. + */ + private emitChangeEvent() { + this.onModelChange(this.checked); + this.change.emit(this.checked); + // this.syncChecked(this.checked); // TODO JFG ??? See function + } + + // /** + // * Set the checked property on the button html element + // */ + // private syncChecked(value: boolean) { // TODO JFG ??? checked does not exist on button element + // if (this.buttonElement) { + // this.buttonElement.nativeElement.checked = value; + // } + // } + + // /** + // * Set the indeterminate property on the button html element + // */ + // private syncIndeterminate(value: boolean) { // TODO JFG ??? indeterminate does not exist on button element + // if (this.buttonElement) { + // this.buttonElement.nativeElement.indeterminate = value; + // } + // } + + // // // /** + // // // * Set the aria-describedby property to bao-toggle-description + // // // */ + // // // private setAriaDescribedByToDescription() { + // // // const childNodes = Array.from(this.nativeElement.childNodes); + // // // const labelNode = childNodes.find(x => { + // // // return x.nodeName === 'LABEL'; + // // // }); + // // // if (labelNode) { + // // // const labelChildNodes = Array.from(labelNode.childNodes); + // // // const descriptionNode = labelChildNodes.find(x => { + // // // return x.nodeName === 'BAO-TOGGLE-DESCRIPTION'; + // // // }); + + // // // if (descriptionNode) { + // // // this.ariaDescribedby = `${this.id}-ariadescribedby`; + // // // (descriptionNode as HTMLElement).setAttribute( + // // // 'id', + // // // this.ariaDescribedby + // // // ); + // // // } else { + // // // this.ariaDescribedby = undefined; + // // // } + + // // // this.cdr.detectChanges(); + // // // } + // // // } + + /** + * Set checked value + */ + private toggle() { + this.checked = !this.checked; + } + + private onModelChange: (value: any) => void = () => undefined; + private onTouch: () => void = () => undefined; +} + +// // // @Directive({ +// // // selector: +// // // 'bao-toggle-description, [bao-toggle-description], [baoToggleDescription]', +// // // host: { class: 'bao-toggle-description' } +// // // }) +// // // export class BaoToggleDescription {} diff --git a/projects/angular-ui/src/public-api.ts b/projects/angular-ui/src/public-api.ts index 5042080e..f28363c8 100644 --- a/projects/angular-ui/src/public-api.ts +++ b/projects/angular-ui/src/public-api.ts @@ -24,3 +24,4 @@ export * from './lib/modal'; export * from './lib/hyperlink'; export * from './lib/dropdown-menu'; export * from './lib/file'; +export * from './lib/toggle/index'; diff --git a/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts b/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts index 64fe3e78..d5ab605b 100644 --- a/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts +++ b/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts @@ -156,7 +156,7 @@ export const CheckboxDescriptionHiddenLabel: Story = args => ({ props: args, template: ` - Liste d'options avec l'étiquette invisble + Liste d'options avec l'étiquette invisible Label Est est et dolores dolore sed justo ipsum et sit. diff --git a/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts new file mode 100644 index 00000000..4b56762c --- /dev/null +++ b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts @@ -0,0 +1,191 @@ +/* + * 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 { moduleMetadata } from '@storybook/angular'; +import { Meta, Story } from '@storybook/angular/types-6-0'; +import { + BaoToggleComponent, + BaoToggleModule, + BaoCommonComponentsModule +} from 'angular-ui'; +// import { BaoToggleExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/form/toggle-example.component'; +// import { BaoToggleReactiveFormExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/reactiveForm/toggle-example.component'; +// import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +const description = ` +## Documentation +The full documentation of this component is available in the Hochelaga design system documentation under "[Interrupteur](https://zeroheight.com/575tugn0n/p/63ca9f-interrupteur)". +`; + +export default { + title: 'Components/Toggle', + decorators: [ + moduleMetadata({ + declarations: [], + imports: [BaoToggleModule, BaoCommonComponentsModule] + }) + ], + parameters: { + docs: { + description: { + component: description + } + } + }, + component: BaoToggleComponent, + argTypes: {} +} as Meta; + +const Template: Story = ( + args: BaoToggleComponent +) => ({ + component: BaoToggleComponent, + template: ` + + {{label}} + + `, + props: args +}); + +export const Primary = Template.bind({}); + +Primary.args = { + label: 'Label' +}; + +// // // export const ToggleSimple: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // Liste d'options +// // // +// // // Label +// // // +// // // +// // // Label (checked) +// // // +// // // +// // // Label (indeterminate) +// // // +// // // +// // // Label (disabled) +// // // +// // // +// // // ` +// // // }); +// // // ToggleSimple.storyName = 'Toggle - Simple'; +// // // ToggleSimple.args = { +// // // ...Primary.args +// // // }; + +// export const InlineToggleWithGuidingText: Story = args => ({ +// props: args, +// template: ` +// Exemple d'interrupteur +// +// Label +// +// +// Label (on) +// +// +// Label (hiddenLabel) +// +// +// Label (disabled) +// +// ` +// }); +// InlineToggleWithGuidingText.storyName = "Exemple d'interrupteur"; +// InlineToggleWithGuidingText.args = { +// ...Primary.args +// }; + +// // // export const ToggleDescriptionError: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // Liste d'options avec erreur +// // // +// // // Label +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (checked) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (indeterminate) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (disabled) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // Erreur pour le groupe +// // // +// // // ` +// // // }); +// // // ToggleDescriptionError.storyName = 'Toggle - Description & error'; +// // // ToggleDescriptionError.args = { +// // // ...Primary.args +// // // }; + +// // // export const ToggleDescriptionHiddenLabel: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // Liste d'options avec l'étiquette invisble +// // // +// // // Label +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (checked) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (indeterminate) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (disabled) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // ` +// // // }); +// // // ToggleDescriptionHiddenLabel.storyName = +// // // 'Toggle - Description & hidden label'; +// // // ToggleDescriptionHiddenLabel.args = { +// // // ...Primary.args +// // // }; + +// // // export const ToggleExample: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // ` +// // // }); +// // // ToggleExample.storyName = 'Basic example'; +// // // ToggleExample.args = { +// // // ...Primary.args +// // // }; + +// // // export const ToggleReactiveExample: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // ` +// // // }); +// // // ToggleReactiveExample.storyName = 'Toggle - Reactive form example'; +// // // ToggleReactiveExample.args = { +// // // ...Primary.args +// // // }; From 146a023e0f6f74bb25e0f950d89bb5838cfb39b4 Mon Sep 17 00:00:00 2001 From: Jean-francois Gamache Date: Wed, 1 Mar 2023 16:18:52 -0500 Subject: [PATCH 2/6] fix component fix storybook --- .../src/lib/toggle/temp-fromBOA4.scss | 93 ------- .../src/lib/toggle/toggle.component.html | 18 +- .../src/lib/toggle/toggle.component.scss | 100 ++++++-- .../src/lib/toggle/toggle.component.ts | 235 +++--------------- .../src/stories/Toggle/Toggle.stories.ts | 201 ++++++--------- 5 files changed, 196 insertions(+), 451 deletions(-) delete mode 100644 projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss diff --git a/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss b/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss deleted file mode 100644 index 7a5f9209..00000000 --- a/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss +++ /dev/null @@ -1,93 +0,0 @@ -$action: #097d6c; -$custom-switch-width: 2.5rem; -$custom-control-gutter: 0.75rem; -$highlight-dark: #d0f3ee; -$neutral-stroke: #ced4da; -$neutral-primary-reversed: #ffffff; -$neutral-primary-reverse: $neutral-primary-reversed; -$neutral-underground-2: #dee2e6; -$white: $neutral-primary-reversed; -$custom-switch-indicator-bg: $highlight-dark; -$custom-switch-indicator-border-radius: 0.5rem; -$custom-switch-indicator-size: 1.25rem; - -$custom-switch-indicator-bg: $highlight-dark; -$custom-switch-indicator-bg-disabled: $neutral-stroke; -$custom-switch-indicator-handle: $white; -$custom-switch-indicator-handle-disabled: $neutral-underground-2; -$custom-switch-indicator-checked-handle: $action; -$shadow-moderate: 0px 2px 8px rgba(0, 0, 0, 0.1); - -// switches -// -// Tweak a few things for switches -.custom-switch { - padding-left: 0; - - .custom-control-label { - width: 100%; - padding-right: $custom-switch-width + $custom-control-gutter; - &::before { - // Center vertically - top: calc(50% - (#{0.75rem}/ 2)); - right: 0; - left: auto; - width: 2rem; - height: 0.75rem; - margin: 0 0.25rem; - background-color: $custom-switch-indicator-bg; - border: none; - border-radius: $custom-switch-indicator-border-radius; - } - - &::after { - // Center vertically - top: calc(50% - (#{$custom-switch-indicator-size}/ 2)); - right: ($custom-switch-width/2); - left: auto; - width: $custom-switch-indicator-size; - height: $custom-switch-indicator-size; - margin: 0; - background-color: $custom-switch-indicator-handle; - border: 1px solid $action; - border-radius: 50%; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); - @include transition( - transform 0.15s ease-in-out, - $custom-forms-transition - ); - } - } - - .custom-control-input:checked ~ .custom-control-label { - &::before { - background-color: $custom-switch-indicator-bg; - } - &::after { - background-color: $custom-switch-indicator-checked-handle; - border-color: $custom-switch-indicator-checked-handle; - } - } - - .custom-control-input:disabled { - ~ .custom-control-label::before { - cursor: not-allowed; - } - // Inactive disabled state - ~ .custom-control-label::after { - background-color: $custom-switch-indicator-handle-disabled; - border-color: $custom-switch-indicator-handle-disabled; - cursor: not-allowed; - } - // Active disabled background - &:checked ~ .custom-control-label::before { - cursor: not-allowed; - } - // Active disabled handle state - &:checked ~ .custom-control-label::after { - background-color: $custom-switch-indicator-handle-disabled; - border-color: $custom-switch-indicator-handle-disabled; - cursor: not-allowed; - } - } -} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.html b/projects/angular-ui/src/lib/toggle/toggle.component.html index b7b1f923..aa2303ac 100644 --- a/projects/angular-ui/src/lib/toggle/toggle.component.html +++ b/projects/angular-ui/src/lib/toggle/toggle.component.html @@ -1,11 +1,9 @@ -
- -
- - - + +
\ No newline at end of file +
+ \ No newline at end of file diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.scss b/projects/angular-ui/src/lib/toggle/toggle.component.scss index d8351400..92315261 100644 --- a/projects/angular-ui/src/lib/toggle/toggle.component.scss +++ b/projects/angular-ui/src/lib/toggle/toggle.component.scss @@ -1,14 +1,28 @@ -@import '../core/colors'; +@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; - .bao-toggle-label { - width: 100%; + 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; @@ -38,7 +52,7 @@ position: absolute; box-sizing: border-box; content: ""; - top: calc(50% - 0.625rem); + top: calc(50% - 0.6rem); right: 1.25rem; left: auto; width: 1.25rem; @@ -48,41 +62,75 @@ border: $neutral-stroke 1px solid; border-radius: 50%; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); - transition: transform .15s ease-in-out; + transition: transform 0.15s ease-in-out; } } - .bao-toggle-button { - display: none; + .bao-toggle-container { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + justify-content: space-between; + align-items: center; } - .bao-toggle-label-checked { - &::before { - background-color: $highlight-dark; + &.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); + } } - &::after { - background-color: $action; - border-color: $action; - transition: transform .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-label-disabled { - &::before { - background-color: $neutral-stroke; - border-color: $neutral-stroke; - cursor: not-allowed; + &.bao-toggle-switch-hidden-label { + .bao-toggle-switch { + &::before { + top: calc(50% + (#{0.65rem}/ 2)); + } + &::after { + top: calc(50% + 0.075rem); + } } - &::after { - background-color: $underground-2; - border-color: $underground-2; + } + + &.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 { + &.bao-toggle-label-hidden { + .bao-toggle-label { clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); height: 1px; @@ -91,6 +139,6 @@ overflow: hidden; padding: 0; position: absolute; - // } + } } } diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.ts b/projects/angular-ui/src/lib/toggle/toggle.component.ts index b5313802..fca9f832 100644 --- a/projects/angular-ui/src/lib/toggle/toggle.component.ts +++ b/projects/angular-ui/src/lib/toggle/toggle.component.ts @@ -3,7 +3,7 @@ * Licensed under the MIT license. * See LICENSE file in the project root for full license information. */ -import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; +import { FocusMonitor } from '@angular/cdk/a11y'; import { AfterViewInit, ChangeDetectionStrategy, @@ -11,23 +11,13 @@ import { Component, ElementRef, EventEmitter, - forwardRef, Input, OnDestroy, OnInit, Output, ViewChild, - ViewEncapsulation + ViewEncapsulation, } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -/** - * Toggle state for aria-checked property - */ -export const enum eToggleAriaState { - ON = 'on', - OFF = 'off' -} /** * Unique ID for each toggle counter @@ -38,23 +28,25 @@ let toggleNextUniqueId = 0; selector: 'bao-toggle, [bao-toggle]', templateUrl: 'toggle.component.html', styleUrls: ['./toggle.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, // TODO JFG No form control ????? - useExisting: forwardRef(() => BaoToggleComponent), - multi: true - } - ], + providers: [], encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'bao-toggle', + '[class.bao-toggle-label-hidden]': 'hiddenLabel', + '[class.bao-toggle-switch-hidden-label]': 'hiddenLabel', + '[class.bao-toggle-switch-checked]': 'checked', + '[class.bao-toggle-switch-disabled]': 'disabled', + '[class.bao-toggle-label-disabled]': 'disabled', + '[class.bao-toggle-switch-focus]': 'isFocus', + }, }) -export class BaoToggleComponent - implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy -{ +export class BaoToggleComponent implements AfterViewInit, OnInit, OnDestroy { /** * The toggle ID. It is set dynamically with an unique ID by default */ - @Input() public id: string; + @Input() + public id!: string; /** * The name property of the toggle @@ -66,10 +58,10 @@ export class BaoToggleComponent */ @Input('aria-label') public ariaLabel?: string; - // // // /** - // // // * The visible state of the label - // // // */ - // // // @Input() public hiddenLabel = false; + /** + * The tooltip to show if disabled + */ + @Input() public toolTip?: string; /** * Emitted object on change event @@ -77,22 +69,11 @@ export class BaoToggleComponent @Output() public readonly change: EventEmitter = new EventEmitter(); - // // // /** - // // // * Inderminate value of the toggle whenever - // // // */ - // // // @Output() public readonly indeterminateChange: EventEmitter = - // // // new EventEmitter(); - /** * Reference to the button html element */ @ViewChild('button', { static: false }) - private buttonElement: ElementRef; - - /** - * The aria-describedby id for web accessibilty - */ - public ariaDescribedby?: string; + private buttonElement!: ElementRef; /** * The aria-labeledby id for web accessibilty @@ -102,16 +83,19 @@ export class BaoToggleComponent /** * The ID of the button html element */ - public buttonID: string; + public buttonId!: string; + + /** + * The focus status of the button html element + */ + public isFocus: boolean = false; private _disabled = false; private _checked = false; private _hiddenLabel = false; private _uniqueId = `bao-toggle-${++toggleNextUniqueId}`; - // // // private _required: boolean; constructor( - private elementRef: ElementRef, private cdr: ChangeDetectorRef, private focusMonitor: FocusMonitor ) { @@ -125,7 +109,6 @@ export class BaoToggleComponent */ @Input() get checked(): boolean { - console.log('checked GET - this._checked ===', this._checked); return this._checked; } @@ -134,7 +117,6 @@ export class BaoToggleComponent */ @Input() get disabled() { - console.log('disabled GET - this._disabled ===', this._disabled); return this._disabled; } @@ -143,118 +125,49 @@ export class BaoToggleComponent */ @Input() get hiddenLabel() { - console.log('hiddenLabel GET - this._hiddenLabel ===', this._hiddenLabel); return this._hiddenLabel; } - // // // /** - // // // * Whether the toggle is required. Default value : false - // // // */ - // // // @Input() - // // // get required(): boolean { - // // // return this._required; - // // // } - - get nativeElement(): HTMLElement { - return this.elementRef.nativeElement; - } - set checked(value: boolean) { - console.log('checked SET - value ===', value); if (value !== this.checked) { - console.log('checked SET change - value ===', value); this._checked = value; this.cdr.markForCheck(); } } set disabled(value: boolean) { - console.log('disabled SET - value ===', value); if (value !== this.disabled) { - console.log('disabled SET change - value ===', value); this._disabled = value; this.cdr.markForCheck(); } } set hiddenLabel(value: boolean) { - console.log('hiddenLabel SET - value ===', value); if (value !== this.hiddenLabel) { - console.log('hiddenLabel SET change - value ===', value); this._hiddenLabel = value; this.cdr.markForCheck(); } } - // // // set required(value: boolean) { - // // // this._required = value; - // // // } - public ngOnInit() { // Set all unique ids for the html elements - this.buttonID = `${this.id}-button`; + this.buttonId = `${this.id}-button`; this.ariaLabelledby = `${this.id}-arialabelledby`; } public ngAfterViewInit() { - this.focusMonitor.monitor(this.elementRef, true).subscribe(focusOrigin => { - if (!focusOrigin) { - // When a focused element becomes disabled, the browser *immediately* fires a blur event. - // Angular does not expect events to be raised during change detection, so any state change - // (such as a form control's 'ng-touched') will cause a changed-after-checked error. - // See https://github.com/angular/angular/issues/17793. To work around this, we defer - // telling the form control it has been touched until the next tick. - Promise.resolve() - .then(() => { - this.onTouch(); - this.cdr.markForCheck(); - }) - .catch(() => undefined); - } - }); - - // // // this.setAriaDescribedByToDescription(); - // this.syncIndeterminate(this.indeterminate); // TODO JFG ??? See function + this.focusMonitor + .monitor(this.buttonElement, false) + .subscribe((focusOrigin) => { + if (!this.disabled) { + this.isFocus = !this.isFocus; + this.cdr.markForCheck(); + } + }); } public ngOnDestroy() { - this.focusMonitor.stopMonitoring(this.elementRef); - } - - /** - * Implement ControlValueAccessor - */ - public writeValue(value: any) { // TODO JFG No form control ????? - this.checked = !!value; - } - - /** - * Implement ControlValueAccessor - */ - public registerOnChange(fn: (value: any) => void) { // TODO JFG No form control ????? - this.onModelChange = fn; - } - - /** - * Implement ControlValueAccessor - */ - public registerOnTouched(fn: any) { // TODO JFG No form control ????? - this.onTouch = fn; - } - - /** - * Implement ControlValueAccessor - */ - public setDisabledState(isDisabled: boolean) { // TODO JFG No form control ????? - this.disabled = isDisabled; - } - - public focus(origin?: FocusOrigin, options?: FocusOptions): void { - if (origin) { - this.focusMonitor.focusVia(this.buttonElement, origin, options); - } else { - this.buttonElement.nativeElement.focus(options); - } + this.focusMonitor.stopMonitoring(this.buttonElement); } /** @@ -276,85 +189,19 @@ export class BaoToggleComponent this.emitChangeEvent(); } - /** - * Get the value for the aria-checked property (web accessibility) - */ - public getAriaState(): eToggleAriaState { - if (this.checked) { - return eToggleAriaState.ON; - } - return eToggleAriaState.OFF; - } - /** * Emit new values whenever the toggle object has change. */ private emitChangeEvent() { - this.onModelChange(this.checked); - this.change.emit(this.checked); - // this.syncChecked(this.checked); // TODO JFG ??? See function + if (!this.disabled) { + this.change.emit(this.checked); + } } - // /** - // * Set the checked property on the button html element - // */ - // private syncChecked(value: boolean) { // TODO JFG ??? checked does not exist on button element - // if (this.buttonElement) { - // this.buttonElement.nativeElement.checked = value; - // } - // } - - // /** - // * Set the indeterminate property on the button html element - // */ - // private syncIndeterminate(value: boolean) { // TODO JFG ??? indeterminate does not exist on button element - // if (this.buttonElement) { - // this.buttonElement.nativeElement.indeterminate = value; - // } - // } - - // // // /** - // // // * Set the aria-describedby property to bao-toggle-description - // // // */ - // // // private setAriaDescribedByToDescription() { - // // // const childNodes = Array.from(this.nativeElement.childNodes); - // // // const labelNode = childNodes.find(x => { - // // // return x.nodeName === 'LABEL'; - // // // }); - // // // if (labelNode) { - // // // const labelChildNodes = Array.from(labelNode.childNodes); - // // // const descriptionNode = labelChildNodes.find(x => { - // // // return x.nodeName === 'BAO-TOGGLE-DESCRIPTION'; - // // // }); - - // // // if (descriptionNode) { - // // // this.ariaDescribedby = `${this.id}-ariadescribedby`; - // // // (descriptionNode as HTMLElement).setAttribute( - // // // 'id', - // // // this.ariaDescribedby - // // // ); - // // // } else { - // // // this.ariaDescribedby = undefined; - // // // } - - // // // this.cdr.detectChanges(); - // // // } - // // // } - /** * Set checked value */ private toggle() { - this.checked = !this.checked; + if (!this.disabled) this.checked = !this.checked; } - - private onModelChange: (value: any) => void = () => undefined; - private onTouch: () => void = () => undefined; } - -// // // @Directive({ -// // // selector: -// // // 'bao-toggle-description, [bao-toggle-description], [baoToggleDescription]', -// // // host: { class: 'bao-toggle-description' } -// // // }) -// // // export class BaoToggleDescription {} diff --git a/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts index 4b56762c..8e0f4185 100644 --- a/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts +++ b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts @@ -10,9 +10,6 @@ import { BaoToggleModule, BaoCommonComponentsModule } from 'angular-ui'; -// import { BaoToggleExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/form/toggle-example.component'; -// import { BaoToggleReactiveFormExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/reactiveForm/toggle-example.component'; -// import { FormsModule, ReactiveFormsModule } from '@angular/forms'; const description = ` ## Documentation @@ -27,6 +24,7 @@ export default { imports: [BaoToggleModule, BaoCommonComponentsModule] }) ], + component: BaoToggleComponent, parameters: { docs: { description: { @@ -34,7 +32,6 @@ export default { } } }, - component: BaoToggleComponent, argTypes: {} } as Meta; @@ -60,132 +57,80 @@ Primary.args = { label: 'Label' }; -// // // export const ToggleSimple: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // Liste d'options -// // // -// // // Label -// // // -// // // -// // // Label (checked) -// // // -// // // -// // // Label (indeterminate) -// // // -// // // -// // // Label (disabled) -// // // -// // // -// // // ` -// // // }); -// // // ToggleSimple.storyName = 'Toggle - Simple'; -// // // ToggleSimple.args = { -// // // ...Primary.args -// // // }; +export const ToggleSimple: Story = args => ({ + props: args, + template: ` + + Interrupteur supplémentaire non fontionnel en position 1 + + + Interrupteur OFF + + + Interrupteur ON + + + Interrupteur OFF désactivé + + + Interrupteur ON désactivé + + ` +}); -// export const InlineToggleWithGuidingText: Story = args => ({ -// props: args, -// template: ` -// Exemple d'interrupteur -// -// Label -// -// -// Label (on) -// -// -// Label (hiddenLabel) -// -// -// Label (disabled) -// -// ` -// }); -// InlineToggleWithGuidingText.storyName = "Exemple d'interrupteur"; -// InlineToggleWithGuidingText.args = { -// ...Primary.args -// }; +ToggleSimple.storyName = 'Example with Label'; +ToggleSimple.args = { + ...Primary.args +}; -// // // export const ToggleDescriptionError: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // Liste d'options avec erreur -// // // -// // // Label -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (checked) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (indeterminate) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (disabled) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // Erreur pour le groupe -// // // -// // // ` -// // // }); -// // // ToggleDescriptionError.storyName = 'Toggle - Description & error'; -// // // ToggleDescriptionError.args = { -// // // ...Primary.args -// // // }; +export const ToggleLongLabel: Story = args => ({ + props: args, + template: ` + + Interrupteur supplémentaire non fontionnel en position 1 + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + ` +}); -// // // export const ToggleDescriptionHiddenLabel: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // Liste d'options avec l'étiquette invisble -// // // -// // // Label -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (checked) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (indeterminate) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (disabled) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // ` -// // // }); -// // // ToggleDescriptionHiddenLabel.storyName = -// // // 'Toggle - Description & hidden label'; -// // // ToggleDescriptionHiddenLabel.args = { -// // // ...Primary.args -// // // }; +ToggleLongLabel.storyName = 'Example with long Label'; +ToggleLongLabel.args = { + ...Primary.args +}; -// // // export const ToggleExample: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // ` -// // // }); -// // // ToggleExample.storyName = 'Basic example'; -// // // ToggleExample.args = { -// // // ...Primary.args -// // // }; +export const ToggleHidden: Story = args => ({ + props: args, + template: ` + + Interrupteur supplémentaire avec un libellé invisible non fontionnel en position 1 + + + Interrupteur OFF avec un libellé invisible + + + Interrupteur ON avec un libellé invisible + + + Interrupteur OFF désactivé avec un libellé invisible + + + Interrupteur ON désactivé avec un libellé invisible + + ` +}); -// // // export const ToggleReactiveExample: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // ` -// // // }); -// // // ToggleReactiveExample.storyName = 'Toggle - Reactive form example'; -// // // ToggleReactiveExample.args = { -// // // ...Primary.args -// // // }; +ToggleHidden.storyName = 'Example with hidden Label'; +ToggleHidden.args = { + ...Primary.args +}; From ab8430b7a7e081d2c9f04d41f5b4d7f251fc4c56 Mon Sep 17 00:00:00 2001 From: Jean-francois Gamache Date: Mon, 6 Mar 2023 10:16:29 -0500 Subject: [PATCH 3/6] Add unit tests --- .../toggle/tests/toggle.hostcomponent.spec.ts | 22 +++ .../src/lib/toggle/toggle.component.spec.ts | 139 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.spec.ts diff --git a/projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts b/projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts new file mode 100644 index 00000000..d5a9300a --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts @@ -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: ` + + Label + + ` +}) +export class TestToggleHostComponent { + checked: boolean; + disabled: boolean; + hiddenLabel: boolean; + isFocus: boolean; + ariaLabel: string; +} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.spec.ts b/projects/angular-ui/src/lib/toggle/toggle.component.spec.ts new file mode 100644 index 00000000..2d9a3859 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.spec.ts @@ -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; + 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'); + }); + }); +}); From 3a28d7beda85317f5c509211cf9167d156b3ebe2 Mon Sep 17 00:00:00 2001 From: Jean-francois Gamache Date: Mon, 20 Feb 2023 16:28:33 -0500 Subject: [PATCH 4/6] creation Signed-off-by: Jean-Francois Gamache --- projects/angular-ui/src/lib/bao.module.ts | 4 +- projects/angular-ui/src/lib/toggle/index.ts | 7 + projects/angular-ui/src/lib/toggle/module.ts | 19 + .../src/lib/toggle/temp-fromBOA4.scss | 93 +++++ .../src/lib/toggle/toggle.component.html | 11 + .../src/lib/toggle/toggle.component.scss | 96 +++++ .../src/lib/toggle/toggle.component.ts | 360 ++++++++++++++++++ projects/angular-ui/src/public-api.ts | 1 + .../src/stories/Checkbox/Checkbox.stories.ts | 2 +- .../src/stories/Toggle/Toggle.stories.ts | 191 ++++++++++ 10 files changed, 782 insertions(+), 2 deletions(-) create mode 100644 projects/angular-ui/src/lib/toggle/index.ts create mode 100644 projects/angular-ui/src/lib/toggle/module.ts create mode 100644 projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.html create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.scss create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.ts create mode 100644 projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts diff --git a/projects/angular-ui/src/lib/bao.module.ts b/projects/angular-ui/src/lib/bao.module.ts index b9595871..ff366c48 100644 --- a/projects/angular-ui/src/lib/bao.module.ts +++ b/projects/angular-ui/src/lib/bao.module.ts @@ -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'; @NgModule({ imports: [ @@ -50,7 +51,8 @@ import { BaoFileModule } from './file/module'; BaoModalModule, BaoHyperlinkModule, BaoDropdownMenuModule, - BaoFileModule + BaoFileModule, + BaoToggleModule, // TODO: reactivate once component does not depend on global css BaoBadgeModule, // TODO: reactivate once component does not depend on global css BaoSnackBarModule, ] diff --git a/projects/angular-ui/src/lib/toggle/index.ts b/projects/angular-ui/src/lib/toggle/index.ts new file mode 100644 index 00000000..eb08c0f4 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/index.ts @@ -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'; diff --git a/projects/angular-ui/src/lib/toggle/module.ts b/projects/angular-ui/src/lib/toggle/module.ts new file mode 100644 index 00000000..6478474e --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/module.ts @@ -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 {} diff --git a/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss b/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss new file mode 100644 index 00000000..7a5f9209 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss @@ -0,0 +1,93 @@ +$action: #097d6c; +$custom-switch-width: 2.5rem; +$custom-control-gutter: 0.75rem; +$highlight-dark: #d0f3ee; +$neutral-stroke: #ced4da; +$neutral-primary-reversed: #ffffff; +$neutral-primary-reverse: $neutral-primary-reversed; +$neutral-underground-2: #dee2e6; +$white: $neutral-primary-reversed; +$custom-switch-indicator-bg: $highlight-dark; +$custom-switch-indicator-border-radius: 0.5rem; +$custom-switch-indicator-size: 1.25rem; + +$custom-switch-indicator-bg: $highlight-dark; +$custom-switch-indicator-bg-disabled: $neutral-stroke; +$custom-switch-indicator-handle: $white; +$custom-switch-indicator-handle-disabled: $neutral-underground-2; +$custom-switch-indicator-checked-handle: $action; +$shadow-moderate: 0px 2px 8px rgba(0, 0, 0, 0.1); + +// switches +// +// Tweak a few things for switches +.custom-switch { + padding-left: 0; + + .custom-control-label { + width: 100%; + padding-right: $custom-switch-width + $custom-control-gutter; + &::before { + // Center vertically + top: calc(50% - (#{0.75rem}/ 2)); + right: 0; + left: auto; + width: 2rem; + height: 0.75rem; + margin: 0 0.25rem; + background-color: $custom-switch-indicator-bg; + border: none; + border-radius: $custom-switch-indicator-border-radius; + } + + &::after { + // Center vertically + top: calc(50% - (#{$custom-switch-indicator-size}/ 2)); + right: ($custom-switch-width/2); + left: auto; + width: $custom-switch-indicator-size; + height: $custom-switch-indicator-size; + margin: 0; + background-color: $custom-switch-indicator-handle; + border: 1px solid $action; + border-radius: 50%; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + @include transition( + transform 0.15s ease-in-out, + $custom-forms-transition + ); + } + } + + .custom-control-input:checked ~ .custom-control-label { + &::before { + background-color: $custom-switch-indicator-bg; + } + &::after { + background-color: $custom-switch-indicator-checked-handle; + border-color: $custom-switch-indicator-checked-handle; + } + } + + .custom-control-input:disabled { + ~ .custom-control-label::before { + cursor: not-allowed; + } + // Inactive disabled state + ~ .custom-control-label::after { + background-color: $custom-switch-indicator-handle-disabled; + border-color: $custom-switch-indicator-handle-disabled; + cursor: not-allowed; + } + // Active disabled background + &:checked ~ .custom-control-label::before { + cursor: not-allowed; + } + // Active disabled handle state + &:checked ~ .custom-control-label::after { + background-color: $custom-switch-indicator-handle-disabled; + border-color: $custom-switch-indicator-handle-disabled; + cursor: not-allowed; + } + } +} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.html b/projects/angular-ui/src/lib/toggle/toggle.component.html new file mode 100644 index 00000000..b7b1f923 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.html @@ -0,0 +1,11 @@ +
+ +
+ + + +
+
\ No newline at end of file diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.scss b/projects/angular-ui/src/lib/toggle/toggle.component.scss new file mode 100644 index 00000000..d8351400 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.scss @@ -0,0 +1,96 @@ +@import '../core/colors'; + +.bao-toggle { + padding-left: 0; + display: block; + font-size: 1rem; + position: relative; + z-index: 1; + + .bao-toggle-label { + width: 100%; + 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.625rem); + 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 .15s ease-in-out; + } + } + + .bao-toggle-button { + display: none; + } + + .bao-toggle-label-checked { + &::before { + background-color: $highlight-dark; + } + &::after { + background-color: $action; + border-color: $action; + transition: transform .15s ease-in-out; + transform: translateX(1.2rem); + } + } + + .bao-toggle-label-disabled { + &::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-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; + // } + } +} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.ts b/projects/angular-ui/src/lib/toggle/toggle.component.ts new file mode 100644 index 00000000..b5313802 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.ts @@ -0,0 +1,360 @@ +/* + * 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 { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +/** + * Toggle state for aria-checked property + */ +export const enum eToggleAriaState { + ON = 'on', + OFF = 'off' +} + +/** + * Unique ID for each toggle counter + */ +let toggleNextUniqueId = 0; + +@Component({ + selector: 'bao-toggle, [bao-toggle]', + templateUrl: 'toggle.component.html', + styleUrls: ['./toggle.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, // TODO JFG No form control ????? + useExisting: forwardRef(() => BaoToggleComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BaoToggleComponent + implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy +{ + /** + * The toggle ID. It is set dynamically with an unique ID by default + */ + @Input() public id: string; + + /** + * The name property of the toggle + */ + @Input() public name?: string; + + /** + * The aria-label for web accessibility + */ + @Input('aria-label') public ariaLabel?: string; + + // // // /** + // // // * The visible state of the label + // // // */ + // // // @Input() public hiddenLabel = false; + + /** + * Emitted object on change event + */ + @Output() public readonly change: EventEmitter = + new EventEmitter(); + + // // // /** + // // // * Inderminate value of the toggle whenever + // // // */ + // // // @Output() public readonly indeterminateChange: EventEmitter = + // // // new EventEmitter(); + + /** + * Reference to the button html element + */ + @ViewChild('button', { static: false }) + private buttonElement: ElementRef; + + /** + * The aria-describedby id for web accessibilty + */ + public ariaDescribedby?: string; + + /** + * The aria-labeledby id for web accessibilty + */ + public ariaLabelledby?: string; + + /** + * The ID of the button html element + */ + public buttonID: string; + + private _disabled = false; + private _checked = false; + private _hiddenLabel = false; + private _uniqueId = `bao-toggle-${++toggleNextUniqueId}`; + // // // private _required: boolean; + + constructor( + private elementRef: ElementRef, + private cdr: ChangeDetectorRef, + private focusMonitor: FocusMonitor + ) { + if (!this.id) { + this.id = this._uniqueId; + } + } + + /** + * Whether the toggle is checked. Default value : false + */ + @Input() + get checked(): boolean { + console.log('checked GET - this._checked ===', this._checked); + return this._checked; + } + + /** + * Whether the toggle is disabled. Default value : false + */ + @Input() + get disabled() { + console.log('disabled GET - this._disabled ===', this._disabled); + return this._disabled; + } + + /** + * Whether the toggle label is visible. Default value : false + */ + @Input() + get hiddenLabel() { + console.log('hiddenLabel GET - this._hiddenLabel ===', this._hiddenLabel); + return this._hiddenLabel; + } + + // // // /** + // // // * Whether the toggle is required. Default value : false + // // // */ + // // // @Input() + // // // get required(): boolean { + // // // return this._required; + // // // } + + get nativeElement(): HTMLElement { + return this.elementRef.nativeElement; + } + + set checked(value: boolean) { + console.log('checked SET - value ===', value); + if (value !== this.checked) { + console.log('checked SET change - value ===', value); + this._checked = value; + this.cdr.markForCheck(); + } + } + + set disabled(value: boolean) { + console.log('disabled SET - value ===', value); + if (value !== this.disabled) { + console.log('disabled SET change - value ===', value); + this._disabled = value; + this.cdr.markForCheck(); + } + } + + set hiddenLabel(value: boolean) { + console.log('hiddenLabel SET - value ===', value); + if (value !== this.hiddenLabel) { + console.log('hiddenLabel SET change - value ===', value); + this._hiddenLabel = value; + this.cdr.markForCheck(); + } + } + + // // // set required(value: boolean) { + // // // this._required = value; + // // // } + + public ngOnInit() { + // Set all unique ids for the html elements + this.buttonID = `${this.id}-button`; + this.ariaLabelledby = `${this.id}-arialabelledby`; + } + + public ngAfterViewInit() { + this.focusMonitor.monitor(this.elementRef, true).subscribe(focusOrigin => { + if (!focusOrigin) { + // When a focused element becomes disabled, the browser *immediately* fires a blur event. + // Angular does not expect events to be raised during change detection, so any state change + // (such as a form control's 'ng-touched') will cause a changed-after-checked error. + // See https://github.com/angular/angular/issues/17793. To work around this, we defer + // telling the form control it has been touched until the next tick. + Promise.resolve() + .then(() => { + this.onTouch(); + this.cdr.markForCheck(); + }) + .catch(() => undefined); + } + }); + + // // // this.setAriaDescribedByToDescription(); + // this.syncIndeterminate(this.indeterminate); // TODO JFG ??? See function + } + + public ngOnDestroy() { + this.focusMonitor.stopMonitoring(this.elementRef); + } + + /** + * Implement ControlValueAccessor + */ + public writeValue(value: any) { // TODO JFG No form control ????? + this.checked = !!value; + } + + /** + * Implement ControlValueAccessor + */ + public registerOnChange(fn: (value: any) => void) { // TODO JFG No form control ????? + this.onModelChange = fn; + } + + /** + * Implement ControlValueAccessor + */ + public registerOnTouched(fn: any) { // TODO JFG No form control ????? + this.onTouch = fn; + } + + /** + * Implement ControlValueAccessor + */ + public setDisabledState(isDisabled: boolean) { // TODO JFG No form control ????? + this.disabled = isDisabled; + } + + public focus(origin?: FocusOrigin, options?: FocusOptions): void { + if (origin) { + this.focusMonitor.focusVia(this.buttonElement, origin, options); + } else { + this.buttonElement.nativeElement.focus(options); + } + } + + /** + * Whenever there is change on the button html element + */ + public onInteractionEvent(event: Event) { + // We always have to stop propagation on the change event. + // Otherwise the change event, from the button element, will bubble up and + // emit its event object to the `change` output. + event.stopPropagation(); + } + + /** + * Whenever there is click event triggered on the toggle + */ + public onButtonClick(event: Event) { + event.stopPropagation(); + this.toggle(); + this.emitChangeEvent(); + } + + /** + * Get the value for the aria-checked property (web accessibility) + */ + public getAriaState(): eToggleAriaState { + if (this.checked) { + return eToggleAriaState.ON; + } + return eToggleAriaState.OFF; + } + + /** + * Emit new values whenever the toggle object has change. + */ + private emitChangeEvent() { + this.onModelChange(this.checked); + this.change.emit(this.checked); + // this.syncChecked(this.checked); // TODO JFG ??? See function + } + + // /** + // * Set the checked property on the button html element + // */ + // private syncChecked(value: boolean) { // TODO JFG ??? checked does not exist on button element + // if (this.buttonElement) { + // this.buttonElement.nativeElement.checked = value; + // } + // } + + // /** + // * Set the indeterminate property on the button html element + // */ + // private syncIndeterminate(value: boolean) { // TODO JFG ??? indeterminate does not exist on button element + // if (this.buttonElement) { + // this.buttonElement.nativeElement.indeterminate = value; + // } + // } + + // // // /** + // // // * Set the aria-describedby property to bao-toggle-description + // // // */ + // // // private setAriaDescribedByToDescription() { + // // // const childNodes = Array.from(this.nativeElement.childNodes); + // // // const labelNode = childNodes.find(x => { + // // // return x.nodeName === 'LABEL'; + // // // }); + // // // if (labelNode) { + // // // const labelChildNodes = Array.from(labelNode.childNodes); + // // // const descriptionNode = labelChildNodes.find(x => { + // // // return x.nodeName === 'BAO-TOGGLE-DESCRIPTION'; + // // // }); + + // // // if (descriptionNode) { + // // // this.ariaDescribedby = `${this.id}-ariadescribedby`; + // // // (descriptionNode as HTMLElement).setAttribute( + // // // 'id', + // // // this.ariaDescribedby + // // // ); + // // // } else { + // // // this.ariaDescribedby = undefined; + // // // } + + // // // this.cdr.detectChanges(); + // // // } + // // // } + + /** + * Set checked value + */ + private toggle() { + this.checked = !this.checked; + } + + private onModelChange: (value: any) => void = () => undefined; + private onTouch: () => void = () => undefined; +} + +// // // @Directive({ +// // // selector: +// // // 'bao-toggle-description, [bao-toggle-description], [baoToggleDescription]', +// // // host: { class: 'bao-toggle-description' } +// // // }) +// // // export class BaoToggleDescription {} diff --git a/projects/angular-ui/src/public-api.ts b/projects/angular-ui/src/public-api.ts index 5042080e..f28363c8 100644 --- a/projects/angular-ui/src/public-api.ts +++ b/projects/angular-ui/src/public-api.ts @@ -24,3 +24,4 @@ export * from './lib/modal'; export * from './lib/hyperlink'; export * from './lib/dropdown-menu'; export * from './lib/file'; +export * from './lib/toggle/index'; diff --git a/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts b/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts index 64fe3e78..d5ab605b 100644 --- a/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts +++ b/projects/storybook-angular/src/stories/Checkbox/Checkbox.stories.ts @@ -156,7 +156,7 @@ export const CheckboxDescriptionHiddenLabel: Story = args => ({ props: args, template: ` - Liste d'options avec l'étiquette invisble + Liste d'options avec l'étiquette invisible Label Est est et dolores dolore sed justo ipsum et sit. diff --git a/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts new file mode 100644 index 00000000..4b56762c --- /dev/null +++ b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts @@ -0,0 +1,191 @@ +/* + * 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 { moduleMetadata } from '@storybook/angular'; +import { Meta, Story } from '@storybook/angular/types-6-0'; +import { + BaoToggleComponent, + BaoToggleModule, + BaoCommonComponentsModule +} from 'angular-ui'; +// import { BaoToggleExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/form/toggle-example.component'; +// import { BaoToggleReactiveFormExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/reactiveForm/toggle-example.component'; +// import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +const description = ` +## Documentation +The full documentation of this component is available in the Hochelaga design system documentation under "[Interrupteur](https://zeroheight.com/575tugn0n/p/63ca9f-interrupteur)". +`; + +export default { + title: 'Components/Toggle', + decorators: [ + moduleMetadata({ + declarations: [], + imports: [BaoToggleModule, BaoCommonComponentsModule] + }) + ], + parameters: { + docs: { + description: { + component: description + } + } + }, + component: BaoToggleComponent, + argTypes: {} +} as Meta; + +const Template: Story = ( + args: BaoToggleComponent +) => ({ + component: BaoToggleComponent, + template: ` + + {{label}} + + `, + props: args +}); + +export const Primary = Template.bind({}); + +Primary.args = { + label: 'Label' +}; + +// // // export const ToggleSimple: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // Liste d'options +// // // +// // // Label +// // // +// // // +// // // Label (checked) +// // // +// // // +// // // Label (indeterminate) +// // // +// // // +// // // Label (disabled) +// // // +// // // +// // // ` +// // // }); +// // // ToggleSimple.storyName = 'Toggle - Simple'; +// // // ToggleSimple.args = { +// // // ...Primary.args +// // // }; + +// export const InlineToggleWithGuidingText: Story = args => ({ +// props: args, +// template: ` +// Exemple d'interrupteur +// +// Label +// +// +// Label (on) +// +// +// Label (hiddenLabel) +// +// +// Label (disabled) +// +// ` +// }); +// InlineToggleWithGuidingText.storyName = "Exemple d'interrupteur"; +// InlineToggleWithGuidingText.args = { +// ...Primary.args +// }; + +// // // export const ToggleDescriptionError: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // Liste d'options avec erreur +// // // +// // // Label +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (checked) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (indeterminate) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (disabled) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // Erreur pour le groupe +// // // +// // // ` +// // // }); +// // // ToggleDescriptionError.storyName = 'Toggle - Description & error'; +// // // ToggleDescriptionError.args = { +// // // ...Primary.args +// // // }; + +// // // export const ToggleDescriptionHiddenLabel: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // Liste d'options avec l'étiquette invisble +// // // +// // // Label +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (checked) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (indeterminate) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // Label (disabled) +// // // Est est et dolores dolore sed justo ipsum et sit. +// // // +// // // +// // // ` +// // // }); +// // // ToggleDescriptionHiddenLabel.storyName = +// // // 'Toggle - Description & hidden label'; +// // // ToggleDescriptionHiddenLabel.args = { +// // // ...Primary.args +// // // }; + +// // // export const ToggleExample: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // ` +// // // }); +// // // ToggleExample.storyName = 'Basic example'; +// // // ToggleExample.args = { +// // // ...Primary.args +// // // }; + +// // // export const ToggleReactiveExample: Story = args => ({ +// // // props: args, +// // // template: ` +// // // +// // // ` +// // // }); +// // // ToggleReactiveExample.storyName = 'Toggle - Reactive form example'; +// // // ToggleReactiveExample.args = { +// // // ...Primary.args +// // // }; From 485d87e0cfd8b024a33073c20d93e62a96a3e3b6 Mon Sep 17 00:00:00 2001 From: Jean-francois Gamache Date: Wed, 1 Mar 2023 16:18:52 -0500 Subject: [PATCH 5/6] fix component fix storybook Signed-off-by: Jean-Francois Gamache --- .../src/lib/toggle/temp-fromBOA4.scss | 93 ------- .../src/lib/toggle/toggle.component.html | 18 +- .../src/lib/toggle/toggle.component.scss | 100 ++++++-- .../src/lib/toggle/toggle.component.ts | 235 +++--------------- .../src/stories/Toggle/Toggle.stories.ts | 201 ++++++--------- 5 files changed, 196 insertions(+), 451 deletions(-) delete mode 100644 projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss diff --git a/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss b/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss deleted file mode 100644 index 7a5f9209..00000000 --- a/projects/angular-ui/src/lib/toggle/temp-fromBOA4.scss +++ /dev/null @@ -1,93 +0,0 @@ -$action: #097d6c; -$custom-switch-width: 2.5rem; -$custom-control-gutter: 0.75rem; -$highlight-dark: #d0f3ee; -$neutral-stroke: #ced4da; -$neutral-primary-reversed: #ffffff; -$neutral-primary-reverse: $neutral-primary-reversed; -$neutral-underground-2: #dee2e6; -$white: $neutral-primary-reversed; -$custom-switch-indicator-bg: $highlight-dark; -$custom-switch-indicator-border-radius: 0.5rem; -$custom-switch-indicator-size: 1.25rem; - -$custom-switch-indicator-bg: $highlight-dark; -$custom-switch-indicator-bg-disabled: $neutral-stroke; -$custom-switch-indicator-handle: $white; -$custom-switch-indicator-handle-disabled: $neutral-underground-2; -$custom-switch-indicator-checked-handle: $action; -$shadow-moderate: 0px 2px 8px rgba(0, 0, 0, 0.1); - -// switches -// -// Tweak a few things for switches -.custom-switch { - padding-left: 0; - - .custom-control-label { - width: 100%; - padding-right: $custom-switch-width + $custom-control-gutter; - &::before { - // Center vertically - top: calc(50% - (#{0.75rem}/ 2)); - right: 0; - left: auto; - width: 2rem; - height: 0.75rem; - margin: 0 0.25rem; - background-color: $custom-switch-indicator-bg; - border: none; - border-radius: $custom-switch-indicator-border-radius; - } - - &::after { - // Center vertically - top: calc(50% - (#{$custom-switch-indicator-size}/ 2)); - right: ($custom-switch-width/2); - left: auto; - width: $custom-switch-indicator-size; - height: $custom-switch-indicator-size; - margin: 0; - background-color: $custom-switch-indicator-handle; - border: 1px solid $action; - border-radius: 50%; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); - @include transition( - transform 0.15s ease-in-out, - $custom-forms-transition - ); - } - } - - .custom-control-input:checked ~ .custom-control-label { - &::before { - background-color: $custom-switch-indicator-bg; - } - &::after { - background-color: $custom-switch-indicator-checked-handle; - border-color: $custom-switch-indicator-checked-handle; - } - } - - .custom-control-input:disabled { - ~ .custom-control-label::before { - cursor: not-allowed; - } - // Inactive disabled state - ~ .custom-control-label::after { - background-color: $custom-switch-indicator-handle-disabled; - border-color: $custom-switch-indicator-handle-disabled; - cursor: not-allowed; - } - // Active disabled background - &:checked ~ .custom-control-label::before { - cursor: not-allowed; - } - // Active disabled handle state - &:checked ~ .custom-control-label::after { - background-color: $custom-switch-indicator-handle-disabled; - border-color: $custom-switch-indicator-handle-disabled; - cursor: not-allowed; - } - } -} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.html b/projects/angular-ui/src/lib/toggle/toggle.component.html index b7b1f923..aa2303ac 100644 --- a/projects/angular-ui/src/lib/toggle/toggle.component.html +++ b/projects/angular-ui/src/lib/toggle/toggle.component.html @@ -1,11 +1,9 @@ -
- -
- - - + +
\ No newline at end of file +
+ \ No newline at end of file diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.scss b/projects/angular-ui/src/lib/toggle/toggle.component.scss index d8351400..92315261 100644 --- a/projects/angular-ui/src/lib/toggle/toggle.component.scss +++ b/projects/angular-ui/src/lib/toggle/toggle.component.scss @@ -1,14 +1,28 @@ -@import '../core/colors'; +@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; - .bao-toggle-label { - width: 100%; + 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; @@ -38,7 +52,7 @@ position: absolute; box-sizing: border-box; content: ""; - top: calc(50% - 0.625rem); + top: calc(50% - 0.6rem); right: 1.25rem; left: auto; width: 1.25rem; @@ -48,41 +62,75 @@ border: $neutral-stroke 1px solid; border-radius: 50%; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); - transition: transform .15s ease-in-out; + transition: transform 0.15s ease-in-out; } } - .bao-toggle-button { - display: none; + .bao-toggle-container { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + justify-content: space-between; + align-items: center; } - .bao-toggle-label-checked { - &::before { - background-color: $highlight-dark; + &.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); + } } - &::after { - background-color: $action; - border-color: $action; - transition: transform .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-label-disabled { - &::before { - background-color: $neutral-stroke; - border-color: $neutral-stroke; - cursor: not-allowed; + &.bao-toggle-switch-hidden-label { + .bao-toggle-switch { + &::before { + top: calc(50% + (#{0.65rem}/ 2)); + } + &::after { + top: calc(50% + 0.075rem); + } } - &::after { - background-color: $underground-2; - border-color: $underground-2; + } + + &.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 { + &.bao-toggle-label-hidden { + .bao-toggle-label { clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); height: 1px; @@ -91,6 +139,6 @@ overflow: hidden; padding: 0; position: absolute; - // } + } } } diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.ts b/projects/angular-ui/src/lib/toggle/toggle.component.ts index b5313802..fca9f832 100644 --- a/projects/angular-ui/src/lib/toggle/toggle.component.ts +++ b/projects/angular-ui/src/lib/toggle/toggle.component.ts @@ -3,7 +3,7 @@ * Licensed under the MIT license. * See LICENSE file in the project root for full license information. */ -import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; +import { FocusMonitor } from '@angular/cdk/a11y'; import { AfterViewInit, ChangeDetectionStrategy, @@ -11,23 +11,13 @@ import { Component, ElementRef, EventEmitter, - forwardRef, Input, OnDestroy, OnInit, Output, ViewChild, - ViewEncapsulation + ViewEncapsulation, } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -/** - * Toggle state for aria-checked property - */ -export const enum eToggleAriaState { - ON = 'on', - OFF = 'off' -} /** * Unique ID for each toggle counter @@ -38,23 +28,25 @@ let toggleNextUniqueId = 0; selector: 'bao-toggle, [bao-toggle]', templateUrl: 'toggle.component.html', styleUrls: ['./toggle.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, // TODO JFG No form control ????? - useExisting: forwardRef(() => BaoToggleComponent), - multi: true - } - ], + providers: [], encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'bao-toggle', + '[class.bao-toggle-label-hidden]': 'hiddenLabel', + '[class.bao-toggle-switch-hidden-label]': 'hiddenLabel', + '[class.bao-toggle-switch-checked]': 'checked', + '[class.bao-toggle-switch-disabled]': 'disabled', + '[class.bao-toggle-label-disabled]': 'disabled', + '[class.bao-toggle-switch-focus]': 'isFocus', + }, }) -export class BaoToggleComponent - implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy -{ +export class BaoToggleComponent implements AfterViewInit, OnInit, OnDestroy { /** * The toggle ID. It is set dynamically with an unique ID by default */ - @Input() public id: string; + @Input() + public id!: string; /** * The name property of the toggle @@ -66,10 +58,10 @@ export class BaoToggleComponent */ @Input('aria-label') public ariaLabel?: string; - // // // /** - // // // * The visible state of the label - // // // */ - // // // @Input() public hiddenLabel = false; + /** + * The tooltip to show if disabled + */ + @Input() public toolTip?: string; /** * Emitted object on change event @@ -77,22 +69,11 @@ export class BaoToggleComponent @Output() public readonly change: EventEmitter = new EventEmitter(); - // // // /** - // // // * Inderminate value of the toggle whenever - // // // */ - // // // @Output() public readonly indeterminateChange: EventEmitter = - // // // new EventEmitter(); - /** * Reference to the button html element */ @ViewChild('button', { static: false }) - private buttonElement: ElementRef; - - /** - * The aria-describedby id for web accessibilty - */ - public ariaDescribedby?: string; + private buttonElement!: ElementRef; /** * The aria-labeledby id for web accessibilty @@ -102,16 +83,19 @@ export class BaoToggleComponent /** * The ID of the button html element */ - public buttonID: string; + public buttonId!: string; + + /** + * The focus status of the button html element + */ + public isFocus: boolean = false; private _disabled = false; private _checked = false; private _hiddenLabel = false; private _uniqueId = `bao-toggle-${++toggleNextUniqueId}`; - // // // private _required: boolean; constructor( - private elementRef: ElementRef, private cdr: ChangeDetectorRef, private focusMonitor: FocusMonitor ) { @@ -125,7 +109,6 @@ export class BaoToggleComponent */ @Input() get checked(): boolean { - console.log('checked GET - this._checked ===', this._checked); return this._checked; } @@ -134,7 +117,6 @@ export class BaoToggleComponent */ @Input() get disabled() { - console.log('disabled GET - this._disabled ===', this._disabled); return this._disabled; } @@ -143,118 +125,49 @@ export class BaoToggleComponent */ @Input() get hiddenLabel() { - console.log('hiddenLabel GET - this._hiddenLabel ===', this._hiddenLabel); return this._hiddenLabel; } - // // // /** - // // // * Whether the toggle is required. Default value : false - // // // */ - // // // @Input() - // // // get required(): boolean { - // // // return this._required; - // // // } - - get nativeElement(): HTMLElement { - return this.elementRef.nativeElement; - } - set checked(value: boolean) { - console.log('checked SET - value ===', value); if (value !== this.checked) { - console.log('checked SET change - value ===', value); this._checked = value; this.cdr.markForCheck(); } } set disabled(value: boolean) { - console.log('disabled SET - value ===', value); if (value !== this.disabled) { - console.log('disabled SET change - value ===', value); this._disabled = value; this.cdr.markForCheck(); } } set hiddenLabel(value: boolean) { - console.log('hiddenLabel SET - value ===', value); if (value !== this.hiddenLabel) { - console.log('hiddenLabel SET change - value ===', value); this._hiddenLabel = value; this.cdr.markForCheck(); } } - // // // set required(value: boolean) { - // // // this._required = value; - // // // } - public ngOnInit() { // Set all unique ids for the html elements - this.buttonID = `${this.id}-button`; + this.buttonId = `${this.id}-button`; this.ariaLabelledby = `${this.id}-arialabelledby`; } public ngAfterViewInit() { - this.focusMonitor.monitor(this.elementRef, true).subscribe(focusOrigin => { - if (!focusOrigin) { - // When a focused element becomes disabled, the browser *immediately* fires a blur event. - // Angular does not expect events to be raised during change detection, so any state change - // (such as a form control's 'ng-touched') will cause a changed-after-checked error. - // See https://github.com/angular/angular/issues/17793. To work around this, we defer - // telling the form control it has been touched until the next tick. - Promise.resolve() - .then(() => { - this.onTouch(); - this.cdr.markForCheck(); - }) - .catch(() => undefined); - } - }); - - // // // this.setAriaDescribedByToDescription(); - // this.syncIndeterminate(this.indeterminate); // TODO JFG ??? See function + this.focusMonitor + .monitor(this.buttonElement, false) + .subscribe((focusOrigin) => { + if (!this.disabled) { + this.isFocus = !this.isFocus; + this.cdr.markForCheck(); + } + }); } public ngOnDestroy() { - this.focusMonitor.stopMonitoring(this.elementRef); - } - - /** - * Implement ControlValueAccessor - */ - public writeValue(value: any) { // TODO JFG No form control ????? - this.checked = !!value; - } - - /** - * Implement ControlValueAccessor - */ - public registerOnChange(fn: (value: any) => void) { // TODO JFG No form control ????? - this.onModelChange = fn; - } - - /** - * Implement ControlValueAccessor - */ - public registerOnTouched(fn: any) { // TODO JFG No form control ????? - this.onTouch = fn; - } - - /** - * Implement ControlValueAccessor - */ - public setDisabledState(isDisabled: boolean) { // TODO JFG No form control ????? - this.disabled = isDisabled; - } - - public focus(origin?: FocusOrigin, options?: FocusOptions): void { - if (origin) { - this.focusMonitor.focusVia(this.buttonElement, origin, options); - } else { - this.buttonElement.nativeElement.focus(options); - } + this.focusMonitor.stopMonitoring(this.buttonElement); } /** @@ -276,85 +189,19 @@ export class BaoToggleComponent this.emitChangeEvent(); } - /** - * Get the value for the aria-checked property (web accessibility) - */ - public getAriaState(): eToggleAriaState { - if (this.checked) { - return eToggleAriaState.ON; - } - return eToggleAriaState.OFF; - } - /** * Emit new values whenever the toggle object has change. */ private emitChangeEvent() { - this.onModelChange(this.checked); - this.change.emit(this.checked); - // this.syncChecked(this.checked); // TODO JFG ??? See function + if (!this.disabled) { + this.change.emit(this.checked); + } } - // /** - // * Set the checked property on the button html element - // */ - // private syncChecked(value: boolean) { // TODO JFG ??? checked does not exist on button element - // if (this.buttonElement) { - // this.buttonElement.nativeElement.checked = value; - // } - // } - - // /** - // * Set the indeterminate property on the button html element - // */ - // private syncIndeterminate(value: boolean) { // TODO JFG ??? indeterminate does not exist on button element - // if (this.buttonElement) { - // this.buttonElement.nativeElement.indeterminate = value; - // } - // } - - // // // /** - // // // * Set the aria-describedby property to bao-toggle-description - // // // */ - // // // private setAriaDescribedByToDescription() { - // // // const childNodes = Array.from(this.nativeElement.childNodes); - // // // const labelNode = childNodes.find(x => { - // // // return x.nodeName === 'LABEL'; - // // // }); - // // // if (labelNode) { - // // // const labelChildNodes = Array.from(labelNode.childNodes); - // // // const descriptionNode = labelChildNodes.find(x => { - // // // return x.nodeName === 'BAO-TOGGLE-DESCRIPTION'; - // // // }); - - // // // if (descriptionNode) { - // // // this.ariaDescribedby = `${this.id}-ariadescribedby`; - // // // (descriptionNode as HTMLElement).setAttribute( - // // // 'id', - // // // this.ariaDescribedby - // // // ); - // // // } else { - // // // this.ariaDescribedby = undefined; - // // // } - - // // // this.cdr.detectChanges(); - // // // } - // // // } - /** * Set checked value */ private toggle() { - this.checked = !this.checked; + if (!this.disabled) this.checked = !this.checked; } - - private onModelChange: (value: any) => void = () => undefined; - private onTouch: () => void = () => undefined; } - -// // // @Directive({ -// // // selector: -// // // 'bao-toggle-description, [bao-toggle-description], [baoToggleDescription]', -// // // host: { class: 'bao-toggle-description' } -// // // }) -// // // export class BaoToggleDescription {} diff --git a/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts index 4b56762c..8e0f4185 100644 --- a/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts +++ b/projects/storybook-angular/src/stories/Toggle/Toggle.stories.ts @@ -10,9 +10,6 @@ import { BaoToggleModule, BaoCommonComponentsModule } from 'angular-ui'; -// import { BaoToggleExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/form/toggle-example.component'; -// import { BaoToggleReactiveFormExampleComponent } from 'projects/storybook-angular-examples/src/app/toggle/reactiveForm/toggle-example.component'; -// import { FormsModule, ReactiveFormsModule } from '@angular/forms'; const description = ` ## Documentation @@ -27,6 +24,7 @@ export default { imports: [BaoToggleModule, BaoCommonComponentsModule] }) ], + component: BaoToggleComponent, parameters: { docs: { description: { @@ -34,7 +32,6 @@ export default { } } }, - component: BaoToggleComponent, argTypes: {} } as Meta; @@ -60,132 +57,80 @@ Primary.args = { label: 'Label' }; -// // // export const ToggleSimple: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // Liste d'options -// // // -// // // Label -// // // -// // // -// // // Label (checked) -// // // -// // // -// // // Label (indeterminate) -// // // -// // // -// // // Label (disabled) -// // // -// // // -// // // ` -// // // }); -// // // ToggleSimple.storyName = 'Toggle - Simple'; -// // // ToggleSimple.args = { -// // // ...Primary.args -// // // }; +export const ToggleSimple: Story = args => ({ + props: args, + template: ` + + Interrupteur supplémentaire non fontionnel en position 1 + + + Interrupteur OFF + + + Interrupteur ON + + + Interrupteur OFF désactivé + + + Interrupteur ON désactivé + + ` +}); -// export const InlineToggleWithGuidingText: Story = args => ({ -// props: args, -// template: ` -// Exemple d'interrupteur -// -// Label -// -// -// Label (on) -// -// -// Label (hiddenLabel) -// -// -// Label (disabled) -// -// ` -// }); -// InlineToggleWithGuidingText.storyName = "Exemple d'interrupteur"; -// InlineToggleWithGuidingText.args = { -// ...Primary.args -// }; +ToggleSimple.storyName = 'Example with Label'; +ToggleSimple.args = { + ...Primary.args +}; -// // // export const ToggleDescriptionError: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // Liste d'options avec erreur -// // // -// // // Label -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (checked) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (indeterminate) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (disabled) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // Erreur pour le groupe -// // // -// // // ` -// // // }); -// // // ToggleDescriptionError.storyName = 'Toggle - Description & error'; -// // // ToggleDescriptionError.args = { -// // // ...Primary.args -// // // }; +export const ToggleLongLabel: Story = args => ({ + props: args, + template: ` + + Interrupteur supplémentaire non fontionnel en position 1 + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + + ` +}); -// // // export const ToggleDescriptionHiddenLabel: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // Liste d'options avec l'étiquette invisble -// // // -// // // Label -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (checked) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (indeterminate) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // Label (disabled) -// // // Est est et dolores dolore sed justo ipsum et sit. -// // // -// // // -// // // ` -// // // }); -// // // ToggleDescriptionHiddenLabel.storyName = -// // // 'Toggle - Description & hidden label'; -// // // ToggleDescriptionHiddenLabel.args = { -// // // ...Primary.args -// // // }; +ToggleLongLabel.storyName = 'Example with long Label'; +ToggleLongLabel.args = { + ...Primary.args +}; -// // // export const ToggleExample: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // ` -// // // }); -// // // ToggleExample.storyName = 'Basic example'; -// // // ToggleExample.args = { -// // // ...Primary.args -// // // }; +export const ToggleHidden: Story = args => ({ + props: args, + template: ` + + Interrupteur supplémentaire avec un libellé invisible non fontionnel en position 1 + + + Interrupteur OFF avec un libellé invisible + + + Interrupteur ON avec un libellé invisible + + + Interrupteur OFF désactivé avec un libellé invisible + + + Interrupteur ON désactivé avec un libellé invisible + + ` +}); -// // // export const ToggleReactiveExample: Story = args => ({ -// // // props: args, -// // // template: ` -// // // -// // // ` -// // // }); -// // // ToggleReactiveExample.storyName = 'Toggle - Reactive form example'; -// // // ToggleReactiveExample.args = { -// // // ...Primary.args -// // // }; +ToggleHidden.storyName = 'Example with hidden Label'; +ToggleHidden.args = { + ...Primary.args +}; From 403b9e2130c0dfb052c0610300983384c2e50396 Mon Sep 17 00:00:00 2001 From: Jean-francois Gamache Date: Mon, 6 Mar 2023 10:16:29 -0500 Subject: [PATCH 6/6] Add unit tests Signed-off-by: Jean-Francois Gamache --- .../toggle/tests/toggle.hostcomponent.spec.ts | 22 +++ .../src/lib/toggle/toggle.component.spec.ts | 139 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts create mode 100644 projects/angular-ui/src/lib/toggle/toggle.component.spec.ts diff --git a/projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts b/projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts new file mode 100644 index 00000000..d5a9300a --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/tests/toggle.hostcomponent.spec.ts @@ -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: ` + + Label + + ` +}) +export class TestToggleHostComponent { + checked: boolean; + disabled: boolean; + hiddenLabel: boolean; + isFocus: boolean; + ariaLabel: string; +} diff --git a/projects/angular-ui/src/lib/toggle/toggle.component.spec.ts b/projects/angular-ui/src/lib/toggle/toggle.component.spec.ts new file mode 100644 index 00000000..2d9a3859 --- /dev/null +++ b/projects/angular-ui/src/lib/toggle/toggle.component.spec.ts @@ -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; + 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'); + }); + }); +});