noph-ui 0.18.2 → 0.18.4

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.
@@ -30,7 +30,7 @@
30
30
 
31
31
  const uid = $props.id()
32
32
  let defaultOptionsFilter = (option: AutoCompleteOption) => {
33
- return !value || option.label.includes(value)
33
+ return !value || option.label.toLocaleLowerCase().includes(value.toLocaleLowerCase())
34
34
  }
35
35
  let displayOptions = $derived(options.filter(optionsFilter || defaultOptionsFilter))
36
36
  let useVirtualList = $derived(displayOptions.length > 4000)
@@ -1,20 +1,35 @@
1
1
  <script lang="ts">
2
- import type { ChipSetProps } from './types.ts'
2
+ import { setContext } from 'svelte'
3
+ import type { ChipSetContext, ChipSetProps } from './types.ts'
3
4
 
4
5
  let { children, ...attributes }: ChipSetProps = $props()
6
+ let chipSet: ChipSetContext = $state({
7
+ chips: [],
8
+ })
9
+ setContext('chipSet', chipSet)
5
10
  </script>
6
11
 
7
12
  {#if children}
8
- <div class={['np-chip-set', attributes.class]} style={attributes.style} role="toolbar">
13
+ <div
14
+ class={['np-chip-set', chipSet.chips.length > 0 && 'np-chip-set-has-chips', attributes.class]}
15
+ style={attributes.style}
16
+ role="toolbar"
17
+ >
9
18
  {@render children()}
10
19
  </div>
11
20
  {/if}
12
21
 
13
22
  <style>
23
+ .np-chip-set-has-chips {
24
+ margin-right: 0.5rem;
25
+ padding-block: 2px;
26
+ padding-right: 2px;
27
+ }
14
28
  .np-chip-set {
15
29
  display: flex;
16
30
  flex-wrap: wrap;
17
31
  gap: 0.5rem;
32
+ overflow: auto;
18
33
  align-items: center;
19
34
  min-width: 0;
20
35
  }
@@ -3,7 +3,8 @@
3
3
  import CheckIcon from '../icons/CheckIcon.svelte'
4
4
  import CloseIcon from '../icons/CloseIcon.svelte'
5
5
  import Ripple from '../ripple/Ripple.svelte'
6
- import type { FilterChipProps } from './types.ts'
6
+ import { getContext, onMount } from 'svelte'
7
+ import type { ChipSetContext, FilterChipProps } from './types.ts'
7
8
 
8
9
  let {
9
10
  selected = $bindable(),
@@ -23,6 +24,17 @@
23
24
  }: FilterChipProps = $props()
24
25
 
25
26
  let chipLabel: HTMLLabelElement | undefined = $state()
27
+ let chipSet: ChipSetContext = getContext('chipSet')
28
+
29
+ onMount(() => {
30
+ chipSet.chips.push({ label: label, name: name, value: value })
31
+ return () => {
32
+ const index = chipSet.chips.findIndex((chip) => chip.value === value)
33
+ if (index !== -1) {
34
+ chipSet.chips.splice(index, 1)
35
+ }
36
+ }
37
+ })
26
38
 
27
39
  $effect(() => {
28
40
  if (group && value) {
@@ -2,8 +2,8 @@
2
2
  import IconButton from '../button/IconButton.svelte'
3
3
  import CloseIcon from '../icons/CloseIcon.svelte'
4
4
  import Ripple from '../ripple/Ripple.svelte'
5
- import { onMount } from 'svelte'
6
- import type { InputChipProps } from './types.ts'
5
+ import { getContext, onMount } from 'svelte'
6
+ import type { ChipSetContext, InputChipProps } from './types.ts'
7
7
 
8
8
  let {
9
9
  selected = $bindable(),
@@ -20,6 +20,7 @@
20
20
 
21
21
  let chipLabel: HTMLDivElement | undefined = $state()
22
22
  let visible = $state(false)
23
+ let chipSet: ChipSetContext = getContext('chipSet')
23
24
 
24
25
  onMount(() => {
25
26
  const observer = new IntersectionObserver((entries) => {
@@ -34,8 +35,15 @@
34
35
  if (element) {
35
36
  observer.observe(element)
36
37
  }
38
+ chipSet.chips.push({ label: label, name: name, value: value })
37
39
 
38
- return () => observer.disconnect()
40
+ return () => {
41
+ observer.disconnect()
42
+ const index = chipSet.chips.findIndex((chip) => chip.value === value)
43
+ if (index !== -1) {
44
+ chipSet.chips.splice(index, 1)
45
+ }
46
+ }
39
47
  })
40
48
  </script>
41
49
 
@@ -46,4 +46,11 @@ export interface InputChipProps extends HTMLAttributes<HTMLDivElement> {
46
46
  currentTarget: EventTarget & HTMLButtonElement;
47
47
  }) => void;
48
48
  }
49
+ export interface ChipSetContext {
50
+ chips: {
51
+ label: string;
52
+ name: string | undefined;
53
+ value: string | number | undefined;
54
+ }[];
55
+ }
49
56
  export {};
@@ -78,6 +78,10 @@
78
78
  return window
79
79
  }
80
80
 
81
+ const onScroll = () => {
82
+ refreshValues()
83
+ }
84
+
81
85
  $effect(() => {
82
86
  if (anchor && element) {
83
87
  if (style) {
@@ -89,13 +93,7 @@
89
93
  element?.style.setProperty(key, value)
90
94
  })
91
95
  }
92
- getScrollableParent(element).addEventListener(
93
- 'scroll',
94
- () => {
95
- refreshValues()
96
- },
97
- { passive: true },
98
- )
96
+ getScrollableParent(element).addEventListener('scroll', onScroll, { passive: true })
99
97
  if (
100
98
  'anchorName' in document.documentElement.style &&
101
99
  !anchor.style.getPropertyValue('anchor-name')
@@ -105,6 +103,11 @@
105
103
  anchor.style.setProperty('anchor-name', generatedId)
106
104
  }
107
105
  }
106
+ return () => {
107
+ if (element) {
108
+ getScrollableParent(element).removeEventListener('scroll', onScroll)
109
+ }
110
+ }
108
111
  })
109
112
  </script>
110
113
 
@@ -258,7 +258,7 @@
258
258
  endPressAnimation()
259
259
  }
260
260
 
261
- const addEvents = (el: HTMLElement) => {
261
+ const removeEvents = (el: HTMLElement) => {
262
262
  el.removeEventListener('click', handleClick)
263
263
  el.removeEventListener('contextmenu', handleContextmenu)
264
264
  el.removeEventListener('pointercancel', handlePointercancel)
@@ -266,6 +266,10 @@
266
266
  el.removeEventListener('pointerenter', handlePointerenter)
267
267
  el.removeEventListener('pointerleave', handlePointerleave)
268
268
  el.removeEventListener('pointerup', handlePointerup)
269
+ }
270
+
271
+ const addEvents = (el: HTMLElement) => {
272
+ removeEvents(el)
269
273
 
270
274
  el.addEventListener('click', handleClick)
271
275
  el.addEventListener('contextmenu', handleContextmenu)
@@ -285,6 +289,14 @@
285
289
  addEvents(element)
286
290
  }
287
291
  }
292
+ return () => {
293
+ if (forElement) {
294
+ removeEvents(forElement)
295
+ }
296
+ if (element) {
297
+ removeEvents(element)
298
+ }
299
+ }
288
300
  })
