ng-hub-ui-forms 22.12.2 → 22.14.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/README.md +50 -5
- package/fesm2022/ng-hub-ui-forms.mjs +1259 -141
- package/fesm2022/ng-hub-ui-forms.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_field.scss +66 -0
- package/styles/_tokens.scss +18 -1
- package/styles/mixins/_file-input-theme.scss +71 -36
- package/styles/mixins/_forms-theme.scss +11 -6
- package/styles/mixins/_segmented-theme.scss +13 -7
- package/styles/readonly-theme.spec.ts +71 -0
- package/types/ng-hub-ui-forms.d.ts +189 -33
- package/ng-hub-ui-forms-22.12.2.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ng-hub-ui-forms",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Accessible, signal-based form fields for Angular (input, textarea, slider, segmented, select, datepicker, file input with drag & drop and upload progress) with automatic error display for controls, FormGroups and FormArrays. Reactive Forms today, Signal Forms ready. Part of the ng-hub-ui family.",
|
|
6
6
|
"author": "Carlos Morcillo <carlos.morcillo@me.com> (https://www.carlosmorcillo.com)",
|
package/styles/_field.scss
CHANGED
|
@@ -146,3 +146,69 @@
|
|
|
146
146
|
color: var(--hub-form-valid-feedback-color);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read-only fields.
|
|
152
|
+
*
|
|
153
|
+
* Applied from the field's own `readonly` state, so nothing has to be remembered at
|
|
154
|
+
* the call site: mark a field read-only and it looks read-only.
|
|
155
|
+
*
|
|
156
|
+
* The border, the background and the focus ring exist to say "you can type here".
|
|
157
|
+
* On a field that refuses input they are a promise the field then breaks — and next
|
|
158
|
+
* to an editable neighbour there was nothing at all to tell the two apart.
|
|
159
|
+
*
|
|
160
|
+
* Deliberately not the disabled treatment. Disabled means "not applicable now" and
|
|
161
|
+
* fades to make that plain; read-only means "this is the value, it is simply not
|
|
162
|
+
* yours to change here", so the text stays at full contrast and remains selectable —
|
|
163
|
+
* copying a tax id out of a document is the whole point of showing it.
|
|
164
|
+
*
|
|
165
|
+
* The padding stays, so a read-only field keeps the same box and the same baseline
|
|
166
|
+
* as the editable ones beside it in a grid.
|
|
167
|
+
*/
|
|
168
|
+
.hub-field--readonly {
|
|
169
|
+
.hub-field__control,
|
|
170
|
+
.hub-input__control,
|
|
171
|
+
.hub-textarea__control,
|
|
172
|
+
.hub-select__control {
|
|
173
|
+
background-color: var(--hub-input-readonly-bg);
|
|
174
|
+
border-color: var(--hub-input-readonly-border-color);
|
|
175
|
+
color: var(--hub-input-readonly-color);
|
|
176
|
+
cursor: var(--hub-input-readonly-cursor);
|
|
177
|
+
box-shadow: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// No focus affordance either: nothing here answers to being focused.
|
|
181
|
+
.hub-field__control:focus,
|
|
182
|
+
.hub-field__control:focus-visible,
|
|
183
|
+
.hub-input__control:focus,
|
|
184
|
+
.hub-textarea__control:focus,
|
|
185
|
+
.hub-select__control.ng-select-focused {
|
|
186
|
+
border-color: var(--hub-input-readonly-border-color);
|
|
187
|
+
box-shadow: none;
|
|
188
|
+
outline: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A caret, a clear cross and a calendar all promise a choice that is not on
|
|
192
|
+
// offer. The datepicker already hides its own clear when read-only; the icon it
|
|
193
|
+
// opens with is the same promise.
|
|
194
|
+
.hub-select__control {
|
|
195
|
+
.ng-arrow-wrapper,
|
|
196
|
+
.ng-clear-wrapper {
|
|
197
|
+
display: none;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.hub-datepicker__icon {
|
|
202
|
+
display: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The error border survives. Dropping the chrome is about not promising input;
|
|
206
|
+
// an invalid value is a different message, and one the user still has to see —
|
|
207
|
+
// the feedback text alone is easy to miss in a long form. Needs the explicit
|
|
208
|
+
// exception because the rules above outrank `--invalid` on their own.
|
|
209
|
+
.hub-field__control--invalid,
|
|
210
|
+
.hub-field__control--invalid:focus,
|
|
211
|
+
.hub-select__control--invalid {
|
|
212
|
+
border-color: var(--hub-form-invalid-border-color);
|
|
213
|
+
}
|
|
214
|
+
}
|
package/styles/_tokens.scss
CHANGED
|
@@ -50,7 +50,16 @@
|
|
|
50
50
|
// ── Input ─────────────────────────────────────────────────────────────────
|
|
51
51
|
--hub-input-color: var(--hub-sys-text-primary, #212529);
|
|
52
52
|
--hub-input-bg: var(--hub-sys-surface-page, #fff);
|
|
53
|
-
--hub-input-font-family: var(
|
|
53
|
+
--hub-input-font-family: var(
|
|
54
|
+
--hub-ref-font-family-base,
|
|
55
|
+
system-ui,
|
|
56
|
+
-apple-system,
|
|
57
|
+
'Segoe UI',
|
|
58
|
+
Roboto,
|
|
59
|
+
'Helvetica Neue',
|
|
60
|
+
Arial,
|
|
61
|
+
sans-serif
|
|
62
|
+
);
|
|
54
63
|
--hub-input-font-size: var(--hub-ref-font-size-base, 1rem);
|
|
55
64
|
--hub-input-font-weight: var(--hub-ref-font-weight-base, 400);
|
|
56
65
|
--hub-input-line-height: var(--hub-ref-line-height-base, 1.5);
|
|
@@ -62,6 +71,14 @@
|
|
|
62
71
|
--hub-input-focus-border-color: var(--hub-sys-color-primary, #0d6efd);
|
|
63
72
|
--hub-input-focus-box-shadow: 0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-focus-ring-color);
|
|
64
73
|
--hub-input-placeholder-color: var(--hub-sys-text-muted, #6c757d);
|
|
74
|
+
|
|
75
|
+
// Read-only fields. Not a faded variant of disabled: a read-only value is there
|
|
76
|
+
// to be read, so it keeps full contrast and loses only the chrome that promises
|
|
77
|
+
// you can type in it.
|
|
78
|
+
--hub-input-readonly-bg: transparent;
|
|
79
|
+
--hub-input-readonly-border-color: transparent;
|
|
80
|
+
--hub-input-readonly-color: var(--hub-input-color);
|
|
81
|
+
--hub-input-readonly-cursor: default;
|
|
65
82
|
--hub-input-transition: var(--hub-form-transition);
|
|
66
83
|
--hub-input-wrapper-gap: var(--hub-ref-space-2, 0.5rem);
|
|
67
84
|
|
|
@@ -34,47 +34,82 @@
|
|
|
34
34
|
// scss-docs-start hub-file-input-theme
|
|
35
35
|
@mixin hub-file-input-theme(
|
|
36
36
|
// ── Dropzone surface ──
|
|
37
|
-
$bg: null,
|
|
38
|
-
|
|
39
|
-
$
|
|
40
|
-
|
|
41
|
-
$border-
|
|
42
|
-
|
|
43
|
-
$
|
|
44
|
-
|
|
45
|
-
$
|
|
46
|
-
|
|
37
|
+
$bg: null,
|
|
38
|
+
// dropzone background
|
|
39
|
+
$color: null,
|
|
40
|
+
// dropzone text colour
|
|
41
|
+
$border-width: null,
|
|
42
|
+
// dropzone border width
|
|
43
|
+
$border-style: null,
|
|
44
|
+
// dropzone border style (defaults to `dashed`)
|
|
45
|
+
$border-color: null,
|
|
46
|
+
// dropzone border colour at rest
|
|
47
|
+
$border-radius: null,
|
|
48
|
+
// dropzone corner radius
|
|
49
|
+
$padding-x: null,
|
|
50
|
+
// dropzone horizontal padding
|
|
51
|
+
$padding-y: null,
|
|
52
|
+
// dropzone vertical padding
|
|
53
|
+
$min-height: null,
|
|
54
|
+
// dropzone minimum height
|
|
55
|
+
$gap: null,
|
|
56
|
+
// gutter between icon, prompt and hint
|
|
47
57
|
// ── States ──
|
|
48
|
-
$hover-bg: null,
|
|
49
|
-
|
|
50
|
-
$
|
|
51
|
-
|
|
58
|
+
$hover-bg: null,
|
|
59
|
+
// background on hover
|
|
60
|
+
$hover-border-color: null,
|
|
61
|
+
// border colour on hover
|
|
62
|
+
$dragover-bg: null,
|
|
63
|
+
// background while a drag hovers the zone
|
|
64
|
+
$dragover-border-color: null,
|
|
65
|
+
// border colour while a drag hovers the zone
|
|
52
66
|
// ── Copy ──
|
|
53
|
-
$prompt-color: null,
|
|
54
|
-
|
|
55
|
-
$
|
|
67
|
+
$prompt-color: null,
|
|
68
|
+
// "drag files here" colour
|
|
69
|
+
$browse-color: null,
|
|
70
|
+
// "browse files" colour
|
|
71
|
+
$hint-color: null,
|
|
72
|
+
// constraints-hint colour
|
|
56
73
|
// ── Glyphs (CSS masks) ──
|
|
57
|
-
$icon: null,
|
|
58
|
-
|
|
59
|
-
$icon-
|
|
60
|
-
|
|
61
|
-
$
|
|
62
|
-
|
|
63
|
-
$
|
|
64
|
-
|
|
65
|
-
$
|
|
66
|
-
|
|
74
|
+
$icon: null,
|
|
75
|
+
// upload glyph shown in the dropzone
|
|
76
|
+
$icon-size: null,
|
|
77
|
+
// upload glyph size
|
|
78
|
+
$icon-color: null,
|
|
79
|
+
// upload glyph tint
|
|
80
|
+
$file-icon: null,
|
|
81
|
+
// per-file glyph, for files with no thumbnail
|
|
82
|
+
$file-icon-color: null,
|
|
83
|
+
// per-file glyph tint
|
|
84
|
+
$remove-icon: null,
|
|
85
|
+
// remove-button glyph
|
|
86
|
+
$cancel-icon: null,
|
|
87
|
+
// cancel-upload glyph
|
|
88
|
+
$retry-icon: null,
|
|
89
|
+
// retry-upload glyph
|
|
90
|
+
$done-icon: null,
|
|
91
|
+
// finished-upload tick
|
|
92
|
+
$done-color: null,
|
|
93
|
+
// finished-upload tick + item border colour
|
|
67
94
|
// ── File list ──
|
|
68
|
-
$item-bg: null,
|
|
69
|
-
|
|
70
|
-
$item-border-
|
|
71
|
-
|
|
72
|
-
$
|
|
73
|
-
|
|
95
|
+
$item-bg: null,
|
|
96
|
+
// background of a file row / tile
|
|
97
|
+
$item-border-color: null,
|
|
98
|
+
// border of a file row / tile
|
|
99
|
+
$item-border-radius: null,
|
|
100
|
+
// corner radius of a file row / tile
|
|
101
|
+
$thumb-size: null,
|
|
102
|
+
// thumbnail size in list preview
|
|
103
|
+
$grid-min-width: null,
|
|
104
|
+
// minimum tile width in grid preview
|
|
105
|
+
$grid-thumb-height: null,
|
|
106
|
+
// thumbnail height in grid preview
|
|
74
107
|
// ── Upload progress ──
|
|
75
|
-
$progress-height: null,
|
|
76
|
-
|
|
77
|
-
$progress-
|
|
108
|
+
$progress-height: null,
|
|
109
|
+
// progress-bar thickness
|
|
110
|
+
$progress-track-bg: null,
|
|
111
|
+
// progress-bar track colour
|
|
112
|
+
$progress-bar-bg: null // progress-bar fill colour
|
|
78
113
|
) {
|
|
79
114
|
// Dropzone surface
|
|
80
115
|
@if $bg != null {
|
|
@@ -26,15 +26,20 @@
|
|
|
26
26
|
// scss-docs-start hub-forms-theme
|
|
27
27
|
@mixin hub-forms-theme(
|
|
28
28
|
// ── Focus ring (shared by every control) ──
|
|
29
|
-
$focus-ring-color: null,
|
|
30
|
-
|
|
29
|
+
$focus-ring-color: null,
|
|
30
|
+
// focus-ring colour
|
|
31
|
+
$focus-ring-width: null,
|
|
32
|
+
// focus-ring width
|
|
31
33
|
// ── Validation accents ──
|
|
32
|
-
$invalid-color: null,
|
|
33
|
-
|
|
34
|
+
$invalid-color: null,
|
|
35
|
+
// invalid text + border colour
|
|
36
|
+
$valid-color: null,
|
|
37
|
+
// valid text + border colour
|
|
34
38
|
// ── State ──
|
|
35
|
-
$disabled-opacity: null,
|
|
39
|
+
$disabled-opacity: null,
|
|
40
|
+
// opacity applied to a disabled control
|
|
36
41
|
// ── Motion ──
|
|
37
|
-
$transition: null
|
|
42
|
+
$transition: null // border/box-shadow transition shared by inputs
|
|
38
43
|
) {
|
|
39
44
|
// Focus ring
|
|
40
45
|
@if $focus-ring-color != null {
|
|
@@ -26,15 +26,21 @@
|
|
|
26
26
|
// scss-docs-start hub-segmented-theme
|
|
27
27
|
@mixin hub-segmented-theme(
|
|
28
28
|
// ── Colour ──
|
|
29
|
-
$bg: null,
|
|
30
|
-
|
|
31
|
-
$selected-
|
|
29
|
+
$bg: null,
|
|
30
|
+
// the bar (track) surface
|
|
31
|
+
$selected-bg: null,
|
|
32
|
+
// selected pill / sliding-indicator fill
|
|
33
|
+
$selected-color: null,
|
|
34
|
+
// selected option text colour
|
|
32
35
|
// ── Shape ──
|
|
33
|
-
$radius: null,
|
|
36
|
+
$radius: null,
|
|
37
|
+
// bar corner radius (pill radius derives from it)
|
|
34
38
|
// ── Density / spacing ──
|
|
35
|
-
$gap: null,
|
|
36
|
-
|
|
37
|
-
$padding-
|
|
39
|
+
$gap: null,
|
|
40
|
+
// padding around, and gutter between, the options
|
|
41
|
+
$padding-x: null,
|
|
42
|
+
// option horizontal padding
|
|
43
|
+
$padding-y: null // option vertical padding
|
|
38
44
|
) {
|
|
39
45
|
// Colour
|
|
40
46
|
@if $bg != null {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Component, provideZonelessChangeDetection, signal } from '@angular/core';
|
|
2
|
+
import { TestBed } from '@angular/core/testing';
|
|
3
|
+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { provideHubForms } from '../services/forms-config';
|
|
5
|
+
import { HubDatepickerComponent } from '../components/datepicker/datepicker.component';
|
|
6
|
+
import { HubInputComponent } from '../components/input/input.component';
|
|
7
|
+
import { HubTextareaComponent } from '../components/textarea/textarea.component';
|
|
8
|
+
import { HubSelectComponent } from '../select/select.component';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A read-only field has to look read-only.
|
|
12
|
+
*
|
|
13
|
+
* `readonly` reached the native attributes and stopped there, so the field went on
|
|
14
|
+
* drawing the border, the background and the focus affordance of something you can
|
|
15
|
+
* type in. It invites the click it then refuses — and next to a genuinely editable
|
|
16
|
+
* field there was nothing to tell the two apart.
|
|
17
|
+
*
|
|
18
|
+
* Read-only is not disabled, and the theme keeps them distinct on purpose: disabled
|
|
19
|
+
* says "not applicable now" and fades; read-only says "this is the value, just not
|
|
20
|
+
* yours to change here" and stays at full contrast, because it is there to be read.
|
|
21
|
+
*/
|
|
22
|
+
@Component({
|
|
23
|
+
standalone: true,
|
|
24
|
+
imports: [HubInputComponent, HubTextareaComponent, HubSelectComponent, HubDatepickerComponent, ReactiveFormsModule],
|
|
25
|
+
template: `
|
|
26
|
+
<hub-input [formControl]="ctrl" [readonly]="ro()" label="Text" />
|
|
27
|
+
<hub-textarea [formControl]="ctrl" [readonly]="ro()" label="Notes" />
|
|
28
|
+
<hub-select [formControl]="ctrl" [readonly]="ro()" label="Pick" />
|
|
29
|
+
<hub-datepicker [formControl]="ctrl" [readonly]="ro()" label="Date" />
|
|
30
|
+
`
|
|
31
|
+
})
|
|
32
|
+
class ReadonlyHostComponent {
|
|
33
|
+
readonly ctrl = new FormControl<any>('Something');
|
|
34
|
+
readonly ro = signal(false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('read-only field theme', () => {
|
|
38
|
+
let fixture: any;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
TestBed.resetTestingModule();
|
|
42
|
+
await TestBed.configureTestingModule({
|
|
43
|
+
imports: [ReadonlyHostComponent],
|
|
44
|
+
providers: [provideZonelessChangeDetection(), provideHubForms()]
|
|
45
|
+
}).compileComponents();
|
|
46
|
+
|
|
47
|
+
fixture = TestBed.createComponent(ReadonlyHostComponent);
|
|
48
|
+
await fixture.whenStable();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const roots = (): HTMLElement[] => Array.from(fixture.nativeElement.querySelectorAll('.hub-field'));
|
|
52
|
+
|
|
53
|
+
it('marks nothing while the fields are editable', () => {
|
|
54
|
+
expect(roots().length).toBe(4);
|
|
55
|
+
expect(roots().every((el) => !el.classList.contains('hub-field--readonly'))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('marks every field when it turns read-only', async () => {
|
|
59
|
+
fixture.componentInstance.ro.set(true);
|
|
60
|
+
await fixture.whenStable();
|
|
61
|
+
|
|
62
|
+
expect(roots().every((el) => el.classList.contains('hub-field--readonly'))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not confuse read-only with disabled', async () => {
|
|
66
|
+
fixture.componentInstance.ro.set(true);
|
|
67
|
+
await fixture.whenStable();
|
|
68
|
+
|
|
69
|
+
expect(roots().some((el) => el.classList.contains('hub-field--disabled'))).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|