ng-comps 1.0.0 → 1.0.2
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/angular.json +9 -8
- package/documentation.json +32 -32
- package/package.json +1 -1
- package/scripts/prepare-package.mjs +38 -19
- package/src/app/components/alert/mf-alert.component.spec.ts +2 -2
- package/src/app/components/alert/mf-alert.component.ts +1 -1
- package/src/app/components/chip/mf-chip.component.ts +1 -1
- package/src/app/components/datepicker/mf-datepicker.component.spec.ts +4 -4
- package/src/app/components/datepicker/mf-datepicker.component.ts +1 -1
- package/src/app/components/dialog/mf-dialog.component.ts +1 -1
- package/src/app/components/dialog/mf-dialog.service.spec.ts +2 -2
- package/src/app/components/menu/mf-menu.component.spec.ts +1 -1
- package/src/app/components/menu/mf-menu.component.ts +1 -1
- package/src/app/components/progress-bar/mf-progress-bar.component.spec.ts +2 -2
- package/src/app/components/progress-spinner/mf-progress-spinner.component.spec.ts +3 -3
- package/src/app/components/sidenav/mf-sidenav.component.ts +2 -2
- package/src/app/components/table/mf-table.component.ts +1 -1
- package/src/stories/About.mdx +72 -72
- package/src/stories/Welcome.mdx +26 -27
- package/src/stories/mf-a11y-contracts.stories.ts +49 -49
- package/src/stories/mf-autocomplete.stories.ts +194 -188
- package/src/stories/mf-button.stories.ts +152 -156
- package/src/stories/mf-card.stories.ts +147 -148
- package/src/stories/mf-checkbox.stories.ts +88 -88
- package/src/stories/mf-datepicker.stories.ts +118 -118
- package/src/stories/mf-dialog.stories.ts +159 -167
- package/src/stories/mf-form-field.stories.ts +108 -108
- package/src/stories/mf-grid-list.stories.ts +104 -105
- package/src/stories/mf-icon.stories.ts +133 -130
- package/src/stories/mf-input.stories.ts +158 -158
- package/src/stories/mf-menu.stories.ts +10 -10
- package/src/stories/mf-progress-bar.stories.ts +119 -119
- package/src/stories/mf-progress-spinner.stories.ts +124 -124
- package/src/stories/mf-radio-button.stories.ts +111 -111
- package/src/stories/mf-select.stories.ts +184 -178
- package/src/stories/mf-sidenav.stories.ts +331 -334
- package/src/stories/mf-table.stories.ts +13 -13
- package/src/stories/mf-toolbar.stories.ts +112 -112
|
@@ -31,7 +31,7 @@ export type MfChipColor = 'brand' | 'accent' | 'error' | 'neutral';
|
|
|
31
31
|
<mat-icon matChipAvatar aria-hidden="true">{{ leadingIcon() }}</mat-icon>
|
|
32
32
|
}
|
|
33
33
|
{{ label() }}
|
|
34
|
-
<button matChipRemove [attr.aria-label]="'
|
|
34
|
+
<button matChipRemove [attr.aria-label]="'Remove ' + label()">
|
|
35
35
|
<mat-icon>cancel</mat-icon>
|
|
36
36
|
</button>
|
|
37
37
|
</mat-chip>
|
|
@@ -41,19 +41,19 @@ describe('MfDatepickerComponent', () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it('should apply error class when error is provided', () => {
|
|
44
|
-
fixture.componentRef.setInput('error', '
|
|
44
|
+
fixture.componentRef.setInput('error', 'Date required');
|
|
45
45
|
expect(component.hostClasses()).toContain('mf-datepicker--error');
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it('should render label when provided', () => {
|
|
49
|
-
fixture.componentRef.setInput('label', '
|
|
49
|
+
fixture.componentRef.setInput('label', 'Start date');
|
|
50
50
|
fixture.detectChanges();
|
|
51
51
|
const label = fixture.nativeElement.querySelector('mat-label');
|
|
52
|
-
expect(label?.textContent).toContain('
|
|
52
|
+
expect(label?.textContent).toContain('Start date');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it('should render hint when provided', () => {
|
|
56
|
-
fixture.componentRef.setInput('hint', '
|
|
56
|
+
fixture.componentRef.setInput('hint', 'Select a date');
|
|
57
57
|
fixture.detectChanges();
|
|
58
58
|
const hint = fixture.nativeElement.querySelector('mat-hint');
|
|
59
59
|
expect(hint).toBeTruthy();
|
|
@@ -145,7 +145,7 @@ export class MfDatepickerComponent implements ControlValueAccessor {
|
|
|
145
145
|
/** Ancho completo */
|
|
146
146
|
readonly fullWidth = input(false);
|
|
147
147
|
/** Etiqueta accesible del botón para abrir el calendario */
|
|
148
|
-
readonly toggleAriaLabel = input('
|
|
148
|
+
readonly toggleAriaLabel = input('Open calendar');
|
|
149
149
|
|
|
150
150
|
readonly mfChange = output<Date | null>();
|
|
151
151
|
readonly mfBlur = output<void>();
|
|
@@ -113,7 +113,7 @@ export class MfDialogComponent {
|
|
|
113
113
|
/** Rol del diálogo */
|
|
114
114
|
readonly role = input<'dialog' | 'alertdialog'>('dialog');
|
|
115
115
|
/** Etiqueta accesible del botón de cierre */
|
|
116
|
-
readonly closeButtonLabel = input('
|
|
116
|
+
readonly closeButtonLabel = input('Close dialog');
|
|
117
117
|
|
|
118
118
|
readonly mfClose = output<void>();
|
|
119
119
|
|
|
@@ -29,7 +29,7 @@ describe('MfDialogService', () => {
|
|
|
29
29
|
class DialogContentComponent {}
|
|
30
30
|
|
|
31
31
|
service.open(DialogContentComponent, {
|
|
32
|
-
ariaLabel: '
|
|
32
|
+
ariaLabel: 'Delete project',
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
expect(dialogSpy.open).toHaveBeenCalledWith(
|
|
@@ -38,7 +38,7 @@ describe('MfDialogService', () => {
|
|
|
38
38
|
role: 'dialog',
|
|
39
39
|
autoFocus: 'first-tabbable',
|
|
40
40
|
restoreFocus: true,
|
|
41
|
-
ariaLabel: '
|
|
41
|
+
ariaLabel: 'Delete project',
|
|
42
42
|
panelClass: ['mf-dialog-panel'],
|
|
43
43
|
}),
|
|
44
44
|
);
|
|
@@ -37,7 +37,7 @@ describe('MfMenuComponent', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('should have accessible trigger label', () => {
|
|
40
|
-
expect(component.triggerLabel()).toBe('
|
|
40
|
+
expect(component.triggerLabel()).toBe('Open menu');
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it('should emit item click', () => {
|
|
@@ -57,7 +57,7 @@ export class MfMenuComponent {
|
|
|
57
57
|
/** Icono del trigger */
|
|
58
58
|
readonly triggerIcon = input<string>('more_vert');
|
|
59
59
|
/** Label accesible del trigger */
|
|
60
|
-
readonly triggerLabel = input<string>('
|
|
60
|
+
readonly triggerLabel = input<string>('Open menu');
|
|
61
61
|
|
|
62
62
|
readonly mfItemClick = output<string>();
|
|
63
63
|
|
|
@@ -40,10 +40,10 @@ describe('MfProgressBarComponent', () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it('should render label when provided', () => {
|
|
43
|
-
fixture.componentRef.setInput('label', '
|
|
43
|
+
fixture.componentRef.setInput('label', 'Loading...');
|
|
44
44
|
fixture.detectChanges();
|
|
45
45
|
const label = fixture.nativeElement.querySelector('.mf-progress-bar__label');
|
|
46
|
-
expect(label?.textContent).toContain('
|
|
46
|
+
expect(label?.textContent).toContain('Loading...');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('should show percentage when showValue is true and mode is determinate', () => {
|
|
@@ -40,10 +40,10 @@ describe('MfProgressSpinnerComponent', () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it('should render label when provided', () => {
|
|
43
|
-
fixture.componentRef.setInput('label', '
|
|
43
|
+
fixture.componentRef.setInput('label', 'Processing...');
|
|
44
44
|
fixture.detectChanges();
|
|
45
45
|
const label = fixture.nativeElement.querySelector('.mf-progress-spinner__label');
|
|
46
|
-
expect(label?.textContent).toContain('
|
|
46
|
+
expect(label?.textContent).toContain('Processing...');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('should not render label when not provided', () => {
|
|
@@ -53,7 +53,7 @@ describe('MfProgressSpinnerComponent', () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it('should add labeled class when label is provided', () => {
|
|
56
|
-
fixture.componentRef.setInput('label', '
|
|
56
|
+
fixture.componentRef.setInput('label', 'Loading');
|
|
57
57
|
expect(component.wrapperClasses()).toContain('mf-progress-spinner__wrapper--labeled');
|
|
58
58
|
});
|
|
59
59
|
});
|
|
@@ -74,7 +74,7 @@ export interface MfSidenavNavItem {
|
|
|
74
74
|
<mat-icon class="mf-sidenav__item-icon" aria-hidden="true">{{ item.icon }}</mat-icon>
|
|
75
75
|
<span class="mf-sidenav__item-label">{{ item.label }}</span>
|
|
76
76
|
@if (item.badge && item.badge > 0) {
|
|
77
|
-
<span class="mf-sidenav__item-badge" aria-label="{{ item.badge }}
|
|
77
|
+
<span class="mf-sidenav__item-badge" aria-label="{{ item.badge }} notifications">
|
|
78
78
|
{{ item.badge > 99 ? '99+' : item.badge }}
|
|
79
79
|
</span>
|
|
80
80
|
}
|
|
@@ -112,7 +112,7 @@ export class MfSidenavComponent {
|
|
|
112
112
|
/** Icono Material de la cabecera */
|
|
113
113
|
readonly headerIcon = input<string | undefined>(undefined);
|
|
114
114
|
/** Aria-label del elemento nav */
|
|
115
|
-
readonly navAriaLabel = input('
|
|
115
|
+
readonly navAriaLabel = input('Primary navigation');
|
|
116
116
|
|
|
117
117
|
readonly mfOpenedChange = output<boolean>();
|
|
118
118
|
/** Emite el ítem de navegación pulsado */
|
|
@@ -84,7 +84,7 @@ export class MfTableComponent {
|
|
|
84
84
|
/** Texto visible del botón de acción por fila */
|
|
85
85
|
readonly rowActionLabel = input<string | undefined>(undefined);
|
|
86
86
|
/** Cabecera visible de la columna de acción */
|
|
87
|
-
readonly rowActionHeader = input('
|
|
87
|
+
readonly rowActionHeader = input('Actions');
|
|
88
88
|
readonly rowActionAriaLabel = input<
|
|
89
89
|
((row: Record<string, unknown>) => string) | undefined
|
|
90
90
|
>(undefined);
|
package/src/stories/About.mdx
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
-
|
|
3
|
-
<Meta title="About" />
|
|
4
|
-
|
|
5
|
-
# ng-comps:
|
|
6
|
-
|
|
7
|
-
[ng-comps](https://www.npmjs.com/package/ng-comps)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<div style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '12px', display: 'flex', gap: '12px', alignItems: 'center', maxWidth: '560px' }}>
|
|
12
|
-
<div style={{ flex: '0 0 64px', height: '64px', background: '#f3f4f6', borderRadius: '6px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700, color: '#111827' }}>PE</div>
|
|
13
|
-
<div>
|
|
14
|
-
<a href="https://preguntas-entrevista.vercel.app/" target="_blank" rel="noreferrer" style={{ fontWeight: 600, color: '#111827', textDecoration: 'none' }}>preguntas-entrevista</a>
|
|
15
|
-
<div style={{ color: '#6b7280', fontSize: '0.9rem' }}>
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
## 1)
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm i ng-comps
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## 2)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```css
|
|
30
|
-
@import 'ng-comps/theme/tokens.css';
|
|
31
|
-
@import 'ng-comps/styles.css';
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## 3)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
import { Component } from '@angular/core';
|
|
40
|
-
import { MfButtonComponent, MfInputComponent } from 'ng-comps';
|
|
41
|
-
|
|
42
|
-
@Component({
|
|
43
|
-
selector: 'app-example',
|
|
44
|
-
imports: [MfButtonComponent, MfInputComponent],
|
|
45
|
-
template: `
|
|
46
|
-
<mf-input label="Email" placeholder="you@company.com" />
|
|
47
|
-
<mf-button label="
|
|
48
|
-
`,
|
|
49
|
-
})
|
|
50
|
-
export class ExampleComponent {}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## 4)
|
|
54
|
-
|
|
55
|
-
`ng-comps`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- Angular `^21`
|
|
60
|
-
- RxJS `~7.8`
|
|
61
|
-
|
|
62
|
-
## 5)
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="About" />
|
|
4
|
+
|
|
5
|
+
# ng-comps: Quick Start Guide
|
|
6
|
+
|
|
7
|
+
[ng-comps](https://www.npmjs.com/package/ng-comps) is an Angular component library with a custom design layer built on top of Angular Material.
|
|
8
|
+
|
|
9
|
+
The library was created by [Manuel Ferrer](https://manuelferrer.vercel.app/).
|
|
10
|
+
|
|
11
|
+
<div style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '12px', display: 'flex', gap: '12px', alignItems: 'center', maxWidth: '560px' }}>
|
|
12
|
+
<div style={{ flex: '0 0 64px', height: '64px', background: '#f3f4f6', borderRadius: '6px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700, color: '#111827' }}>PE</div>
|
|
13
|
+
<div>
|
|
14
|
+
<a href="https://preguntas-entrevista.vercel.app/" target="_blank" rel="noreferrer" style={{ fontWeight: 600, color: '#111827', textDecoration: 'none' }}>preguntas-entrevista</a>
|
|
15
|
+
<div style={{ color: '#6b7280', fontSize: '0.9rem' }}>Sample project built with ng-comps.</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## 1) Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i ng-comps
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 2) Load global styles
|
|
26
|
+
|
|
27
|
+
In your app global stylesheet, for example `src/styles.css`, import:
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
@import 'ng-comps/theme/tokens.css';
|
|
31
|
+
@import 'ng-comps/styles.css';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 3) Use components (Standalone)
|
|
35
|
+
|
|
36
|
+
Import only the components you actually use to maximize tree-shaking:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Component } from '@angular/core';
|
|
40
|
+
import { MfButtonComponent, MfInputComponent } from 'ng-comps';
|
|
41
|
+
|
|
42
|
+
@Component({
|
|
43
|
+
selector: 'app-example',
|
|
44
|
+
imports: [MfButtonComponent, MfInputComponent],
|
|
45
|
+
template: `
|
|
46
|
+
<mf-input label="Email" placeholder="you@company.com" />
|
|
47
|
+
<mf-button label="Save" variant="filled" />
|
|
48
|
+
`,
|
|
49
|
+
})
|
|
50
|
+
export class ExampleComponent {}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 4) Consumer project requirements
|
|
54
|
+
|
|
55
|
+
`ng-comps` uses `peerDependencies`, so your project must have Angular and RxJS installed.
|
|
56
|
+
|
|
57
|
+
Recommended:
|
|
58
|
+
|
|
59
|
+
- Angular `^21`
|
|
60
|
+
- RxJS `~7.8`
|
|
61
|
+
|
|
62
|
+
## 5) Integration best practices
|
|
63
|
+
|
|
64
|
+
- Import components directly from `ng-comps` as needed.
|
|
65
|
+
- Keep your global styles aligned with the library `tokens.css`.
|
|
66
|
+
- Use Storybook as the visual and API reference for each component.
|
|
67
|
+
|
|
68
|
+
## Useful references in this repo
|
|
69
|
+
|
|
70
|
+
- Design tokens: `src/theme/tokens.css`
|
|
71
|
+
- Material theme: `src/theme/material-theme.scss`
|
|
72
|
+
- Base example: `src/app/components/button/mf-button.component.ts`
|
package/src/stories/Welcome.mdx
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
-
|
|
3
|
-
<Meta title="Welcome" />
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Si necesitas que añada más secciones (Guías de diseño, Tokens, Roadmap), dímelo y las agregaré.
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Welcome" />
|
|
4
|
+
|
|
5
|
+
# Welcome to MF Components
|
|
6
|
+
|
|
7
|
+
This Storybook showcases the project components implemented with Angular Material under our custom styling layer.
|
|
8
|
+
|
|
9
|
+
- Centralized theme: design tokens and Material theme.
|
|
10
|
+
- Available components: `MfButton` (production ready).
|
|
11
|
+
|
|
12
|
+
[View `MfButton`](?path=/story/components-mfbutton--filled)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## How to run Storybook
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Static build
|
|
20
|
+
npm run build-storybook
|
|
21
|
+
|
|
22
|
+
# Run Storybook in development (if available)
|
|
23
|
+
npx storybook dev -p 6006
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If you want me to add more sections such as Design Guides, Tokens, or a Roadmap, I can add them.
|
|
@@ -20,21 +20,21 @@ const FRAMEWORK_OPTIONS = [
|
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
const COUNTRY_OPTIONS = [
|
|
23
|
-
{ value: 'es', label: '
|
|
23
|
+
{ value: 'es', label: 'Spain' },
|
|
24
24
|
{ value: 'mx', label: 'Mexico' },
|
|
25
25
|
{ value: 'ar', label: 'Argentina' },
|
|
26
26
|
{ value: 'co', label: 'Colombia' },
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
const MENU_ITEMS: MfMenuItem[] = [
|
|
30
|
-
{ value: 'edit', label: '
|
|
31
|
-
{ value: 'duplicate', label: '
|
|
32
|
-
{ value: 'archive', label: '
|
|
30
|
+
{ value: 'edit', label: 'Edit', icon: 'edit' },
|
|
31
|
+
{ value: 'duplicate', label: 'Duplicate', icon: 'content_copy' },
|
|
32
|
+
{ value: 'archive', label: 'Archive', icon: 'archive' },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
35
|
const TABLE_COLUMNS = [
|
|
36
|
-
{ key: 'name', header: '
|
|
37
|
-
{ key: 'team', header: '
|
|
36
|
+
{ key: 'name', header: 'Name' },
|
|
37
|
+
{ key: 'team', header: 'Team' },
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
const TABLE_DATA = [
|
|
@@ -47,15 +47,15 @@ const TABLE_DATA = [
|
|
|
47
47
|
imports: [MfDialogComponent, MfButtonComponent],
|
|
48
48
|
template: `
|
|
49
49
|
<mf-dialog
|
|
50
|
-
title="
|
|
51
|
-
message="
|
|
50
|
+
title="Delete project"
|
|
51
|
+
message="This action permanently deletes the project."
|
|
52
52
|
role="alertdialog"
|
|
53
53
|
[showClose]="false"
|
|
54
54
|
[showActions]="true"
|
|
55
55
|
>
|
|
56
56
|
<div mfDialogActions>
|
|
57
|
-
<mf-button label="
|
|
58
|
-
<mf-button label="
|
|
57
|
+
<mf-button label="Cancel" variant="outlined" size="sm" (mfClick)="close()" />
|
|
58
|
+
<mf-button label="Delete" variant="filled" size="sm" (mfClick)="close()" />
|
|
59
59
|
</div>
|
|
60
60
|
</mf-dialog>
|
|
61
61
|
`,
|
|
@@ -75,7 +75,7 @@ class A11yDialogContentStoryComponent {
|
|
|
75
75
|
imports: [MfButtonComponent],
|
|
76
76
|
template: `
|
|
77
77
|
<mf-button
|
|
78
|
-
label="
|
|
78
|
+
label="Open delete dialog"
|
|
79
79
|
variant="filled"
|
|
80
80
|
(mfClick)="open()"
|
|
81
81
|
/>
|
|
@@ -107,7 +107,7 @@ class A11yDialogLauncherStoryComponent {
|
|
|
107
107
|
(mfNavItemClick)="onNavItemClick($event)"
|
|
108
108
|
>
|
|
109
109
|
<div style="padding: 24px;">
|
|
110
|
-
<p style="margin: 0; font: inherit;">
|
|
110
|
+
<p style="margin: 0; font: inherit;">Active section: {{ activeLabel() }}</p>
|
|
111
111
|
</div>
|
|
112
112
|
</mf-sidenav>
|
|
113
113
|
</div>
|
|
@@ -117,14 +117,14 @@ class A11ySidenavContractStoryComponent {
|
|
|
117
117
|
readonly activeId = signal('home');
|
|
118
118
|
|
|
119
119
|
readonly items = signal<MfSidenavNavItem[]>([
|
|
120
|
-
{ id: 'home', label: '
|
|
120
|
+
{ id: 'home', label: 'Home', icon: 'home', active: true },
|
|
121
121
|
{ id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
|
|
122
|
-
{ id: 'analytics', label: '
|
|
123
|
-
{ id: 'users', label: '
|
|
122
|
+
{ id: 'analytics', label: 'Analytics', icon: 'bar_chart' },
|
|
123
|
+
{ id: 'users', label: 'Users', icon: 'group' },
|
|
124
124
|
]);
|
|
125
125
|
|
|
126
126
|
readonly activeLabel = () =>
|
|
127
|
-
this.items().find((item) => item.id === this.activeId())?.label ?? '
|
|
127
|
+
this.items().find((item) => item.id === this.activeId())?.label ?? 'Home';
|
|
128
128
|
|
|
129
129
|
onNavItemClick(item: MfSidenavNavItem): void {
|
|
130
130
|
this.activeId.set(item.id);
|
|
@@ -161,10 +161,10 @@ export const ButtonCorrectUsage: Story = {
|
|
|
161
161
|
render: () => ({
|
|
162
162
|
template: `
|
|
163
163
|
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
|
164
|
-
<mf-button label="
|
|
164
|
+
<mf-button label="Save changes" variant="filled" />
|
|
165
165
|
<mf-button
|
|
166
166
|
[iconOnly]="true"
|
|
167
|
-
ariaLabel="
|
|
167
|
+
ariaLabel="Open filters"
|
|
168
168
|
leadingIcon="filter_list"
|
|
169
169
|
variant="outlined"
|
|
170
170
|
/>
|
|
@@ -179,14 +179,14 @@ export const ButtonIncorrectUsage: Story = {
|
|
|
179
179
|
render: () => ({
|
|
180
180
|
template: `
|
|
181
181
|
<section style="max-width: 720px; border: 1px solid var(--mf-color-border); border-radius: 16px; padding: 20px; background: var(--mf-color-surface);">
|
|
182
|
-
<h2 style="margin: 0 0 12px; font-size: 1rem;">Anti-
|
|
182
|
+
<h2 style="margin: 0 0 12px; font-size: 1rem;">Anti-patterns</h2>
|
|
183
183
|
<ul style="margin: 0 0 16px; padding-left: 18px;">
|
|
184
|
-
<li>Icon-only button
|
|
185
|
-
<li>
|
|
186
|
-
<li>
|
|
184
|
+
<li>Icon-only button without \`ariaLabel\`.</li>
|
|
185
|
+
<li>Using a clickable \`div\` as a button.</li>
|
|
186
|
+
<li>Removing the focus outline without a visible replacement.</li>
|
|
187
187
|
</ul>
|
|
188
188
|
<pre style="margin: 0; padding: 16px; border-radius: 12px; background: #111827; color: #f9fafb; overflow: auto;"><code><mf-button [iconOnly]="true" leadingIcon="filter_list" />
|
|
189
|
-
<div (click)="save()">
|
|
189
|
+
<div (click)="save()">Save</div></code></pre>
|
|
190
190
|
</section>
|
|
191
191
|
`,
|
|
192
192
|
}),
|
|
@@ -198,9 +198,9 @@ export const InputCorrectUsage: Story = {
|
|
|
198
198
|
template: `
|
|
199
199
|
<div style="max-width: 360px;">
|
|
200
200
|
<mf-input
|
|
201
|
-
label="
|
|
202
|
-
hint="
|
|
203
|
-
placeholder="
|
|
201
|
+
label="Contact email"
|
|
202
|
+
hint="We will use this email for important updates"
|
|
203
|
+
placeholder="name@company.com"
|
|
204
204
|
[required]="true"
|
|
205
205
|
/>
|
|
206
206
|
</div>
|
|
@@ -214,10 +214,10 @@ export const InputIncorrectUsage: Story = {
|
|
|
214
214
|
render: () => ({
|
|
215
215
|
template: `
|
|
216
216
|
<section style="max-width: 720px; border: 1px solid var(--mf-color-border); border-radius: 16px; padding: 20px; background: var(--mf-color-surface);">
|
|
217
|
-
<h2 style="margin: 0 0 12px; font-size: 1rem;">Anti-
|
|
217
|
+
<h2 style="margin: 0 0 12px; font-size: 1rem;">Anti-patterns</h2>
|
|
218
218
|
<ul style="margin: 0 0 16px; padding-left: 18px;">
|
|
219
|
-
<li>
|
|
220
|
-
<li>
|
|
219
|
+
<li>Using only the placeholder as the accessible name.</li>
|
|
220
|
+
<li>Not wiring errors or helper text to the control.</li>
|
|
221
221
|
</ul>
|
|
222
222
|
<pre style="margin: 0; padding: 16px; border-radius: 12px; background: #111827; color: #f9fafb; overflow: auto;"><code><mf-input placeholder="Email" /></code></pre>
|
|
223
223
|
</section>
|
|
@@ -243,7 +243,7 @@ export const DialogFocusContract: Story = {
|
|
|
243
243
|
|
|
244
244
|
await userEvent.tab();
|
|
245
245
|
const opener = canvas.getByRole('button', {
|
|
246
|
-
name: '
|
|
246
|
+
name: 'Open delete dialog',
|
|
247
247
|
});
|
|
248
248
|
await waitFor(() => expect(opener).toHaveFocus());
|
|
249
249
|
|
|
@@ -251,11 +251,11 @@ export const DialogFocusContract: Story = {
|
|
|
251
251
|
|
|
252
252
|
await waitFor(() =>
|
|
253
253
|
expect(
|
|
254
|
-
body.getByRole('alertdialog', { name: '
|
|
254
|
+
body.getByRole('alertdialog', { name: 'Delete project' }),
|
|
255
255
|
).toBeTruthy(),
|
|
256
256
|
);
|
|
257
257
|
|
|
258
|
-
const cancelButton = body.getByRole('button', { name: '
|
|
258
|
+
const cancelButton = body.getByRole('button', { name: 'Cancel' });
|
|
259
259
|
await waitFor(() => expect(cancelButton).toHaveFocus());
|
|
260
260
|
|
|
261
261
|
await userEvent.keyboard('{Enter}');
|
|
@@ -273,7 +273,7 @@ export const MenuKeyboardContract: Story = {
|
|
|
273
273
|
props: {
|
|
274
274
|
items: MENU_ITEMS,
|
|
275
275
|
},
|
|
276
|
-
template: `<mf-menu [items]="items" triggerLabel="
|
|
276
|
+
template: `<mf-menu [items]="items" triggerLabel="Open actions" />`,
|
|
277
277
|
moduleMetadata: { imports: [MfMenuComponent] },
|
|
278
278
|
}),
|
|
279
279
|
play: async ({ canvasElement }) => {
|
|
@@ -282,13 +282,13 @@ export const MenuKeyboardContract: Story = {
|
|
|
282
282
|
const body = within(doc.body);
|
|
283
283
|
|
|
284
284
|
await userEvent.tab();
|
|
285
|
-
const trigger = canvas.getByRole('button', { name: '
|
|
285
|
+
const trigger = canvas.getByRole('button', { name: 'Open actions' });
|
|
286
286
|
await waitFor(() => expect(trigger).toHaveFocus());
|
|
287
287
|
|
|
288
288
|
await userEvent.keyboard('{Enter}');
|
|
289
289
|
|
|
290
290
|
await waitFor(() => expect(body.getByRole('menu')).toBeTruthy());
|
|
291
|
-
expect(body.getByRole('menuitem', { name: '
|
|
291
|
+
expect(body.getByRole('menuitem', { name: 'Edit' })).toBeTruthy();
|
|
292
292
|
|
|
293
293
|
await userEvent.keyboard('{Escape}');
|
|
294
294
|
|
|
@@ -348,8 +348,8 @@ export const AutocompleteKeyboardContract: Story = {
|
|
|
348
348
|
<div style="max-width: 360px;">
|
|
349
349
|
<mf-autocomplete
|
|
350
350
|
[options]="options"
|
|
351
|
-
label="
|
|
352
|
-
placeholder="
|
|
351
|
+
label="Country"
|
|
352
|
+
placeholder="Type a country"
|
|
353
353
|
/>
|
|
354
354
|
</div>
|
|
355
355
|
`,
|
|
@@ -361,13 +361,13 @@ export const AutocompleteKeyboardContract: Story = {
|
|
|
361
361
|
const body = within(doc.body);
|
|
362
362
|
|
|
363
363
|
await userEvent.tab();
|
|
364
|
-
const input = canvas.getByRole('textbox', { name: '
|
|
364
|
+
const input = canvas.getByRole('textbox', { name: 'Country' });
|
|
365
365
|
await waitFor(() => expect(input).toHaveFocus());
|
|
366
366
|
|
|
367
|
-
await userEvent.keyboard('
|
|
367
|
+
await userEvent.keyboard('sp');
|
|
368
368
|
|
|
369
369
|
await waitFor(() => expect(body.getByRole('listbox')).toBeTruthy());
|
|
370
|
-
expect(body.getByText('
|
|
370
|
+
expect(body.getByText('Spain')).toBeTruthy();
|
|
371
371
|
|
|
372
372
|
await userEvent.keyboard('{Escape}');
|
|
373
373
|
await waitFor(() =>
|
|
@@ -381,7 +381,7 @@ export const DatepickerKeyboardContract: Story = {
|
|
|
381
381
|
render: () => ({
|
|
382
382
|
template: `
|
|
383
383
|
<div style="max-width: 360px;">
|
|
384
|
-
<mf-datepicker label="
|
|
384
|
+
<mf-datepicker label="Delivery date" />
|
|
385
385
|
</div>
|
|
386
386
|
`,
|
|
387
387
|
moduleMetadata: { imports: [MfDatepickerComponent] },
|
|
@@ -391,11 +391,11 @@ export const DatepickerKeyboardContract: Story = {
|
|
|
391
391
|
const doc = canvasElement.ownerDocument;
|
|
392
392
|
|
|
393
393
|
await userEvent.tab();
|
|
394
|
-
canvas.getByRole('textbox', { name: '
|
|
394
|
+
canvas.getByRole('textbox', { name: 'Delivery date' });
|
|
395
395
|
|
|
396
396
|
await userEvent.tab();
|
|
397
397
|
const toggleButton = canvas.getByRole('button', {
|
|
398
|
-
name: '
|
|
398
|
+
name: 'Open calendar',
|
|
399
399
|
});
|
|
400
400
|
await waitFor(() => expect(toggleButton).toHaveFocus());
|
|
401
401
|
|
|
@@ -420,13 +420,13 @@ export const TableExplicitActionContract: Story = {
|
|
|
420
420
|
columns: TABLE_COLUMNS,
|
|
421
421
|
data: TABLE_DATA,
|
|
422
422
|
rowActionAriaLabel: (row: Record<string, unknown>) =>
|
|
423
|
-
`
|
|
423
|
+
`View details for ${row['name']}`,
|
|
424
424
|
},
|
|
425
425
|
template: `
|
|
426
426
|
<mf-table
|
|
427
427
|
[columns]="columns"
|
|
428
428
|
[data]="data"
|
|
429
|
-
rowActionLabel="
|
|
429
|
+
rowActionLabel="View details"
|
|
430
430
|
[rowActionAriaLabel]="rowActionAriaLabel"
|
|
431
431
|
/>
|
|
432
432
|
`,
|
|
@@ -435,7 +435,7 @@ export const TableExplicitActionContract: Story = {
|
|
|
435
435
|
play: async ({ canvasElement }) => {
|
|
436
436
|
const canvas = within(canvasElement);
|
|
437
437
|
const detailButtons = canvas.getAllByRole('button', {
|
|
438
|
-
name: /
|
|
438
|
+
name: /View details for/,
|
|
439
439
|
});
|
|
440
440
|
|
|
441
441
|
expect(detailButtons).toHaveLength(2);
|
|
@@ -455,18 +455,18 @@ export const SidenavKeyboardContract: Story = {
|
|
|
455
455
|
const canvas = within(canvasElement);
|
|
456
456
|
|
|
457
457
|
await userEvent.tab();
|
|
458
|
-
const home = canvas.getByRole('button', { name: '
|
|
458
|
+
const home = canvas.getByRole('button', { name: 'Home' });
|
|
459
459
|
await waitFor(() => expect(home).toHaveFocus());
|
|
460
460
|
|
|
461
461
|
await userEvent.tab();
|
|
462
462
|
await userEvent.tab();
|
|
463
|
-
const analytics = canvas.getByRole('button', { name: '
|
|
463
|
+
const analytics = canvas.getByRole('button', { name: 'Analytics' });
|
|
464
464
|
await waitFor(() => expect(analytics).toHaveFocus());
|
|
465
465
|
|
|
466
466
|
await userEvent.keyboard('{Enter}');
|
|
467
467
|
|
|
468
468
|
await waitFor(() =>
|
|
469
|
-
expect(canvas.getByText('
|
|
469
|
+
expect(canvas.getByText('Active section: Analytics')).toBeTruthy(),
|
|
470
470
|
);
|
|
471
471
|
},
|
|
472
472
|
};
|