289
301
  </script>
290
302
 
@@ -102,11 +102,17 @@
102
102
  errorTextRaw = errorText
103
103
  selectElement?.setCustomValidity(error ? errorText : '')
104
104
  })
105
+ const onReset = () => {
106
+ errorRaw = error
107
+ }
105
108
  $effect(() => {
106
109
  if (selectElement) {
107
- selectElement.form?.addEventListener('reset', () => {
108
- errorRaw = error
109
- })
110
+ selectElement.form?.addEventListener('reset', onReset)
111
+ }
112
+ return () => {
113
+ if (selectElement) {
114
+ selectElement.form?.removeEventListener('reset', onReset)
115
+ }
110
116
  }
111
117
  })
112
118
  const handleOptionSelect = (event: Event, option: SelectOption) => {
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { isFirstInvalidControlInForm } from './report-validity.js'
3
- import type { FocusEventHandler } from 'svelte/elements'
3
+ import type { FocusEventHandler, EventHandler } from 'svelte/elements'
4
4
  import type { TextFieldProps } from './types.ts'
5
5
 
6
6
  let {
@@ -22,6 +22,8 @@
22
22
  reportValidity = $bindable(),
23
23
  checkValidity = $bindable(),
24
24
  children,
25
+ oninput,
26
+ oninvalid,
25
27
  onfocus,
26
28
  onblur,
27
29
  focused = $bindable(false),
@@ -60,39 +62,72 @@
60
62
  inputElement?.setCustomValidity(error ? errorText : '')
61
63
  })
