Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumError": "1.2mb"
},
{
"type": "anyComponentStyle",
Expand Down
1 change: 1 addition & 0 deletions apps/design-land/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const appRoutes: Routes = [
{ path: 'sticky', loadChildren: () => import('./sticky/sticky.module').then(m => m.DesignLandStickyModule) },
{ path: 'switch', loadChildren: () => import('./switch/switch.module').then(m => m.DesignLandSwitchModule) },
{ path: 'radio', loadChildren: () => import('./radio/radio.module').then(m => m.DesignLandRadioModule) },
{ path: 'roving-tab-index', loadChildren: () => import('./roving-tab-index/roving-tab-index.routes') },
{ path: 'textarea', loadChildren: () => import('./textarea/textarea.module').then(m => m.DesignLandTextareaModule) },
{ path: 'toast', loadChildren: () => import('./toast/toast.module').then(m => m.DesignLandToastModule) },
{ path: 'tree', loadChildren: () => import('./tree/tree.module').then(m => m.DesignLandTreeModule) },
Expand Down
2 changes: 2 additions & 0 deletions apps/design-land/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { PAGINATOR_EXAMPLES } from '@daffodil/design-examples/paginator';
import { PROGRESS_BAR_EXAMPLES } from '@daffodil/design-examples/progress-bar';
import { QUANTITY_FIELD_EXAMPLES } from '@daffodil/design-examples/quantity-field';
import { RADIO_EXAMPLES } from '@daffodil/design-examples/radio';
import { ROVING_TAB_INDEX_EXAMPLES } from '@daffodil/design-examples/roving-tab-index';
import { SELECT_EXAMPLES } from '@daffodil/design-examples/select';
import { SIDEBAR_EXAMPLES } from '@daffodil/design-examples/sidebar';
import { STICKY_EXAMPLES } from '@daffodil/design-examples/sticky';
Expand Down Expand Up @@ -85,6 +86,7 @@ export class DesignLandAppComponent {
...TEXTAREA_EXAMPLES,
...TABS_EXAMPLES,
...TREE_EXAMPLES,
...ROVING_TAB_INDEX_EXAMPLES,
].map((componentExample) => createCustomElementFromExample(componentExample, injector))
.map((customElement) => {
// Register the custom element with the browser.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1>Roving Tab Index</h1>

<design-land-example-viewer-container example="rti-nested-groups"></design-land-example-viewer-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Component } from '@angular/core';

import { DesignLandArticleEncapsulatedModule } from '../core/article-encapsulated/article-encapsulated.module';
import { DesignLandExampleViewerModule } from '../core/code-preview/container/example-viewer.module';

@Component({
selector: 'design-land-roving-tab-index',
templateUrl: './roving-tab-index.component.html',
imports: [
DesignLandExampleViewerModule,
DesignLandArticleEncapsulatedModule,
],
})
export class DesignLandRtiComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Routes } from '@angular/router';

import { DesignLandRtiComponent } from './roving-tab-index.component';

const routes: Routes = [
{ path: '', component: DesignLandRtiComponent },
];

export default routes;
7 changes: 7 additions & 0 deletions libs/design-examples/roving-tab-index/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../../scss"]
}
}
5 changes: 5 additions & 0 deletions libs/design-examples/roving-tab-index/src/examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NestedGroupsRtiExampleComponent } from './rti-nested-groups/nested-groups.component';

export const ROVING_TAB_INDEX_EXAMPLES = [
NestedGroupsRtiExampleComponent,
];
1 change: 1 addition & 0 deletions libs/design-examples/roving-tab-index/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public_api';
1 change: 1 addition & 0 deletions libs/design-examples/roving-tab-index/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ROVING_TAB_INDEX_EXAMPLES } from './examples';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div rti>I get tabbed to first</div>
<div rti>I get tabbed to second</div>
<button>I'm a button that defaults to the root group</button>
<div rti rtiBoundary="group">
<div rti="group">I only get tabbed to if space is hit on this group</div>
<div rti="group">Use arrow keys to navigate around in the group</div>
<a>I'm an anchor that defaults to the outer group</a>
<div rti="group" rtiBoundary="nested">
<div rti="nested">You have to hit space again to get to me</div>
<button>I'm a button that defaults to the inner group</button>
<div rti="nested">You can hit escape to return to the previous group</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
ChangeDetectionStrategy,
Component,
} from '@angular/core';

