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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ng-hub-ui-forms",
3
- "version": "22.12.2",
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)",
@@ -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
+ }
@@ -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(--hub-ref-font-family-base, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
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, // dropzone background
38
- $color: null, // dropzone text colour
39
- $border-width: null, // dropzone border width
40
- $border-style: null, // dropzone border style (defaults to `dashed`)
41
- $border-color: null, // dropzone border colour at rest
42
- $border-radius: null, // dropzone corner radius
43
- $padding-x: null, // dropzone horizontal padding
44
- $padding-y: null, // dropzone vertical padding
45
- $min-height: null, // dropzone minimum height
46
- $gap: null, // gutter between icon, prompt and hint
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, // background on hover
49
- $hover-border-color: null, // border colour on hover
50
- $dragover-bg: null, // background while a drag hovers the zone
51
- $dragover-border-color: null, // border colour while a drag hovers the zone
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, // "drag files here" colour
54
- $browse-color: null, // "browse files" colour
55
- $hint-color: null, // constraints-hint colour
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, // upload glyph shown in the dropzone
58
- $icon-size: null, // upload glyph size
59
- $icon-color: null, // upload glyph tint
60
- $file-icon: null, // per-file glyph, for files with no thumbnail
61
- $file-icon-color: null, // per-file glyph tint
62
- $remove-icon: null, // remove-button glyph
63
- $cancel-icon: null, // cancel-upload glyph
64
- $retry-icon: null, // retry-upload glyph
65
- $done-icon: null, // finished-upload tick
66
- $done-color: null, // finished-upload tick + item border colour
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, // background of a file row / tile
69
- $item-border-color: null, // border of a file row / tile
70
- $item-border-radius: null, // corner radius of a file row / tile
71
- $thumb-size: null, // thumbnail size in list preview
72
- $grid-min-width: null, // minimum tile width in grid preview
73
- $grid-thumb-height: null, // thumbnail height in grid preview
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, // progress-bar thickness
76
- $progress-track-bg: null, // progress-bar track colour
77
- $progress-bar-bg: null // progress-bar fill colour
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, // focus-ring colour
30
- $focus-ring-width: null, // focus-ring width
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, // invalid text + border colour
33
- $valid-color: null, // valid text + border colour
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, // opacity applied to a disabled control
39
+ $disabled-opacity: null,
40
+ // opacity applied to a disabled control
36
41
  // ── Motion ──
37
- $transition: null // border/box-shadow transition shared by inputs
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, // the bar (track) surface
30
- $selected-bg: null, // selected pill / sliding-indicator fill
31
- $selected-color: null, // selected option text colour
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, // bar corner radius (pill radius derives from it)
36
+ $radius: null,
37
+ // bar corner radius (pill radius derives from it)
34
38
  // ── Density / spacing ──
35
- $gap: null, // padding around, and gutter between, the options
36
- $padding-x: null, // option horizontal padding
37
- $padding-y: null // option vertical 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
+ });