[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 e19216f031.
This commit is contained in:
Shane Melton 2022-11-17 08:10:01 -08:00 committed by GitHub
parent 6049e588e4
commit 3c0beef3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 5 deletions

View File

@ -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: `
<bit-dialog [dialogSize]="large">
<bit-dialog dialogSize="large">
<span bitDialogTitle>Dialog Title</span>
<span bitDialogContent>
Dialog body text goes here.
@ -68,7 +70,7 @@ export default {
DialogTitleContainerDirective,
StoryDialogContentComponent,
],
imports: [ButtonModule, DialogModule],
imports: [SharedModule, ButtonModule, DialogModule, IconButtonModule],
providers: [
DialogService,
{

View File

@ -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"

View File

@ -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<unknown>;
@ContentChild(TabLabelDirective) templateLabel: TabLabelDirective;

View File

@ -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<TabGroupComponent> = (args: any) =>
});
export const PreserveContentTabs = PreserveContentTabGroupTemplate.bind({});
const KeyboardNavTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
props: args,
template: `
<bit-tab-group label="Keyboard Navigation Tabs" class="tw-text-main">
<bit-tab label="Form Tab">
<p>
You can navigate through all tab labels, form inputs, and the button that is outside the tab group via
the keyboard.
</p>
<bit-form-field>
<bit-label>First Input</bit-label>
<input type="text" bitInput />
</bit-form-field>
<bit-form-field>
<bit-label>Second Input</bit-label>
<input type="text" bitInput />
</bit-form-field>
</bit-tab>
<bit-tab label="No Focusable Content Tab" [contentTabIndex]="0">
<p>This tab has no focusable content, but the panel should still be focusable</p>
</bit-tab>
</bit-tab-group>
<button bitButton buttonType="primary" class="tw-mt-5">External Button</button>
`,
});
export const KeyboardNavigation = KeyboardNavTabGroupTemplate.bind({});