From 672e2d7a4121107b9bd40dfe24fd9b0be95186b3 Mon Sep 17 00:00:00 2001 From: gucal Date: Wed, 16 Aug 2023 11:08:11 +0300 Subject: [PATCH 01/54] Fix #13413 - Knob | Accessibility --- src/app/components/knob/knob.ts | 84 ++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/src/app/components/knob/knob.ts b/src/app/components/knob/knob.ts index 3cf07b3230d..de9100c1647 100755 --- a/src/app/components/knob/knob.ts +++ b/src/app/components/knob/knob.ts @@ -1,5 +1,5 @@ -import { NgModule, Component, ChangeDetectionStrategy, ViewEncapsulation, Input, forwardRef, ChangeDetectorRef, ElementRef, Output, EventEmitter, Renderer2, Inject } from '@angular/core'; import { CommonModule, DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, NgModule, Output, Renderer2, ViewEncapsulation, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { VoidListener } from 'primeng/ts-helpers'; @@ -15,16 +15,25 @@ export const KNOB_VALUE_ACCESSOR: any = { @Component({ selector: 'p-knob', template: ` -
+
@@ -51,6 +60,21 @@ export class Knob { * @group Props */ @Input() style: { [klass: string]: any } | null | undefined; + /** + * Defines a string that labels the input for accessibility. + * @group Props + */ + @Input() ariaLabel: string | undefined; + /** + * Specifies one or more IDs in the DOM that labels the input field. + * @group Props + */ + @Input() ariaLabelledBy: string | undefined; + /** + * Index of the element in tabbing order. + * @group Props + */ + @Input() tabindex: number = 0; /** * Background of the value. * @group Props @@ -245,6 +269,62 @@ export class Knob { } } + updateModelValue(newValue) { + if (newValue > this.max) this.value = this.max; + else if (newValue < this.min) this.value = this.min; + else this.value = newValue; + + this.onModelChange(this.value); + this.onChange.emit(this.value); + } + + onKeyDown(event: KeyboardEvent) { + if (!this.disabled && !this.readonly) { + switch (event.code) { + case 'ArrowRight': + + case 'ArrowUp': { + event.preventDefault(); + this.updateModelValue(this._value + 1); + break; + } + + case 'ArrowLeft': + + case 'ArrowDown': { + event.preventDefault(); + this.updateModelValue(this._value - 1); + break; + } + + case 'Home': { + event.preventDefault(); + this.updateModelValue(this.min); + + break; + } + + case 'End': { + event.preventDefault(); + this.updateModelValue(this.max); + break; + } + + case 'PageUp': { + event.preventDefault(); + this.updateModelValue(this._value + 10); + break; + } + + case 'PageDown': { + event.preventDefault(); + this.updateModelValue(this._value - 10); + break; + } + } + } + } + writeValue(value: any): void { this.value = value; this.cd.markForCheck(); From 5452e19392bb7c41eacce4cb3fb5e48c5b1e3cde Mon Sep 17 00:00:00 2001 From: gucal Date: Sat, 26 Aug 2023 01:35:08 +0300 Subject: [PATCH 02/54] Accessibility improvement for InputNumber #13413 #13547 --- src/app/components/inputnumber/inputnumber.ts | 171 +++++++++++------- 1 file changed, 109 insertions(+), 62 deletions(-) diff --git a/src/app/components/inputnumber/inputnumber.ts b/src/app/components/inputnumber/inputnumber.ts index db80e60d0c6..0c684c6039c 100644 --- a/src/app/components/inputnumber/inputnumber.ts +++ b/src/app/components/inputnumber/inputnumber.ts @@ -7,7 +7,6 @@ import { ContentChildren, ElementRef, EventEmitter, - forwardRef, Inject, Injector, Input, @@ -19,16 +18,17 @@ import { SimpleChanges, TemplateRef, ViewChild, - ViewEncapsulation + ViewEncapsulation, + forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms'; +import { PrimeTemplate, SharedModule } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { DomHandler } from 'primeng/dom'; -import { InputTextModule } from 'primeng/inputtext'; -import { PrimeTemplate, SharedModule } from 'primeng/api'; -import { TimesIcon } from 'primeng/icons/times'; -import { AngleUpIcon } from 'primeng/icons/angleup'; import { AngleDownIcon } from 'primeng/icons/angledown'; +import { AngleUpIcon } from 'primeng/icons/angleup'; +import { TimesIcon } from 'primeng/icons/times'; +import { InputTextModule } from 'primeng/inputtext'; import { Nullable } from 'primeng/ts-helpers'; import { InputNumberInputEvent } from './inputnumber.interface'; @@ -53,29 +53,36 @@ export const INPUTNUMBER_VALUE_ACCESSOR: any = { }" [ngStyle]="style" [class]="styleClass" + [attr.data-pc-name]="'inputnumber'" + [attr.data-pc-section]="'root'" > - - + + - - + @@ -120,59 +129,65 @@ export const INPUTNUMBER_VALUE_ACCESSOR: any = { class="p-button-icon-only" [class]="decrementButtonClass" [disabled]="disabled" + tabindex="-1" + [attr.aria-hidden]="true" (mousedown)="onDownButtonMouseDown($event)" (mouseup)="onDownButtonMouseUp()" (mouseleave)="onDownButtonMouseLeave()" (keydown)="onDownButtonKeyDown($event)" (keyup)="onDownButtonKeyUp()" - tabindex="-1" + [attr.data-pc-section]="decrementbutton" > - + - + @@ -245,6 +260,11 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control * @group Props */ @Input() title: string | undefined; + /** + * Specifies one or more IDs in the DOM that labels the input field. + * @group Props + */ + @Input() ariaLabelledBy: string | undefined; /** * Used to define a string that labels the input element. * @group Props @@ -440,9 +460,9 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control value: Nullable; - onModelChange: Function = () => { }; + onModelChange: Function = () => {}; - onModelTouched: Function = () => { }; + onModelTouched: Function = () => {}; focused: Nullable; @@ -482,7 +502,7 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control private ngControl: NgControl | null = null; - constructor(@Inject(DOCUMENT) private document: Document, public el: ElementRef, private cd: ChangeDetectorRef, private readonly injector: Injector) { } + constructor(@Inject(DOCUMENT) private document: Document, public el: ElementRef, private cd: ChangeDetectorRef, private readonly injector: Injector) {} ngOnChanges(simpleChange: SimpleChanges) { const props = ['locale', 'localeMatcher', 'mode', 'currency', 'currencyDisplay', 'useGrouping', 'minFractionDigits', 'maxFractionDigits', 'prefix', 'suffix']; @@ -698,17 +718,23 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control return; } - this.input?.nativeElement.focus(); - this.repeat(event, null, 1); - event.preventDefault(); + if (!this.disabled) { + this.input?.nativeElement.focus(); + this.repeat(event, null, 1); + event.preventDefault(); + } } onUpButtonMouseUp() { - this.clearTimer(); + if (!this.disabled) { + this.clearTimer(); + } } onUpButtonMouseLeave() { - this.clearTimer(); + if (!this.disabled) { + this.clearTimer(); + } } onUpButtonKeyDown(event: KeyboardEvent) { @@ -718,7 +744,9 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control } onUpButtonKeyUp() { - this.clearTimer(); + if (!this.disabled) { + this.clearTimer(); + } } onDownButtonMouseDown(event: MouseEvent) { @@ -726,22 +754,29 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control this.clearTimer(); return; } - - this.input?.nativeElement.focus(); - this.repeat(event, null, -1); - event.preventDefault(); + if (!this.disabled) { + this.input?.nativeElement.focus(); + this.repeat(event, null, -1); + event.preventDefault(); + } } onDownButtonMouseUp() { - this.clearTimer(); + if (!this.disabled) { + this.clearTimer(); + } } onDownButtonMouseLeave() { - this.clearTimer(); + if (!this.disabled) { + this.clearTimer(); + } } onDownButtonKeyUp() { - this.clearTimer(); + if (!this.disabled) { + this.clearTimer(); + } } onDownButtonKeyDown(event: KeyboardEvent) { @@ -781,43 +816,38 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control event.preventDefault(); } - switch (event.which) { - //up - case 38: + switch (event.code) { + case 'ArrowUp': this.spin(event, 1); event.preventDefault(); break; - //down - case 40: + case 'ArrowDown': this.spin(event, -1); event.preventDefault(); break; - //left - case 37: + case 'ArrowLeft': if (!this.isNumeralChar(inputValue.charAt(selectionStart - 1))) { event.preventDefault(); } break; - //right - case 39: + case 'ArrowRight': if (!this.isNumeralChar(inputValue.charAt(selectionStart))) { event.preventDefault(); } break; - //enter - case 13: + case 'Tab': + case 'Enter': newValueStr = this.validateValue(this.parseValue(this.input.nativeElement.value)); this.input.nativeElement.value = this.formatValue(newValueStr); this.input.nativeElement.setAttribute('aria-valuenow', newValueStr); this.updateModel(event, newValueStr); break; - //backspace - case 8: { + case 'Backspace': { event.preventDefault(); if (selectionStart === selectionEnd) { @@ -858,8 +888,7 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control break; } - // del - case 46: + case 'Delete': event.preventDefault(); if (selectionStart === selectionEnd) { @@ -898,6 +927,20 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control } break; + case 'Home': + if (this.min) { + this.updateModel(event, this.min); + event.preventDefault(); + } + break; + + case 'End': + if (this.max) { + this.updateModel(event, this.max); + event.preventDefault(); + } + break; + default: break; } @@ -1128,6 +1171,7 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control onInputClick() { const currentValue = this.input?.nativeElement.value; + if (!this.readonly && currentValue !== DomHandler.getSelection()) { this.initCursor(); } @@ -1316,11 +1360,14 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control onInputBlur(event: Event) { this.focused = false; + let newValue = this.validateValue(this.parseValue(this.input.nativeElement.value)); + + this.onBlur.emit(event); + this.input.nativeElement.value = this.formatValue(newValue); this.input.nativeElement.setAttribute('aria-valuenow', newValue); this.updateModel(event, newValue); - this.onBlur.emit(event); } formattedValue() { @@ -1381,4 +1428,4 @@ export class InputNumber implements OnInit, AfterContentInit, OnChanges, Control exports: [InputNumber, SharedModule], declarations: [InputNumber] }) -export class InputNumberModule { } +export class InputNumberModule {} From 28cd9413cee1c87e667533bbe78a11dca585e911 Mon Sep 17 00:00:00 2001 From: gucal Date: Wed, 30 Aug 2023 14:26:01 +0300 Subject: [PATCH 03/54] #13413 Accessibility Input Section --- src/app/components/checkbox/checkbox.ts | 109 ++++++------- src/app/components/colorpicker/colorpicker.ts | 63 +++++--- src/app/components/inputswitch/inputswitch.ts | 76 +++++---- src/app/components/radiobutton/radiobutton.ts | 43 ++--- .../components/selectbutton/selectbutton.ts | 135 ++++++++++++---- src/app/components/slider/slider.ts | 150 +++++++++++++----- .../components/togglebutton/togglebutton.ts | 35 +++- .../tristatecheckbox/tristatecheckbox.ts | 53 ++++--- src/app/showcase/doc/checkbox/multipledoc.ts | 4 +- 9 files changed, 423 insertions(+), 245 deletions(-) diff --git a/src/app/components/checkbox/checkbox.ts b/src/app/components/checkbox/checkbox.ts index d36ad5fbf5d..3304ad1b732 100755 --- a/src/app/components/checkbox/checkbox.ts +++ b/src/app/components/checkbox/checkbox.ts @@ -1,10 +1,10 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, NgModule, Output, QueryList, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ObjectUtils } from 'primeng/utils'; import { PrimeTemplate, SharedModule } from 'primeng/api'; import { CheckIcon } from 'primeng/icons/check'; import { Nullable } from 'primeng/ts-helpers'; +import { ObjectUtils } from 'primeng/utils'; import { CheckboxChangeEvent } from './checkbox.interface'; export const CHECKBOX_VALUE_ACCESSOR: any = { @@ -19,46 +19,62 @@ export const CHECKBOX_VALUE_ACCESSOR: any = { @Component({ selector: 'p-checkbox', template: ` -
-
+
+
-
+
- - + + - +
+ {{ label }} `, providers: [CHECKBOX_VALUE_ACCESSOR], @@ -167,7 +183,7 @@ export class Checkbox implements ControlValueAccessor { */ @Output() onChange: EventEmitter = new EventEmitter(); - @ViewChild('cb') inputViewChild: Nullable; + @ViewChild('input') inputViewChild: Nullable; @ContentChildren(PrimeTemplate) templates: Nullable>; @@ -193,45 +209,28 @@ export class Checkbox implements ControlValueAccessor { }); } - onClick(event: Event, checkbox: HTMLElement, focus: boolean) { - event.preventDefault(); - - if (this.disabled || this.readonly) { - return; - } - - this.updateModel(event); + onClick(event: Event) { + if (!this.disabled && !this.readonly) { + this.inputViewChild.nativeElement.focus(); + let newModelValue; - if (focus) { - checkbox.focus(); - } - } + if (!this.binary) { + if (this.checked()) newModelValue = this.model.filter((val: object) => !ObjectUtils.equals(val, this.value)); + else newModelValue = this.model ? [...this.model, this.value] : [this.value]; - updateModel(event: Event) { - let newModelValue; + this.onModelChange(newModelValue); + this.model = newModelValue; - if (!this.binary) { - if (this.checked()) newModelValue = this.model.filter((val: object) => !ObjectUtils.equals(val, this.value)); - else newModelValue = this.model ? [...this.model, this.value] : [this.value]; - - this.onModelChange(newModelValue); - this.model = newModelValue; - - if (this.formControl) { - this.formControl.setValue(newModelValue); + if (this.formControl) { + this.formControl.setValue(newModelValue); + } + } else { + newModelValue = this.checked() ? this.falseValue : this.trueValue; + this.model = newModelValue; + this.onModelChange(newModelValue); } - } else { - newModelValue = this.checked() ? this.falseValue : this.trueValue; - this.model = newModelValue; - this.onModelChange(newModelValue); - } - this.onChange.emit({ checked: newModelValue, originalEvent: event }); - } - - handleChange(event: Event) { - if (!this.readonly) { - this.updateModel(event); + this.onChange.emit({ checked: newModelValue, originalEvent: event }); } } @@ -244,10 +243,6 @@ export class Checkbox implements ControlValueAccessor { this.onModelTouched(); } - focus() { - this.inputViewChild?.nativeElement.focus(); - } - writeValue(model: any): void { this.model = model; this.cd.markForCheck(); diff --git a/src/app/components/colorpicker/colorpicker.ts b/src/app/components/colorpicker/colorpicker.ts index b8e4d5f1584..5816868295a 100755 --- a/src/app/components/colorpicker/colorpicker.ts +++ b/src/app/components/colorpicker/colorpicker.ts @@ -1,11 +1,11 @@ -import { NgModule, Component, ElementRef, Input, Output, OnDestroy, EventEmitter, forwardRef, Renderer2, ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, ViewEncapsulation, Inject, PLATFORM_ID, TemplateRef } from '@angular/core'; -import { trigger, state, style, transition, animate, AnimationEvent } from '@angular/animations'; +import { AnimationEvent, animate, style, transition, trigger } from '@angular/animations'; import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common'; -import { DomHandler, ConnectedOverlayScrollHandler } from 'primeng/dom'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, NgModule, OnDestroy, Output, PLATFORM_ID, Renderer2, TemplateRef, ViewChild, ViewEncapsulation, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { OverlayService, PrimeNGConfig } from 'primeng/api'; -import { ZIndexUtils } from 'primeng/utils'; +import { ConnectedOverlayScrollHandler, DomHandler } from 'primeng/dom'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; +import { ZIndexUtils } from 'primeng/utils'; import { ColorPickerChangeEvent } from './colorpicker.interface'; export const COLORPICKER_VALUE_ACCESSOR: any = { @@ -20,21 +20,29 @@ export const COLORPICKER_VALUE_ACCESSOR: any = { @Component({ selector: 'p-colorPicker', template: ` -
+
-
-
-
-
+
+
+
+
-
-
+
+
@@ -233,7 +242,7 @@ export class ColorPicker implements ControlValueAccessor, OnDestroy { this.pickHue(event); } - onHueTouchStart(event: TouchEvent) { + onHueDragStart(event: TouchEvent) { if (this.disabled) { return; } @@ -242,7 +251,7 @@ export class ColorPicker implements ControlValueAccessor, OnDestroy { this.pickHue(event, (event as TouchEvent).changedTouches[0]); } - onColorTouchStart(event: TouchEvent) { + onColorDragStart(event: TouchEvent) { if (this.disabled) { return; } @@ -278,7 +287,7 @@ export class ColorPicker implements ControlValueAccessor, OnDestroy { this.pickColor(event); } - onMove(event: TouchEvent) { + onDrag(event: TouchEvent) { if (this.colorDragging) { this.pickColor(event, event.changedTouches[0]); event.preventDefault(); @@ -473,18 +482,20 @@ export class ColorPicker implements ControlValueAccessor, OnDestroy { } onInputKeydown(event: KeyboardEvent) { - switch (event.which) { - //space - case 32: + switch (event.code) { + case 'Space': this.togglePanel(); event.preventDefault(); break; - //escape and tab - case 27: - case 9: + case 'Escape': + case 'Tab': this.hide(); break; + + default: + //NoOp + break; } } diff --git a/src/app/components/inputswitch/inputswitch.ts b/src/app/components/inputswitch/inputswitch.ts index 6498da3fb07..9390d594981 100755 --- a/src/app/components/inputswitch/inputswitch.ts +++ b/src/app/components/inputswitch/inputswitch.ts @@ -1,6 +1,6 @@ -import { NgModule, Component, Input, forwardRef, EventEmitter, Output, ChangeDetectorRef, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, NgModule, Output, ViewChild, ViewEncapsulation, forwardRef } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { InputSwitchOnChangeEvent } from './inputswitch.interface'; export const INPUTSWITCH_VALUE_ACCESSOR: any = { @@ -15,26 +15,33 @@ export const INPUTSWITCH_VALUE_ACCESSOR: any = { @Component({ selector: 'p-inputSwitch', template: ` -
-
+
+
- +
`, providers: [INPUTSWITCH_VALUE_ACCESSOR], @@ -45,7 +52,7 @@ export const INPUTSWITCH_VALUE_ACCESSOR: any = { class: 'p-element' } }) -export class InputSwitch implements ControlValueAccessor { +export class InputSwitch { /** * Inline style of the component. * @group Props @@ -108,6 +115,8 @@ export class InputSwitch implements ControlValueAccessor { */ @Output() onChange: EventEmitter = new EventEmitter(); + @ViewChild('input') input!: ElementRef; + modelValue: any = false; focused: boolean = false; @@ -118,39 +127,26 @@ export class InputSwitch implements ControlValueAccessor { constructor(private cd: ChangeDetectorRef) {} - onClick(event: Event, cb: HTMLInputElement) { + onClick(event: Event) { if (!this.disabled && !this.readonly) { - event.preventDefault(); - this.toggle(event); - cb.focus(); - } - } - - onInputChange(event: Event) { - if (!this.readonly) { - const inputChecked = (event.target).checked; - this.updateModel(event, inputChecked); - } - } + this.modelValue = this.checked() ? this.falseValue : this.trueValue; - toggle(event: Event) { - this.updateModel(event, !this.checked()); - } + this.onModelChange(this.modelValue); + this.onChange.emit({ + originalEvent: event, + checked: this.modelValue + }); - updateModel(event: Event, value: boolean) { - this.modelValue = value ? this.trueValue : this.falseValue; - this.onModelChange(this.modelValue); - this.onChange.emit({ - originalEvent: event, - checked: this.modelValue - }); + event.preventDefault(); + this.input.nativeElement.focus(); + } } - onFocus(event: Event) { + onFocus() { this.focused = true; } - onBlur(event: Event) { + onBlur() { this.focused = false; this.onModelTouched(); } diff --git a/src/app/components/radiobutton/radiobutton.ts b/src/app/components/radiobutton/radiobutton.ts index fc0d124e4d6..930c92c5278 100755 --- a/src/app/components/radiobutton/radiobutton.ts +++ b/src/app/components/radiobutton/radiobutton.ts @@ -1,6 +1,6 @@ -import { NgModule, Component, Input, Output, ElementRef, EventEmitter, forwardRef, ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, Injectable, Injector, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor, NgControl } from '@angular/forms'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Injectable, Injector, Input, NgModule, OnDestroy, OnInit, Output, ViewChild, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms'; import { Nullable } from 'primeng/ts-helpers'; import { RadioButtonClickEvent } from './radiobutton.interface'; @@ -49,35 +49,43 @@ export class RadioControlRegistry { @Component({ selector: 'p-radioButton', template: ` -
-
+
+
-
- +
+
`, @@ -167,7 +175,7 @@ export class RadioButton implements ControlValueAccessor, OnInit, OnDestroy { */ @Output() onBlur: EventEmitter = new EventEmitter(); - @ViewChild('rb') inputViewChild!: ElementRef; + @ViewChild('input') inputViewChild!: ElementRef; public onModelChange: Function = () => {}; @@ -245,9 +253,6 @@ export class RadioButton implements ControlValueAccessor, OnInit, OnDestroy { this.onBlur.emit(event); } - onChange(event: Event) { - this.select(event); - } /** * Applies focus to input field. * @group Method diff --git a/src/app/components/selectbutton/selectbutton.ts b/src/app/components/selectbutton/selectbutton.ts index d4dee41bc19..f31ab9054ed 100755 --- a/src/app/components/selectbutton/selectbutton.ts +++ b/src/app/components/selectbutton/selectbutton.ts @@ -1,8 +1,9 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, NgModule, Output, TemplateRef, ViewEncapsulation, forwardRef } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, NgModule, Output, TemplateRef, ViewChild, ViewEncapsulation, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { PrimeTemplate, SharedModule } from 'primeng/api'; import { RippleModule } from 'primeng/ripple'; +import { Nullable } from 'primeng/ts-helpers'; import { ObjectUtils } from 'primeng/utils'; import { SelectButtonChangeEvent, SelectButtonOptionClickEvent } from './selectbutton.interface'; @@ -18,27 +19,30 @@ export const SELECTBUTTON_VALUE_ACCESSOR: any = { @Component({ selector: 'p-selectButton', template: ` -
+
- - {{ getOptionLabel(option) }} + + {{ getOptionLabel(option) }} @@ -75,6 +79,11 @@ export class SelectButton implements ControlValueAccessor { * @group Props */ @Input() optionDisabled: string | undefined; + /** + * Whether selection can be cleared. + * @group Props + */ + @Input() unselectable: boolean = false; /** * Index of the element in tabbing order. * @group Props @@ -123,18 +132,26 @@ export class SelectButton implements ControlValueAccessor { */ @Output() onChange: EventEmitter = new EventEmitter(); + @ViewChild('container') container: Nullable; + @ContentChild(PrimeTemplate) itemTemplate!: PrimeTemplate; public get selectButtonTemplate(): TemplateRef { return this.itemTemplate?.template; } + get equalityKey() { + return this.optionValue ? null : this.dataKey; + } + value: any; onModelChange: Function = () => {}; onModelTouched: Function = () => {}; + focusedIndex: number = 0; + constructor(public cd: ChangeDetectorRef) {} getOptionLabel(option: any) { @@ -167,35 +184,34 @@ export class SelectButton implements ControlValueAccessor { this.cd.markForCheck(); } - onItemClick(event: Event, option: any, index: number) { + onOptionSelect(event, option, index) { if (this.disabled || this.isOptionDisabled(option)) { return; } - if (this.multiple) { - if (this.isSelected(option)) this.removeOption(option); - else this.value = [...(this.value || []), this.getOptionValue(option)]; - - this.onModelChange(this.value); + let selected = this.isSelected(option); - this.onChange.emit({ - originalEvent: event, - value: this.value - }); - } else { - let value = this.getOptionValue(option); + if (selected && this.unselectable) { + return; + } - if (this.value !== value) { - this.value = this.getOptionValue(option); - this.onModelChange(this.value); + let optionValue = this.getOptionValue(option); + let newValue; - this.onChange.emit({ - originalEvent: event, - value: this.value - }); - } + if (this.multiple) { + if (selected) newValue = this.value.filter((val) => !ObjectUtils.equals(val, optionValue, this.equalityKey)); + else newValue = this.value ? [...this.value, optionValue] : [optionValue]; + } else { + newValue = selected ? null : optionValue; } + this.focusedIndex = index; + this.value = newValue; + + this.onChange.emit({ + originalEvent: event, + value: this.value + }); this.onOptionClick.emit({ originalEvent: event, option: option, @@ -203,6 +219,59 @@ export class SelectButton implements ControlValueAccessor { }); } + onKeyDown(event, option, index) { + switch (event.code) { + case 'Space': { + this.onOptionSelect(event, option, index); + event.preventDefault(); + break; + } + + case 'ArrowDown': + + case 'ArrowRight': { + this.changeTabIndexes(event, 'next'); + event.preventDefault(); + break; + } + + case 'ArrowUp': + + case 'ArrowLeft': { + this.changeTabIndexes(event, 'prev'); + event.preventDefault(); + break; + } + + default: + //no op + break; + } + } + + changeTabIndexes(event, direction) { + let firstTabableChild, index; + + for (let i = 0; i <= this.container.nativeElement.children.length - 1; i++) { + if (this.container.nativeElement.children[i].getAttribute('tabindex') === '0') firstTabableChild = { elem: this.container.nativeElement.children[i], index: i }; + } + + if (direction === 'prev') { + if (firstTabableChild.index === 0) index = this.container.nativeElement.children.length - 1; + else index = firstTabableChild.index - 1; + } else { + if (firstTabableChild.index === this.container.nativeElement.children.length - 1) index = 0; + else index = firstTabableChild.index + 1; + } + + this.focusedIndex = index; + this.container.nativeElement.children[index].focus(); + } + + onFocus(event: Event, index: number) { + this.focusedIndex = index; + } + onBlur() { this.onModelTouched(); } @@ -225,7 +294,7 @@ export class SelectButton implements ControlValueAccessor { } } } else { - selected = ObjectUtils.equals(this.getOptionValue(option), this.value, this.dataKey); + selected = ObjectUtils.equals(this.getOptionValue(option), this.value, this.equalityKey); } return selected; diff --git a/src/app/components/slider/slider.ts b/src/app/components/slider/slider.ts index 782e715c7d4..65c499d5677 100755 --- a/src/app/components/slider/slider.ts +++ b/src/app/components/slider/slider.ts @@ -1,5 +1,5 @@ import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Inject, Input, NgModule, NgZone, OnDestroy, Output, PLATFORM_ID, Renderer2, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, NgModule, NgZone, OnDestroy, Output, PLATFORM_ID, Renderer2, ViewChild, ViewEncapsulation, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DomHandler } from 'primeng/dom'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; @@ -22,71 +22,86 @@ export const SLIDER_VALUE_ACCESSOR: any = { [class]="styleClass" [ngClass]="{ 'p-slider p-component': true, 'p-disabled': disabled, 'p-slider-horizontal': orientation == 'horizontal', 'p-slider-vertical': orientation == 'vertical', 'p-slider-animate': animate }" (click)="onBarClick($event)" + [attr.data-pc-name]="'slider'" + [attr.data-pc-section]="'root'" > - - + +
`, @@ -144,6 +159,11 @@ export class Slider implements OnDestroy, ControlValueAccessor { * @group Props */ @Input() styleClass: string | undefined; + /** + * Defines a string that labels the input for accessibility. + * @group Props + */ + @Input() ariaLabel: string | undefined; /** * Establishes relationships between the component and label(s) where its value should be one or more element IDs. * @group Props @@ -240,7 +260,7 @@ export class Slider implements OnDestroy, ControlValueAccessor { } } - onTouchStart(event: TouchEvent, index?: number) { + onDragStart(event: TouchEvent, index?: number) { if (this.disabled) { return; } @@ -269,7 +289,7 @@ export class Slider implements OnDestroy, ControlValueAccessor { event.preventDefault(); } - onTouchMove(event: TouchEvent) { + onDrag(event: TouchEvent) { if (this.disabled) { return; } @@ -288,7 +308,7 @@ export class Slider implements OnDestroy, ControlValueAccessor { event.preventDefault(); } - onTouchEnd(event: TouchEvent) { + onDragEnd(event: TouchEvent) { if (this.disabled) { return; } @@ -318,29 +338,76 @@ export class Slider implements OnDestroy, ControlValueAccessor { this.sliderHandleClick = false; } - onHandleKeydown(event: KeyboardEvent, handleIndex?: number) { - if (this.disabled) { - return; + onKeyDown(event, index) { + this.handleIndex = index; + + switch (event.code) { + case 'ArrowDown': + case 'ArrowLeft': + this.decrementValue(event, index); + event.preventDefault(); + break; + + case 'ArrowUp': + case 'ArrowRight': + this.incrementValue(event, index); + event.preventDefault(); + break; + + case 'PageDown': + this.decrementValue(event, index, true); + event.preventDefault(); + break; + + case 'PageUp': + this.incrementValue(event, index, true); + event.preventDefault(); + break; + + case 'Home': + this.updateValue(this.min, event); + event.preventDefault(); + break; + + case 'End': + this.updateValue(this.max, event); + event.preventDefault(); + break; + + default: + break; } - if (event.which == 38 || event.which == 39) { - this.spin(event, 1, handleIndex); - } else if (event.which == 37 || event.which == 40) { - this.spin(event, -1, handleIndex); + } + + decrementValue(event, index, pageKey = false) { + let newValue; + + if (this.range) { + if (this.step) newValue = this.values[index] - this.step; + else newValue = this.values[index] - 1; + } else { + if (this.step) newValue = this.value - this.step; + else if (!this.step && pageKey) newValue = this.value - 10; + else newValue = this.value - 1; } + + this.updateValue(newValue, event); + event.preventDefault(); } - spin(event: Event, dir: number, handleIndex?: number) { - let step = (this.step || 1) * dir; + incrementValue(event, index, pageKey = false) { + let newValue; if (this.range) { - this.handleIndex = handleIndex as number; - this.updateValue((this.values as number[])[this.handleIndex] + step); - this.updateHandleValue(); + if (this.step) newValue = this.values[index] + this.step; + else newValue = this.values[index] + 1; } else { - this.updateValue((this.value as number) + step); - this.updateHandleValue(); + if (this.step) newValue = this.value + this.step; + else if (!this.step && pageKey) newValue = this.value + 10; + else newValue = this.value + 1; } + this.updateValue(newValue, event); event.preventDefault(); } @@ -571,6 +638,7 @@ export class Slider implements OnDestroy, ControlValueAccessor { this.onChange.emit({ event: event as Event, value: this.value }); this.sliderHandle?.nativeElement.focus(); } + this.updateHandleValue(); } getValueFromHandle(handleValue: number): number { diff --git a/src/app/components/togglebutton/togglebutton.ts b/src/app/components/togglebutton/togglebutton.ts index c874a9c87f8..9c05cccf370 100755 --- a/src/app/components/togglebutton/togglebutton.ts +++ b/src/app/components/togglebutton/togglebutton.ts @@ -23,14 +23,23 @@ export const TOGGLEBUTTON_VALUE_ACCESSOR: any = { [ngStyle]="style" [class]="styleClass" (click)="toggle($event)" - (keydown.enter)="toggle($event)" + (keydown)="onKeyDown($event)" [attr.tabindex]="disabled ? null : '0'" - role="checkbox" + role="switch" [attr.aria-checked]="checked" + [attr.aria-labelledby]="ariaLabelledBy" + [attr.aria-label]="ariaLabel" pRipple + [attr.data-pc-name]="'togglebutton'" + [attr.data-pc-section]="'root'" > - - {{ checked ? (hasOnLabel ? onLabel : '') : hasOffLabel ? offLabel : '' }} + + {{ checked ? (hasOnLabel ? onLabel : '') : hasOffLabel ? offLabel : '' }}
`, providers: [TOGGLEBUTTON_VALUE_ACCESSOR], @@ -61,6 +70,11 @@ export class ToggleButton implements ControlValueAccessor { * @group Props */ @Input() offIcon: string | undefined; + /** + * Defines a string that labels the input for accessibility. + * @group Props + */ + @Input() ariaLabel: string | undefined; /** * Establishes relationships between the component and label(s) where its value should be one or more element IDs. * @group Props @@ -125,6 +139,19 @@ export class ToggleButton implements ControlValueAccessor { } } + onKeyDown(event: KeyboardEvent) { + switch (event.code) { + case 'Enter': + this.toggle(event); + event.preventDefault(); + break; + case 'Space': + this.toggle(event); + event.preventDefault(); + break; + } + } + onBlur() { this.onModelTouched(); } diff --git a/src/app/components/tristatecheckbox/tristatecheckbox.ts b/src/app/components/tristatecheckbox/tristatecheckbox.ts index 26d1e0cb2e4..de25abd47a1 100755 --- a/src/app/components/tristatecheckbox/tristatecheckbox.ts +++ b/src/app/components/tristatecheckbox/tristatecheckbox.ts @@ -1,9 +1,9 @@ -import { NgModule, Component, Input, Output, EventEmitter, forwardRef, ChangeDetectorRef, ChangeDetectionStrategy, ViewEncapsulation, TemplateRef, ContentChildren, QueryList } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, Input, NgModule, Output, QueryList, TemplateRef, ViewEncapsulation, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PrimeTemplate, SharedModule } from 'primeng/api'; import { CheckIcon } from 'primeng/icons/check'; import { TimesIcon } from 'primeng/icons/times'; -import { PrimeTemplate, SharedModule } from 'primeng/api'; import { Nullable } from 'primeng/ts-helpers'; import { TriStateCheckboxChangeEvent } from './tristatecheckbox.interface'; @@ -19,39 +19,47 @@ export const TRISTATECHECKBOX_VALUE_ACCESSOR: any = { @Component({ selector: 'p-triStateCheckbox', template: ` -
-
+
+
-