noph-ui 0.26.13 → 0.27.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.
@@ -11,15 +11,13 @@
11
11
  variant = 'outlined',
12
12
  element = $bindable(),
13
13
  populated,
14
- reportValidity = $bindable(),
15
- checkValidity = $bindable(),
16
14
  clampMenuWidth = false,
17
15
  children,
18
16
  optionsFilter,
19
- onoptionselect = (option: AutoCompleteOption) => {
17
+ onoptionselect = (option, menuElement) => {
20
18
  value = option.label
21
19
  finalPopulated = populated
22
- menuElement?.hidePopover()
20
+ menuElement.hidePopover()
23
21
  },
24
22
  onkeydown,
25
23
  onclick,
@@ -67,7 +65,9 @@
67
65
  }
68
66
 
69
67
  const selectOption = (option: AutoCompleteOption) => {
70
- onoptionselect(option)
68
+ if (menuElement) {
69
+ onoptionselect(option, menuElement)
70
+ }
71
71
  }
72
72
 
73
73
  $effect(() => {
@@ -195,8 +195,6 @@
195
195
  }
196
196
  onkeydown?.(event)
197
197
  }}
198
- bind:reportValidity
199
- bind:checkValidity
200
198
  bind:element
201
199
  >{@render children?.()}
202
200
  </TextField>
@@ -1,4 +1,4 @@
1
1
  import type { AutoCompleteProps } from './types.ts';
2
- declare const AutoComplete: import("svelte").Component<AutoCompleteProps, {}, "element" | "value" | "reportValidity" | "checkValidity">;
2
+ declare const AutoComplete: import("svelte").Component<AutoCompleteProps, {}, "element" | "value">;
3
3
  type AutoComplete = ReturnType<typeof AutoComplete>;
4
4
  export default AutoComplete;
@@ -6,7 +6,7 @@ export interface AutoCompleteOption {
6
6
  export interface AutoCompleteProps extends Omit<InputFieldProps, 'clientWidth' | 'clientHeight'> {
7
7
  options: AutoCompleteOption[];
8
8
  optionsFilter?: (option: AutoCompleteOption) => boolean;
9
- onoptionselect?: (option: AutoCompleteOption) => void;
9
+ onoptionselect?: (option: AutoCompleteOption, menuElement: HTMLDivElement) => void;
10
10
  clampMenuWidth?: boolean;
11
11
  showPopover?: () => void;
12
12
  hidePopover?: () => void;
@@ -4,16 +4,18 @@
4
4
  label?: string
5
5
  noAsterisk?: boolean
6
6
  supportingText?: string
7
- error?: boolean
8
- errorText?: string
7
+ issues?:
8
+ | {
9
+ message: string
10
+ }[]
11
+ | undefined
9
12
  variant?: 'outlined' | 'filled'
10
13
  element?: HTMLSpanElement
11
14
  }
12
15
  let {
13
16
  id,
14
17
  supportingText,
15
- error,
16
- errorText,
18
+ issues,
17
19
  variant = 'outlined',
18
20
  value = $bindable(),
19
21
  label,
@@ -21,30 +23,26 @@
21
23
  required,
22
24
  noAsterisk,
23
25
  children,
24
- onchange,
25
- oninvalid,
26
+ oninput,
26
27
  ...attributes
27
28
  }: SelectProps = $props()
28
29
  const uid = $props.id()
29
30
  const selectId = id ?? `select-${uid}`
30
31
 
31
32
  let animateLabel = $state(false)
32
- let errorTextRaw = $derived(errorText)
33
- let errorRaw = $derived(error)
33
+ let errorText = $derived(issues?.map((issue) => issue.message).join(', '))
34
34
  </script>
35
35
 
36
36
  <div
37
37
  class={[
38
38
  'np-select-container',
39
39
  variant,
40
- errorRaw && 'error',
41
40
  disabled && 'disabled',
42
41
  required && !noAsterisk && 'asterisk',
43
42
  (value === null || value === undefined || value === '') && 'is-empty',
44
43
  animateLabel && 'animate-label',
45
44
  attributes.class,
46
45
  ]}
47
- aria-disabled={disabled}
48
46
  >
49
47
  {#if variant === 'outlined'}
50
48
  <div class="np-select-outline">
@@ -77,34 +75,22 @@
77
75
  ) {
78
76
  animateLabel = true
79
77
  }
80
- onchange?.(event)
81
- }}
82
- oninvalid={(event) => {
83
- event.preventDefault()
84
- const { currentTarget } = event
85
- errorRaw = true
86
- if (errorText === undefined) {
87
- errorTextRaw = currentTarget.validationMessage
88
- }
89
- oninvalid?.(event)
78
+ oninput?.(event)
90
79
  }}
