noph-ui 0.17.13 → 0.18.1

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.
@@ -2,7 +2,6 @@
2
2
  import Item from '../list/Item.svelte'
3
3
  import Menu from '../menu/Menu.svelte'
4
4
  import VirtualList from '../select/VirtualList.svelte'
5
- import { tick } from 'svelte'
6
5
  import type { AutoCompleteOption, AutoCompleteProps } from './types.ts'
7
6
  import TextField from '../text-field/TextField.svelte'
8
7
 
@@ -11,56 +10,39 @@
11
10
  value = $bindable(),
12
11
  variant = 'filled',
13
12
  element = $bindable(),
14
- onchange,
15
- oninput,
13
+ populated,
16
14
  reportValidity = $bindable(),
17
15
  checkValidity = $bindable(),
18
16
  clampMenuWidth = false,
19
17
  children,
20
- optionsFilter = (option) => {
21
- return !value || option.value.includes(value)
22
- },
18
+ optionsFilter,
23
19
  oncomplete = (option) => {
24
- value = option.value
25
- console.log(menuElement)
20
+ value = option.label
21
+ finalPopulated = populated
26
22
  menuElement?.hidePopover()
27
23
  },
28
- onblur,
24
+ onkeydown,
29
25
  onclick,
26
+ oninput,
27
+ focused = $bindable(false),
30
28
  ...attributes
31
29
  }: AutoCompleteProps = $props()
32
30
 
33
31
  const uid = $props.id()
34
- let displayOptions = $derived(options.filter(optionsFilter))
32
+ let defaultOptionsFilter = (option: AutoCompleteOption) => {
33
+ return !value || option.label.includes(value)
34
+ }
35
+ let displayOptions = $derived(options.filter(optionsFilter || defaultOptionsFilter))
35
36
  let useVirtualList = $derived(displayOptions.length > 4000)
36
37
  let clientWidth = $state(0)
37
- let menuOpen = $state(false)
38
38
  let menuElement: HTMLDivElement | undefined = $state()
39
-
40
- const handleOptionSelect = (event: Event, option: AutoCompleteOption) => {
41
- value = option.value
42
- menuElement?.hidePopover()
43
- event.preventDefault()
44
- // tick().then(() => {
45
- // if (checkValidity?.()) {
46
- // errorRaw = error
47
- // errorTextRaw = errorText
48
- // }
49
- // element?.dispatchEvent(new Event('change', { bubbles: true }))
50
- // })
51
- }
52
- $effect(() => {
53
- if (displayOptions.length) {
54
- menuElement?.showPopover()
55
- } else {
56
- menuElement?.hidePopover()
57
- }
58
- })
39
+ let finalPopulated = $state(populated)
59
40
  </script>
60
41
 
61
42
  {#snippet item(option: AutoCompleteOption)}
62
43
  <Item
63
- onclick={() => {
44
+ onclick={(event) => {
45
+ event.preventDefault()
64
46
  oncomplete(option)
65
47
  element?.focus()
66
48
  }}
@@ -78,44 +60,57 @@
78
60
  oncomplete(option)
79
61
  }
80
62
  if (event.key === 'Tab') {
63
+ finalPopulated = populated
81
64
  menuElement?.hidePopover()
82
65
  }
83
66
  }}
84
67
  variant="button"
85
- >{option.value}
68
+ >{option.label}
86
69
  </Item>
87
70
  {/snippet}
88
71
 
89
72
  <TextField
90
73
  {variant}
74
+ type="text"
75
+ populated={finalPopulated}
91
76
  bind:clientWidth
92
77
  bind:value
78
+ bind:focused
93
79
  style="anchor-name:--{uid};"
