noph-ui 0.8.10 → 0.8.12

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.
@@ -28,6 +28,16 @@
28
28
  })
29
29
  }
30
30
  })
31
+ $effect(() => {
32
+ if (element) {
33
+ element.addEventListener('focus', () => {
34
+ focused = true
35
+ })
36
+ element.addEventListener('blur', () => {
37
+ focused = false
38
+ })
39
+ }
40
+ })
31
41
  </script>
32
42
 
33
43
  {#snippet content()}
@@ -78,15 +88,9 @@
78
88
  </div>
79
89
  {:else if attributes.type === 'button'}
80
90
  <button
91
+ aria-disabled={disabled}
81
92
  {...attributes}
82
93
  bind:this={element}
83
- aria-disabled={disabled}
84
- onfocusin={() => {
85
- focused = true
86
- }}
87
- onfocusout={() => {
88
- focused = false
89
- }}
90
94
  {disabled}
91
95
  class={[
92
96
  'np-card-container',
@@ -13,6 +13,17 @@
13
13
  }: ItemProps = $props()
14
14
 
15
15
  let focused = $state(false)
16
+ let element: HTMLButtonElement | HTMLAnchorElement | undefined = $state()
17
+ $effect(() => {
18
+ if (element) {
19
+ element.addEventListener('focus', () => {
20
+ focused = true
21
+ })
22
+ element.addEventListener('blur', () => {
23
+ focused = false
24
+ })
25
+ }
26
+ })
16
27
  </script>
17
28
 
18
29
  {#snippet content()}
@@ -56,23 +67,13 @@
56
67
  <button
57
68
  {...attributes}
58
69
  class={['np-item', selected && 'selected', attributes.class]}
59
- onfocus={() => {
60
- focused = true
61
- }}
62
- onfocusout={() => {
63
- focused = false
64
- }}>{@render content()}</button
70
+ bind:this={element}>{@render content()}</button
65
71
  >
66
72
  {:else if attributes.variant === 'link'}
67
73
  <a
68
74
  {...attributes}
69
75
  class={['np-item', selected && 'selected', attributes.class]}
70
- onfocus={() => {
71
- focused = true
72
- }}
73
- onfocusout={() => {
74
- focused = false
75
- }}>{@render content()}</a
76
+ bind:this={element}>{@render content()}</a
76
77
  >
77
78
  {/if}
78
79
 
@@ -50,6 +50,23 @@
50
50
 
51
51
  $effect(() => {
52
52
  if (anchor && element) {
53
+ element.addEventListener('toggle', (event) => {
54
+ const { newState, currentTarget } = event as ToggleEvent & {
55
+ currentTarget: EventTarget & HTMLDivElement
56
+ }
57
+ if (newState === 'open') {
58
+ const rect = currentTarget.getBoundingClientRect()
59
+ const viewportHeight = window.innerHeight
60
+
61
+ if (rect.bottom > viewportHeight) {
62
+ const maxHeight = viewportHeight - rect.top - 18
63
+ currentTarget.style.maxHeight = `${maxHeight}px`
64
+ }
65
+ }
66
+ if (newState === 'closed') {
67
+ currentTarget.style.maxHeight = '80dvh'
68
+ }
69
+ })
53
70
  if (!('anchorName' in document.documentElement.style)) {
54
71
  anchor.addEventListener('click', () => {
55
72
  refreshValues()
@@ -86,20 +103,6 @@
86
103
  popover="auto"
87
104
  class={[position, 'np-menu', attributes.class]}
88
105
  role="menu"
89
- ontoggle={(event) => {
90
- if (event.newState === 'open') {
91
- const rect = event.currentTarget.getBoundingClientRect()
92
- const viewportHeight = window.innerHeight
93
-
94
- if (rect.bottom > viewportHeight) {
95
- const maxHeight = viewportHeight - rect.top - 18
96
- event.currentTarget.style.maxHeight = `${maxHeight}px`
97
- }
98
- }
99
- if (event.newState === 'closed') {
100
- event.currentTarget.style.maxHeight = '80dvh'
101
- }
102
- }}
103
106
  >
104
107
  {@render children()}
105
108
  </div>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Menu from '../menu/Menu.svelte'
3
3
  import { isFirstInvalidControlInForm } from '../text-field/report-validity.js'
4
- import { generateUUIDv4, isIOS } from '../utils.js'
4
+ import { generateUUIDv4 } from '../utils.js'
5
5
  import type { SelectProps } from './types.ts'
6
6
  import Item from '../list/Item.svelte'
7
7
 
@@ -11,6 +11,7 @@
11
11
  error = false,
12
12
  errorText = '',
13
13
  supportingText = '',
14
+ tabindex = 0,
14
15
  start,
15
16
  label,
16
17
  style,
@@ -26,6 +27,7 @@
26
27
  })
27
28
  let selectElement: HTMLSelectElement | undefined = $state()
28
29
  let menuElement: HTMLDivElement | undefined = $state()
30
+ let field: HTMLDivElement | undefined = $state()
29
31
  let menuId = $state(`--select-${generateUUIDv4()}`)
30
32
  let menuOpen = $state(false)
31
33
  let selectedLabel = $derived.by<string>(() => {
@@ -42,7 +44,6 @@
42
44
  if (selectElement) {
43
45
  selectElement.form?.addEventListener('reset', () => {
44
46
  error = false
45
- value = ''
46
47
  })
47
48
  selectElement.addEventListener('invalid', (event) => {
48
49
  event.preventDefault()
@@ -78,8 +79,7 @@
78
79
  </svg>
79
80
  {/snippet}
80
81
 
81
- <!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
82
- <label
82
+ <div
83
83
  style={(variant === 'outlined'
84
84
  ? '--top-space:1rem;--bottom-space:1rem;--floating-label-top:-0.5rem;--floating-label-left:-2.25rem;--_focus-outline-width:3px;'
85
85
  : !label?.length
@@ -88,28 +88,6 @@
88
88
  class={['text-field', attributes.class]}
89
89
  bind:this={element}
90
90
  bind:clientWidth
91
- role="combobox"
92
- aria-controls="listbox"
93
- aria-expanded={menuOpen}
94
- onclick={(event) => {
95
- event.preventDefault()
96
- menuElement?.showPopover()
97
- menuElement?.focus()
98
- }}
99
- onkeydown={(event) => {
100
- if (event.key === 'Tab') {
101
- if (isIOS()) {
102
- event.preventDefault()
103
- }
104
- menuElement?.hidePopover()
105
- } else {
106
- event.preventDefault()
107
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
108
- menuElement?.showPopover()
109
- ;(menuElement?.firstElementChild as HTMLElement)?.focus()
110
- }
111
- }
112
- }}
113
91
  >
114
92
  <div
115
93
  class="field"
@@ -120,6 +98,29 @@
120
98
  class:with-end={true}
121
99
  class:disabled={attributes.disabled}
122
100
  class:outlined={variant === 'outlined'}
101
+ role="combobox"
102
+ tabindex={attributes.disabled ? -1 : tabindex}
103
+ aria-controls="listbox"
104
+ aria-expanded={menuOpen}
105
+ aria-label={label}
106
+ bind:this={field}
107
+ onclick={(event) => {
108
+ event.preventDefault()
109
+ menuElement?.showPopover()
110
+ menuElement?.focus()
111
+ }}
112
+ onkeydown={(event) => {
113
+ if (event.key === 'Tab') {
114
+ event.preventDefault()
115
+ menuElement?.hidePopover()
116
+ } else {
117
+ event.preventDefault()
118
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
119
+ menuElement?.showPopover()
120
+ ;(menuElement?.firstElementChild as HTMLElement)?.focus()
121
+ }
122
+ }
123
+ }}
123
124
  >
124
125
  <div class="container-overflow">
125
126
  {#if variant === 'filled'}
@@ -158,7 +159,13 @@
158
159
  </div>
159
160
  {/if}
160
161
  <div class="content">
161
- <select aria-label={label} {...attributes} bind:value bind:this={selectElement}>
162
+ <select
163
+ tabindex="-1"
164
+ aria-label={label}
165
+ {...attributes}
166
+ bind:value
167
+ bind:this={selectElement}
168
+ >
162
169
  {#each options as option}
163
170
  <option value={option.value} selected={option.selected}>{option.label}</option>
164
171
  {/each}
@@ -187,7 +194,7 @@
187
194
  </div>
188
195
  {/if}
189
196
  </div>
190
- </label>
197
+ </div>
191
198
 
192
199
  <Menu
193
200
  style="position-anchor:{menuId};min-width: {clientWidth}px;"
@@ -208,9 +215,7 @@
208
215
  onclick={(event) => {
209
216
  value = option.value
210
217
  menuElement?.hidePopover()
211
- if (!isIOS()) {
212
- element?.focus()
213
- }
218
+ field?.focus()
214
219
  event.preventDefault()
215
220
  }}
216
221
  onkeydown={(event) => {
@@ -243,7 +248,7 @@
243
248
  z-index: 1;
244
249
  }
245
250
  .field.menu-open .active-indicator::after,
246
- .field:has(select:focus-visible) .active-indicator::after {
251
+ .field:focus .active-indicator::after {
247
252
  opacity: 1;
248
253
  }
249
254
  .active-indicator::after {
@@ -332,6 +337,7 @@
332
337
  writing-mode: horizontal-tb;
333
338
  max-width: var(--np-select-max-width, 100%);
334
339
  min-width: var(--np-select-min-width, 210px);
340
+ outline: none;
335
341
  }
336
342
 
337
343
  .supporting-text {
@@ -412,13 +418,13 @@
412
418
 
413
419
  .no-label .content,
414
420
  .field.menu-open .content,
415
- .field:has(select:focus-visible) .content,
421
+ .field:focus .content,
416
422
  .field:has(select option:checked:not([value=''])) .content {
417
423
  opacity: 1;
418
424
  }
419
425
 
420
426
  .field:not(.error).menu-open .down,
421
- .field:not(.error):has(select:focus-visible) .down {
427
+ .field:not(.error):focus .down {
422
428
  color: var(--np-color-primary);
423
429
  }
424
430
  .icon .down {
@@ -540,9 +546,8 @@
540
546
  }
541
547
 
542
548
  .with-end.menu-open .label-wrapper,
543
- .with-end:has(select:focus-visible option:checked:not([value=''])) .label-wrapper,
544
- .with-end:has(select option:checked:not([value=''])) .label-wrapper,
545
- .with-end:has(select:focus-visible) .label-wrapper {
549
+ .with-end:focus:has(select option:checked:not([value=''])) .label-wrapper,
550
+ .with-end:focus .label-wrapper {
546
551
  margin-inline-end: 1rem;
547
552
  }
548
553
  .notch {
@@ -557,16 +562,16 @@
557
562
  opacity: 0;
558
563
  }
559
564
 
560
- .field:not(.menu-open):has(select:not(:focus-visible)) .label {
565
+ .field:not(.menu-open):not(:focus) .label {
561
566
  position: absolute;
562
567
  top: 1rem;
563
568
  left: 0rem;
564
569
  }
565
570
 
566
571
  .field.menu-open .label,
567
- .field:has(select:focus-visible option:checked:not([value=''])) .label,
572
+ .field:focus:has(select option:checked:not([value=''])) .label,
568
573
  .field:has(select option:checked:not([value=''])) .label,
569
- .field:has(select:focus-visible) .label {
574
+ .field:focus .label {
570
575
  font-size: 0.75rem;
571
576
  line-height: 1rem;
572
577
  transform-origin: top left;
@@ -597,12 +602,12 @@
597
602
  }
598
603
 
599
604
  .field.menu-open .label,
600
- .field:has(select:focus-visible) .label {
605
+ .field:focus .label {
601
606
  color: var(--np-color-primary);
602
607
  }
603
608
  .error .label,
604
609
  .error.menu-open .label,
605
- .error:has(select:focus-visible) .label {
610
+ .error:focus .label {
606
611
  color: var(--np-color-error);
607
612
  }
608
613
  .disabled .label {
@@ -692,7 +697,7 @@
692
697
  }
693
698
 
694
699
  .field.menu-open .outline-notch::before,
695
- .field:has(select:focus-visible) .outline-notch::before,
700
+ .field:focus .outline-notch::before,
696
701
  .field:has(select option:checked:not([value=''])) .outline-notch::before {
697
702
  border-top-style: none;
698
703
  }
@@ -735,9 +740,9 @@
735
740
  .field.menu-open .outline-start::after,
736
741
  .field.menu-open .outline-end::after,
737
742
  .field.menu-open .outline-notch::after,
738
- .field:has(select:focus-visible) .outline-start::after,
739
- .field:has(select:focus-visible) .outline-end::after,
740
- .field:has(select:focus-visible) .outline-notch::after {
743
+ .field:focus .outline-start::after,
744
+ .field:focus .outline-end::after,
745
+ .field:focus .outline-notch::after {
741
746
  opacity: 1;
742
747
  }
743
748
  .np-outline {
@@ -752,13 +757,13 @@
752
757
  }
753
758
 
754
759
  .field.menu-open .np-outline,
755
- .field:has(select:focus-visible) .np-outline {
760
+ .field:focus .np-outline {
756
761
  border-color: var(--np-color-primary);
757
762
  color: var(--np-color-primary);
758
763
  }
759
764
  .error .np-outline,
760
765
  .error.menu-open .np-outline,
761
- .error:has(select:focus-visible) .np-outline {
766
+ .error:focus .np-outline {
762
767
  border-color: var(--np-color-error);
763
768
  }
764
769
  .disabled .np-outline {
@@ -31,25 +31,25 @@
31
31
  element?.hidePopover()
32
32
  }
33
33
 
34
- const toggleHandler = (event: ToggleEvent) => {
35
- if (event.newState === 'closed') {
34
+ const toggleHandler = (event: Event) => {
35
+ let { newState } = event as ToggleEvent
36
+ if (newState === 'closed') {
36
37
  clearTimeout(timeoutId)
37
38
  }
38
- if (event.newState === 'open' && timeout > 0) {
39
+ if (newState === 'open' && timeout > 0) {
39
40
  timeoutId = setTimeout(() => {
40
41
  element?.hidePopover()
41
42
  }, timeout)
42
43
  }
43
44
  }
45
+ $effect(() => {
46
+ if (element) {
47
+ element.addEventListener('beforetoggle', toggleHandler)
48
+ }
49
+ })
44
50
  </script>
45
51
 
46
- <div
47
- {...attributes}
48
- {popover}
49
- class={['np-snackbar', attributes.class]}
50
- onbeforetoggle={toggleHandler}
51
- bind:this={element}
52
- >
52
+ <div {...attributes} {popover} class={['np-snackbar', attributes.class]} bind:this={element}>
53
53
  <div class="np-snackbar-inner">
54
54
  <div class="np-snackbar-label-container">
55
55
  <div class="np-snackbar-label">{label}</div>
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  declare const generateUUIDv4: () => string;
2
- declare const isIOS: () => boolean;
3
- export { generateUUIDv4, isIOS };
2
+ export { generateUUIDv4 };
package/dist/utils.js CHANGED
@@ -4,7 +4,4 @@ const generateUUIDv4 = () => {
4
4
  return v.toString(16);
5
5
  });
6
6
  };
7
- const isIOS = () => {
8
- return /iPhone|iPad|iPod/i.test(navigator.userAgent);
9
- };
10
- export { generateUUIDv4, isIOS };
7
+ export { generateUUIDv4 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.8.10",
3
+ "version": "0.8.12",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {
@@ -66,11 +66,11 @@
66
66
  "prettier": "^3.4.2",
67
67
  "prettier-plugin-svelte": "^3.3.2",
68
68
  "publint": "^0.2.12",
69
- "svelte": "^5.16.0",
69
+ "svelte": "^5.16.1",
70
70
  "svelte-check": "^4.1.1",
71
71
  "typescript": "^5.7.2",
72
72
  "typescript-eslint": "^8.19.0",
73
- "vite": "^6.0.6",
73
+ "vite": "^6.0.7",
74
74
  "vitest": "^2.1.8"
75
75
  },
76
76
  "svelte": "./dist/index.js",