91
80
  {disabled}
92
81
  {required}
93
82
  id={selectId}
94
- aria-invalid={error}
95
- aria-errormessage={errorTextRaw && errorRaw ? `supporting-text-${uid}` : undefined}
96
- aria-describedby={supportingText && (!errorTextRaw || !errorRaw)
97
- ? `supporting-text-${uid}`
98
- : undefined}
83
+ aria-errormessage={errorText ? `supporting-text-${uid}` : undefined}
84
+ aria-describedby={supportingText && !errorText ? `supporting-text-${uid}` : undefined}
99
85
  bind:value
100
86
  {...attributes}
101
87
  class="np-select"
102
88
  >
103
89
  {@render children?.()}
104
90
  </select>
105
- {#if supportingText || (errorTextRaw && errorRaw)}
106
- <div id="supporting-text-{uid}" class="supporting-text" role={errorRaw ? 'alert' : undefined}>
107
- {errorRaw && errorTextRaw ? errorTextRaw : supportingText}
91
+ {#if supportingText || errorText}
92
+ <div id="supporting-text-{uid}" class="supporting-text" role={errorText ? 'alert' : undefined}>
93
+ {errorText ?? supportingText}
108
94
  </div>
109
95
  {/if}
110
96
  </div>
@@ -392,27 +378,31 @@
392
378
  padding: 0.25rem 1rem 0;
393
379
  }
394
380
 
395
- .error .supporting-text,
396
- .error label,
397
- .error .arrow,
398
- .error:focus-within label {
381
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])) .supporting-text,
382
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])) label,
383
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])) .arrow,
384
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):focus-within label {
399
385
  color: var(--np-color-error);
400
386
  }
401
387
 
402
- .error:hover label,
403
- .error:hover .arrow {
388
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):hover label,
389
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):hover .arrow {
404
390
  color: var(--np-color-on-error-container);
405
391
  }
406
392
 
407
- .error .np-select-outline,
408
- .error:focus-within .np-select-outline,
409
- .error .np-select-filled,
410
- .error:focus-within .np-select-filled {
393
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])) .np-select-outline,
394
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):focus-within
395
+ .np-select-outline,
396
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])) .np-select-filled,
397
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):focus-within
398
+ .np-select-filled {
411
399
  border-color: var(--np-color-error);
412
400
  }
413
401
 
414
- .error:hover .np-select-outline,
415
- .error:hover .np-select-filled {
402
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):hover
403
+ .np-select-outline,
404
+ .np-select-container:has(.np-select:is(:user-invalid, [aria-invalid='true'])):hover
405
+ .np-select-filled {
416
406
  border-color: var(--np-color-on-error-container);
417
407
  }
418
408
 
@@ -3,8 +3,9 @@ interface SelectProps extends HTMLSelectAttributes {
3
3
  label?: string;
4
4
  noAsterisk?: boolean;
5
5
  supportingText?: string;
6
- error?: boolean;
7
- errorText?: string;
6
+ issues?: {
7
+ message: string;
8
+ }[] | undefined;
8
9
  variant?: 'outlined' | 'filled';
9
10
  element?: HTMLSpanElement;
10
11
  }
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import Menu from '../menu/Menu.svelte'
3
- import { isFirstInvalidControlInForm } from '../text-field/report-validity.js'
4
3
  import type { SelectOption, SelectProps } from './types.ts'
5
4
  import Item from '../list/Item.svelte'
6
5
  import { tick } from 'svelte'
@@ -10,8 +9,7 @@
10
9
  let {
11
10
  options = [],
12
11
  value = $bindable(),
13
- error = false,
14
- errorText = '',
12
+ issues,
15
13
  supportingText = '',
16
14
  tabindex = 0,
17
15
  start,
@@ -29,8 +27,6 @@
29
27
  autofocus,
30
28
  onchange,
31
29
  oninput,
32
- reportValidity = $bindable(),
33
- checkValidity = $bindable(),
34
30
  multiple,
35
31
  virtualThreshold = 300,
36
32
  clampMenuWidth = false,
@@ -38,7 +34,6 @@
38
34
  }: SelectProps = $props()
39
35
 
40
36
  const uid = $props.id()
