semmet-angular 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +16 -17
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.html.template +14 -34
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.spec.ts.template +52 -62
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.ts.template +88 -82
- package/src/accordion/schema.json +1 -1
- package/src/block/files/__name@dasherize__/__name@dasherize__.html.template +51 -23
- package/src/block/files/__name@dasherize__/__name@dasherize__.ts.template +16 -10
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.css.template +0 -32
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +5 -2
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.html.template +8 -7
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.spec.ts.template +51 -0
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.ts.template +18 -19
- package/src/breadcrumb/index.js +1 -1
- package/src/breadcrumb/index.js.map +1 -1
- package/src/breadcrumb/index.ts +1 -1
- package/src/breadcrumb/schema.json +1 -1
- package/src/collection.json +9 -9
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.css.template +0 -48
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +14 -35
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.spec.ts.template +53 -11
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.ts.template +128 -54
- package/src/menu-button/schema.json +1 -1
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -119
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +10 -7
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +18 -42
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +46 -23
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +54 -50
- package/src/navbar/schema.json +1 -1
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.css.template +0 -35
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +11 -13
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.spec.ts.template +58 -8
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.ts.template +17 -31
- package/src/pagination/schema.json +1 -1
- package/src/table/files/__name@dasherize__/__name@dasherize__.css.template +0 -17
- package/src/table/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +1 -1
- package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +13 -25
- package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +3 -35
- package/src/table/schema.json +1 -1
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.css.template +0 -40
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.html.template +18 -41
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.spec.ts.template +39 -31
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.ts.template +111 -113
- package/src/tabs/schema.json +1 -1
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -19
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +3 -4
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +7 -16
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +26 -11
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.ts.template +59 -37
- package/src/toolbar/schema.json +1 -1
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +4 -5
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +12 -31
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.spec.ts.template +80 -43
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.ts.template +105 -88
- package/src/tree-view/schema.json +1 -1
|
@@ -1,64 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Component, Directive, TemplateRef, computed, contentChild, inject, input, signal } from '@angular/core';
|
|
3
|
-
|
|
4
|
-
export interface <%= classify(name) %>Item {
|
|
5
|
-
label: string;
|
|
6
|
-
href: string;
|
|
7
|
-
current?: boolean;
|
|
8
|
-
disabled?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
|
|
12
|
-
{ label: 'Home', href: '/', current: true },
|
|
13
|
-
{ label: 'Projects', href: '/projects' },
|
|
14
|
-
{ label: 'About', href: '/about' },
|
|
15
|
-
{ label: 'Contact', href: '/contact' },
|
|
16
|
-
];
|
|
1
|
+
import { booleanAttribute, computed, Directive, inject, input, model } from '@angular/core';
|
|
17
2
|
|
|
18
3
|
let nextId = 0;
|
|
19
4
|
|
|
20
5
|
@Directive({
|
|
21
|
-
selector: '
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
@Directive({
|
|
28
|
-
selector: 'ng-template[<%= camelize(name) %>Actions]',
|
|
29
|
-
})
|
|
30
|
-
export class <%= classify(name) %>Actions {
|
|
31
|
-
readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@Component({
|
|
35
|
-
selector: '<%= selector %>',
|
|
36
|
-
imports: [NgTemplateOutlet],
|
|
37
|
-
templateUrl: './<%= dasherize(name) %>.html',
|
|
38
|
-
styleUrl: './<%= dasherize(name) %>.css',
|
|
6
|
+
selector: '[<%= camelize(name) %>]',
|
|
7
|
+
exportAs: '<%= camelize(name) %>',
|
|
8
|
+
host: {
|
|
9
|
+
'[attr.aria-label]': 'label() || null',
|
|
10
|
+
},
|
|
39
11
|
})
|
|
40
12
|
export class <%= classify(name) %> {
|
|
41
|
-
|
|
42
|
-
protected readonly actions = contentChild(<%= classify(name) %>Actions);
|
|
43
|
-
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
44
|
-
protected readonly expanded = signal(false);
|
|
45
|
-
|
|
46
|
-
readonly label = input('Primary navigation');
|
|
47
|
-
readonly brand = input('Semmet');
|
|
48
|
-
readonly brandHref = input('/');
|
|
49
|
-
readonly toggleLabel = input('Toggle navigation');
|
|
50
|
-
readonly items = input<readonly <%= classify(name) %>Item[]>(DEFAULT_ITEMS);
|
|
13
|
+
private readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
51
14
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
].filter(Boolean).join(' '));
|
|
15
|
+
readonly label = input('');
|
|
16
|
+
readonly expanded = model(false);
|
|
17
|
+
readonly menuId = computed(() => `${this.instanceId}-menu`);
|
|
56
18
|
|
|
57
|
-
|
|
19
|
+
toggle(): void {
|
|
58
20
|
this.expanded.update((value) => !value);
|
|
59
21
|
}
|
|
60
22
|
|
|
61
|
-
|
|
23
|
+
close(): void {
|
|
62
24
|
this.expanded.set(false);
|
|
63
25
|
}
|
|
64
26
|
}
|
|
27
|
+
|
|
28
|
+
@Directive({
|
|
29
|
+
selector: '[<%= camelize(name) %>Item]',
|
|
30
|
+
exportAs: '<%= camelize(name) %>Item',
|
|
31
|
+
host: {
|
|
32
|
+
'[attr.aria-current]': 'current() ? ariaCurrent() : null',
|
|
33
|
+
'[attr.aria-disabled]': 'disabled() ? "true" : null',
|
|
34
|
+
'(click)': 'nav.close()',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
export class <%= classify(name) %>Item {
|
|
38
|
+
protected readonly nav = inject(<%= classify(name) %>);
|
|
39
|
+
|
|
40
|
+
readonly current = input(false, { transform: booleanAttribute });
|
|
41
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
42
|
+
readonly ariaCurrent = input('page');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Directive({
|
|
46
|
+
selector: '[<%= camelize(name) %>Toggle]',
|
|
47
|
+
exportAs: '<%= camelize(name) %>Toggle',
|
|
48
|
+
host: {
|
|
49
|
+
'[attr.aria-expanded]': 'nav.expanded()',
|
|
50
|
+
'[attr.aria-controls]': 'nav.menuId()',
|
|
51
|
+
'(click)': 'nav.toggle()',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
export class <%= classify(name) %>Toggle {
|
|
55
|
+
protected readonly nav = inject(<%= classify(name) %>);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Directive({
|
|
59
|
+
selector: '[<%= camelize(name) %>Menu]',
|
|
60
|
+
exportAs: '<%= camelize(name) %>Menu',
|
|
61
|
+
host: {
|
|
62
|
+
'[id]': 'nav.menuId()',
|
|
63
|
+
'[hidden]': '!nav.expanded()',
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
export class <%= classify(name) %>Menu {
|
|
67
|
+
protected readonly nav = inject(<%= classify(name) %>);
|
|
68
|
+
}
|
package/src/navbar/schema.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "SemmetAngularNavbar",
|
|
4
4
|
"title": "Semmet Angular Navbar Options Schema",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"description": "Generates
|
|
6
|
+
"description": "Generates headless Angular Navbar directives (root, item, toggle, menu) for semantic navigation, responsive disclosure toggle, and current-page state, coordinated over consumer-owned markup.",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: block;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
ul {
|
|
6
|
-
display: flex;
|
|
7
|
-
align-items: center;
|
|
8
|
-
margin: 0;
|
|
9
|
-
padding: 0;
|
|
10
|
-
list-style: none;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
a,
|
|
14
|
-
button {
|
|
15
|
-
display: inline-flex;
|
|
16
|
-
align-items: center;
|
|
17
|
-
justify-content: center;
|
|
18
|
-
font: inherit;
|
|
19
|
-
color: inherit;
|
|
20
|
-
background: none;
|
|
21
|
-
text-decoration: none;
|
|
22
|
-
cursor: pointer;
|
|
23
|
-
box-sizing: border-box;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
a:focus-visible,
|
|
27
|
-
button:focus-visible {
|
|
28
|
-
outline: 2px solid currentColor;
|
|
29
|
-
outline-offset: 2px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
button:disabled {
|
|
33
|
-
opacity: 0.5;
|
|
34
|
-
cursor: not-allowed;
|
|
35
|
-
}
|
|
@@ -4,10 +4,10 @@ test.describe('<%= classify(name) %>', () => {
|
|
|
4
4
|
test('moves mounted pagination current page with next and previous controls', async ({ page }) => {
|
|
5
5
|
await page.goto('/');
|
|
6
6
|
|
|
7
|
-
const component = page.locator('<%=
|
|
7
|
+
const component = page.locator('[<%= camelize(name) %>]').first();
|
|
8
8
|
test.skip(
|
|
9
9
|
(await component.count()) === 0,
|
|
10
|
-
'Mount
|
|
10
|
+
'Mount an element with [<%= camelize(name) %>] in a route or host component to enable this generated e2e test.'
|
|
11
11
|
);
|
|
12
12
|
|
|
13
13
|
const current = component.locator('[aria-current="page"]').first();
|
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
<!-- Semmet Angular: Pagination — https://www.w3.org/TR/wai-aria-1.2/#aria-current -->
|
|
2
2
|
<!--
|
|
3
3
|
Usage from a parent component template:
|
|
4
|
-
Import <%= classify(name) %> in that parent component.
|
|
4
|
+
Import <%= classify(name) %> and <%= classify(name) %>Item in that parent component.
|
|
5
|
+
This directive set never owns markup: bring your own nav/ul/li structure, previous/next
|
|
6
|
+
controls and page links. Native `disabled`/`aria-label` on the previous/next buttons
|
|
7
|
+
already communicate everything they need — no directive required for those.
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
-->
|
|
8
|
-
<nav [attr.aria-label]="label()">
|
|
9
|
+
<nav <%= camelize(name) %> label="Search results pages">
|
|
9
10
|
<ul>
|
|
10
11
|
<li>
|
|
11
|
-
<button type="button" [disabled]="!hasPrevious()" (click)="goToPrevious()"
|
|
12
|
-
|
|
12
|
+
<button type="button" [disabled]="!hasPrevious()" (click)="goToPrevious()" aria-label="Previous page">
|
|
13
|
+
Previous
|
|
13
14
|
</button>
|
|
14
15
|
</li>
|
|
15
16
|
@for (page of pages; track page) {
|
|
16
17
|
<li>
|
|
17
|
-
<a
|
|
18
|
-
[href]="'?page=' + page"
|
|
19
|
-
[attr.aria-current]="page === currentPage() ? 'page' : null"
|
|
20
|
-
(click)="goToPage(page)"
|
|
21
|
-
>
|
|
18
|
+
<a <%= camelize(name) %>Item [href]="'?page=' + page" [current]="page === currentPage()" (click)="goToPage(page)">
|
|
22
19
|
{{ page }}
|
|
23
20
|
</a>
|
|
24
21
|
</li>
|
|
25
22
|
}
|
|
26
23
|
<li>
|
|
27
|
-
<button type="button" [disabled]="!hasNext()" (click)="goToNext()"
|
|
28
|
-
|
|
24
|
+
<button type="button" [disabled]="!hasNext()" (click)="goToNext()" aria-label="Next page">
|
|
25
|
+
Next
|
|
29
26
|
</button>
|
|
30
27
|
</li>
|
|
31
28
|
</ul>
|
|
32
29
|
</nav>
|
|
30
|
+
-->
|
|
@@ -1,25 +1,75 @@
|
|
|
1
|
+
import { Component, computed, signal } from '@angular/core';
|
|
1
2
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
import { <%= classify(name) %>, <%= classify(name) %>Item } from './<%= dasherize(name) %>';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
imports: [<%= classify(name) %>, <%= classify(name) %>Item],
|
|
8
|
+
template: `
|
|
9
|
+
<nav <%= camelize(name) %>>
|
|
10
|
+
<ul>
|
|
11
|
+
<li>
|
|
12
|
+
<button type="button" [disabled]="!hasPrevious()" (click)="goToPrevious()">Previous</button>
|
|
13
|
+
</li>
|
|
14
|
+
@for (page of pages; track page) {
|
|
15
|
+
<li>
|
|
16
|
+
<a <%= camelize(name) %>Item [href]="'?page=' + page" [current]="page === currentPage()" (click)="goToPage(page)">
|
|
17
|
+
{{ page }}
|
|
18
|
+
</a>
|
|
19
|
+
</li>
|
|
20
|
+
}
|
|
21
|
+
<li>
|
|
22
|
+
<button type="button" [disabled]="!hasNext()" (click)="goToNext()">Next</button>
|
|
23
|
+
</li>
|
|
24
|
+
</ul>
|
|
25
|
+
</nav>
|
|
26
|
+
`,
|
|
27
|
+
})
|
|
28
|
+
class HostComponent {
|
|
29
|
+
readonly pages = [1, 2, 3, 4, 5];
|
|
30
|
+
readonly currentPage = signal(1);
|
|
31
|
+
|
|
32
|
+
readonly hasPrevious = computed(() => this.currentPage() > 1);
|
|
33
|
+
readonly hasNext = computed(() => this.currentPage() < this.pages.length);
|
|
34
|
+
|
|
35
|
+
goToPage(page: number): void {
|
|
36
|
+
this.currentPage.set(page);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
goToPrevious(): void {
|
|
40
|
+
if (this.hasPrevious()) {
|
|
41
|
+
this.currentPage.update((page) => page - 1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
goToNext(): void {
|
|
46
|
+
if (this.hasNext()) {
|
|
47
|
+
this.currentPage.update((page) => page + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
3
51
|
|
|
4
52
|
describe('<%= classify(name) %>', () => {
|
|
5
|
-
let fixture: ComponentFixture
|
|
53
|
+
let fixture: ComponentFixture<HostComponent>;
|
|
54
|
+
let host: HTMLElement;
|
|
6
55
|
|
|
7
56
|
beforeEach(async () => {
|
|
8
|
-
await TestBed.configureTestingModule({ imports: [
|
|
9
|
-
fixture = TestBed.createComponent(
|
|
57
|
+
await TestBed.configureTestingModule({ imports: [HostComponent] }).compileComponents();
|
|
58
|
+
fixture = TestBed.createComponent(HostComponent);
|
|
10
59
|
fixture.detectChanges();
|
|
60
|
+
host = fixture.nativeElement as HTMLElement;
|
|
11
61
|
});
|
|
12
62
|
|
|
13
63
|
function buttons(): HTMLButtonElement[] {
|
|
14
|
-
return Array.from(
|
|
64
|
+
return Array.from(host.querySelectorAll('button'));
|
|
15
65
|
}
|
|
16
66
|
|
|
17
67
|
function links(): HTMLAnchorElement[] {
|
|
18
|
-
return Array.from(
|
|
68
|
+
return Array.from(host.querySelectorAll('a'));
|
|
19
69
|
}
|
|
20
70
|
|
|
21
|
-
it('
|
|
22
|
-
expect(
|
|
71
|
+
it('decorates consumer-owned markup with navigation and current-page state', () => {
|
|
72
|
+
expect(host.querySelector('nav')?.getAttribute('aria-label')).toBe('Pagination');
|
|
23
73
|
expect(buttons()[0].disabled).toBe(true);
|
|
24
74
|
expect(links()[0].getAttribute('aria-current')).toBe('page');
|
|
25
75
|
expect(buttons()[1].disabled).toBe(false);
|
|
@@ -1,37 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Directive, booleanAttribute, input } from '@angular/core';
|
|
2
2
|
|
|
3
|
-
@
|
|
4
|
-
selector: '<%=
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[<%= camelize(name) %>]',
|
|
5
|
+
exportAs: '<%= camelize(name) %>',
|
|
6
|
+
host: {
|
|
7
|
+
'[attr.aria-label]': 'label() || null',
|
|
8
|
+
},
|
|
8
9
|
})
|
|
9
10
|
export class <%= classify(name) %> {
|
|
10
11
|
readonly label = input('Pagination');
|
|
11
|
-
|
|
12
|
-
readonly previousAriaLabel = input('Previous page');
|
|
13
|
-
readonly nextLabel = input('Next');
|
|
14
|
-
readonly nextAriaLabel = input('Next page');
|
|
15
|
-
|
|
16
|
-
protected readonly pages = [1, 2, 3, 4, 5];
|
|
17
|
-
protected readonly currentPage = signal(1);
|
|
18
|
-
|
|
19
|
-
protected readonly hasPrevious = computed(() => this.currentPage() > 1);
|
|
20
|
-
protected readonly hasNext = computed(() => this.currentPage() < this.pages.length);
|
|
21
|
-
|
|
22
|
-
protected goToPage(page: number): void {
|
|
23
|
-
this.currentPage.set(page);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
protected goToPrevious(): void {
|
|
27
|
-
if (this.hasPrevious()) {
|
|
28
|
-
this.currentPage.update((page) => page - 1);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
12
|
+
}
|
|
31
13
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
14
|
+
@Directive({
|
|
15
|
+
selector: '[<%= camelize(name) %>Item]',
|
|
16
|
+
exportAs: '<%= camelize(name) %>Item',
|
|
17
|
+
host: {
|
|
18
|
+
'[attr.aria-current]': 'current() ? "page" : null',
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
export class <%= classify(name) %>Item {
|
|
22
|
+
readonly current = input(false, { transform: booleanAttribute });
|
|
37
23
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "SemmetAngularPagination",
|
|
4
4
|
"title": "Semmet Angular Pagination Options Schema",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"description": "Generates
|
|
6
|
+
"description": "Generates headless Angular Pagination directives (root, item) coordinated over consumer-owned markup, following W3C ARIA guidance on aria-current.",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -9,7 +9,7 @@ test.describe('<%= classify(name) %>', () => {
|
|
|
9
9
|
const table = component.locator('table').first();
|
|
10
10
|
await expect(table).toBeVisible();
|
|
11
11
|
await expect(table.locator('caption')).toBeVisible();
|
|
12
|
-
await expect(table.locator('th[scope="col"]')).toHaveCount(
|
|
12
|
+
await expect(table.locator('th[scope="col"]')).not.toHaveCount(0);
|
|
13
13
|
await expect(table.locator('tbody tr')).not.toHaveCount(0);
|
|
14
14
|
});
|
|
15
15
|
});
|
|
@@ -1,45 +1,33 @@
|
|
|
1
1
|
<!-- Semmet Angular: Table — https://www.w3.org/WAI/ARIA/apg/patterns/table/ -->
|
|
2
2
|
<!--
|
|
3
3
|
Usage from a parent component template:
|
|
4
|
-
Import <%= classify(name) %> in that parent component.
|
|
4
|
+
Import <%= classify(name) %> in that parent component. This component only owns the <table>
|
|
5
|
+
wrapper — caption, column count, row shape, and the empty state are entirely up to what you
|
|
6
|
+
project. A <caption> as the first projected child already gives the table its accessible name
|
|
7
|
+
natively; no aria-labelledby wiring needed.
|
|
5
8
|
|
|
6
|
-
<<%= selector %> caption="Quarterly revenue" [rows]="results" />
|
|
7
9
|
<<%= selector %>>
|
|
8
|
-
<
|
|
9
|
-
Quarterly results, fiscal year 2026
|
|
10
|
-
</ng-template>
|
|
11
|
-
</<%= selector %>>
|
|
12
|
-
-->
|
|
13
|
-
<table>
|
|
14
|
-
<caption>
|
|
15
|
-
@if (captionTemplate(); as captionSlot) {
|
|
16
|
-
<ng-container [ngTemplateOutlet]="captionSlot.templateRef" />
|
|
17
|
-
} @else {
|
|
18
|
-
{{ caption() }}
|
|
19
|
-
}
|
|
20
|
-
</caption>
|
|
10
|
+
<caption>Quarterly revenue, fiscal year 2026</caption>
|
|
21
11
|
<thead>
|
|
22
12
|
<tr>
|
|
23
|
-
<th scope="col">
|
|
24
|
-
<th scope="col">
|
|
13
|
+
<th scope="col">Quarter</th>
|
|
14
|
+
<th scope="col">Revenue</th>
|
|
25
15
|
</tr>
|
|
26
16
|
</thead>
|
|
27
17
|
<tbody>
|
|
28
|
-
@for (row of
|
|
18
|
+
@for (row of results; track row.quarter) {
|
|
29
19
|
<tr>
|
|
30
20
|
<th scope="row">{{ row.quarter }}</th>
|
|
31
21
|
<td>{{ row.revenue }}</td>
|
|
32
22
|
</tr>
|
|
33
23
|
} @empty {
|
|
34
24
|
<tr>
|
|
35
|
-
<td colspan="2">
|
|
36
|
-
@if (emptyTemplate(); as emptySlot) {
|
|
37
|
-
<ng-container [ngTemplateOutlet]="emptySlot.templateRef" />
|
|
38
|
-
} @else {
|
|
39
|
-
{{ emptyLabel() }}
|
|
40
|
-
}
|
|
41
|
-
</td>
|
|
25
|
+
<td colspan="2">No results available.</td>
|
|
42
26
|
</tr>
|
|
43
27
|
}
|
|
44
28
|
</tbody>
|
|
29
|
+
</<%= selector %>>
|
|
30
|
+
-->
|
|
31
|
+
<table>
|
|
32
|
+
<ng-content />
|
|
45
33
|
</table>
|
|
@@ -1,41 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Component, Directive, TemplateRef, contentChild, inject, input } from '@angular/core';
|
|
3
|
-
|
|
4
|
-
export interface <%= classify(name) %>Row {
|
|
5
|
-
quarter: string;
|
|
6
|
-
revenue: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
@Directive({
|
|
10
|
-
selector: 'ng-template[<%= camelize(name) %>Caption]',
|
|
11
|
-
})
|
|
12
|
-
export class <%= classify(name) %>Caption {
|
|
13
|
-
readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
@Directive({
|
|
17
|
-
selector: 'ng-template[<%= camelize(name) %>Empty]',
|
|
18
|
-
})
|
|
19
|
-
export class <%= classify(name) %>Empty {
|
|
20
|
-
readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
|
|
21
|
-
}
|
|
1
|
+
import { Component } from '@angular/core';
|
|
22
2
|
|
|
23
3
|
@Component({
|
|
24
4
|
selector: '<%= selector %>',
|
|
25
|
-
imports: [
|
|
5
|
+
imports: [],
|
|
26
6
|
templateUrl: './<%= dasherize(name) %>.html',
|
|
27
7
|
styleUrl: './<%= dasherize(name) %>.css',
|
|
28
8
|
})
|
|
29
|
-
export class <%= classify(name) %> {
|
|
30
|
-
protected readonly captionTemplate = contentChild(<%= classify(name) %>Caption);
|
|
31
|
-
protected readonly emptyTemplate = contentChild(<%= classify(name) %>Empty);
|
|
32
|
-
|
|
33
|
-
readonly caption = input('Quarterly results');
|
|
34
|
-
readonly quarterHeader = input('Quarter');
|
|
35
|
-
readonly revenueHeader = input('Revenue');
|
|
36
|
-
readonly emptyLabel = input('No results available.');
|
|
37
|
-
readonly rows = input<readonly <%= classify(name) %>Row[]>([
|
|
38
|
-
{ quarter: 'Q1', revenue: '$12,000' },
|
|
39
|
-
{ quarter: 'Q2', revenue: '$15,500' },
|
|
40
|
-
]);
|
|
41
|
-
}
|
|
9
|
+
export class <%= classify(name) %> {}
|
package/src/table/schema.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "SemmetAngularTable",
|
|
4
4
|
"title": "Semmet Angular Table Options Schema",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"description": "Generates a standalone Angular
|
|
6
|
+
"description": "Generates a minimal standalone Angular table wrapper (a bare <table> with content projection) — caption, column count, and row shape are entirely up to what you project, following the W3C ARIA Authoring Practices Guide.",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: block;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
[role='tablist'] {
|
|
6
|
-
display: flex;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
[role='tab'] {
|
|
10
|
-
position: relative;
|
|
11
|
-
font: inherit;
|
|
12
|
-
background: none;
|
|
13
|
-
border: none;
|
|
14
|
-
cursor: pointer;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
[role='tab']:focus-visible {
|
|
18
|
-
outline: 2px solid currentColor;
|
|
19
|
-
outline-offset: 2px;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
[role='tab']::after {
|
|
23
|
-
content: '';
|
|
24
|
-
position: absolute;
|
|
25
|
-
left: 0;
|
|
26
|
-
right: 0;
|
|
27
|
-
bottom: -1px;
|
|
28
|
-
height: 2px;
|
|
29
|
-
background: currentColor;
|
|
30
|
-
transform: scaleX(0);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
[role='tab'][aria-selected='true']::after {
|
|
34
|
-
transform: scaleX(1);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
[role='tabpanel']:focus-visible {
|
|
38
|
-
outline: 2px solid currentColor;
|
|
39
|
-
outline-offset: 2px;
|
|
40
|
-
}
|
|
@@ -4,10 +4,10 @@ test.describe('<%= classify(name) %>', () => {
|
|
|
4
4
|
test('switches mounted tabs through click and keyboard navigation', async ({ page }) => {
|
|
5
5
|
await page.goto('/');
|
|
6
6
|
|
|
7
|
-
const component = page.locator('<%=
|
|
7
|
+
const component = page.locator('[<%= camelize(name) %>]').first();
|
|
8
8
|
test.skip(
|
|
9
9
|
(await component.count()) === 0,
|
|
10
|
-
'Mount
|
|
10
|
+
'Mount an element with [<%= camelize(name) %>] in a route or host component to enable this generated e2e test.'
|
|
11
11
|
);
|
|
12
12
|
|
|
13
13
|
await expect(component).toBeVisible();
|
|
@@ -1,48 +1,25 @@
|
|
|
1
1
|
<!-- Semmet Angular: Tabs — https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ -->
|
|
2
2
|
<!--
|
|
3
3
|
Usage from a parent component template:
|
|
4
|
-
Import <%= classify(name) %>
|
|
4
|
+
Import <%= classify(name) %>, <%= classify(name) %>List, <%= classify(name) %>Tab and <%= classify(name) %>Panel
|
|
5
|
+
in that parent component. This directive set never owns markup: bring your own wrapper, tab strip,
|
|
6
|
+
buttons and panels, in whatever structure the layout needs.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
8
|
+
<div <%= camelize(name) %> [(activeId)]="activeTab">
|
|
9
|
+
<div <%= camelize(name) %>List label="Account">
|
|
10
|
+
<button type="button" <%= camelize(name) %>Tab value="profile" [selected]="true">Profile</button>
|
|
11
|
+
<button type="button" <%= camelize(name) %>Tab value="settings">Settings</button>
|
|
12
|
+
<button type="button" <%= camelize(name) %>Tab value="billing" [disabled]="true">Billing</button>
|
|
13
|
+
</div>
|
|
10
14
|
|
|
11
|
-
<
|
|
15
|
+
<div <%= camelize(name) %>Panel value="profile">
|
|
16
|
+
<app-profile-panel />
|
|
17
|
+
</div>
|
|
18
|
+
<div <%= camelize(name) %>Panel value="settings">
|
|
12
19
|
<app-settings-panel [userId]="userId()" (saved)="reload()" />
|
|
13
|
-
</ng-template>
|
|
14
|
-
</<%= selector %>>
|
|
15
|
-
|
|
16
|
-
Set [preserveContent]="false" on <<%= selector %>> to destroy inactive tab content.
|
|
17
|
-
-->
|
|
18
|
-
<div role="tablist" [attr.aria-label]="label()">
|
|
19
|
-
@for (item of items(); track item.id(); let i = $index) {
|
|
20
|
-
<button
|
|
21
|
-
#tabButton
|
|
22
|
-
type="button"
|
|
23
|
-
role="tab"
|
|
24
|
-
[disabled]="item.disabled()"
|
|
25
|
-
[attr.aria-selected]="isActive(item.id())"
|
|
26
|
-
[attr.aria-controls]="instanceId + '-panel-' + item.id()"
|
|
27
|
-
[id]="instanceId + '-tab-' + item.id()"
|
|
28
|
-
[tabIndex]="isActive(item.id()) ? 0 : -1"
|
|
29
|
-
(click)="select(item)"
|
|
30
|
-
(keydown)="navigateTabs($event, i)"
|
|
31
|
-
>
|
|
32
|
-
{{ item.label() }}
|
|
33
|
-
</button>
|
|
34
|
-
}
|
|
35
|
-
</div>
|
|
36
|
-
@for (item of items(); track item.id()) {
|
|
37
|
-
<div
|
|
38
|
-
[id]="instanceId + '-panel-' + item.id()"
|
|
39
|
-
role="tabpanel"
|
|
40
|
-
tabindex="0"
|
|
41
|
-
[attr.aria-labelledby]="instanceId + '-tab-' + item.id()"
|
|
42
|
-
[hidden]="!isActive(item.id())"
|
|
43
|
-
>
|
|
44
|
-
@if (shouldRender(item.id())) {
|
|
45
|
-
<ng-container [ngTemplateOutlet]="item.templateRef" />
|
|
46
|
-
}
|
|
47
20
|
</div>
|
|
48
|
-
|
|
21
|
+
<div <%= camelize(name) %>Panel value="billing">
|
|
22
|
+
<app-billing-panel />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
-->
|