import {
DaffRovingTabIndexBoundaryDirective,
DaffRovingTabIndexDirective,
} from '@daffodil/design';

@Component({
selector: 'rti-nested-groups-example',
templateUrl: './nested-groups.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DaffRovingTabIndexDirective,
DaffRovingTabIndexBoundaryDirective,
],
})
export class NestedGroupsRtiExampleComponent {}
1 change: 1 addition & 0 deletions libs/design/src/core/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './selectable/public_api';
export * from './sticky/public_api';
export * from './disableable/public_api';
export * from './error-state-matcher/public_api';
export * from './roving-tab-index/public_api';
3 changes: 3 additions & 0 deletions libs/design/src/core/roving-tab-index/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { DaffRovingTabIndexService } from './roving-tab-index-group.service';
export { DaffRovingTabIndexDirective } from './roving-tab-index.directive';
export { DaffRovingTabIndexBoundaryDirective } from './roving-tab-index-boundary.directive';
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CdkTrapFocus } from '@angular/cdk/a11y';
import {
Directive,
effect,
input,
} from '@angular/core';

import { DaffRovingTabIndexService } from './roving-tab-index-group.service';

@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[rtiBoundary]',
host: {
'[attr.data-rti-boundary]': 'rtiBoundary()',
'(keydown.space)': 'enterGroup($event)',
},
hostDirectives: [
CdkTrapFocus,
],
})
export class DaffRovingTabIndexBoundaryDirective {
readonly rtiBoundary = input<string | null>(null);

constructor(
private groupService: DaffRovingTabIndexService,
private focusTrap: CdkTrapFocus,
) {
effect(() => {
this.focusTrap.enabled = this.rtiBoundary() === this.groupService.group();
});
}

enterGroup(evt: Event) {
evt.preventDefault();
evt.stopPropagation();
const group = this.rtiBoundary();
if (group) {
this.groupService.enter(group);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
DOCUMENT,
Inject,
Injectable,
signal,
} from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class DaffRovingTabIndexService {
private readonly _hierarchy: Array<string> = [];
private readonly _group = signal('');

readonly group = this._group.asReadonly();

constructor(
@Inject(DOCUMENT) private document: Document,
) {
// this.document.addEventListener('keydown', this.onKeydown.bind(this));
}

enter(group: string) {
if (this._group() !== group) {
this._hierarchy.push(group);
this._group.set(group);
const el = this.document.querySelector<HTMLElement>(`[data-rti="${group}"]`);
if (el) {
(<HTMLElement>this.document.activeElement).blur();
el.focus();
}
}
}

leave() {
const prev = this._hierarchy.pop();
if (prev) {
const group = this._hierarchy[this._hierarchy.length - 1] || '';
this._group.set(group);
(<HTMLElement>this.document.activeElement).blur();
const boundary = this.document.querySelector<HTMLElement>(`[data-rti-boundary="${prev}"][data-rti="${group}"]`);
if (boundary) {
boundary.focus();
} else {
console.warn(`The boundary for RTI group ${prev} does not have a reference to the parent group ${group}`);
}
}
}

next() {
this._changeFocus();
}

previous() {
this._changeFocus(true);
}

private _changeFocus(up = false) {
if (this._group()) {
const ary = Array.from(this.document.querySelectorAll<HTMLElement>(`[data-rti="${this._group()}"]`));
const index = ary.findIndex((el) => el === this.document.activeElement);
(<HTMLElement>this.document.activeElement).blur();
(up
? ary[index === 0 ? ary.length - 1 : index - 1]
: ary[index === ary.length - 1 ? 0 : index + 1]).focus();
}
}

onKeydown(evt: Event) {
if ('key' in evt) {
switch ((<KeyboardEvent>evt).key) {
case 'ArrowUp':
case 'ArrowDown':
if (this._group()) {
evt.preventDefault();
const ary = Array.from(this.document.querySelectorAll<HTMLElement>(`[data-rti="${this._group()}"]`));
const index = ary.findIndex((el) => el === this.document.activeElement);
(<HTMLElement>this.document.activeElement).blur();
((<KeyboardEvent>evt).key === 'ArrowUp'
? ary[index === 0 ? ary.length - 1 : index - 1]
: ary[index === ary.length - 1 ? 0 : index + 1]).focus();
}
break;

default:
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
computed,
Directive,
input,
Optional,
signal,
SkipSelf,
} from '@angular/core';

import { DaffRovingTabIndexBoundaryDirective } from './roving-tab-index-boundary.directive';
import { DaffRovingTabIndexService } from './roving-tab-index-group.service';

@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[rti],a,button',
host: {
'[attr.tabindex]': 'tabindex()',
'[attr.data-rti]': 'group()',
'(keydown.escape)': 'leaveGroup($event)',
'(keydown.arrowup)': 'previous($event)',
'(keydown.arrowdown)': 'next($event)',
'(focus)': 'onFocus()',
'(blur)': 'onBlur()',
},
})
export class DaffRovingTabIndexDirective {
private readonly _focused = signal(false);

/**
* Allows the RTI group to be overriden.
* By default it will be the nearest ancestor with an `rtiBoundary` defined.
* @see {@link DaffRovingTabIndexBoundaryDirective}.
*/
readonly rti = input<string>();
readonly group = computed(() => this.parent?.rtiBoundary() || '');
readonly tabindex = computed(() =>
this.groupService.group() === this.group()
? 0
: -1,
);

constructor(
private groupService: DaffRovingTabIndexService,
@Optional() @SkipSelf() private parent: DaffRovingTabIndexBoundaryDirective,
) {}

leaveGroup(evt: Event) {
evt.stopPropagation();
this.groupService.leave();
}

next(evt: Event) {
evt.stopPropagation();
this.groupService.next();
}

previous(evt: Event) {
evt.stopPropagation();
this.groupService.previous();
}

onFocus() {
this._focused.set(true);
}

onBlur() {
this._focused.set(false);
}
}
3 changes: 3 additions & 0 deletions libs/design/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
"@daffodil/design/radio": [
"libs/design/radio/src"
],
"@daffodil/design/roving-tab-index": [
"libs/design/roving-tab-index/src"
],
"@daffodil/design/scss": [
"libs/design/scss/src"
],
Expand Down
3 changes: 2 additions & 1 deletion libs/docs-utils/src/api/parse-host-directive.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const REGEX = /(directive: (?<directive>\w+))|(inputs: (?<inputs>\[.+\]))|(outputs: (?<outputs>\[.+\]))/g;
const REGEX = /(directive: (?<directive>\w+))|(inputs: (?<inputs>\[.+\]))|(outputs: (?<outputs>\[.+\]))|((?<type>^\w+$))/g;

type Groups = Partial<{
directive: string;
type: string;
inputs: string;
outputs: string;
}>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,8 @@ export class RoleProcessor implements FilterableProcessor {
doc.selector = directiveArg.selector;
doc.hostDirectives = (<Array<string>>directiveArg.hostDirectives)
?.map(daffDocsApiParseHostDirective)
.map<DaffDocsApiHostDirective>(({ directive, inputs, outputs }) => ({
directive: createRef(directive),
.map<DaffDocsApiHostDirective>(({ directive, inputs, outputs, type }) => ({
directive: createRef(directive || type),
inputs: inputs ? JSON.parse(inputs.replaceAll('\'', '\"')).map(daffDocsApiParseHostDirectiveField) : [],
outputs: outputs ? JSON.parse(outputs.replaceAll('\'', '\"')).map(daffDocsApiParseHostDirectiveField) : [],
})) || [];
Expand Down