41
- let doValidity = $state(false)
42
37
  if (value === undefined) {
43
38
  if (multiple) {
44
39
  value = options.filter((option) => option.selected).map((option) => option.value)
@@ -59,8 +54,7 @@
59
54
 
60
55
  let widthProp = $derived(clampMenuWidth || useVirtualList ? 'width' : 'min-width')
61
56
 
62
- let errorTextRaw: string = $state(errorText)
63
- let errorRaw = $state(error)
57
+ let errorText = $derived(issues?.map((i) => i.message).join(', '))
64
58
  let selectElement = $state<HTMLSelectElement>()
65
59
  let menuElement = $state<HTMLDivElement>()
66
60
  let anchorElement = $state<HTMLDivElement>()
@@ -94,44 +88,6 @@
94
88
  return options.find((option) => option.value === value)?.label || ''
95
89
  })
96
90
 
97
- reportValidity = () => {
98
- if (selectElement) {
99
- const valid = selectElement.reportValidity()
100
- if (valid) {
101
- errorRaw = error
102
- errorTextRaw = errorText
103
- }
104
- return valid
105
- }
106
- return false
107
- }
108
-
109
- checkValidity = () => {
110
- if (selectElement) {
111
- return selectElement.checkValidity()
112
- }
113
- return false
114
- }
115
-
116
- $effect(() => {
117
- errorRaw = error
118
- errorTextRaw = errorText
119
- selectElement?.setCustomValidity(error ? errorText : '')
120
- })
121
- const onReset = () => {
122
- errorRaw = error
123
- }
124
- $effect(() => {
125
- if (selectElement) {
126
- selectElement.form?.addEventListener('reset', onReset)
127
- }
128
- return () => {
129
- if (selectElement) {
130
- selectElement.form?.removeEventListener('reset', onReset)
131
- }
132
- }
133
- })
134
-
135
91
  let cachedRowHeight = 0