62
64
 
65
+ const onReset = () => {
66
+ errorRaw = error
67
+ value = ''
68
+ }
69
+
70
+ const onInputEvent = (
71
+ event: Event & {
72
+ currentTarget: (EventTarget & HTMLInputElement) | HTMLTextAreaElement
73
+ },
74
+ ) => {
75
+ doValidity = true
76
+ ;(oninput as EventHandler)?.(event)
77
+ }
78
+
79
+ const onInvalidEvent = (
80
+ event: Event & {
81
+ currentTarget: HTMLInputElement | HTMLTextAreaElement
82
+ },
83
+ ) => {
84
+ event.preventDefault()
85
+ const { currentTarget } = event
86
+ errorRaw = true
87
+ if (errorText === '') {
88
+ errorTextRaw = currentTarget.validationMessage
89
+ }
90
+ if (focusOnInvalid && isFirstInvalidControlInForm(currentTarget.form, currentTarget)) {
91
+ currentTarget.focus()
92
+ }
93
+ ;(oninvalid as EventHandler)?.(event)
94
+ }
95
+
96
+ const onFocusEvent = (
97
+ event: FocusEvent & {
98
+ currentTarget: EventTarget & (HTMLInputElement | HTMLTextAreaElement)
99
+ },
100
+ ) => {
101
+ focused = true
102
+ ;(onfocus as FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>)?.(event)
103
+ }
104
+
105
+ const onBlurEvent = (
106
+ event: FocusEvent & {
107
+ currentTarget: EventTarget & (HTMLInputElement | HTMLTextAreaElement)
108
+ },
109
+ ) => {
110
+ focused = false
111
+ if (doValidity) {
112
+ focusOnInvalid = false
113
+ if (checkValidity()) {
114
+ errorRaw = error
115
+ errorTextRaw = errorText
116
+ }
117
+ } else {
118
+ focusOnInvalid = true
119
+ }
120
+ ;(onblur as FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>)?.(event)
121
+ }
122
+
63
123
  $effect(() => {
64
124
  if (inputElement) {
65
- inputElement.form?.addEventListener('reset', () => {
66
- errorRaw = error
67
- value = ''
68
- })
69
- inputElement.addEventListener('input', () => {
70
- doValidity = true
71
- })
72
- inputElement.addEventListener('invalid', (event) => {
73
- event.preventDefault()
74
- const { currentTarget } = event as Event & {
75
- currentTarget: HTMLInputElement | HTMLTextAreaElement
76
- }
77
- errorRaw = true
78
- if (errorText === '') {
79
- errorTextRaw = currentTarget.validationMessage
80
- }
81
- if (focusOnInvalid && isFirstInvalidControlInForm(currentTarget.form, currentTarget)) {
82
- currentTarget.focus()
83
- }
84
- })
85
-
86
- inputElement.addEventListener('blur', () => {
87
- if (doValidity) {
88
- focusOnInvalid = false
89
- if (checkValidity()) {
90
- errorRaw = error
91
- errorTextRaw = errorText
92
- }
93
- focusOnInvalid = true
94
- }
95
- })
125
+ inputElement.form?.addEventListener('reset', onReset)
126
+ }
127
+ return () => {
128
+ if (inputElement) {
129
+ inputElement.form?.removeEventListener('reset', onReset)
130
+ }
96
131
  }
97
132
  })
98
133
  </script>
@@ -175,14 +210,10 @@
175
210
  <textarea
176
211
  aria-label={label}
177
212
  {...attributes}
178
- onfocus={(event) => {
179
- focused = true
180
- ;(onfocus as FocusEventHandler<HTMLTextAreaElement>)?.(event)
181
- }}
182
- onblur={(event) => {
183
- focused = false
184
- ;(onblur as FocusEventHandler<HTMLTextAreaElement>)?.(event)
185
- }}
213
+ oninput={onInputEvent}
214
+ oninvalid={onInvalidEvent}
215
+ onfocus={onFocusEvent}
216
+ onblur={onBlurEvent}
186
217
  bind:value
187
218
  bind:this={inputElement}
188
219
  class="input"
@@ -201,14 +232,10 @@
201
232
  {...attributes}
202
233
  bind:value
203
234
  bind:this={inputElement}
204
- onfocus={(event) => {
205
- focused = true
206
- ;(onfocus as FocusEventHandler<HTMLInputElement>)?.(event)
207
- }}
208
- onblur={(event) => {
209
- focused = false
210
- ;(onblur as FocusEventHandler<HTMLInputElement>)?.(event)
211
- }}
235
+ oninput={onInputEvent}
236
+ oninvalid={onInvalidEvent}
237
+ onfocus={onFocusEvent}
238
+ onblur={onBlurEvent}
212
239
  class="input"
213
240
  aria-invalid={errorRaw}
214
241
  />
@@ -497,7 +524,6 @@
497
524
  }
498
525
  :global(.content .input-wrapper .np-chip-set) {
499
526
  margin-top: calc(var(--top-space, 1.5rem) - 4px);
500
- margin-right: 0.5rem;
501
527
  }
502
528
 
503
529
  .prefix {
@@ -41,6 +41,24 @@
41
41
  return event.pointerType === 'touch'
42
42
  }
43
43
 
44
+ const onPointerenter = (event: PointerEvent) => {
45
+ if (!isTouch(event)) {
46
+ element?.showPopover()
47
+ }
48
+ }
49
+
50
+ const onPointerleave = (event: PointerEvent) => {
51
+ if (!isTouch(event)) {
52
+ element?.hidePopover()
53
+ }
54
+ }
55
+
56
+ const onPointerup = (event: PointerEvent) => {
57
+ if (!isTouch(event)) {
58
+ element?.hidePopover()
59
+ }
60
+ }
61
+
44
62
  $effect(() => {
45
63
  if (anchor && element) {
46
64
  if ('anchorName' in document.documentElement.style) {
@@ -51,22 +69,17 @@
51
69
  anchor.style.setProperty('anchor-name', generatedId)
52
70
  }
53
71
  }
54
- anchor.addEventListener('pointerenter', (event: PointerEvent) => {
55
- if (!isTouch(event)) {
56
- element?.showPopover()
57
- }
58
- })
59
- anchor.addEventListener('pointerleave', (event: PointerEvent) => {
60
- if (!isTouch(event)) {
61
- element?.hidePopover()
62
- }
63
- })
72
+ anchor.addEventListener('pointerenter', onPointerenter)
73
+ anchor.addEventListener('pointerleave', onPointerleave)
64
74
  if (!keepTooltipOnClick) {
65
- anchor.addEventListener('pointerup', (event: PointerEvent) => {
66
- if (!isTouch(event)) {
67
- element?.hidePopover()
68
- }
69
- })
75
+ anchor.addEventListener('pointerup', onPointerup)
76
+ }
77
+ }
78
+ return () => {
79
+ if (anchor) {
80
+ anchor.removeEventListener('pointerenter', onPointerenter)
81
+ anchor.removeEventListener('pointerleave', onPointerleave)
82
+ anchor.removeEventListener('pointerup', onPointerup)
70
83
  }
71
84
  }
72
85
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {
@@ -55,25 +55,25 @@
55
55
  "devDependencies": {
56
56
  "@eslint/js": "^9.29.0",
57
57
  "@material/material-color-utilities": "^0.3.0",
58
- "@playwright/test": "^1.53.0",
58
+ "@playwright/test": "^1.53.1",
59
59
  "@sveltejs/adapter-vercel": "^5.7.2",
60
- "@sveltejs/kit": "^2.21.5",
60
+ "@sveltejs/kit": "^2.22.0",
61
61
  "@sveltejs/package": "^2.3.11",
62
62
  "@sveltejs/vite-plugin-svelte": "^5.1.0",
63
63
  "@types/eslint": "^9.6.1",
64
64
  "eslint": "^9.29.0",
65
65
  "eslint-config-prettier": "^10.1.5",
66
- "eslint-plugin-svelte": "^3.9.2",
66
+ "eslint-plugin-svelte": "^3.9.3",
67
67
  "globals": "^16.2.0",
68
- "prettier": "^3.5.3",
68
+ "prettier": "^3.6.0",
69
69
  "prettier-plugin-svelte": "^3.4.0",
70
70
  "publint": "^0.3.12",
71
- "svelte": "^5.34.3",
72
- "svelte-check": "^4.2.1",
71
+ "svelte": "^5.34.7",
72
+ "svelte-check": "^4.2.2",
73
73
  "typescript": "^5.8.3",
74
- "typescript-eslint": "^8.34.1",
74
+ "typescript-eslint": "^8.35.0",
75
75
  "vite": "^6.3.5",
76
- "vitest": "^3.2.3"
76
+ "vitest": "^3.2.4"
77
77
  },
78
78
  "svelte": "./dist/index.js",
79
79
  "types": "./dist/index.d.ts",