diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1b94808ec..49a6a7d5b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [17.0.2] 📅 2025-08-19 +### Bugfix +- `@nova-ui/bits` | fix accessibility for button + ## [17.0.1] 📅 2025-07-31 ### Added diff --git a/packages/bits/src/lib/button/button.component.html b/packages/bits/src/lib/button/button.component.html index e512ee974..a86a9e49b 100644 --- a/packages/bits/src/lib/button/button.component.html +++ b/packages/bits/src/lib/button/button.component.html @@ -2,6 +2,9 @@ *ngIf="isBusy" [ngStyle]="getRippleContainerStyle()" class="nui-button-ripple-container" + aria-live="polite" + aria-atomic="true" + aria-label="Loading" >
diff --git a/packages/bits/src/lib/button/button.component.ts b/packages/bits/src/lib/button/button.component.ts index 49d2b68dc..e72d269fd 100644 --- a/packages/bits/src/lib/button/button.component.ts +++ b/packages/bits/src/lib/button/button.component.ts @@ -90,6 +90,12 @@ export class ButtonComponent implements OnInit, OnDestroy, AfterContentChecked { /** Sets aria-label for the component */ @Input() public ariaLabel: string = ""; + /** Sets aria-disabled for disabled state programmatic indication */ + @HostBinding("attr.aria-disabled") + public get ariaDisabled(): string | null { + return this.getHostElement().disabled ? "true" : null; + } + /** * Optionally, set whether to fire a "click" event repeatedly while the button is pressed. */ @@ -188,6 +194,9 @@ should be set explicitly: `, el.nativeElement ); } + + // Validate accessibility for icon-only buttons + this.validateIconOnlyButtonAccessibility(); } public ngOnInit(): void { @@ -251,6 +260,12 @@ should be set explicitly: `, const mouseLeave$ = fromEvent(hostElement, "mouseleave").pipe( takeUntil(this.ngUnsubscribe) ); + const keyUp$ = fromEvent(hostElement, "keyup").pipe( + takeUntil(this.ngUnsubscribe), + filter((event: KeyboardEvent) => event.key === " " || event.key === "Enter") + ); + + // Handle mouse-based repeat events fromEvent(hostElement, "mousedown") .pipe( takeUntil(this.ngUnsubscribe), @@ -274,9 +289,49 @@ should be set explicitly: `, } }); }); + + // Handle keyboard-based repeat events for accessibility + fromEvent(hostElement, "keydown") + .pipe( + takeUntil(this.ngUnsubscribe), + filter((event: KeyboardEvent) => { + return this.isRepeat && (event.key === " " || event.key === "Enter"); + }) + ) + .subscribe((event: KeyboardEvent) => { + event.preventDefault(); // Prevent default space/enter behavior + const repeatSubscription = timer( + buttonConstants.repeatDelay, + buttonConstants.repeatInterval + ) + .pipe( + takeUntil( + merge(keyUp$, this.ngUnsubscribe) + ) + ) + .subscribe(() => { + if (hostElement.disabled) { + repeatSubscription.unsubscribe(); + } else { + hostElement.click(); + } + }); + }); } private getHostElement() { return this.el.nativeElement; } + + private validateIconOnlyButtonAccessibility(): void { + // Check if button will be icon-only and validate accessibility + setTimeout(() => { + if (this._isContentEmpty && this.icon && !this.ariaLabel) { + this.logger.warn( + "Icon-only button detected without aria-label. Consider providing a meaningful aria-label for screen readers: ", + this.el.nativeElement + ); + } + }); + } }