136
92
  const ensureRowHeight = () => {
137
93
  if (!cachedRowHeight && menuElement) {
@@ -158,14 +114,6 @@
158
114
  else if (bottom > scrollTop + clientHeight) viewport.scrollTop = bottom - clientHeight
159
115
  }
160
116
 
161
- const finalizeSelection = async () => {
162
- await tick()
163
- if (doValidity && checkValidity()) {
164
- errorRaw = error
165
- errorTextRaw = errorText
166
- }
167
- selectElement?.dispatchEvent(new Event('change', { bubbles: true }))
168
- }
169
117
  const toggleValue = (option: SelectOption) => {
170
118
  if (multiple) {
171
119
  let arr = Array.isArray(value) ? [...value] : []
@@ -185,7 +133,8 @@
185
133
  toggleValue(option)
186
134
  if (!multiple) menuElement?.hidePopover()
187
135
  event.preventDefault()
188
- await finalizeSelection()
136
+ await tick()
137
+ selectElement?.dispatchEvent(new Event('change', { bubbles: true }))
189
138
  }
190
139
 
191
140
  const openMenuAndFocus = async (index: number) => {
@@ -262,23 +211,6 @@
262
211
  performTypeahead('')
263
212
  }
264
213
  }
265
-
266
- const handleInvalid = (
267
- event: Event & {
268
- currentTarget: EventTarget & HTMLSelectElement
269
- },
270
- ) => {
271
- event.preventDefault()
272
- const { currentTarget } = event
273
- errorRaw = true
274
- doValidity = true
275
- if (errorText === '') {
276
- errorTextRaw = currentTarget.validationMessage
277
- }
278
- if (isFirstInvalidControlInForm(currentTarget.form, currentTarget)) {
279
- field?.focus()
280
- }
281
- }
282
214
  </script>
283
215
 
284
216
  {#snippet arrows()}
@@ -301,7 +233,6 @@
301
233
  <div
302
234
  {id}
303
235
  class="field"
304
- class:error={errorRaw}
305
236
  class:no-label={!label?.length}
306
237
  class:with-start={start}
307
238
  class:menu-open={menuOpen}
@@ -416,7 +347,7 @@
416
347
  {#if multiple}
417
348
  <select
418
349
  tabindex="-1"
419
- aria-invalid={errorRaw}
350
+ aria-invalid={attributes['aria-invalid']}
420
351
  {disabled}
421
352
  {required}
422
353
  {name}
@@ -424,7 +355,6 @@
424
355
  multiple
425
356
  {onchange}
426
357
  {oninput}
427
- oninvalid={handleInvalid}
428
358
  bind:value
429
359
  bind:this={selectElement}
430
360
  >
@@ -437,13 +367,13 @@
437
367
  {:else}
438
368
  <select
439
369
  tabindex="-1"
370
+ aria-invalid={attributes['aria-invalid']}
440
371
  {disabled}
441
372
  {required}
442
373
  {name}
443
374
  {form}
444
375
  {onchange}
445
376
  {oninput}
446
- oninvalid={handleInvalid}
447
377
  bind:value
448
378
  bind:this={selectElement}
449
379
  >
@@ -470,10 +400,10 @@
470
400
  </div>
471
401
  </div>
472
402
  </div>
473
- {#if supportingText || (errorTextRaw && errorRaw)}
474
- <div class="supporting-text" role={errorRaw ? 'alert' : undefined}>
403
+ {#if supportingText || errorText}
404
+ <div class="supporting-text" role={errorText ? 'alert' : undefined}>
475
405
  <span>
476
- {errorRaw && errorTextRaw ? errorTextRaw : supportingText}
406
+ {errorText ?? supportingText}
477
407
  </span>
478
408
  </div>
479
409
  {/if}
@@ -651,10 +581,10 @@
651
581
  border-bottom-color: var(--np-color-primary);
652
582
  border-bottom-width: 3px;
653
583
  }
654
- .error .active-indicator::before {
584
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .active-indicator::before {
655
585
  border-bottom-color: var(--np-color-error);
656
586
  }
657
- .error .active-indicator::after {
587
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .active-indicator::after {
658
588
  border-bottom-color: var(--np-color-error);
659
589
  }
660
590
  .disabled .active-indicator::before {
@@ -752,7 +682,7 @@
752
682
  justify-content: space-between;
753
683
  padding: 0.25rem 1rem 0;
754
684
  }
755
- .error .supporting-text {
685
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .supporting-text {
756
686
  color: var(--np-color-error);
757
687
  }
758
688
  .disabled .supporting-text {
@@ -840,8 +770,8 @@
840
770
  opacity: 1;
841
771
  }
842
772
 
843
- .field:not(.error).menu-open .down,
844
- .field:not(.error):focus .down {
773
+ .field:not(:has(select:is(:user-invalid, [aria-invalid='true']))).menu-open .down,
774
+ .field:not(:has(select:is(:user-invalid, [aria-invalid='true']))):focus .down {
845
775
  color: var(--np-color-primary);
846
776
  }
847
777
  .icon .down {
@@ -886,8 +816,8 @@
886
816
  margin-inline-start: 1rem;
887
817
  margin-inline-end: 0.75rem;
888
818
  }
889
- .error .start,
890
- .error .end {
819
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .start,
820
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .end {
891
821
  color: var(--np-color-error);
892
822
  }
893
823
  .disabled .start,
@@ -1006,9 +936,9 @@
1006
936
  .field:focus .label {
1007
937
  color: var(--np-color-primary);
1008
938
  }
1009
- .error .label,
1010
- .error.menu-open .label,
1011
- .error:focus .label {
939
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .label,
940
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])).menu-open .label,
941
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])):focus .label {
1012
942
  color: var(--np-color-error);
1013
943
  }
1014
944
  .disabled .label {
@@ -1162,9 +1092,9 @@
1162
1092
  border-color: var(--np-color-primary);
1163
1093
  color: var(--np-color-primary);
1164
1094
  }
1165
- .error .np-outline,
1166
- .error.menu-open .np-outline,
1167
- .error:focus .np-outline {
1095
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])) .np-outline,
1096
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])).menu-open .np-outline,
1097
+ .field:has(select:is(:user-invalid, [aria-invalid='true'])):focus .np-outline {
1168
1098
  border-color: var(--np-color-error);
1169
1099
  }