94
- onclick={async (
95
- event: MouseEvent & {
96
- currentTarget: EventTarget & HTMLInputElement
97
- },
98
- ) => {
99
- if (displayOptions.length) {
100
- menuElement?.showPopover()
101
- }
80
+ onclick={(event) => {
81
+ finalPopulated = true
82
+ menuElement?.showPopover()
102
83
  onclick?.(event)
103
84
  }}
104
- onfocus={() => {
105
- if (displayOptions.length) {
106
- menuElement?.showPopover()
85
+ oninput={(event) => {
86
+ menuElement?.showPopover()
87
+ oninput?.(event)
88
+ }}
89
+ onkeydown={(event) => {
90
+ if (event.key === 'Tab' || event.key === 'Escape') {
91
+ menuElement?.hidePopover()
92
+ } else {
93
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
94
+ event.preventDefault()
95
+ finalPopulated = true
96
+ menuElement?.showPopover()
97
+ ;(menuElement?.firstElementChild?.firstElementChild as HTMLElement)?.focus()
98
+ }
107
99
  }
100
+ onkeydown?.(event)
108
101
  }}
109
102
  bind:reportValidity
110
103
  bind:checkValidity
111
104
  bind:element
112
- {...attributes}>{@render children?.()}</TextField
113
- >
105
+ {...attributes}
106
+ >{@render children?.()}
107
+ </TextField>
114
108
  <Menu
115
109
  style="position-anchor:--{uid};{clampMenuWidth || useVirtualList
116
110
  ? 'width'
117
111
  : 'min-width'}:{clientWidth}px"
118
112
  role="listbox"
113
+ class={[!displayOptions.length && 'np-auto-complete-empty']}
119
114
  --np-menu-justify-self="none"
120
115
  --np-menu-position-area="bottom span-right"
121
116
  --np-menu-margin="2px 0"
@@ -123,13 +118,6 @@
123
118
  ? 'var(--np-outlined-select-text-field-container-shape)'
124
119
  : 'var(--np-filled-select-text-field-container-shape)'}
125
120
  anchor={element}
126
- ontoggle={({ newState }) => {
127
- if (newState === 'open') {
128
- menuOpen = true
129
- } else {
130
- menuOpen = false
131
- }
132
- }}
133
121
  bind:element={menuElement}
134
122
  >
135
123
  {#if useVirtualList}
@@ -144,3 +132,10 @@
144
132
  {/each}
145
133
  {/if}
146
134
  </Menu>
135
+
136
+ <style>
137
+ :global(.np-auto-complete-empty) {
138
+ display: none !important;
139
+ opacity: 0 !important;
140
+ }
141
+ </style>
@@ -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" | "reportValidity" | "checkValidity" | "focused">;
3
3
  type AutoComplete = ReturnType<typeof AutoComplete>;
4
4
  export default AutoComplete;
@@ -1,6 +1,7 @@
1
1
  import type { InputFieldProps } from '../types.ts';
2
2
  export interface AutoCompleteOption {
3
- value: string;
3
+ value?: string | number;
4
+ label: string;
4
5
  disabled?: boolean;
5
6
  selected?: boolean | undefined | null;
6
7
  }
@@ -41,7 +41,7 @@ export interface InputChipProps extends HTMLAttributes<HTMLDivElement> {
41
41
  ariaLabelRemove?: string;
42
42
  element?: HTMLDivElement;
43
43
  name?: string;
44
- value?: string;
44
+ value?: string | number;
45
45
  onremove?: (event: MouseEvent & {
46
46
  currentTarget: EventTarget & HTMLButtonElement;
47
47
  }) => void;
@@ -35,11 +35,16 @@
35
35
  const oldIndicatorRect = oldIndicator?.getBoundingClientRect()
36
36
  if (oldIndicatorRect) {
37
37
  const newIndicator = el.querySelector<HTMLElement>('.np-indicator')
38
- newIndicator?.style.setProperty(
39
- '--np-tab-indicator-start',
40
- `${oldIndicatorRect.x - newIndicator.getBoundingClientRect().x}px`,
41
- )
42
- newIndicator?.style.setProperty('--np-tab-indicator-width', `${oldIndicatorRect.width}px`)
38
+ if (newIndicator) {
39
+ newIndicator.style.setProperty(
40
+ '--np-tab-indicator-start',
41
+ `${oldIndicatorRect.x - newIndicator.getBoundingClientRect().x}px`,
42
+ )
43
+ newIndicator.style.setProperty(
44
+ '--np-tab-indicator-scale',
45
+ `${oldIndicatorRect.width / newIndicator.clientWidth}`,
46
+ )
47
+ }
43
48
  }
44
49
  activeTab.value = value
45
50
  activeTab.node = el
@@ -99,12 +104,7 @@
99
104
  role="tab"
100
105
  bind:this={element}
101
106
  {href}
102
- class={[
103
- 'np-tab',
104
- variant === 'secondary' && 'np-tab-secondary',
105
- isActive && 'np-tab-content-active',
106
- attributes.class,
107
- ]}
107
+ class={['np-tab', isActive && 'np-tab-content-active', attributes.class]}
108
108
  onclick={onClick}
109
109
  onkeydown={onKeyDown}
110
110
  >
@@ -117,12 +117,7 @@
117
117
  tabindex={isActive ? 0 : -1}
118
118
  role="tab"
119
119
  bind:this={element}
120
- class={[
121
- 'np-tab',
122
- variant === 'secondary' && 'np-tab-secondary',
123
- isActive && 'np-tab-content-active',
124
- attributes.class,
125
- ]}
120
+ class={['np-tab', isActive && 'np-tab-content-active', attributes.class]}
126
121
  onclick={onClick}
127
122
  onkeydown={onKeyDown}
128
123
  >
@@ -188,17 +183,16 @@
188
183
  }
189
184
  .np-tab-content-active .np-indicator {
190
185
  opacity: 1;
186
+ transform-origin: left center;
191
187
  animation: slide 0.3s ease-in-out;
192
188
  }
193
189
 
194
190
  @keyframes slide {
195
191
  0% {
196
- width: var(--np-tab-indicator-width, 100%);
197
- transform: translateX(var(--np-tab-indicator-start));
192
+ transform: translateX(var(--np-tab-indicator-start)) scaleX(var(--np-tab-indicator-scale, 1));
198
193
  }
199
194
  100% {
200
- width: 100%;
201
- transform: translateX(0);
195
+ transform: translateX(0) scaleX(1);
202
196
  }
203
197
  }
204
198
 
@@ -24,6 +24,7 @@
24
24
  children,
25
25
  onfocus,
26
26
  onblur,
27
+ focused = $bindable(false),
27
28
  clientWidth = $bindable(),
28
29
  clientHeight = $bindable(),
29
30
  ...attributes
@@ -33,7 +34,6 @@
33
34
  let errorTextRaw: string = $state(errorText)
34
35
  let focusOnInvalid = $state(true)
35
36
  let doValidity = $state(false)
36
- let focused = $state(false)
37
37
 
38
38
  reportValidity = () => {
39
39
  if (inputElement) {
@@ -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">;
2
+ declare const TextField: import("svelte").Component<TextFieldProps, {}, "element" | "value" | "reportValidity" | "checkValidity" | "inputElement" | "clientWidth" | "clientHeight" | "focused">;
3
3
  type TextField = ReturnType<typeof TextField>;
4
4
  export default TextField;
@@ -18,6 +18,7 @@ interface FieldProps {
18
18
  checkValidity?: () => boolean;
19
19
  clientWidth?: number;
20
20
  clientHeight?: number;
21
+ focused?: boolean;
21
22
  }
22
23
  export interface InputFieldProps extends HTMLInputAttributes, FieldProps {
23
24
  type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url' | 'datetime-local';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.17.13",
3
+ "version": "0.18.1",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {