@rabassoft/schema-engine-angular-aria 0.1.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/LICENSE +661 -0
- package/NOTICE.md +12 -0
- package/README.md +70 -0
- package/SOURCE.md +33 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/source-build/package.json +25 -0
- package/source-build/pnpm-lock.yaml +672 -0
- package/source-build/prepare-schema-engine.mjs +49 -0
- package/source-build/tsconfig.json +34 -0
- package/src/index.ts +254 -0
- package/styles.css +63 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const sourceBuildDirectory = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const pilotPackage = resolve(sourceBuildDirectory, '..');
|
|
13
|
+
const packageRoots = Object.freeze([
|
|
14
|
+
{
|
|
15
|
+
directory: resolve(sourceBuildDirectory, '../../../core/package'),
|
|
16
|
+
name: '@rabassoft/schema-engine',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
directory: resolve(sourceBuildDirectory, '../../../angular/package'),
|
|
20
|
+
name: '@rabassoft/schema-engine-angular',
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
for (const { directory, name } of packageRoots) {
|
|
25
|
+
const manifest = JSON.parse(
|
|
26
|
+
readFileSync(join(directory, 'package.json'), 'utf8'),
|
|
27
|
+
);
|
|
28
|
+
if (manifest.name !== name)
|
|
29
|
+
throw new Error(`Unexpected sibling package: ${name}`);
|
|
30
|
+
const target = join(pilotPackage, 'node_modules', ...name.split('/'));
|
|
31
|
+
rmSync(target, { recursive: true, force: true });
|
|
32
|
+
mkdirSync(target, { recursive: true });
|
|
33
|
+
cpSync(join(directory, 'rebuilt-dist'), join(target, 'dist'), {
|
|
34
|
+
recursive: true,
|
|
35
|
+
});
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(target, 'package.json'),
|
|
38
|
+
`${JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
name: manifest.name,
|
|
41
|
+
version: manifest.version,
|
|
42
|
+
type: manifest.type,
|
|
43
|
+
exports: manifest.exports,
|
|
44
|
+
},
|
|
45
|
+
null,
|
|
46
|
+
2,
|
|
47
|
+
)}\n`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowUnreachableCode": false,
|
|
4
|
+
"allowUnusedLabels": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"exactOptionalPropertyTypes": true,
|
|
8
|
+
"experimentalDecorators": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"lib": ["ES2022", "DOM"],
|
|
11
|
+
"module": "NodeNext",
|
|
12
|
+
"moduleResolution": "NodeNext",
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noImplicitOverride": true,
|
|
15
|
+
"noImplicitReturns": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"outDir": "../rebuilt-dist",
|
|
20
|
+
"rootDir": "../src",
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"strict": true,
|
|
23
|
+
"target": "ES2022",
|
|
24
|
+
"useDefineForClassFields": false,
|
|
25
|
+
"useUnknownInCatchVariables": true,
|
|
26
|
+
"verbatimModuleSyntax": true
|
|
27
|
+
},
|
|
28
|
+
"angularCompilerOptions": {
|
|
29
|
+
"compilationMode": "partial",
|
|
30
|
+
"strictInjectionParameters": true,
|
|
31
|
+
"strictTemplates": true
|
|
32
|
+
},
|
|
33
|
+
"include": ["../src/**/*.ts"]
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Copyright (C) 2026 Ricardo Rabassó Rodríguez, operating as Rabassoft
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ChangeDetectionStrategy,
|
|
6
|
+
Component,
|
|
7
|
+
input,
|
|
8
|
+
makeEnvironmentProviders,
|
|
9
|
+
signal,
|
|
10
|
+
type EnvironmentProviders,
|
|
11
|
+
type OnInit,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { Tab, TabList, TabPanel, Tabs } from '@angular/aria/tabs';
|
|
14
|
+
import {
|
|
15
|
+
SchemaPresentationEntryOutletComponent,
|
|
16
|
+
SchemaPresentationPanelOutletComponent,
|
|
17
|
+
provideSchemaPresentationContainer,
|
|
18
|
+
type AngularPresentationContainerDefinition,
|
|
19
|
+
type AngularPresentationContainerRegistration,
|
|
20
|
+
type AngularPresentationContainerRenderer,
|
|
21
|
+
type AngularPresentationContainerRenderModel,
|
|
22
|
+
} from '@rabassoft/schema-engine-angular';
|
|
23
|
+
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'schema-aria-presentation-section',
|
|
26
|
+
standalone: true,
|
|
27
|
+
imports: [SchemaPresentationEntryOutletComponent],
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
template: `
|
|
30
|
+
@if (section(); as model) {
|
|
31
|
+
<fieldset>
|
|
32
|
+
<legend [id]="model.legendId">{{ model.label }}</legend>
|
|
33
|
+
@for (
|
|
34
|
+
child of model.definition.children;
|
|
35
|
+
track child.kind === 'form-node' ? child.node.key : child.key
|
|
36
|
+
) {
|
|
37
|
+
<schema-presentation-entry-outlet [entry]="child" />
|
|
38
|
+
}
|
|
39
|
+
</fieldset>
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
})
|
|
43
|
+
class AngularAriaPresentationSectionComponent implements AngularPresentationContainerRenderer {
|
|
44
|
+
readonly presentation =
|
|
45
|
+
input.required<AngularPresentationContainerRenderModel>();
|
|
46
|
+
protected readonly section = () => {
|
|
47
|
+
const model = this.presentation();
|
|
48
|
+
return model.kind === 'section' ? model : undefined;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Component({
|
|
53
|
+
selector: 'schema-aria-presentation-tabs',
|
|
54
|
+
standalone: true,
|
|
55
|
+
imports: [
|
|
56
|
+
Tabs,
|
|
57
|
+
TabList,
|
|
58
|
+
Tab,
|
|
59
|
+
TabPanel,
|
|
60
|
+
SchemaPresentationPanelOutletComponent,
|
|
61
|
+
],
|
|
62
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
63
|
+
template: `
|
|
64
|
+
@if (tabs(); as model) {
|
|
65
|
+
<div ngTabs>
|
|
66
|
+
<div
|
|
67
|
+
ngTabList
|
|
68
|
+
orientation="horizontal"
|
|
69
|
+
focusMode="roving"
|
|
70
|
+
selectionMode="follow"
|
|
71
|
+
[wrap]="true"
|
|
72
|
+
[(selectedTab)]="selectedTab"
|
|
73
|
+
[id]="model.tablistId"
|
|
74
|
+
[attr.aria-label]="model.label"
|
|
75
|
+
>
|
|
76
|
+
@for (panel of model.panels; track panel.definition.key) {
|
|
77
|
+
<button
|
|
78
|
+
ngTab
|
|
79
|
+
type="button"
|
|
80
|
+
[id]="panel.tabId"
|
|
81
|
+
[value]="panel.tabpanelId"
|
|
82
|
+
>
|
|
83
|
+
{{ panel.label }}
|
|
84
|
+
</button>
|
|
85
|
+
}
|
|
86
|
+
</div>
|
|
87
|
+
@for (panel of model.panels; track panel.definition.key) {
|
|
88
|
+
<div
|
|
89
|
+
ngTabPanel
|
|
90
|
+
[preserveContent]="true"
|
|
91
|
+
[id]="panel.tabpanelId"
|
|
92
|
+
[value]="panel.tabpanelId"
|
|
93
|
+
[hidden]="selectedTab() !== panel.tabpanelId"
|
|
94
|
+
[attr.inert]="selectedTab() !== panel.tabpanelId ? '' : null"
|
|
95
|
+
>
|
|
96
|
+
<schema-presentation-panel-outlet [panel]="panel.definition" />
|
|
97
|
+
</div>
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
}
|
|
101
|
+
`,
|
|
102
|
+
})
|
|
103
|
+
class AngularAriaPresentationTabsComponent
|
|
104
|
+
implements AngularPresentationContainerRenderer, OnInit
|
|
105
|
+
{
|
|
106
|
+
readonly presentation =
|
|
107
|
+
input.required<AngularPresentationContainerRenderModel>();
|
|
108
|
+
protected readonly selectedTab = signal<string | undefined>(undefined);
|
|
109
|
+
protected readonly tabs = () => {
|
|
110
|
+
const model = this.presentation();
|
|
111
|
+
return model.kind === 'tabs' ? model : undefined;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
ngOnInit(): void {
|
|
115
|
+
this.selectedTab.set(this.tabs()?.panels[0]?.tabpanelId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Component({
|
|
120
|
+
selector: 'schema-aria-presentation-accordion',
|
|
121
|
+
standalone: true,
|
|
122
|
+
imports: [SchemaPresentationPanelOutletComponent],
|
|
123
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
124
|
+
template: `
|
|
125
|
+
@if (accordion(); as model) {
|
|
126
|
+
<div
|
|
127
|
+
role="group"
|
|
128
|
+
[id]="model.accordionId"
|
|
129
|
+
[attr.aria-label]="model.label"
|
|
130
|
+
>
|
|
131
|
+
@for (
|
|
132
|
+
panel of model.panels;
|
|
133
|
+
track panel.definition.key;
|
|
134
|
+
let index = $index
|
|
135
|
+
) {
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
[id]="panel.triggerId"
|
|
139
|
+
[attr.aria-expanded]="expanded().has(index)"
|
|
140
|
+
[attr.aria-controls]="panel.regionId"
|
|
141
|
+
(click)="toggle(index)"
|
|
142
|
+
>
|
|
143
|
+
{{ panel.label }}
|
|
144
|
+
</button>
|
|
145
|
+
<div
|
|
146
|
+
role="region"
|
|
147
|
+
[id]="panel.regionId"
|
|
148
|
+
[attr.aria-labelledby]="panel.triggerId"
|
|
149
|
+
[hidden]="!expanded().has(index)"
|
|
150
|
+
[attr.inert]="!expanded().has(index) ? '' : null"
|
|
151
|
+
>
|
|
152
|
+
<schema-presentation-panel-outlet [panel]="panel.definition" />
|
|
153
|
+
</div>
|
|
154
|
+
}
|
|
155
|
+
</div>
|
|
156
|
+
}
|
|
157
|
+
`,
|
|
158
|
+
})
|
|
159
|
+
class AngularAriaPresentationAccordionComponent implements AngularPresentationContainerRenderer {
|
|
160
|
+
readonly presentation =
|
|
161
|
+
input.required<AngularPresentationContainerRenderModel>();
|
|
162
|
+
protected readonly expanded = signal<ReadonlySet<number>>(new Set());
|
|
163
|
+
protected readonly accordion = () => {
|
|
164
|
+
const model = this.presentation();
|
|
165
|
+
return model.kind === 'accordion' ? model : undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
protected toggle(index: number): void {
|
|
169
|
+
const next = new Set(this.expanded());
|
|
170
|
+
if (next.has(index)) next.delete(index);
|
|
171
|
+
else next.add(index);
|
|
172
|
+
this.expanded.set(next);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Component({
|
|
177
|
+
selector: 'schema-aria-presentation-grid',
|
|
178
|
+
standalone: true,
|
|
179
|
+
imports: [SchemaPresentationEntryOutletComponent],
|
|
180
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
181
|
+
template: `
|
|
182
|
+
@if (grid(); as model) {
|
|
183
|
+
<div
|
|
184
|
+
class="se-aria-grid"
|
|
185
|
+
role="group"
|
|
186
|
+
[id]="model.gridId"
|
|
187
|
+
[attr.aria-label]="model.label"
|
|
188
|
+
[style.--se-aria-grid-columns]="model.definition.columns"
|
|
189
|
+
>
|
|
190
|
+
@for (item of model.items; track item.definition.key) {
|
|
191
|
+
<div
|
|
192
|
+
class="se-aria-grid-cell"
|
|
193
|
+
[id]="item.cellId"
|
|
194
|
+
[style.--se-aria-grid-span]="item.definition.span"
|
|
195
|
+
>
|
|
196
|
+
<schema-presentation-entry-outlet [entry]="item.definition.child" />
|
|
197
|
+
</div>
|
|
198
|
+
}
|
|
199
|
+
</div>
|
|
200
|
+
}
|
|
201
|
+
`,
|
|
202
|
+
})
|
|
203
|
+
class AngularAriaPresentationGridComponent implements AngularPresentationContainerRenderer {
|
|
204
|
+
readonly presentation =
|
|
205
|
+
input.required<AngularPresentationContainerRenderModel>();
|
|
206
|
+
protected readonly grid = () => {
|
|
207
|
+
const model = this.presentation();
|
|
208
|
+
return model.kind === 'grid' ? model : undefined;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const registrations: readonly AngularPresentationContainerRegistration[] =
|
|
213
|
+
Object.freeze([
|
|
214
|
+
registration(
|
|
215
|
+
'angular-aria-section',
|
|
216
|
+
AngularAriaPresentationSectionComponent,
|
|
217
|
+
'section',
|
|
218
|
+
),
|
|
219
|
+
registration(
|
|
220
|
+
'angular-aria-tabs',
|
|
221
|
+
AngularAriaPresentationTabsComponent,
|
|
222
|
+
'tabs',
|
|
223
|
+
),
|
|
224
|
+
registration(
|
|
225
|
+
'angular-aria-accordion',
|
|
226
|
+
AngularAriaPresentationAccordionComponent,
|
|
227
|
+
'accordion',
|
|
228
|
+
),
|
|
229
|
+
registration(
|
|
230
|
+
'angular-aria-grid',
|
|
231
|
+
AngularAriaPresentationGridComponent,
|
|
232
|
+
'grid',
|
|
233
|
+
),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
function registration(
|
|
237
|
+
id: string,
|
|
238
|
+
renderer: AngularPresentationContainerRegistration['renderer'],
|
|
239
|
+
kind: AngularPresentationContainerDefinition['kind'],
|
|
240
|
+
): AngularPresentationContainerRegistration {
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
id,
|
|
243
|
+
renderer,
|
|
244
|
+
tester: (definition: AngularPresentationContainerDefinition) =>
|
|
245
|
+
definition.kind === kind ? 10 : null,
|
|
246
|
+
priority: 0,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function provideSchemaEngineAngularAriaContainers(): EnvironmentProviders {
|
|
251
|
+
return makeEnvironmentProviders(
|
|
252
|
+
registrations.map((value) => provideSchemaPresentationContainer(value)),
|
|
253
|
+
);
|
|
254
|
+
}
|
package/styles.css
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Ricardo Rabassó Rodríguez, operating as Rabassoft */
|
|
2
|
+
/* SPDX-License-Identifier: AGPL-3.0-only */
|
|
3
|
+
|
|
4
|
+
schema-aria-presentation-section,
|
|
5
|
+
schema-aria-presentation-tabs,
|
|
6
|
+
schema-aria-presentation-accordion,
|
|
7
|
+
schema-aria-presentation-grid {
|
|
8
|
+
--se-aria-container-surface: Canvas;
|
|
9
|
+
--se-aria-container-text: CanvasText;
|
|
10
|
+
--se-aria-container-border: currentColor;
|
|
11
|
+
--se-aria-container-accent: LinkText;
|
|
12
|
+
--se-aria-container-radius: 0.5rem;
|
|
13
|
+
--se-aria-container-gap: 1rem;
|
|
14
|
+
|
|
15
|
+
display: block;
|
|
16
|
+
color: var(--se-aria-container-text);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
schema-aria-presentation-section fieldset,
|
|
20
|
+
schema-aria-presentation-tabs [ngtabs],
|
|
21
|
+
schema-aria-presentation-accordion > [role='group'],
|
|
22
|
+
schema-aria-presentation-grid .se-aria-grid {
|
|
23
|
+
padding: var(--se-aria-container-gap);
|
|
24
|
+
border: 1px solid var(--se-aria-container-border);
|
|
25
|
+
border-radius: var(--se-aria-container-radius);
|
|
26
|
+
background: var(--se-aria-container-surface);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
schema-aria-presentation-tabs [ngtablist] {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
gap: var(--se-aria-container-gap);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
schema-aria-presentation-tabs [ngtab][aria-selected='true'] {
|
|
36
|
+
color: var(--se-aria-container-accent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
schema-aria-presentation-accordion > [role='group'] {
|
|
40
|
+
display: grid;
|
|
41
|
+
gap: var(--se-aria-container-gap);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
schema-aria-presentation-grid .se-aria-grid {
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: repeat(var(--se-aria-grid-columns), minmax(0, 1fr));
|
|
47
|
+
gap: var(--se-aria-container-gap);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
schema-aria-presentation-grid .se-aria-grid-cell {
|
|
51
|
+
min-width: 0;
|
|
52
|
+
grid-column: span var(--se-aria-grid-span);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (max-width: 40rem) {
|
|
56
|
+
schema-aria-presentation-grid .se-aria-grid {
|
|
57
|
+
grid-template-columns: minmax(0, 1fr);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
schema-aria-presentation-grid .se-aria-grid-cell {
|
|
61
|
+
grid-column: span 1;
|
|
62
|
+
}
|
|
63
|
+
}
|