1170
1100
  .disabled .np-outline {
@@ -1,4 +1,4 @@
1
1
  import type { SelectProps } from './types.ts';
2
- declare const Select: import("svelte").Component<SelectProps, {}, "element" | "value" | "reportValidity" | "checkValidity">;
2
+ declare const Select: import("svelte").Component<SelectProps, {}, "element" | "value">;
3
3
  type Select = ReturnType<typeof Select>;
4
4
  export default Select;
@@ -9,8 +9,9 @@ export interface SelectOption {
9
9
  export interface SelectProps extends Omit<HTMLSelectAttributes, 'size' | 'autocomplete'> {
10
10
  label?: string;
11
11
  supportingText?: string;
12
- error?: boolean;
13
- errorText?: string;
12
+ issues?: {
13
+ message: string;
14
+ }[];
14
15
  variant?: 'outlined' | 'filled';
15
16
  start?: Snippet;
16
17
  end?: Snippet;
@@ -18,7 +19,5 @@ export interface SelectProps extends Omit<HTMLSelectAttributes, 'size' | 'autoco
18
19
  element?: HTMLSpanElement;
19
20
  options: SelectOption[];
20
21
  clampMenuWidth?: boolean;
21
- reportValidity?: () => boolean;
22
- checkValidity?: () => boolean;
23
22
  virtualThreshold?: number;
24
23
  }
@@ -1,12 +1,9 @@
1
1
  <script lang="ts">
2
- import { isFirstInvalidControlInForm } from './report-validity.js'
3
- import type { FocusEventHandler, EventHandler } from 'svelte/elements'
4
2
  import type { TextFieldProps } from './types.ts'
5
3
 
6
4
  let {
7
5
  value = $bindable(),
8
- error = false,
9
- errorText = '',
6
+ issues,
10
7
  prefixText = '',
11
8
  suffixText = '',
12
9
  supportingText = '',
@@ -19,13 +16,8 @@
19
16
  element = $bindable(),
20
17
  populated = false,
21
18
  inputElement = $bindable(),
22
- reportValidity = $bindable(),
23
- checkValidity = $bindable(),
19
+ placeholder = ' ',
24
20
  children,
25
- oninput,
26
- oninvalid,
27
- onfocus,
28
- onblur,
29
21
  focused = $bindable(false),
30
22
  clientWidth = $bindable(),
31
23
  clientHeight = $bindable(),
@@ -33,106 +25,7 @@
33
25
  }: TextFieldProps = $props()
34
26
 
35
27
  const uid = $props.id()
36
-
37
- let errorRaw: boolean = $state(error)
38
- let errorTextRaw: string = $state(errorText)
39
- let focusOnInvalid = $state(true)
40
- let doValidity = $state(false)
41
-
42
- reportValidity = () => {
43
- if (inputElement) {
44
- const valid = inputElement.reportValidity()
45
- if (valid) {
46
- errorRaw = error
47
- errorTextRaw = errorText
48
- }
49
- return valid
50
- }
51
- return false
52
- }
53
-
54
- checkValidity = () => {
55
- if (inputElement) {
56
- return inputElement.checkValidity()
57
- }
58
- return false
59
- }
60
-
61
- $effect(() => {
62
- errorRaw = error
63
- errorTextRaw = errorText
64
- inputElement?.setCustomValidity(error ? errorText : '')
65
- })
66
-
67
- const onReset = () => {
68
- errorRaw = error
69
- }
70
-
71
- const onInputEvent = (
72
- event: Event & {
73
- currentTarget: (EventTarget & HTMLInputElement) | HTMLTextAreaElement
74
- },
75
- ) => {
76
- doValidity = true
77
- ;(oninput as EventHandler)?.(event)
78
- }
79
-
80
- const onInvalidEvent = (
81
- event: Event & {
82
- currentTarget: HTMLInputElement | HTMLTextAreaElement
83
- },
84
- ) => {
85
- event.preventDefault()
86
- const { currentTarget } = event
87
- errorRaw = true
88
- if (errorText === '') {
89
- errorTextRaw = currentTarget.validationMessage
90
- }
91
- if (focusOnInvalid && isFirstInvalidControlInForm(currentTarget.form, currentTarget)) {
92
- currentTarget.focus()
93
- }
94
- ;(oninvalid as EventHandler)?.(event)
95
- }
96
-
97
- const onFocusEvent = (
98
- event: FocusEvent & {
99
- currentTarget: EventTarget & (HTMLInputElement | HTMLTextAreaElement)
100
- },
101
- ) => {
102
- focused = true
103
- ;(onfocus as FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>)?.(event)
104
- }
105
-
106
- const onBlurEvent = (
107
- event: FocusEvent & {
108
- currentTarget: EventTarget & (HTMLInputElement | HTMLTextAreaElement)
109
- },
110
- ) => {
111
- focused = false
112
- if (doValidity) {
113
- focusOnInvalid = false
114
- if (checkValidity()) {
115
- errorRaw = error
116
- errorTextRaw = errorText
117
- } else {
118
- focusOnInvalid = true
119
- }
120
- } else {
121
- focusOnInvalid = true
122
- }
123
- ;(onblur as FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>)?.(event)
124
- }
125
-
126
- $effect(() => {
127
- if (inputElement) {
128
- inputElement.form?.addEventListener('reset', onReset)
129
- }
130
- return () => {
131
- if (inputElement) {
132
- inputElement.form?.removeEventListener('reset', onReset)
133
- }
134
- }
135
- })
28
+ let errorText = $derived(issues?.map((issue) => issue.message).join(', '))
136
29
  </script>
137
30
 
138
31
  <label
@@ -149,14 +42,12 @@
149
42
  >
150
43
  <div
151
44
  class="field"
152
- class:error={errorRaw}
153
45
  class:resizable={attributes.type === 'textarea'}
154
46
  class:no-label={!label?.length}
155
47
  class:with-start={start}
156
48
  class:with-end={end}
157
49
  class:disabled={attributes.disabled}
158
- class:populated={(value !== '' && value !== undefined && value !== null) || populated}
159
- class:focused
50
+ class:populated
160
51
  class:outlined={variant === 'outlined'}
161
52
  >
162
53
  <div class="container-overflow">
@@ -206,16 +97,13 @@
206
97
  <div class="content">
207
98
  {#if attributes.type === 'textarea'}
208
99
  <textarea
209
- aria-errormessage={errorTextRaw && errorRaw ? `supporting-text-${uid}` : undefined}
210
- aria-describedby={supportingText && (!errorTextRaw || !errorRaw)
100
+ aria-errormessage={errorText ? `supporting-text-${uid}` : undefined}
101
+ aria-describedby={supportingText && !errorText
211
102
  ? `supporting-text-${uid}`
212
103
  : undefined}
213
104
  {...attributes}
214
- aria-invalid={errorRaw}
215
- oninput={onInputEvent}
216
- oninvalid={onInvalidEvent}
217
- onfocus={onFocusEvent}
218
- onblur={onBlurEvent}
105
+ {placeholder}
106
+ bind:focused
219
107
  bind:value
220
108
  bind:this={inputElement}
221
109
  class="input"
@@ -229,20 +117,14 @@
229
117
  </span>
230
118
  {/if}
231
119
  <input
232
- aria-errormessage={errorTextRaw && errorRaw
233
- ? `supporting-text-${uid}`
234
- : undefined}
235
- aria-describedby={supportingText && (!errorTextRaw || !errorRaw)
120
+ aria-errormessage={errorText ? `supporting-text-${uid}` : undefined}
121
+ aria-describedby={supportingText && !errorText
236
122
  ? `supporting-text-${uid}`
237
123
  : undefined}
238
124
  {...attributes}
125
+ {placeholder}
239
126
  bind:value
240
127
  bind:this={inputElement}
241
- aria-invalid={errorRaw}
242
- oninput={onInputEvent}
243
- oninvalid={onInvalidEvent}
244
- onfocus={onFocusEvent}
245
- onblur={onBlurEvent}
246
128
  class="input"
247
129
  />
248
130
  {@render children?.()}
@@ -262,10 +144,10 @@
262
144
  {/if}
263
145
  </div>
264
146
  </div>
265
- {#if supportingText || (errorTextRaw && errorRaw) || attributes.maxlength}
266
- <div class="supporting-text" role={errorRaw ? 'alert' : undefined}>
147
+ {#if supportingText || errorText || attributes.maxlength}
148
+ <div class="supporting-text" role={errorText ? 'alert' : undefined}>
267
149
  <span id="supporting-text-{uid}">
268
- {errorRaw && errorTextRaw ? errorTextRaw : supportingText}
150
+ {errorText ?? supportingText}
269
151
  </span>
270
152
  {#if attributes.maxlength}
271
153
  <span>{value?.length || 0}/{attributes.maxlength}</span>
@@ -306,13 +188,16 @@
306
188
  );
307
189
  border-bottom-width: 3px;
308
190
  }
309
- .error .active-indicator::before {
191
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])) .active-indicator::before,
192
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .active-indicator::before {
310
193
  border-bottom-color: var(--np-color-error);
311
194
  }
312
- .error .active-indicator::after {
195
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])) .active-indicator::after,
196
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .active-indicator::after {
313
197
  border-bottom-color: var(--np-color-error);
314
198
  }
315
- .error:hover .active-indicator::after {
199
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])):hover .active-indicator::after,
200
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])):hover .active-indicator::after {
316
201
  border-bottom-color: var(--np-color-on-error-container);
317
202
  }
