ng-comps 1.0.2 → 2.0.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/fesm2022/ng-comps.mjs +4254 -0
- package/package.json +54 -58
- package/src/styles.css +54 -0
- package/types/ng-comps.d.ts +1348 -0
- package/.editorconfig +0 -17
- package/.github/copilot-instructions.md +0 -55
- package/.github/workflows/ci.yml +0 -29
- package/.prettierrc +0 -12
- package/.storybook/main.ts +0 -21
- package/.storybook/preview.ts +0 -27
- package/.storybook/tsconfig.doc.json +0 -10
- package/.storybook/tsconfig.json +0 -15
- package/.storybook/typings.d.ts +0 -4
- package/.vscode/extensions.json +0 -4
- package/.vscode/launch.json +0 -20
- package/.vscode/mcp.json +0 -9
- package/.vscode/tasks.json +0 -42
- package/ACCESSIBILITY.md +0 -127
- package/angular.json +0 -106
- package/documentation.json +0 -13394
- package/ng-package.json +0 -27
- package/public/favicon.ico +0 -0
- package/scripts/prepare-package.mjs +0 -80
- package/src/app/a11y/accessibility.utils.ts +0 -35
- package/src/app/a11y/index.ts +0 -6
- package/src/app/accessibility/ng-comps.a11y.spec.ts +0 -108
- package/src/app/app.config.ts +0 -11
- package/src/app/app.css +0 -107
- package/src/app/app.html +0 -48
- package/src/app/app.routes.ts +0 -3
- package/src/app/app.spec.ts +0 -23
- package/src/app/app.ts +0 -10
- package/src/app/components/accordion/index.ts +0 -2
- package/src/app/components/accordion/mf-accordion.component.css +0 -38
- package/src/app/components/accordion/mf-accordion.component.spec.ts +0 -48
- package/src/app/components/accordion/mf-accordion.component.ts +0 -53
- package/src/app/components/alert/index.ts +0 -2
- package/src/app/components/alert/mf-alert.component.css +0 -100
- package/src/app/components/alert/mf-alert.component.spec.ts +0 -59
- package/src/app/components/alert/mf-alert.component.ts +0 -68
- package/src/app/components/autocomplete/index.ts +0 -5
- package/src/app/components/autocomplete/mf-autocomplete.component.css +0 -105
- package/src/app/components/autocomplete/mf-autocomplete.component.spec.ts +0 -116
- package/src/app/components/autocomplete/mf-autocomplete.component.ts +0 -307
- package/src/app/components/avatar/index.ts +0 -2
- package/src/app/components/avatar/mf-avatar.component.css +0 -27
- package/src/app/components/avatar/mf-avatar.component.spec.ts +0 -49
- package/src/app/components/avatar/mf-avatar.component.ts +0 -99
- package/src/app/components/badge/index.ts +0 -2
- package/src/app/components/badge/mf-badge.component.css +0 -32
- package/src/app/components/badge/mf-badge.component.spec.ts +0 -40
- package/src/app/components/badge/mf-badge.component.ts +0 -105
- package/src/app/components/breadcrumb/index.ts +0 -2
- package/src/app/components/breadcrumb/mf-breadcrumb.component.css +0 -61
- package/src/app/components/breadcrumb/mf-breadcrumb.component.spec.ts +0 -61
- package/src/app/components/breadcrumb/mf-breadcrumb.component.ts +0 -75
- package/src/app/components/button/index.ts +0 -2
- package/src/app/components/button/mf-button.component.css +0 -136
- package/src/app/components/button/mf-button.component.ts +0 -174
- package/src/app/components/card/index.ts +0 -2
- package/src/app/components/card/mf-card.component.css +0 -82
- package/src/app/components/card/mf-card.component.ts +0 -59
- package/src/app/components/checkbox/index.ts +0 -1
- package/src/app/components/checkbox/mf-checkbox.component.css +0 -75
- package/src/app/components/checkbox/mf-checkbox.component.ts +0 -187
- package/src/app/components/chip/index.ts +0 -2
- package/src/app/components/chip/mf-chip.component.css +0 -69
- package/src/app/components/chip/mf-chip.component.spec.ts +0 -47
- package/src/app/components/chip/mf-chip.component.ts +0 -77
- package/src/app/components/datepicker/index.ts +0 -2
- package/src/app/components/datepicker/mf-datepicker.component.css +0 -102
- package/src/app/components/datepicker/mf-datepicker.component.spec.ts +0 -69
- package/src/app/components/datepicker/mf-datepicker.component.ts +0 -233
- package/src/app/components/dialog/index.ts +0 -3
- package/src/app/components/dialog/mf-dialog.component.css +0 -73
- package/src/app/components/dialog/mf-dialog.component.ts +0 -160
- package/src/app/components/dialog/mf-dialog.service.spec.ts +0 -61
- package/src/app/components/dialog/mf-dialog.service.ts +0 -52
- package/src/app/components/divider/index.ts +0 -2
- package/src/app/components/divider/mf-divider.component.css +0 -38
- package/src/app/components/divider/mf-divider.component.spec.ts +0 -40
- package/src/app/components/divider/mf-divider.component.ts +0 -44
- package/src/app/components/form-field/index.ts +0 -1
- package/src/app/components/form-field/mf-form-field.component.css +0 -51
- package/src/app/components/form-field/mf-form-field.component.ts +0 -74
- package/src/app/components/grid-list/index.ts +0 -2
- package/src/app/components/grid-list/mf-grid-list.component.css +0 -47
- package/src/app/components/grid-list/mf-grid-list.component.spec.ts +0 -57
- package/src/app/components/grid-list/mf-grid-list.component.ts +0 -68
- package/src/app/components/icon/index.ts +0 -2
- package/src/app/components/icon/mf-icon.component.css +0 -56
- package/src/app/components/icon/mf-icon.component.ts +0 -41
- package/src/app/components/input/index.ts +0 -2
- package/src/app/components/input/mf-input.component.css +0 -105
- package/src/app/components/input/mf-input.component.ts +0 -217
- package/src/app/components/menu/index.ts +0 -2
- package/src/app/components/menu/mf-menu.component.css +0 -31
- package/src/app/components/menu/mf-menu.component.spec.ts +0 -49
- package/src/app/components/menu/mf-menu.component.ts +0 -66
- package/src/app/components/paginator/index.ts +0 -1
- package/src/app/components/paginator/mf-paginator.component.css +0 -32
- package/src/app/components/paginator/mf-paginator.component.spec.ts +0 -44
- package/src/app/components/paginator/mf-paginator.component.ts +0 -52
- package/src/app/components/progress-bar/index.ts +0 -2
- package/src/app/components/progress-bar/mf-progress-bar.component.css +0 -53
- package/src/app/components/progress-bar/mf-progress-bar.component.spec.ts +0 -65
- package/src/app/components/progress-bar/mf-progress-bar.component.ts +0 -79
- package/src/app/components/progress-spinner/index.ts +0 -2
- package/src/app/components/progress-spinner/mf-progress-spinner.component.css +0 -38
- package/src/app/components/progress-spinner/mf-progress-spinner.component.spec.ts +0 -59
- package/src/app/components/progress-spinner/mf-progress-spinner.component.ts +0 -81
- package/src/app/components/radio-button/index.ts +0 -2
- package/src/app/components/radio-button/mf-radio-button.component.css +0 -86
- package/src/app/components/radio-button/mf-radio-button.component.spec.ts +0 -55
- package/src/app/components/radio-button/mf-radio-button.component.ts +0 -219
- package/src/app/components/select/index.ts +0 -2
- package/src/app/components/select/mf-select.component.css +0 -121
- package/src/app/components/select/mf-select.component.spec.ts +0 -108
- package/src/app/components/select/mf-select.component.ts +0 -252
- package/src/app/components/sidenav/index.ts +0 -2
- package/src/app/components/sidenav/mf-sidenav.component.css +0 -168
- package/src/app/components/sidenav/mf-sidenav.component.spec.ts +0 -57
- package/src/app/components/sidenav/mf-sidenav.component.ts +0 -126
- package/src/app/components/slide-toggle/index.ts +0 -1
- package/src/app/components/slide-toggle/mf-slide-toggle.component.css +0 -42
- package/src/app/components/slide-toggle/mf-slide-toggle.component.spec.ts +0 -43
- package/src/app/components/slide-toggle/mf-slide-toggle.component.ts +0 -188
- package/src/app/components/snackbar/index.ts +0 -2
- package/src/app/components/snackbar/mf-snackbar.service.css +0 -31
- package/src/app/components/snackbar/mf-snackbar.service.spec.ts +0 -81
- package/src/app/components/snackbar/mf-snackbar.service.ts +0 -77
- package/src/app/components/table/index.ts +0 -2
- package/src/app/components/table/mf-table.component.css +0 -68
- package/src/app/components/table/mf-table.component.spec.ts +0 -76
- package/src/app/components/table/mf-table.component.ts +0 -117
- package/src/app/components/tabs/index.ts +0 -2
- package/src/app/components/tabs/mf-tabs.component.css +0 -31
- package/src/app/components/tabs/mf-tabs.component.spec.ts +0 -50
- package/src/app/components/tabs/mf-tabs.component.ts +0 -62
- package/src/app/components/textarea/index.ts +0 -2
- package/src/app/components/textarea/mf-textarea.component.css +0 -48
- package/src/app/components/textarea/mf-textarea.component.spec.ts +0 -55
- package/src/app/components/textarea/mf-textarea.component.ts +0 -227
- package/src/app/components/toolbar/index.ts +0 -2
- package/src/app/components/toolbar/mf-toolbar.component.css +0 -77
- package/src/app/components/toolbar/mf-toolbar.component.ts +0 -56
- package/src/app/components/tooltip/index.ts +0 -3
- package/src/app/components/tooltip/mf-tooltip.component.css +0 -7
- package/src/app/components/tooltip/mf-tooltip.component.spec.ts +0 -37
- package/src/app/components/tooltip/mf-tooltip.component.ts +0 -47
- package/src/app/components/tooltip/mf-tooltip.directive.ts +0 -22
- package/src/index.html +0 -18
- package/src/main.ts +0 -6
- package/src/public-api.ts +0 -31
- package/src/stories/About.mdx +0 -72
- package/src/stories/Accessibility.mdx +0 -59
- package/src/stories/Welcome.mdx +0 -26
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +0 -1
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +0 -1
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +0 -1
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +0 -1
- package/src/stories/assets/youtube.svg +0 -1
- package/src/stories/mf-a11y-contracts.stories.ts +0 -472
- package/src/stories/mf-autocomplete.stories.ts +0 -194
- package/src/stories/mf-button.stories.ts +0 -152
- package/src/stories/mf-card.stories.ts +0 -147
- package/src/stories/mf-checkbox.stories.ts +0 -88
- package/src/stories/mf-datepicker.stories.ts +0 -118
- package/src/stories/mf-dialog.stories.ts +0 -159
- package/src/stories/mf-form-field.stories.ts +0 -108
- package/src/stories/mf-grid-list.stories.ts +0 -104
- package/src/stories/mf-icon.stories.ts +0 -133
- package/src/stories/mf-input.stories.ts +0 -158
- package/src/stories/mf-menu.stories.ts +0 -71
- package/src/stories/mf-progress-bar.stories.ts +0 -119
- package/src/stories/mf-progress-spinner.stories.ts +0 -124
- package/src/stories/mf-radio-button.stories.ts +0 -111
- package/src/stories/mf-select.stories.ts +0 -184
- package/src/stories/mf-sidenav.stories.ts +0 -331
- package/src/stories/mf-table.stories.ts +0 -80
- package/src/stories/mf-toolbar.stories.ts +0 -112
- package/src/stories/user.ts +0 -3
- package/tsconfig.app.json +0 -15
- package/tsconfig.json +0 -33
- package/tsconfig.spec.json +0 -15
- package/vercel.json +0 -6
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
effect,
|
|
6
|
-
input,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
9
|
-
import { hasAccessibleName, warnInDev } from '../../a11y';
|
|
10
|
-
|
|
11
|
-
export type MfProgressBarMode = 'determinate' | 'indeterminate' | 'buffer' | 'query';
|
|
12
|
-
export type MfProgressBarColor = 'brand' | 'accent' | 'warn';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Barra de progreso de la librerÃa ng-comps.
|
|
16
|
-
* Envuelve Angular Material `mat-progress-bar` y expone una API uniforme
|
|
17
|
-
* con look and feel de marca.
|
|
18
|
-
*/
|
|
19
|
-
@Component({
|
|
20
|
-
selector: 'mf-progress-bar',
|
|
21
|
-
imports: [MatProgressBarModule],
|
|
22
|
-
template: `
|
|
23
|
-
<div class="mf-progress-bar__wrapper">
|
|
24
|
-
@if (label()) {
|
|
25
|
-
<span class="mf-progress-bar__label">{{ label() }}</span>
|
|
26
|
-
}
|
|
27
|
-
<mat-progress-bar
|
|
28
|
-
[class]="hostClasses()"
|
|
29
|
-
[mode]="mode()"
|
|
30
|
-
[value]="value()"
|
|
31
|
-
[bufferValue]="bufferValue()"
|
|
32
|
-
[attr.aria-label]="label() || null"
|
|
33
|
-
[attr.aria-valuetext]="valueText() || null"
|
|
34
|
-
[attr.aria-valuenow]="showsNumericValue() ? value() : null"
|
|
35
|
-
aria-valuemin="0"
|
|
36
|
-
aria-valuemax="100"
|
|
37
|
-
/>
|
|
38
|
-
@if (showValue() && mode() === 'determinate') {
|
|
39
|
-
<span class="mf-progress-bar__value" aria-hidden="true">{{ value() }}%</span>
|
|
40
|
-
}
|
|
41
|
-
</div>
|
|
42
|
-
`,
|
|
43
|
-
styleUrl: './mf-progress-bar.component.css',
|
|
44
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
45
|
-
})
|
|
46
|
-
export class MfProgressBarComponent {
|
|
47
|
-
/** Modo de la barra de progreso */
|
|
48
|
-
readonly mode = input<MfProgressBarMode>('determinate');
|
|
49
|
-
/** Valor actual (0–100) */
|
|
50
|
-
readonly value = input(0);
|
|
51
|
-
/** Valor del buffer (0–100, solo en modo buffer) */
|
|
52
|
-
readonly bufferValue = input(0);
|
|
53
|
-
/** Color de la barra */
|
|
54
|
-
readonly color = input<MfProgressBarColor>('brand');
|
|
55
|
-
/** Etiqueta accesible */
|
|
56
|
-
readonly label = input<string | undefined>(undefined);
|
|
57
|
-
/** Texto accesible opcional para lectores de pantalla */
|
|
58
|
-
readonly valueText = input<string | undefined>(undefined);
|
|
59
|
-
/** Muestra el porcentaje junto a la barra */
|
|
60
|
-
readonly showValue = input(false);
|
|
61
|
-
/** Altura de la barra en px */
|
|
62
|
-
readonly height = input(4);
|
|
63
|
-
|
|
64
|
-
constructor() {
|
|
65
|
-
effect(() => {
|
|
66
|
-
if (!hasAccessibleName(this.label())) {
|
|
67
|
-
warnInDev(
|
|
68
|
-
'mf-progress-bar debería incluir `label` para anunciar el progreso de forma accesible.',
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
readonly showsNumericValue = computed(() => this.mode() === 'determinate');
|
|
75
|
-
|
|
76
|
-
readonly hostClasses = computed(() => {
|
|
77
|
-
return `mf-progress-bar mf-progress-bar--${this.color()}`;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: inline-flex;
|
|
3
|
-
align-items: center;
|
|
4
|
-
justify-content: center;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/* ── Wrapper ───────────────────────────────────────────────────── */
|
|
8
|
-
.mf-progress-spinner__wrapper {
|
|
9
|
-
display: inline-flex;
|
|
10
|
-
align-items: center;
|
|
11
|
-
gap: var(--mf-space-3);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.mf-progress-spinner__wrapper--labeled {
|
|
15
|
-
flex-direction: row;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.mf-progress-spinner__label {
|
|
19
|
-
font-family: var(--mf-font-base);
|
|
20
|
-
font-size: var(--mf-text-sm);
|
|
21
|
-
font-weight: var(--mf-weight-medium);
|
|
22
|
-
color: var(--mf-color-on-surface);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/* ── Color: brand ─────────────────────────────────────────────── */
|
|
26
|
-
.mf-progress-spinner--brand.mat-mdc-progress-spinner {
|
|
27
|
-
--mdc-circular-progress-active-indicator-color: var(--mf-color-brand) !important;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* ── Color: accent ────────────────────────────────────────────── */
|
|
31
|
-
.mf-progress-spinner--accent.mat-mdc-progress-spinner {
|
|
32
|
-
--mdc-circular-progress-active-indicator-color: var(--mf-color-accent-500) !important;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/* ── Color: warn ──────────────────────────────────────────────── */
|
|
36
|
-
.mf-progress-spinner--warn.mat-mdc-progress-spinner {
|
|
37
|
-
--mdc-circular-progress-active-indicator-color: var(--mf-color-error-500) !important;
|
|
38
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
-
import { MfProgressSpinnerComponent } from './mf-progress-spinner.component';
|
|
3
|
-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
4
|
-
|
|
5
|
-
describe('MfProgressSpinnerComponent', () => {
|
|
6
|
-
let fixture: ComponentFixture<MfProgressSpinnerComponent>;
|
|
7
|
-
let component: MfProgressSpinnerComponent;
|
|
8
|
-
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
await TestBed.configureTestingModule({
|
|
11
|
-
imports: [MfProgressSpinnerComponent, NoopAnimationsModule],
|
|
12
|
-
}).compileComponents();
|
|
13
|
-
|
|
14
|
-
fixture = TestBed.createComponent(MfProgressSpinnerComponent);
|
|
15
|
-
component = fixture.componentInstance;
|
|
16
|
-
fixture.detectChanges();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should create', () => {
|
|
20
|
-
expect(component).toBeTruthy();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should render mat-progress-spinner', () => {
|
|
24
|
-
const spinner = fixture.nativeElement.querySelector('mat-progress-spinner');
|
|
25
|
-
expect(spinner).toBeTruthy();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should apply brand class by default', () => {
|
|
29
|
-
expect(component.hostClasses()).toContain('mf-progress-spinner--brand');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should apply accent class when color is accent', () => {
|
|
33
|
-
fixture.componentRef.setInput('color', 'accent');
|
|
34
|
-
expect(component.hostClasses()).toContain('mf-progress-spinner--accent');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should apply warn class when color is warn', () => {
|
|
38
|
-
fixture.componentRef.setInput('color', 'warn');
|
|
39
|
-
expect(component.hostClasses()).toContain('mf-progress-spinner--warn');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should render label when provided', () => {
|
|
43
|
-
fixture.componentRef.setInput('label', 'Processing...');
|
|
44
|
-
fixture.detectChanges();
|
|
45
|
-
const label = fixture.nativeElement.querySelector('.mf-progress-spinner__label');
|
|
46
|
-
expect(label?.textContent).toContain('Processing...');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should not render label when not provided', () => {
|
|
50
|
-
fixture.detectChanges();
|
|
51
|
-
const label = fixture.nativeElement.querySelector('.mf-progress-spinner__label');
|
|
52
|
-
expect(label).toBeFalsy();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should add labeled class when label is provided', () => {
|
|
56
|
-
fixture.componentRef.setInput('label', 'Loading');
|
|
57
|
-
expect(component.wrapperClasses()).toContain('mf-progress-spinner__wrapper--labeled');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
effect,
|
|
6
|
-
input,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
9
|
-
import { hasAccessibleName, warnInDev } from '../../a11y';
|
|
10
|
-
|
|
11
|
-
export type MfProgressSpinnerMode = 'determinate' | 'indeterminate';
|
|
12
|
-
export type MfProgressSpinnerColor = 'brand' | 'accent' | 'warn';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Spinner de progreso de la librerÃa ng-comps.
|
|
16
|
-
* Envuelve Angular Material `mat-progress-spinner` y expone una API uniforme
|
|
17
|
-
* con look and feel de marca.
|
|
18
|
-
*/
|
|
19
|
-
@Component({
|
|
20
|
-
selector: 'mf-progress-spinner',
|
|
21
|
-
imports: [MatProgressSpinnerModule],
|
|
22
|
-
template: `
|
|
23
|
-
<div [class]="wrapperClasses()">
|
|
24
|
-
<mat-progress-spinner
|
|
25
|
-
[class]="hostClasses()"
|
|
26
|
-
[mode]="mode()"
|
|
27
|
-
[value]="value()"
|
|
28
|
-
[diameter]="diameter()"
|
|
29
|
-
[strokeWidth]="strokeWidth()"
|
|
30
|
-
[attr.aria-label]="label() || null"
|
|
31
|
-
[attr.aria-valuetext]="valueText() || null"
|
|
32
|
-
[attr.aria-valuenow]="showsNumericValue() ? value() : null"
|
|
33
|
-
aria-valuemin="0"
|
|
34
|
-
aria-valuemax="100"
|
|
35
|
-
/>
|
|
36
|
-
@if (label()) {
|
|
37
|
-
<span class="mf-progress-spinner__label">{{ label() }}</span>
|
|
38
|
-
}
|
|
39
|
-
</div>
|
|
40
|
-
`,
|
|
41
|
-
styleUrl: './mf-progress-spinner.component.css',
|
|
42
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
43
|
-
})
|
|
44
|
-
export class MfProgressSpinnerComponent {
|
|
45
|
-
/** Modo del spinner */
|
|
46
|
-
readonly mode = input<MfProgressSpinnerMode>('indeterminate');
|
|
47
|
-
/** Valor actual (0–100, solo en modo determinate) */
|
|
48
|
-
readonly value = input(0);
|
|
49
|
-
/** Diámetro en px */
|
|
50
|
-
readonly diameter = input(40);
|
|
51
|
-
/** Grosor del trazo en px */
|
|
52
|
-
readonly strokeWidth = input(4);
|
|
53
|
-
/** Color del spinner */
|
|
54
|
-
readonly color = input<MfProgressSpinnerColor>('brand');
|
|
55
|
-
/** Etiqueta accesible y visible */
|
|
56
|
-
readonly label = input<string | undefined>(undefined);
|
|
57
|
-
/** Texto accesible opcional para lectores de pantalla */
|
|
58
|
-
readonly valueText = input<string | undefined>(undefined);
|
|
59
|
-
|
|
60
|
-
constructor() {
|
|
61
|
-
effect(() => {
|
|
62
|
-
if (!hasAccessibleName(this.label())) {
|
|
63
|
-
warnInDev(
|
|
64
|
-
'mf-progress-spinner debería incluir `label` para anunciar el estado de carga de forma accesible.',
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
readonly showsNumericValue = computed(() => this.mode() === 'determinate');
|
|
71
|
-
|
|
72
|
-
readonly hostClasses = computed(() => {
|
|
73
|
-
return `mf-progress-spinner mf-progress-spinner--${this.color()}`;
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
readonly wrapperClasses = computed(() => {
|
|
77
|
-
const classes = ['mf-progress-spinner__wrapper'];
|
|
78
|
-
if (this.label()) classes.push('mf-progress-spinner__wrapper--labeled');
|
|
79
|
-
return classes.join(' ');
|
|
80
|
-
});
|
|
81
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: block;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
.mf-radio__fieldset {
|
|
6
|
-
margin: 0;
|
|
7
|
-
padding: 0;
|
|
8
|
-
border: 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.mf-radio__legend {
|
|
12
|
-
margin-bottom: var(--mf-space-2);
|
|
13
|
-
font-size: var(--mf-text-sm);
|
|
14
|
-
font-weight: var(--mf-weight-medium);
|
|
15
|
-
color: var(--mf-color-on-surface);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/* ── Group layout ──────────────────────────────────────────────── */
|
|
19
|
-
.mf-radio {
|
|
20
|
-
display: flex;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.mf-radio--vertical {
|
|
24
|
-
flex-direction: column;
|
|
25
|
-
gap: var(--mf-space-2);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.mf-radio--horizontal {
|
|
29
|
-
flex-direction: row;
|
|
30
|
-
flex-wrap: wrap;
|
|
31
|
-
gap: var(--mf-space-4);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* ── Option ────────────────────────────────────────────────────── */
|
|
35
|
-
.mf-radio__option {
|
|
36
|
-
font-family: var(--mf-font-base) !important;
|
|
37
|
-
font-size: var(--mf-text-sm) !important;
|
|
38
|
-
color: var(--mf-color-on-surface) !important;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/* ── Radio button circle ──────────────────────────────────────── */
|
|
42
|
-
.mf-radio__option .mdc-radio .mdc-radio__outer-circle {
|
|
43
|
-
border-color: var(--mf-color-border) !important;
|
|
44
|
-
transition: border-color var(--mf-duration-base) var(--mf-ease-standard);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.mf-radio__option.mat-mdc-radio-checked .mdc-radio .mdc-radio__outer-circle {
|
|
48
|
-
border-color: var(--mf-color-brand) !important;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.mf-radio__option.mat-mdc-radio-checked .mdc-radio .mdc-radio__inner-circle {
|
|
52
|
-
border-color: var(--mf-color-brand) !important;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* ── Ripple ────────────────────────────────────────────────────── */
|
|
56
|
-
.mf-radio__option .mat-ripple-element {
|
|
57
|
-
background-color: var(--mf-color-brand-light) !important;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* ── Label text ────────────────────────────────────────────────── */
|
|
61
|
-
.mf-radio__option .mdc-label {
|
|
62
|
-
font-family: var(--mf-font-base) !important;
|
|
63
|
-
color: var(--mf-color-on-surface) !important;
|
|
64
|
-
cursor: pointer;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* ── Disabled ──────────────────────────────────────────────────── */
|
|
68
|
-
.mf-radio__option.mat-mdc-radio-disabled .mdc-label {
|
|
69
|
-
color: var(--mf-color-neutral-400) !important;
|
|
70
|
-
cursor: default;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.mf-radio__hint,
|
|
74
|
-
.mf-radio__error {
|
|
75
|
-
margin: var(--mf-space-2) 0 0;
|
|
76
|
-
font-size: var(--mf-text-xs);
|
|
77
|
-
line-height: var(--mf-leading-normal);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.mf-radio__hint {
|
|
81
|
-
color: var(--mf-color-neutral-600);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.mf-radio__error {
|
|
85
|
-
color: var(--mf-color-error-500);
|
|
86
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
-
import { MfRadioButtonComponent } from './mf-radio-button.component';
|
|
3
|
-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
4
|
-
|
|
5
|
-
const SAMPLE_OPTIONS = [
|
|
6
|
-
{ value: 'a', label: 'Opción A' },
|
|
7
|
-
{ value: 'b', label: 'Opción B' },
|
|
8
|
-
{ value: 'c', label: 'Opción C', disabled: true },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
describe('MfRadioButtonComponent', () => {
|
|
12
|
-
let fixture: ComponentFixture<MfRadioButtonComponent>;
|
|
13
|
-
let component: MfRadioButtonComponent;
|
|
14
|
-
|
|
15
|
-
beforeEach(async () => {
|
|
16
|
-
await TestBed.configureTestingModule({
|
|
17
|
-
imports: [MfRadioButtonComponent, NoopAnimationsModule],
|
|
18
|
-
}).compileComponents();
|
|
19
|
-
|
|
20
|
-
fixture = TestBed.createComponent(MfRadioButtonComponent);
|
|
21
|
-
component = fixture.componentInstance;
|
|
22
|
-
fixture.componentRef.setInput('options', SAMPLE_OPTIONS);
|
|
23
|
-
fixture.detectChanges();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should create', () => {
|
|
27
|
-
expect(component).toBeTruthy();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should render mat-radio-group', () => {
|
|
31
|
-
const group = fixture.nativeElement.querySelector('mat-radio-group');
|
|
32
|
-
expect(group).toBeTruthy();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should render all options', () => {
|
|
36
|
-
const buttons = fixture.nativeElement.querySelectorAll('mat-radio-button');
|
|
37
|
-
expect(buttons.length).toBe(SAMPLE_OPTIONS.length);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should apply vertical class by default', () => {
|
|
41
|
-
expect(component.hostClasses()).toContain('mf-radio--vertical');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should apply horizontal class when direction is horizontal', () => {
|
|
45
|
-
fixture.componentRef.setInput('direction', 'horizontal');
|
|
46
|
-
expect(component.hostClasses()).toContain('mf-radio--horizontal');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should emit mfChange when selection changes', () => {
|
|
50
|
-
const spy = vi.fn();
|
|
51
|
-
component.mfChange.subscribe(spy);
|
|
52
|
-
component.onChange({ value: 'b' });
|
|
53
|
-
expect(spy).toHaveBeenCalledWith('b');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
ChangeDetectorRef,
|
|
4
|
-
Component,
|
|
5
|
-
computed,
|
|
6
|
-
effect,
|
|
7
|
-
forwardRef,
|
|
8
|
-
inject,
|
|
9
|
-
input,
|
|
10
|
-
output,
|
|
11
|
-
signal,
|
|
12
|
-
} from '@angular/core';
|
|
13
|
-
import {
|
|
14
|
-
ControlValueAccessor,
|
|
15
|
-
NG_VALUE_ACCESSOR,
|
|
16
|
-
NgControl,
|
|
17
|
-
} from '@angular/forms';
|
|
18
|
-
import { MatRadioModule } from '@angular/material/radio';
|
|
19
|
-
import {
|
|
20
|
-
createUniqueId,
|
|
21
|
-
hasAccessibleName,
|
|
22
|
-
mergeAriaIds,
|
|
23
|
-
warnInDev,
|
|
24
|
-
} from '../../a11y';
|
|
25
|
-
|
|
26
|
-
export interface MfRadioOption {
|
|
27
|
-
/** Valor de la opción */
|
|
28
|
-
value: string;
|
|
29
|
-
/** Texto visible */
|
|
30
|
-
label: string;
|
|
31
|
-
/** Deshabilitada individualmente */
|
|
32
|
-
disabled?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type MfRadioDirection = 'horizontal' | 'vertical';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Grupo de radio buttons de la librerÃa ng-comps.
|
|
39
|
-
* Envuelve Angular Material `mat-radio-group` + `mat-radio-button`
|
|
40
|
-
* y expone una API uniforme con look and feel de marca.
|
|
41
|
-
*/
|
|
42
|
-
@Component({
|
|
43
|
-
selector: 'mf-radio-button',
|
|
44
|
-
imports: [MatRadioModule],
|
|
45
|
-
providers: [
|
|
46
|
-
{
|
|
47
|
-
provide: NG_VALUE_ACCESSOR,
|
|
48
|
-
useExisting: forwardRef(() => MfRadioButtonComponent),
|
|
49
|
-
multi: true,
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
template: `
|
|
53
|
-
<fieldset class="mf-radio__fieldset">
|
|
54
|
-
@if (label()) {
|
|
55
|
-
<legend class="mf-radio__legend" [id]="legendId()">
|
|
56
|
-
{{ label() }}
|
|
57
|
-
@if (required()) {
|
|
58
|
-
<span aria-hidden="true"> *</span>
|
|
59
|
-
}
|
|
60
|
-
</legend>
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
<mat-radio-group
|
|
64
|
-
[class]="hostClasses()"
|
|
65
|
-
[disabled]="isDisabled()"
|
|
66
|
-
[name]="groupName()"
|
|
67
|
-
[required]="required()"
|
|
68
|
-
[value]="internalValue()"
|
|
69
|
-
[attr.aria-label]="resolvedAriaLabel()"
|
|
70
|
-
[attr.aria-labelledby]="computedLabelledby()"
|
|
71
|
-
[attr.aria-describedby]="describedBy()"
|
|
72
|
-
[attr.aria-invalid]="isInvalid() ? 'true' : null"
|
|
73
|
-
[attr.aria-required]="required() ? 'true' : null"
|
|
74
|
-
(change)="onChange($event)"
|
|
75
|
-
>
|
|
76
|
-
@for (option of options(); track option.value) {
|
|
77
|
-
<mat-radio-button
|
|
78
|
-
class="mf-radio__option"
|
|
79
|
-
[value]="option.value"
|
|
80
|
-
[disabled]="option.disabled ?? false"
|
|
81
|
-
>
|
|
82
|
-
{{ option.label }}
|
|
83
|
-
</mat-radio-button>
|
|
84
|
-
}
|
|
85
|
-
</mat-radio-group>
|
|
86
|
-
|
|
87
|
-
@if (hint()) {
|
|
88
|
-
<p class="mf-radio__hint" [attr.id]="hintId()">{{ hint() }}</p>
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@if (error()) {
|
|
92
|
-
<p class="mf-radio__error" [attr.id]="errorId()" role="alert">
|
|
93
|
-
{{ error() }}
|
|
94
|
-
</p>
|
|
95
|
-
}
|
|
96
|
-
</fieldset>
|
|
97
|
-
`,
|
|
98
|
-
styleUrl: './mf-radio-button.component.css',
|
|
99
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
100
|
-
})
|
|
101
|
-
export class MfRadioButtonComponent implements ControlValueAccessor {
|
|
102
|
-
private readonly cdr = inject(ChangeDetectorRef);
|
|
103
|
-
private readonly ngControl = inject(NgControl, { self: true, optional: true });
|
|
104
|
-
private readonly generatedId = createUniqueId('mf-radio');
|
|
105
|
-
private readonly disabledFromForm = signal(false);
|
|
106
|
-
protected readonly internalValue = signal<string | undefined>(undefined);
|
|
107
|
-
private onControlChange: (value: string) => void = () => undefined;
|
|
108
|
-
private onControlTouched: () => void = () => undefined;
|
|
109
|
-
|
|
110
|
-
/** Opciones del grupo */
|
|
111
|
-
readonly options = input.required<MfRadioOption[]>();
|
|
112
|
-
/** ID del control */
|
|
113
|
-
readonly id = input<string | undefined>(undefined);
|
|
114
|
-
/** Etiqueta visible del grupo */
|
|
115
|
-
readonly label = input<string | undefined>(undefined);
|
|
116
|
-
/** Valor seleccionado */
|
|
117
|
-
readonly value = input<string | undefined>(undefined);
|
|
118
|
-
/** Deshabilitado */
|
|
119
|
-
readonly disabled = input(false);
|
|
120
|
-
/** Requerido */
|
|
121
|
-
readonly required = input(false);
|
|
122
|
-
/** Dirección del grupo */
|
|
123
|
-
readonly direction = input<MfRadioDirection>('vertical');
|
|
124
|
-
/** Nombre accesible alternativo del grupo */
|
|
125
|
-
readonly ariaLabel = input<string | undefined>(undefined);
|
|
126
|
-
/** Etiqueta accesible para el grupo */
|
|
127
|
-
readonly ariaLabelledby = input<string | undefined>(undefined);
|
|
128
|
-
/** Referencia externa a elementos descriptivos adicionales */
|
|
129
|
-
readonly ariaDescribedby = input<string | undefined>(undefined);
|
|
130
|
-
/** Texto de ayuda */
|
|
131
|
-
readonly hint = input<string | undefined>(undefined);
|
|
132
|
-
/** Mensaje de error */
|
|
133
|
-
readonly error = input<string | undefined>(undefined);
|
|
134
|
-
/** Name del grupo */
|
|
135
|
-
readonly name = input<string | undefined>(undefined);
|
|
136
|
-
|
|
137
|
-
readonly mfChange = output<string>();
|
|
138
|
-
|
|
139
|
-
constructor() {
|
|
140
|
-
effect(() => {
|
|
141
|
-
this.internalValue.set(this.value());
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
effect(() => {
|
|
145
|
-
if (
|
|
146
|
-
!hasAccessibleName(
|
|
147
|
-
this.label(),
|
|
148
|
-
this.ariaLabel(),
|
|
149
|
-
this.ariaLabelledby(),
|
|
150
|
-
)
|
|
151
|
-
) {
|
|
152
|
-
warnInDev(
|
|
153
|
-
'mf-radio-button requiere `label`, `ariaLabel` o `ariaLabelledby` para exponer un nombre accesible.',
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
readonly controlId = computed(() => this.id() ?? this.generatedId);
|
|
160
|
-
readonly legendId = computed(() => `${this.controlId()}-legend`);
|
|
161
|
-
readonly hintId = computed(() =>
|
|
162
|
-
this.hint() ? `${this.controlId()}-hint` : null,
|
|
163
|
-
);
|
|
164
|
-
readonly errorId = computed(() =>
|
|
165
|
-
this.error() ? `${this.controlId()}-error` : null,
|
|
166
|
-
);
|
|
167
|
-
readonly computedLabelledby = computed(() =>
|
|
168
|
-
mergeAriaIds(
|
|
169
|
-
this.ariaLabelledby(),
|
|
170
|
-
this.label() ? this.legendId() : null,
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
readonly describedBy = computed(() =>
|
|
174
|
-
mergeAriaIds(this.ariaDescribedby(), this.hintId(), this.errorId()),
|
|
175
|
-
);
|
|
176
|
-
readonly groupName = computed(() => this.name() ?? this.controlId());
|
|
177
|
-
readonly resolvedAriaLabel = computed(() =>
|
|
178
|
-
this.label() ? null : this.ariaLabel() ?? null,
|
|
179
|
-
);
|
|
180
|
-
readonly isDisabled = computed(
|
|
181
|
-
() => this.disabled() || this.disabledFromForm(),
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
readonly hostClasses = computed(() => {
|
|
185
|
-
return `mf-radio mf-radio--${this.direction()}`;
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
writeValue(value: string | null): void {
|
|
189
|
-
this.internalValue.set(value ?? undefined);
|
|
190
|
-
this.cdr.markForCheck();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
registerOnChange(fn: (value: string) => void): void {
|
|
194
|
-
this.onControlChange = fn;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
registerOnTouched(fn: () => void): void {
|
|
198
|
-
this.onControlTouched = fn;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
setDisabledState(isDisabled: boolean): void {
|
|
202
|
-
this.disabledFromForm.set(isDisabled);
|
|
203
|
-
this.cdr.markForCheck();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
isInvalid(): boolean {
|
|
207
|
-
const control = this.ngControl?.control;
|
|
208
|
-
return Boolean(
|
|
209
|
-
this.error() || (control?.invalid && (control.touched || control.dirty)),
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
onChange(event: { value: string }): void {
|
|
214
|
-
this.internalValue.set(event.value);
|
|
215
|
-
this.onControlChange(event.value);
|
|
216
|
-
this.onControlTouched();
|
|
217
|
-
this.mfChange.emit(event.value);
|
|
218
|
-
}
|
|
219
|
-
}
|