@nexxtmove/ui 1.32.0 → 1.34.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/dist/components/atoms/FloatingPanel/FloatingPanel.vue.d.ts +48 -7
- package/dist/components/atoms/PickerItem/PickerItem.vue.d.ts +40 -0
- package/dist/components/atoms/PickerPreview/PickerPreview.vue.d.ts +29 -0
- package/dist/components/editor/molecules/EditorRadius/EditorRadius.vue.d.ts +4 -4
- package/dist/components/molecules/PickerGroup/PickerGroup.vue.d.ts +32 -0
- package/dist/components/molecules/PickerList/PickerList.vue.d.ts +107 -0
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +1 -1
- package/dist/components/organisms/PickerPanel/PickerPanel.vue.d.ts +93 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2368 -1643
- package/dist/nuxt.js +5 -0
- package/package.json +1 -1
- package/src/components/atoms/FloatingPanel/FloatingPanel.vue +280 -20
- package/src/components/atoms/PickerItem/PickerItem.vue +113 -0
- package/src/components/atoms/PickerPreview/PickerPreview.vue +99 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.vue +6 -6
- package/src/components/molecules/PickerGroup/PickerGroup.vue +107 -0
- package/src/components/molecules/PickerList/PickerList.vue +649 -0
- package/src/components/organisms/PickerPanel/PickerPanel.vue +267 -0
- package/src/components.json +5 -0
- package/src/index.ts +10 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue'
|
|
3
|
+
import { useResizeObserver } from '@vueuse/core'
|
|
4
|
+
import NexxtPickerPreview from '../../atoms/PickerPreview/PickerPreview.vue'
|
|
5
|
+
import NexxtInputField from '../../molecules/InputField/InputField.vue'
|
|
6
|
+
import NexxtPickerList, {
|
|
7
|
+
type NexxtPickerGroupData,
|
|
8
|
+
type NexxtPickerItemData,
|
|
9
|
+
} from '../../molecules/PickerList/PickerList.vue'
|
|
10
|
+
|
|
11
|
+
interface NexxtPickerPanelProps {
|
|
12
|
+
/** All items; the search field filters them. */
|
|
13
|
+
items: NexxtPickerItemData[]
|
|
14
|
+
/** Groups in display order. Ungrouped items come first. */
|
|
15
|
+
groups?: NexxtPickerGroupData[]
|
|
16
|
+
/** Render the search field. Without it the listbox takes focus on mount. */
|
|
17
|
+
searchable?: boolean
|
|
18
|
+
/** Placeholder of the search field. */
|
|
19
|
+
searchPlaceholder?: string
|
|
20
|
+
/** Accessible name of the search field. Required for an accessible name; the placeholder is only a fallback. */
|
|
21
|
+
searchLabel?: string
|
|
22
|
+
/** Accessible name of the panel (`role="group"`). Required for an accessible name. */
|
|
23
|
+
ariaLabel?: string
|
|
24
|
+
/** Accessible name of the listbox. Required for an accessible name. */
|
|
25
|
+
listLabel?: string
|
|
26
|
+
/** Panel width; a number is in pixels. The columns never shrink with it. */
|
|
27
|
+
width?: number | string
|
|
28
|
+
/** Panel height; a number is in pixels. Clamped to `calc(100dvh - 5rem)`, and only the list gives way. */
|
|
29
|
+
height?: number | string
|
|
30
|
+
/** Below this height of the column row (the panel minus the header), in pixels, the preview column is left out and the list takes the full width. */
|
|
31
|
+
hidePreviewBelow?: number
|
|
32
|
+
/** Whether group headings fold their group. */
|
|
33
|
+
collapsible?: boolean
|
|
34
|
+
/** Heading of the empty state, shown when the search has no results. */
|
|
35
|
+
emptyTitle?: string
|
|
36
|
+
/** Line under the empty heading. A function receives the trimmed search term. */
|
|
37
|
+
emptyDescription?: string | ((search: string) => string)
|
|
38
|
+
/** Label of the button in the empty state that clears the search. Without it there is no button. */
|
|
39
|
+
clearLabel?: string
|
|
40
|
+
/** Text in the preview frame while nothing is highlighted; the `preview-empty` slot replaces it. */
|
|
41
|
+
previewEmptyText?: string
|
|
42
|
+
/** Text announced 500ms after the search changes, e.g. `(n) => \`${n} resultaten\``. */
|
|
43
|
+
resultsAnnouncement?: (count: number) => string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
defineOptions({ name: 'NexxtPickerPanel' })
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<NexxtPickerPanelProps>(), {
|
|
49
|
+
groups: () => [],
|
|
50
|
+
searchable: true,
|
|
51
|
+
width: 640,
|
|
52
|
+
height: 420,
|
|
53
|
+
hidePreviewBelow: 340,
|
|
54
|
+
collapsible: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/** Id of the highlighted item; the list keeps it on a visible item. Drives the preview. */
|
|
58
|
+
const highlighted = defineModel<string | null>('highlighted', { default: null })
|
|
59
|
+
|
|
60
|
+
/** The search term. Bind it to reset or prefill the search; unbound, the panel keeps its own. */
|
|
61
|
+
const search = defineModel<string>('search', { default: '' })
|
|
62
|
+
|
|
63
|
+
const emit = defineEmits<{
|
|
64
|
+
/** Enter, Space or a click on an enabled item. The panel never closes itself. */
|
|
65
|
+
select: [item: NexxtPickerItemData]
|
|
66
|
+
/** The highlighted item changed; `null` when nothing is visible. */
|
|
67
|
+
highlight: [item: NexxtPickerItemData | null]
|
|
68
|
+
/**
|
|
69
|
+
* Escape in an empty search field, or in the listbox. A surrounding `NexxtFloatingPanel`
|
|
70
|
+
* closes on the same key, so keep the handler idempotent.
|
|
71
|
+
*/
|
|
72
|
+
close: []
|
|
73
|
+
}>()
|
|
74
|
+
|
|
75
|
+
defineSlots<{
|
|
76
|
+
/** Above both columns, e.g. `NexxtTabs`. */
|
|
77
|
+
header?(): unknown
|
|
78
|
+
/** Content of the preview frame for the highlighted item. */
|
|
79
|
+
preview?(props: { item: NexxtPickerItemData }): unknown
|
|
80
|
+
/** Replaces `previewEmptyText` in the preview frame. */
|
|
81
|
+
'preview-empty'?(): unknown
|
|
82
|
+
/** Content of an option. */
|
|
83
|
+
item?(props: { item: NexxtPickerItemData; highlighted: boolean }): unknown
|
|
84
|
+
/** Replaces the content of the empty search state. */
|
|
85
|
+
empty?(props: { search: string; clear: () => void }): unknown
|
|
86
|
+
}>()
|
|
87
|
+
|
|
88
|
+
const inputField = useTemplateRef<InstanceType<typeof NexxtInputField>>('inputField')
|
|
89
|
+
const list = useTemplateRef<InstanceType<typeof NexxtPickerList>>('list')
|
|
90
|
+
const row = useTemplateRef<HTMLElement>('row')
|
|
91
|
+
|
|
92
|
+
const PAGE_SIZE = 8
|
|
93
|
+
|
|
94
|
+
const toLength = (value: number | string) => (typeof value === 'number' ? `${value}px` : value)
|
|
95
|
+
|
|
96
|
+
const panelStyle = computed(() => {
|
|
97
|
+
const height = `min(${toLength(props.height)}, calc(100dvh - 5rem))`
|
|
98
|
+
return { width: toLength(props.width), height, maxHeight: height }
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Layout height of the column row. Not `getBoundingClientRect()`: a FloatingPanel scales its
|
|
102
|
+
// content in, which would read 5% short. 0 means unknown (jsdom, a hidden tab): show the preview.
|
|
103
|
+
const rowHeight = ref(0)
|
|
104
|
+
|
|
105
|
+
useResizeObserver(row, ([entry]) => {
|
|
106
|
+
rowHeight.value =
|
|
107
|
+
entry.borderBoxSize?.[0]?.blockSize ?? (entry.target as HTMLElement).offsetHeight
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const showPreview = computed(
|
|
111
|
+
() => rowHeight.value === 0 || rowHeight.value >= props.hidePreviewBelow,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const highlightedItem = computed(
|
|
115
|
+
() => props.items.find((item) => item.id === highlighted.value) ?? null,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/** Focuses the search field, or the listbox when the panel is not searchable. */
|
|
119
|
+
const focus = () => {
|
|
120
|
+
if (props.searchable) inputField.value?.inputEl?.focus({ preventScroll: true })
|
|
121
|
+
else list.value?.focus()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onMounted(async () => {
|
|
125
|
+
// Measured before the first paint, so the preview never flashes in and out
|
|
126
|
+
if (row.value) rowHeight.value = row.value.offsetHeight
|
|
127
|
+
|
|
128
|
+
// One tick for the list's template ref to land, so the field already carries
|
|
129
|
+
// `aria-controls` and `aria-activedescendant` when it receives focus. `preventScroll`: a
|
|
130
|
+
// FloatingPanel is still in its enter transition here.
|
|
131
|
+
await nextTick()
|
|
132
|
+
focus()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const onSearchKeydown = (event: KeyboardEvent) => {
|
|
136
|
+
if (event.isComposing) return
|
|
137
|
+
|
|
138
|
+
switch (event.key) {
|
|
139
|
+
case 'ArrowDown':
|
|
140
|
+
list.value?.move(1)
|
|
141
|
+
break
|
|
142
|
+
case 'ArrowUp':
|
|
143
|
+
list.value?.move(-1)
|
|
144
|
+
break
|
|
145
|
+
case 'PageDown':
|
|
146
|
+
list.value?.move(PAGE_SIZE)
|
|
147
|
+
break
|
|
148
|
+
case 'PageUp':
|
|
149
|
+
list.value?.move(-PAGE_SIZE)
|
|
150
|
+
break
|
|
151
|
+
case 'Enter':
|
|
152
|
+
// Always prevented: the field may sit in a form
|
|
153
|
+
list.value?.selectHighlighted()
|
|
154
|
+
break
|
|
155
|
+
case 'Escape':
|
|
156
|
+
if (search.value === '') {
|
|
157
|
+
// Left to propagate: a surrounding FloatingPanel closes on it too
|
|
158
|
+
emit('close')
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
search.value = ''
|
|
162
|
+
// A FloatingPanel listens on `document` and would close on the first press already
|
|
163
|
+
event.stopPropagation()
|
|
164
|
+
break
|
|
165
|
+
// Home, End, ← and → stay cursor keys in a text field
|
|
166
|
+
default:
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
event.preventDefault()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Focus moves in the same handler, before the clear button leaves the DOM with the empty state
|
|
174
|
+
const onClear = () => {
|
|
175
|
+
search.value = ''
|
|
176
|
+
focus()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
defineExpose({ focus })
|
|
180
|
+
|
|
181
|
+
const COLUMN_CLASSES = {
|
|
182
|
+
withPreview: 'flex w-66 shrink-0 flex-col border-r border-gray-200',
|
|
183
|
+
full: 'flex w-full flex-col',
|
|
184
|
+
}
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<template>
|
|
188
|
+
<div
|
|
189
|
+
role="group"
|
|
190
|
+
:aria-label="ariaLabel"
|
|
191
|
+
class="flex flex-col overflow-hidden"
|
|
192
|
+
:style="panelStyle"
|
|
193
|
+
>
|
|
194
|
+
<div v-if="$slots.header" class="shrink-0 border-b border-gray-200 p-3">
|
|
195
|
+
<slot name="header" />
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div ref="row" class="flex min-h-0 flex-1">
|
|
199
|
+
<div :class="showPreview ? COLUMN_CLASSES.withPreview : COLUMN_CLASSES.full">
|
|
200
|
+
<!-- `text-gray-700` colours the magnifying glass; the input sets its own text colour.
|
|
201
|
+
`[&_input]:min-w-0` lifts the input's own `min-w-60` inside the panel only; with it
|
|
202
|
+
the field overflows the 240px column and a long term loses its caret. -->
|
|
203
|
+
<div v-if="searchable" class="shrink-0 border-b border-gray-200 p-3 text-gray-700">
|
|
204
|
+
<NexxtInputField
|
|
205
|
+
ref="inputField"
|
|
206
|
+
v-model="search"
|
|
207
|
+
type="text"
|
|
208
|
+
role="combobox"
|
|
209
|
+
aria-expanded="true"
|
|
210
|
+
aria-autocomplete="list"
|
|
211
|
+
autocomplete="off"
|
|
212
|
+
spellcheck="false"
|
|
213
|
+
left-icon="magnifying-glass"
|
|
214
|
+
class="w-full [&_input]:min-w-0"
|
|
215
|
+
:placeholder="searchPlaceholder"
|
|
216
|
+
:aria-label="searchLabel"
|
|
217
|
+
:aria-controls="list?.listboxId"
|
|
218
|
+
:aria-activedescendant="list?.activeDescendantId"
|
|
219
|
+
@keydown="onSearchKeydown"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<NexxtPickerList
|
|
224
|
+
ref="list"
|
|
225
|
+
v-model:highlighted="highlighted"
|
|
226
|
+
class="min-h-0 flex-1"
|
|
227
|
+
:items="items"
|
|
228
|
+
:groups="groups"
|
|
229
|
+
:search="search"
|
|
230
|
+
:collapsible="collapsible"
|
|
231
|
+
:aria-label="listLabel"
|
|
232
|
+
:empty-title="emptyTitle"
|
|
233
|
+
:empty-description="emptyDescription"
|
|
234
|
+
:clear-label="clearLabel"
|
|
235
|
+
:results-announcement="resultsAnnouncement"
|
|
236
|
+
@select="emit('select', $event)"
|
|
237
|
+
@highlight="emit('highlight', $event)"
|
|
238
|
+
@close="emit('close')"
|
|
239
|
+
@clear="onClear"
|
|
240
|
+
>
|
|
241
|
+
<template v-if="$slots.item" #item="scope">
|
|
242
|
+
<slot name="item" v-bind="scope" />
|
|
243
|
+
</template>
|
|
244
|
+
<template v-if="$slots.empty" #empty="scope">
|
|
245
|
+
<slot name="empty" v-bind="scope" />
|
|
246
|
+
</template>
|
|
247
|
+
</NexxtPickerList>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div v-if="showPreview" class="min-w-0 flex-1 overflow-hidden p-4">
|
|
251
|
+
<NexxtPickerPreview
|
|
252
|
+
:content-key="highlighted ?? undefined"
|
|
253
|
+
:title="highlightedItem?.label"
|
|
254
|
+
:description="highlightedItem?.description"
|
|
255
|
+
:empty-text="previewEmptyText"
|
|
256
|
+
>
|
|
257
|
+
<template v-if="highlightedItem && $slots.preview" #default>
|
|
258
|
+
<slot name="preview" :item="highlightedItem" />
|
|
259
|
+
</template>
|
|
260
|
+
<template v-if="$slots['preview-empty']" #empty>
|
|
261
|
+
<slot name="preview-empty" />
|
|
262
|
+
</template>
|
|
263
|
+
</NexxtPickerPreview>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</template>
|
package/src/components.json
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"NexxtFloatingPanel": "components/atoms/FloatingPanel/FloatingPanel.vue",
|
|
10
10
|
"NexxtIcon": "components/atoms/Icon/Icon.vue",
|
|
11
11
|
"NexxtLoader": "components/atoms/Loader/Loader.vue",
|
|
12
|
+
"NexxtPickerItem": "components/atoms/PickerItem/PickerItem.vue",
|
|
13
|
+
"NexxtPickerPreview": "components/atoms/PickerPreview/PickerPreview.vue",
|
|
12
14
|
"NexxtPresenceIndicator": "components/atoms/PresenceIndicator/PresenceIndicator.vue",
|
|
13
15
|
"NexxtPropertyListing": "components/atoms/PropertyListing/PropertyListing.vue",
|
|
14
16
|
"NexxtSelectableOption": "components/atoms/SelectableOption/SelectableOption.vue",
|
|
@@ -30,6 +32,8 @@
|
|
|
30
32
|
"NexxtPageMockup": "components/molecules/PageMockup/PageMockup.vue",
|
|
31
33
|
"NexxtPageTypeTile": "components/molecules/PageTypeTile/PageTypeTile.vue",
|
|
32
34
|
"NexxtPagination": "components/molecules/Pagination/Pagination.vue",
|
|
35
|
+
"NexxtPickerGroup": "components/molecules/PickerGroup/PickerGroup.vue",
|
|
36
|
+
"NexxtPickerList": "components/molecules/PickerList/PickerList.vue",
|
|
33
37
|
"NexxtPlaceholderInput": "components/molecules/PlaceholderInput/PlaceholderInput.vue",
|
|
34
38
|
"NexxtPropertyCard": "components/molecules/PropertyCard/PropertyCard.vue",
|
|
35
39
|
"NexxtSelect": "components/molecules/Select/Select.vue",
|
|
@@ -57,6 +61,7 @@
|
|
|
57
61
|
"NexxtDropdownButton": "components/organisms/DropdownButton/DropdownButton.vue",
|
|
58
62
|
"NexxtHeader": "components/organisms/Header/Header.vue",
|
|
59
63
|
"NexxtModal": "components/organisms/Modal/Modal.vue",
|
|
64
|
+
"NexxtPickerPanel": "components/organisms/PickerPanel/PickerPanel.vue",
|
|
60
65
|
"NexxtProgressBar": "components/organisms/ProgressBar/ProgressBar.vue",
|
|
61
66
|
"NexxtReferenceCard": "components/organisms/ReferenceCard/ReferenceCard.vue",
|
|
62
67
|
"NexxtSocialMediaTemplate": "components/organisms/SocialMediaTemplate/SocialMediaTemplate.vue",
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type {
|
|
|
18
18
|
NexxtPageCardSlots,
|
|
19
19
|
} from './components/molecules/PageCard/PageCard.vue'
|
|
20
20
|
export type { NexxtPageMockupType } from './components/molecules/PageMockup/PageMockup.vue'
|
|
21
|
+
export type {
|
|
22
|
+
NexxtPickerGroupData,
|
|
23
|
+
NexxtPickerItemData,
|
|
24
|
+
} from './components/molecules/PickerList/PickerList.vue'
|
|
21
25
|
export type {
|
|
22
26
|
NexxtTreeNode,
|
|
23
27
|
NexxtTreeProps,
|
|
@@ -57,6 +61,7 @@ export type {
|
|
|
57
61
|
EditorHeadingTypeMessages,
|
|
58
62
|
EditorHeadingTypeValue,
|
|
59
63
|
} from './components/editor/molecules/EditorHeadingType/EditorHeadingType.vue'
|
|
64
|
+
export { EDITOR_RADIUS_STEPS } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
|
|
60
65
|
export type {
|
|
61
66
|
EditorRadiusMessages,
|
|
62
67
|
EditorRadiusValue,
|
|
@@ -126,6 +131,11 @@ export { default as NexxtPageContainer } from './components/molecules/PageContai
|
|
|
126
131
|
export { default as NexxtPageMockup } from './components/molecules/PageMockup/PageMockup.vue'
|
|
127
132
|
export { default as NexxtPageTypeTile } from './components/molecules/PageTypeTile/PageTypeTile.vue'
|
|
128
133
|
export { default as NexxtPagination } from './components/molecules/Pagination/Pagination.vue'
|
|
134
|
+
export { default as NexxtPickerGroup } from './components/molecules/PickerGroup/PickerGroup.vue'
|
|
135
|
+
export { default as NexxtPickerItem } from './components/atoms/PickerItem/PickerItem.vue'
|
|
136
|
+
export { default as NexxtPickerList } from './components/molecules/PickerList/PickerList.vue'
|
|
137
|
+
export { default as NexxtPickerPanel } from './components/organisms/PickerPanel/PickerPanel.vue'
|
|
138
|
+
export { default as NexxtPickerPreview } from './components/atoms/PickerPreview/PickerPreview.vue'
|
|
129
139
|
export { default as NexxtPlaceholderInput } from './components/molecules/PlaceholderInput/PlaceholderInput.vue'
|
|
130
140
|
export { default as NexxtPresenceIndicator } from './components/atoms/PresenceIndicator/PresenceIndicator.vue'
|
|
131
141
|
export { default as NexxtProgressBar } from './components/organisms/ProgressBar/ProgressBar.vue'
|