318
203
  .disabled .active-indicator::before {
@@ -412,7 +297,8 @@
412
297
  justify-content: space-between;
413
298
  padding: 0.25rem 1rem 0;
414
299
  }
415
- .error .supporting-text {
300
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])) .supporting-text,
301
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .supporting-text {
416
302
  color: var(--np-color-error);
417
303
  }
418
304
  .disabled .supporting-text {
@@ -494,7 +380,10 @@
494
380
  .no-label .content,
495
381
  .field:has(input:focus-visible) .content,
496
382
  .field:has(textarea:focus-visible) .content,
497
- .field.populated .content {
383
+ .field:has(input:-webkit-autofill) .content,
384
+ .field.populated .content,
385
+ .field:has(input:not(:placeholder-shown)) .content,
386
+ .field:has(textarea:not(:placeholder-shown)) .content {
498
387
  opacity: 1;
499
388
  }
500
389
 
@@ -564,11 +453,13 @@
564
453
  margin-inline-start: 1rem;
565
454
  margin-inline-end: 0.75rem;
566
455
  }
567
- .error .end {
456
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])) .end,
457
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .end {
568
458
  color: var(--np-color-error);
569
459
  }
570
460
 
571
- .error:hover .end {
461
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])):hover .end,
462
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])):hover .end {
572
463
  color: var(--np-color-on-error-container);
573
464
  }
