From 3c0beef3a5a10a406e4f9566ea9c0bcbe0c1e5a2 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 17 Nov 2022 08:10:01 -0800 Subject: [PATCH] [CL-62] Fix Content Tab Keyboard Navigation (#3944) * [CL-62] Add missing modules to Dialog Service story The IconButtonModule and SharedModule need to be available for the service to properly open the dialog. Also fix type error for dialogSize attribute * [CL-62] Add new tabbed dialog service story - Update StoryDialogComponent to support different content components and button text for re-use in multiple stories - Update the story module metadata to include Tabs and FormsField modules for the new tab story - Add StoryTabbedDialogComponent that has tabbed content with input fields which provide tabbing targets - Add storybook actions to provide an example of getting a result from the dialog service * [CL-62] Remove tab panel tabIndex from tab group component The tabIndex attribute broke keyboard navigation in Firefox and is only required on the tab labels. * [CL-62] Introduce contentTabIndex input for bit-tab contentTabIndex provides an interface for setting the tabPanel's tabIndex so that the tabPanel is still included in the tab sequence of the page in case it has no focusable content of its own * [CL-62] Add tab keyboard navigation story * Revert "[CL-62] Add new tabbed dialog service story" This reverts commit e19216f0310fe0fdbb827be25db7aafc15d95f2f. --- .../src/dialog/dialog.service.stories.ts | 8 +++-- .../tabs/tab-group/tab-group.component.html | 2 +- .../src/tabs/tab-group/tab.component.ts | 11 +++++- libs/components/src/tabs/tabs.stories.ts | 34 +++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index 942891ba061..678494cd9f8 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -1,10 +1,12 @@ -import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { DIALOG_DATA, DialogModule, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { Meta, moduleMetadata, Story } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ButtonModule } from "../button"; +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared"; import { I18nMockService } from "../utils/i18n-mock.service"; import { DialogService } from "./dialog.service"; @@ -35,7 +37,7 @@ class StoryDialogComponent { @Component({ selector: "story-dialog-content", template: ` - + Dialog Title Dialog body text goes here. @@ -68,7 +70,7 @@ export default { DialogTitleContainerDirective, StoryDialogContentComponent, ], - imports: [ButtonModule, DialogModule], + imports: [SharedModule, ButtonModule, DialogModule, IconButtonModule], providers: [ DialogService, { diff --git a/libs/components/src/tabs/tab-group/tab-group.component.html b/libs/components/src/tabs/tab-group/tab-group.component.html index dbe71fb4b99..071f5c2259f 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.html +++ b/libs/components/src/tabs/tab-group/tab-group.component.html @@ -34,7 +34,7 @@ role="tabpanel" *ngFor="let tab of tabs; let i = index" [id]="getTabContentId(i)" - [attr.tabindex]="selectedIndex === i ? 0 : -1" + [attr.tabindex]="tab.contentTabIndex" [attr.labeledby]="getTabLabelId(i)" [active]="tab.isActive" [content]="tab.content" diff --git a/libs/components/src/tabs/tab-group/tab.component.ts b/libs/components/src/tabs/tab-group/tab.component.ts index 05e61ffacc2..9a3a9380031 100644 --- a/libs/components/src/tabs/tab-group/tab.component.ts +++ b/libs/components/src/tabs/tab-group/tab.component.ts @@ -20,9 +20,18 @@ import { TabLabelDirective } from "./tab-label.directive"; }) export class TabComponent implements OnInit { @Input() disabled = false; - @Input("label") textLabel = ""; + /** + * Optional tabIndex for the tabPanel that contains this tab's content. + * + * If the tabpanel does not contain any focusable elements or the first element with content is not focusable, + * this should be set to 0 to include it in the tab sequence of the page. + * + * @remarks See note 4 of https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ + */ + @Input() contentTabIndex: number | undefined; + @ViewChild(TemplateRef, { static: true }) implicitContent: TemplateRef; @ContentChild(TabLabelDirective) templateLabel: TabLabelDirective; diff --git a/libs/components/src/tabs/tabs.stories.ts b/libs/components/src/tabs/tabs.stories.ts index a520b50e17a..32c7e950e73 100644 --- a/libs/components/src/tabs/tabs.stories.ts +++ b/libs/components/src/tabs/tabs.stories.ts @@ -3,6 +3,9 @@ import { Component } from "@angular/core"; import { RouterModule } from "@angular/router"; import { Meta, moduleMetadata, Story } from "@storybook/angular"; +import { ButtonModule } from "../button"; +import { FormFieldModule } from "../form-field"; + import { TabGroupComponent } from "./tab-group/tab-group.component"; import { TabsModule } from "./tabs.module"; @@ -44,6 +47,8 @@ export default { imports: [ CommonModule, TabsModule, + ButtonModule, + FormFieldModule, RouterModule.forRoot( [ { path: "", redirectTo: "active", pathMatch: "full" }, @@ -125,3 +130,32 @@ const PreserveContentTabGroupTemplate: Story = (args: any) => }); export const PreserveContentTabs = PreserveContentTabGroupTemplate.bind({}); + +const KeyboardNavTabGroupTemplate: Story = (args: any) => ({ + props: args, + template: ` + + +

+ You can navigate through all tab labels, form inputs, and the button that is outside the tab group via + the keyboard. +

+ + First Input + + + + Second Input + + +
+ + +

This tab has no focusable content, but the panel should still be focusable

+
+
+ +`, +}); + +export const KeyboardNavigation = KeyboardNavTabGroupTemplate.bind({});