574
465
  .disabled .start,
@@ -620,11 +511,21 @@
620
511
  .with-end .np-outline .label-wrapper {
621
512
  margin-inline-end: 3.25rem;
622
513
  }
514
+ .with-start:has(input:-webkit-autofill) .with-start:has(input:focus-visible) .label-wrapper,
515
+ .with-start:has(input:not(:placeholder-shown))
516
+ .with-start:has(input:focus-visible)
517
+ .label-wrapper,
518
+ .with-start:has(textarea:not(:placeholder-shown))
519
+ .with-start:has(input:focus-visible)
520
+ .label-wrapper,
623
521
  .with-start.populated .with-start:has(input:focus-visible) .label-wrapper,
624
522
  .with-start:has(textarea:focus-visible) .label-wrapper {
625
523
  inset-inline-end: -2.25rem;
626
524
  }
627
525
 
526
+ .with-end:has(input:-webkit-autofill) .with-end:has(input:focus-visible) .label-wrapper,
527
+ .with-end:has(input:not(:placeholder-shown)) .with-end:has(input:focus-visible) .label-wrapper,
528
+ .with-end:has(textarea:not(:placeholder-shown)) .with-end:has(input:focus-visible) .label-wrapper,
628
529
  .with-end.populated .with-end:has(input:focus-visible) .label-wrapper,
629
530
  .with-end:has(textarea:focus-visible) .label-wrapper {
630
531
  margin-inline-end: 1rem;
@@ -650,7 +551,10 @@
650
551
  top: 1rem;
651
552
  inset-inline-start: 0rem;
652
553
  }
554
+ .field:has(input:-webkit-autofill) .label,
653
555
  .field.populated .label,
556
+ .field:has(input:not(:placeholder-shown)) .label,
557
+ .field:has(textarea:not(:placeholder-shown)) .label,
654
558
  .field:has(input:focus-visible) .label,
655
559
  .field:has(textarea:focus-visible) .label {
656
560
  font-size: 0.75rem;
@@ -659,7 +563,10 @@
659
563
  position: absolute;
660
564
  top: var(--floating-label-top, 0.5rem);
661
565
  }
566
+ .with-start:has(input:-webkit-autofill) .label,
662
567
  .with-start.populated .label,
568
+ .with-start:has(input:not(:placeholder-shown)) .label,
569
+ .with-start:has(textarea:not(:placeholder-shown)) .label,
663
570
  .with-start:has(input:focus-visible) .label,
664
571
  .with-start:has(textarea:focus-visible) .label {
665
572
  inset-inline-start: var(--floating-label-inline-start, 0);
@@ -688,14 +595,16 @@
688
595
  .field:has(textarea:focus-visible) .label {
689
596
  color: var(--_label-text-color, var(--np-color-primary));
690
597
  }
691
- .error .label,
692
- .error:has(input:focus-visible) .label,
693
- .error:has(textarea:focus-visible) .label {
598
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])) .label,
599
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .label,
600
+ .field:has(input:is(:user-invalid, [aria-invalid='true']):focus-visible) .label,
601
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true']):focus-visible) .label {
694
602
  color: var(--np-color-error);
695
603
  }
696
- .error:hover .label,
697
- .error:has(input:focus-visible):hover .label,
698
- .error:has(textarea:focus-visible):hover .label {
604
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])):hover .label,
605
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])):hover .label,
606
+ .field:has(input:is(:user-invalid, [aria-invalid='true']):focus-visible):hover .label,
607
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true']):focus-visible):hover .label {
699
608
  color: var(--np-color-on-error-container);
700
609
  }
701
610
 
@@ -710,6 +619,9 @@
710
619
  overflow: hidden;
711
620
  }
712
621
  .disabled.no-label .content,
622
+ .disabled:has(input:-webkit-autofill) .content,
623
+ .disabled:has(input:not(:placeholder-shown)) .content,
624
+ .disabled:has(textarea:not(:placeholder-shown)) .content,
713
625
  .disabled.populated .content {
714
626
  opacity: 0.38;
715
627
  }
@@ -786,6 +698,9 @@
786
698
  }
787
699
  .field:has(input:focus-visible) .outline-notch::before,
788
700
  .field:has(textarea:focus-visible) .outline-notch::before,
701
+ .field:has(input:-webkit-autofill) .outline-notch::before,
702
+ .field:has(input:not(:placeholder-shown)) .outline-notch::before,
703
+ .field:has(textarea:not(:placeholder-shown)) .outline-notch::before,
789
704
  .field.populated .outline-notch::before {
790
705
  border-top-style: none;
791
706
  }
@@ -854,9 +769,10 @@
854
769
  color: var(--np-color-on-surface);
855
770
  }
856
771
 
857
- .error .np-outline,
858
- .error:has(input:focus-visible) .np-outline,
859
- .error:has(textarea:focus-visible) .np-outline {
772
+ .field:has(input:is(:user-invalid, [aria-invalid='true'])) .np-outline,
773
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .np-outline,
774
+ .field:has(input:is(:user-invalid, [aria-invalid='true']):focus-visible) .np-outline,
775
+ .field:has(textarea:is(:user-invalid, [aria-invalid='true']):focus-visible) .np-outline {
860
776
  border-color: var(--np-color-error);
861
777
  }
862
778
 
@@ -875,9 +791,4 @@
875
791
  .disabled .outline-notch {
876
792
  opacity: 0.12;
877
793
  }
878
-
879
- input:-webkit-autofill {
880
- -webkit-background-clip: text;
881
- -webkit-text-fill-color: var(--np-color-on-surface);
882
- }
883
794
  </style>
@@ -1,4 +1,4 @@
1
1
  import type { TextFieldProps } from './types.ts';
2
- declare const TextField: import("svelte").Component<TextFieldProps, {}, "element" | "value" | "reportValidity" | "checkValidity" | "inputElement" | "clientWidth" | "clientHeight" | "focused">;
2
+ declare const TextField: import("svelte").Component<TextFieldProps, {}, "element" | "value" | "inputElement" | "clientWidth" | "clientHeight" | "focused">;
3
3
  type TextField = ReturnType<typeof TextField>;
4
4
  export default TextField;
@@ -3,8 +3,9 @@ import type { HTMLInputAttributes, HTMLTextareaAttributes } from 'svelte/element
3
3
  interface FieldProps {
4
4
  label?: string;
5
5
  supportingText?: string;
6
- error?: boolean;
7
- errorText?: string;
6
+ issues?: {
7
+ message: string;
8
+ }[];
8
9
  prefixText?: string;
9
10
  suffixText?: string;
10
11
  variant?: 'outlined' | 'filled';
@@ -14,8 +15,6 @@ interface FieldProps {
14
15
  element?: HTMLSpanElement;
15
16
  inputElement?: HTMLInputElement | HTMLTextAreaElement;
16
17
  populated?: boolean;
17
- reportValidity?: () => boolean;
18
- checkValidity?: () => boolean;
19
18
  clientWidth?: number;
20
19
  clientHeight?: number;
21
20
  focused?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.26.13",
3
+ "version": "0.27.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {
@@ -54,15 +54,15 @@
54
54
  "svelte": "^5.32.1"
55
55
  },
56
56
  "devDependencies": {
57
- "@eslint/js": "^9.36.0",
57
+ "@eslint/js": "^9.37.0",
58
58
  "@material/material-color-utilities": "^0.3.0",
59
59
  "@playwright/test": "^1.55.1",
60
60
  "@sveltejs/adapter-auto": "^6.1.1",
61
- "@sveltejs/kit": "^2.43.7",
61
+ "@sveltejs/kit": "^2.44.0",
62
62
  "@sveltejs/package": "^2.5.4",
63
63
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
64
64
  "@types/eslint": "^9.6.1",
65
- "eslint": "^9.36.0",
65
+ "eslint": "^9.37.0",
66
66
  "eslint-config-prettier": "^10.1.8",
67
67
  "eslint-plugin-svelte": "^3.12.4",
68
68
  "globals": "^16.4.0",
@@ -73,7 +73,7 @@
73
73
  "svelte-check": "^4.3.2",
74
74
  "typescript": "^5.9.3",
75
75
  "typescript-eslint": "^8.45.0",
76
- "vite": "^7.1.8",
76
+ "vite": "^7.1.9",
77
77
  "vitest": "^3.2.4"
78
78
  },
79
79
  "svelte": "./dist/index.js",
@@ -1 +0,0 @@
1
- export declare const isFirstInvalidControlInForm: (form: HTMLFormElement | null, control: HTMLElement) => boolean;
@@ -1,6 +0,0 @@
1
- export const isFirstInvalidControlInForm = (form, control) => {
2
- if (!form) {
3
- return true;
4
- }
5
- return form.querySelector(':invalid') === control;
6
- };