@nexxtmove/ui 1.3.3 → 1.3.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.
- package/dist/components/Announcement/Announcement.vue.d.ts +13 -0
- package/dist/components/Chip/Chip.vue.d.ts +9 -4
- package/dist/components/FloatingPanel/FloatingPanel.vue.d.ts +12 -0
- package/dist/components/ModalFooter/ModalFooter.vue.d.ts +34 -0
- package/dist/components/Select/Select.vue.d.ts +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +776 -637
- package/dist/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/Announcement/Announcement.vue +45 -4
- package/src/components/Chip/Chip.vue +28 -4
- package/src/components/ContactInfoCard/ContactInfoCard.vue +1 -1
- package/src/components/FloatingPanel/FloatingPanel.vue +30 -1
- package/src/components/ModalFooter/ModalFooter.vue +70 -0
- package/src/components/Select/Select.vue +102 -7
- package/src/components.json +1 -0
- package/src/index.ts +1 -0
package/dist/nuxt.js
CHANGED
|
@@ -27,6 +27,7 @@ var s = {
|
|
|
27
27
|
NexxtInputField: "components/InputField/InputField.vue",
|
|
28
28
|
NexxtInputSelect: "components/InputSelect/InputSelect.vue",
|
|
29
29
|
NexxtModal: "components/Modal/Modal.vue",
|
|
30
|
+
NexxtModalFooter: "components/ModalFooter/ModalFooter.vue",
|
|
30
31
|
NexxtPagination: "components/Pagination/Pagination.vue",
|
|
31
32
|
NexxtPresenceIndicator: "components/PresenceIndicator/PresenceIndicator.vue",
|
|
32
33
|
NexxtProgressBar: "components/ProgressBar/ProgressBar.vue",
|
package/package.json
CHANGED
|
@@ -1,28 +1,69 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import NexxtIcon from '../Icon/Icon.vue'
|
|
3
4
|
|
|
5
|
+
type AnnouncementVariant = 'info' | 'success' | 'warning' | 'danger' | 'neutral'
|
|
6
|
+
|
|
4
7
|
interface NexxtAnnouncementProps {
|
|
8
|
+
/**
|
|
9
|
+
* Raw CSS background colour, applied inline. Intended for colours supplied by
|
|
10
|
+
* the API; wins over `variant`. Defaults to `blue` when no `variant` is set.
|
|
11
|
+
*/
|
|
5
12
|
color?: string
|
|
13
|
+
/**
|
|
14
|
+
* Raw CSS text colour, applied inline. Snake_case mirrors the API field name.
|
|
15
|
+
* Wins over `variant`. Defaults to `white` when no `variant` is set.
|
|
16
|
+
*/
|
|
6
17
|
text_color?: string
|
|
18
|
+
/** Icon shown at the start of the banner. */
|
|
7
19
|
icon?: InstanceType<typeof NexxtIcon>['name']
|
|
20
|
+
/** Show the close button that emits `hide`. */
|
|
8
21
|
showHideButton?: boolean
|
|
22
|
+
/** Semantic colour scheme for app-authored banners, applied as design tokens. */
|
|
23
|
+
variant?: AnnouncementVariant
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
const {
|
|
12
|
-
color
|
|
13
|
-
text_color
|
|
27
|
+
color,
|
|
28
|
+
text_color,
|
|
14
29
|
icon = 'megaphone',
|
|
15
30
|
showHideButton = true,
|
|
31
|
+
variant,
|
|
16
32
|
} = defineProps<NexxtAnnouncementProps>()
|
|
17
33
|
|
|
18
34
|
const emit = defineEmits<{ hide: [] }>()
|
|
35
|
+
|
|
36
|
+
// Token-based schemes for app-authored banners.
|
|
37
|
+
const variantClasses: Record<AnnouncementVariant, string> = {
|
|
38
|
+
info: 'bg-cornflower-blue-50 text-cornflower-blue-900',
|
|
39
|
+
success: 'bg-green-50 text-green-900',
|
|
40
|
+
warning: 'bg-orange-50 text-orange-900',
|
|
41
|
+
danger: 'bg-brick-50 text-brick-900',
|
|
42
|
+
neutral: 'bg-gray-50 text-gray-900',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Precedence, deliberate and load-bearing:
|
|
46
|
+
// 1. An explicitly supplied `color` / `text_color` always wins. They carry raw
|
|
47
|
+
// CSS values coming from the API and are applied inline, so they override
|
|
48
|
+
// any variant class regardless of specificity.
|
|
49
|
+
// 2. Otherwise, a `variant` applies token classes.
|
|
50
|
+
// 3. With neither, the historical inline defaults (blue / white) are used, so
|
|
51
|
+
// existing consumers that pass nothing render exactly as before.
|
|
52
|
+
const style = computed(() => ({
|
|
53
|
+
background: color ?? (variant ? undefined : 'blue'),
|
|
54
|
+
color: text_color ?? (variant ? undefined : 'white'),
|
|
55
|
+
}))
|
|
19
56
|
</script>
|
|
20
57
|
|
|
21
58
|
<template>
|
|
22
59
|
<div
|
|
23
60
|
class="grid items-center gap-x-3 overflow-hidden rounded-md px-4 py-3"
|
|
24
|
-
:class="
|
|
25
|
-
|
|
61
|
+
:class="[
|
|
62
|
+
showHideButton ? 'grid-cols-[auto_1fr_auto]' : 'grid-cols-[auto_1fr]',
|
|
63
|
+
variant ? variantClasses[variant] : '',
|
|
64
|
+
]"
|
|
65
|
+
:style="style"
|
|
66
|
+
:data-variant="variant"
|
|
26
67
|
>
|
|
27
68
|
<NexxtIcon class="text-lg" :name="icon" />
|
|
28
69
|
<slot />
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import NexxtIcon, { type NexxtIconName } from '../Icon/Icon.vue'
|
|
3
4
|
|
|
4
5
|
interface NexxtChipProps {
|
|
5
|
-
|
|
6
|
+
/** Colour scheme of the chip. */
|
|
7
|
+
variant?: 'default' | 'warning' | 'success' | 'danger' | 'neutral' | 'accent' | 'yellow'
|
|
8
|
+
/** Render as a light, outlined chip instead of a solid one. */
|
|
6
9
|
ghost?: boolean
|
|
10
|
+
/** Optional icon rendered before the default slot. */
|
|
11
|
+
icon?: NexxtIconName
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
defineOptions({
|
|
10
15
|
name: 'NexxtChip',
|
|
11
16
|
})
|
|
12
17
|
|
|
13
|
-
const { variant = 'default', ghost = false } = defineProps<NexxtChipProps>()
|
|
18
|
+
const { variant = 'default', ghost = false, icon } = defineProps<NexxtChipProps>()
|
|
14
19
|
|
|
20
|
+
// Every variant states its own text colour explicitly rather than relying on a
|
|
21
|
+
// shared default, so a variant that needs to differ can do so without changing
|
|
22
|
+
// the shape of the data.
|
|
15
23
|
const styles = computed(() => {
|
|
16
24
|
if (ghost) {
|
|
17
25
|
switch (variant) {
|
|
@@ -25,6 +33,9 @@ const styles = computed(() => {
|
|
|
25
33
|
return { bg: 'bg-gray-100', border: 'border border-gray-600', text: 'text-gray-700' }
|
|
26
34
|
case 'accent':
|
|
27
35
|
return { bg: 'bg-purple-50', border: 'border border-purple-500', text: 'text-purple-500' }
|
|
36
|
+
case 'yellow':
|
|
37
|
+
// yellow-500 on yellow-50 is unreadable, so the ghost text steps down to yellow-700
|
|
38
|
+
return { bg: 'bg-yellow-50', border: 'border border-yellow-500', text: 'text-yellow-700' }
|
|
28
39
|
default:
|
|
29
40
|
return {
|
|
30
41
|
bg: 'bg-cornflower-blue-50',
|
|
@@ -44,6 +55,11 @@ const styles = computed(() => {
|
|
|
44
55
|
return { bg: 'bg-gray-700', border: '', text: 'text-white' }
|
|
45
56
|
case 'accent':
|
|
46
57
|
return { bg: 'bg-purple-600', border: '', text: 'text-white' }
|
|
58
|
+
case 'yellow':
|
|
59
|
+
// White on yellow-500 measures 1.57:1, below the 4.5:1 WCAG AA threshold
|
|
60
|
+
// that applies to this chip's 12px semibold label. Kept deliberately;
|
|
61
|
+
// tracked with the other contrast findings in the a11y backlog.
|
|
62
|
+
return { bg: 'bg-yellow-500', border: '', text: 'text-white' }
|
|
47
63
|
default:
|
|
48
64
|
return { bg: 'bg-cornflower-blue-600', border: '', text: 'text-white' }
|
|
49
65
|
}
|
|
@@ -53,9 +69,17 @@ const styles = computed(() => {
|
|
|
53
69
|
<template>
|
|
54
70
|
<span
|
|
55
71
|
class="inline-flex h-5 flex-col items-center justify-center gap-2.5 rounded-[200px]"
|
|
56
|
-
:class="[
|
|
72
|
+
:class="[
|
|
73
|
+
styles.bg,
|
|
74
|
+
styles.border,
|
|
75
|
+
icon ? ($slots.close ? 'pr-2 pl-3' : 'px-3') : $slots.close ? 'pr-2 pl-4' : 'px-4',
|
|
76
|
+
]"
|
|
57
77
|
>
|
|
58
|
-
<span
|
|
78
|
+
<span
|
|
79
|
+
class="justify-start text-center extra-small-semibold"
|
|
80
|
+
:class="[styles.text, icon ? 'inline-flex items-center gap-1.5' : '']"
|
|
81
|
+
>
|
|
82
|
+
<NexxtIcon v-if="icon" :name="icon" class="shrink-0" />
|
|
59
83
|
<slot>Chip</slot>
|
|
60
84
|
<slot v-if="$slots.close" name="close"></slot>
|
|
61
85
|
</span>
|
|
@@ -4,9 +4,14 @@ import { useFloating, autoUpdate, flip, shift, offset, size } from '@floating-ui
|
|
|
4
4
|
import type { Placement } from '@floating-ui/vue'
|
|
5
5
|
|
|
6
6
|
export interface NexxtFloatingPanelProps {
|
|
7
|
+
/** Preferred side and alignment of the panel relative to the trigger. */
|
|
7
8
|
placement?: Placement
|
|
9
|
+
/** Distance in pixels between the trigger and the panel. */
|
|
8
10
|
offsetDistance?: number
|
|
11
|
+
/** Stretch the panel to at least the trigger's width. */
|
|
9
12
|
matchWidth?: boolean
|
|
13
|
+
/** Wrap the content slot in the shared panel surface (rounded, bordered, white, shadowed). */
|
|
14
|
+
surface?: boolean
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
defineOptions({ name: 'NexxtFloatingPanel' })
|
|
@@ -15,6 +20,7 @@ const props = withDefaults(defineProps<NexxtFloatingPanelProps>(), {
|
|
|
15
20
|
placement: 'bottom-start',
|
|
16
21
|
offsetDistance: 8,
|
|
17
22
|
matchWidth: false,
|
|
23
|
+
surface: false,
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
const emit = defineEmits<{
|
|
@@ -167,7 +173,30 @@ onUnmounted(() => {
|
|
|
167
173
|
-->
|
|
168
174
|
<Transition v-bind="transitionClasses" @after-leave="isLeaving = false">
|
|
169
175
|
<div v-if="isOpen && isReady">
|
|
170
|
-
|
|
176
|
+
<!--
|
|
177
|
+
FloatingPanel is a positioning primitive: by default it adds no surface
|
|
178
|
+
of its own and the content slot renders bare, exactly as before. The
|
|
179
|
+
surface wrapper is an extra element that only exists when opted into,
|
|
180
|
+
so the default render is unchanged for existing consumers. The radius
|
|
181
|
+
is fixed by design and deliberately not configurable.
|
|
182
|
+
|
|
183
|
+
overflow-hidden belongs with the radius: without it, content drawn to
|
|
184
|
+
the panel's edge -- a scrolling list especially -- paints over the
|
|
185
|
+
rounded corners. Consumers cannot supply it themselves once they adopt
|
|
186
|
+
the surface, because the element carrying the radius is this one, and
|
|
187
|
+
clipping on an inner element has nothing left to clip against.
|
|
188
|
+
|
|
189
|
+
It does not trap nested floating content: FloatingPanel positions with
|
|
190
|
+
strategy 'fixed', and overflow on an ancestor does not clip a fixed
|
|
191
|
+
descendant unless that ancestor also establishes a containing block.
|
|
192
|
+
-->
|
|
193
|
+
<div
|
|
194
|
+
v-if="surface"
|
|
195
|
+
class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm"
|
|
196
|
+
>
|
|
197
|
+
<slot name="content" :is-open="isOpen" :close="close" :toggle="toggle" />
|
|
198
|
+
</div>
|
|
199
|
+
<slot v-else name="content" :is-open="isOpen" :close="close" :toggle="toggle" />
|
|
171
200
|
</div>
|
|
172
201
|
</Transition>
|
|
173
202
|
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import NexxtButton from '../Button/Button.vue'
|
|
3
|
+
|
|
4
|
+
// Kept in step with NexxtButton's own `variant` prop, widened with `string` so a
|
|
5
|
+
// call site may bind a computed value (`options.variant ?? 'primary'`) without a
|
|
6
|
+
// literal union rejecting it. The known values still autocomplete.
|
|
7
|
+
type ButtonVariant = InstanceType<typeof NexxtButton>['$props']['variant']
|
|
8
|
+
|
|
9
|
+
interface NexxtModalFooterProps {
|
|
10
|
+
/**
|
|
11
|
+
* Label of the cancel button. Required: this library has no i18n layer, so
|
|
12
|
+
* copy always arrives from the consumer rather than being defaulted here.
|
|
13
|
+
*/
|
|
14
|
+
cancelLabel: string
|
|
15
|
+
/**
|
|
16
|
+
* Disable the cancel button. Use while a confirmed action is in flight, so a
|
|
17
|
+
* destructive operation cannot be dismissed halfway through.
|
|
18
|
+
*/
|
|
19
|
+
cancelDisabled?: boolean
|
|
20
|
+
/** Label of the action button. */
|
|
21
|
+
confirmLabel: string
|
|
22
|
+
/**
|
|
23
|
+
* Variant of the action button. Left undefined it falls through to
|
|
24
|
+
* NexxtButton's own default, so it renders like a plain `<NexxtButton>`.
|
|
25
|
+
*/
|
|
26
|
+
confirmVariant?: ButtonVariant | (string & {})
|
|
27
|
+
/** Put the action button in its loading state. */
|
|
28
|
+
confirmLoading?: boolean
|
|
29
|
+
/** Disable the action button. */
|
|
30
|
+
confirmDisabled?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
defineOptions({
|
|
34
|
+
name: 'NexxtModalFooter',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
cancelDisabled = false,
|
|
39
|
+
confirmLoading = false,
|
|
40
|
+
confirmDisabled = false,
|
|
41
|
+
} = defineProps<NexxtModalFooterProps>()
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{ cancel: []; confirm: [] }>()
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div class="flex justify-end gap-2">
|
|
48
|
+
<!--
|
|
49
|
+
This comment lives INSIDE the root div on purpose. A comment before the
|
|
50
|
+
root makes the component multi-root, which leaves a consumer's fallthrough
|
|
51
|
+
class/id with nowhere unambiguous to land and changes what the root is
|
|
52
|
+
between dev and production, since the compiler strips comments in prod.
|
|
53
|
+
|
|
54
|
+
Modal's #footer wrapper already supplies the padding, so this component
|
|
55
|
+
adds none. It keeps its own row alignment so it also reads correctly
|
|
56
|
+
outside a Modal, without doubling up on anything Modal provides.
|
|
57
|
+
-->
|
|
58
|
+
<NexxtButton variant="secondary" :disabled="cancelDisabled" @click="emit('cancel')">
|
|
59
|
+
{{ cancelLabel }}
|
|
60
|
+
</NexxtButton>
|
|
61
|
+
<NexxtButton
|
|
62
|
+
:variant="confirmVariant as ButtonVariant"
|
|
63
|
+
:loading="confirmLoading"
|
|
64
|
+
:disabled="confirmDisabled"
|
|
65
|
+
@click="emit('confirm')"
|
|
66
|
+
>
|
|
67
|
+
{{ confirmLabel }}
|
|
68
|
+
</NexxtButton>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
3
3
|
import NexxtIcon from '../Icon/Icon.vue'
|
|
4
4
|
import NexxtFloatingPanel from '../FloatingPanel/FloatingPanel.vue'
|
|
5
5
|
import type Icon from '../Icon/Icon.vue'
|
|
@@ -11,16 +11,25 @@ const getValue = (opt: SelectOption): string | number => (typeof opt === 'string
|
|
|
11
11
|
|
|
12
12
|
const generateId = () => 'select-' + Math.random().toString(36).slice(2, 10)
|
|
13
13
|
const selectId = generateId()
|
|
14
|
+
const listboxId = `${selectId}-listbox`
|
|
15
|
+
const optionId = (index: number) => `${selectId}-option-${index}`
|
|
14
16
|
|
|
15
17
|
defineOptions({ name: 'NexxtSelect' })
|
|
16
18
|
|
|
17
19
|
interface NexxtSelectProps {
|
|
20
|
+
/** Options to choose from; plain strings or `{ label, value }` objects. */
|
|
18
21
|
options: SelectOption[]
|
|
22
|
+
/** Text shown when nothing is selected. */
|
|
19
23
|
placeholder?: string
|
|
24
|
+
/** Label rendered above the field. */
|
|
20
25
|
label?: string
|
|
26
|
+
/** Render the field in its error state. */
|
|
21
27
|
error?: boolean
|
|
28
|
+
/** Message shown below the field while `error` is true. */
|
|
22
29
|
errorMessage?: string
|
|
30
|
+
/** Icon rendered inside the field, before the value. */
|
|
23
31
|
leftIcon?: InstanceType<typeof Icon>['name']
|
|
32
|
+
/** Mark the field as required (renders an asterisk next to the label). */
|
|
24
33
|
required?: boolean
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -30,16 +39,86 @@ const props = withDefaults(defineProps<NexxtSelectProps>(), {
|
|
|
30
39
|
|
|
31
40
|
const model = defineModel<string | number | null>({ default: null })
|
|
32
41
|
|
|
42
|
+
const panel = useTemplateRef('panel')
|
|
43
|
+
|
|
44
|
+
// Index of the keyboard-focused option. -1 means nothing is focused, which is
|
|
45
|
+
// also the state while the listbox is closed.
|
|
46
|
+
const activeIndex = ref(-1)
|
|
47
|
+
|
|
33
48
|
const selectedLabel = computed(() => {
|
|
34
49
|
if (model.value === null || model.value === undefined) return null
|
|
35
50
|
const found = props.options.find((opt) => getValue(opt) === model.value)
|
|
36
51
|
return found ? getLabel(found) : null
|
|
37
52
|
})
|
|
38
53
|
|
|
54
|
+
const selectedIndex = computed(() =>
|
|
55
|
+
props.options.findIndex((opt) => getValue(opt) === model.value),
|
|
56
|
+
)
|
|
57
|
+
|
|
39
58
|
const select = (opt: SelectOption, close: () => void) => {
|
|
40
59
|
model.value = getValue(opt)
|
|
41
60
|
close()
|
|
42
61
|
}
|
|
62
|
+
|
|
63
|
+
// Opening via the keyboard focuses the selected option, or the first one.
|
|
64
|
+
const openWithFocus = () => {
|
|
65
|
+
activeIndex.value = selectedIndex.value >= 0 ? selectedIndex.value : 0
|
|
66
|
+
panel.value?.open()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const move = (key: string) => {
|
|
70
|
+
const count = props.options.length
|
|
71
|
+
if (!count) return
|
|
72
|
+
const current = activeIndex.value
|
|
73
|
+
if (key === 'Home') activeIndex.value = 0
|
|
74
|
+
else if (key === 'End') activeIndex.value = count - 1
|
|
75
|
+
else if (key === 'ArrowDown') activeIndex.value = current < 0 ? 0 : (current + 1) % count
|
|
76
|
+
else if (key === 'ArrowUp')
|
|
77
|
+
activeIndex.value = current < 0 ? count - 1 : (current - 1 + count) % count
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const onKeydown = (event: KeyboardEvent) => {
|
|
81
|
+
const isOpen = panel.value?.isOpen ?? false
|
|
82
|
+
|
|
83
|
+
if (event.key === 'Escape') {
|
|
84
|
+
if (!isOpen) return
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
panel.value?.close()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
if (!isOpen) {
|
|
93
|
+
openWithFocus()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
// Space only toggles; Enter commits the focused option.
|
|
97
|
+
if (event.key !== 'Enter') {
|
|
98
|
+
panel.value?.close()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const opt = props.options[activeIndex.value]
|
|
102
|
+
if (opt !== undefined) select(opt, () => panel.value?.close())
|
|
103
|
+
else panel.value?.close()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
if (!isOpen) {
|
|
110
|
+
openWithFocus()
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
move(event.key)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Keep the focused option in sync with the selection while closed, and drop it
|
|
118
|
+
// entirely once the listbox closes so aria-activedescendant is not left dangling.
|
|
119
|
+
watch(model, () => {
|
|
120
|
+
if (!panel.value?.isOpen) activeIndex.value = -1
|
|
121
|
+
})
|
|
43
122
|
</script>
|
|
44
123
|
|
|
45
124
|
<template>
|
|
@@ -53,19 +132,26 @@ const select = (opt: SelectOption, close: () => void) => {
|
|
|
53
132
|
<span v-if="required" class="text-brick-500">*</span>
|
|
54
133
|
</label>
|
|
55
134
|
|
|
56
|
-
<NexxtFloatingPanel
|
|
135
|
+
<NexxtFloatingPanel
|
|
136
|
+
ref="panel"
|
|
137
|
+
placement="bottom"
|
|
138
|
+
match-width
|
|
139
|
+
@close="activeIndex = -1"
|
|
140
|
+
@open="activeIndex = selectedIndex >= 0 ? selectedIndex : activeIndex"
|
|
141
|
+
>
|
|
57
142
|
<template #trigger="{ isOpen, toggle }">
|
|
58
143
|
<div
|
|
59
144
|
:id="selectId"
|
|
60
145
|
role="combobox"
|
|
61
146
|
:aria-expanded="isOpen"
|
|
62
147
|
aria-haspopup="listbox"
|
|
148
|
+
:aria-controls="isOpen ? listboxId : undefined"
|
|
149
|
+
:aria-activedescendant="isOpen && activeIndex >= 0 ? optionId(activeIndex) : undefined"
|
|
63
150
|
tabindex="0"
|
|
64
151
|
class="group flex w-full cursor-pointer items-center gap-2 rounded-lg ring transition-colors ease-out focus:ring-cornflower-blue-500 focus:outline-none"
|
|
65
152
|
:class="[error ? 'ring-brick-400' : 'ring-gray-200', { 'pl-3': leftIcon, 'pr-3': true }]"
|
|
66
153
|
@click="toggle"
|
|
67
|
-
@keydown
|
|
68
|
-
@keydown.space.prevent="toggle"
|
|
154
|
+
@keydown="onKeydown"
|
|
69
155
|
>
|
|
70
156
|
<span v-if="leftIcon" class="flex items-center">
|
|
71
157
|
<NexxtIcon :name="leftIcon" />
|
|
@@ -85,15 +171,24 @@ const select = (opt: SelectOption, close: () => void) => {
|
|
|
85
171
|
</template>
|
|
86
172
|
|
|
87
173
|
<template #content="{ close }">
|
|
88
|
-
<ul
|
|
174
|
+
<ul
|
|
175
|
+
:id="listboxId"
|
|
176
|
+
role="listbox"
|
|
177
|
+
class="overflow-hidden rounded-lg bg-white shadow-xs ring ring-gray-100"
|
|
178
|
+
>
|
|
89
179
|
<li
|
|
90
|
-
v-for="opt in options"
|
|
180
|
+
v-for="(opt, index) in options"
|
|
181
|
+
:id="optionId(index)"
|
|
91
182
|
:key="getValue(opt)"
|
|
92
183
|
role="option"
|
|
93
184
|
:aria-selected="model === getValue(opt)"
|
|
94
185
|
class="flex cursor-pointer items-center justify-between px-4 py-3 text-sm text-gray-900 transition-colors duration-150 hover:bg-gray-50 hover:text-purple-600"
|
|
95
|
-
:class="
|
|
186
|
+
:class="[
|
|
187
|
+
{ 'font-medium text-cornflower-blue-600': model === getValue(opt) },
|
|
188
|
+
index === activeIndex ? 'bg-gray-50' : '',
|
|
189
|
+
]"
|
|
96
190
|
@click="select(opt, close)"
|
|
191
|
+
@mousemove="activeIndex = index"
|
|
97
192
|
>
|
|
98
193
|
{{ getLabel(opt) }}
|
|
99
194
|
<NexxtIcon v-if="model === getValue(opt)" name="check" class="text-xs" />
|
package/src/components.json
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"NexxtInputField": "components/InputField/InputField.vue",
|
|
25
25
|
"NexxtInputSelect": "components/InputSelect/InputSelect.vue",
|
|
26
26
|
"NexxtModal": "components/Modal/Modal.vue",
|
|
27
|
+
"NexxtModalFooter": "components/ModalFooter/ModalFooter.vue",
|
|
27
28
|
"NexxtPagination": "components/Pagination/Pagination.vue",
|
|
28
29
|
"NexxtPresenceIndicator": "components/PresenceIndicator/PresenceIndicator.vue",
|
|
29
30
|
"NexxtProgressBar": "components/ProgressBar/ProgressBar.vue",
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { default as NexxtInfoBlock } from './components/InfoBlock/InfoBlock.vue'
|
|
|
38
38
|
export { default as NexxtInputField } from './components/InputField/InputField.vue'
|
|
39
39
|
export { default as NexxtInputSelect } from './components/InputSelect/InputSelect.vue'
|
|
40
40
|
export { default as NexxtModal } from './components/Modal/Modal.vue'
|
|
41
|
+
export { default as NexxtModalFooter } from './components/ModalFooter/ModalFooter.vue'
|
|
41
42
|
export { default as NexxtPagination } from './components/Pagination/Pagination.vue'
|
|
42
43
|
export { default as NexxtPresenceIndicator } from './components/PresenceIndicator/PresenceIndicator.vue'
|
|
43
44
|
export { default as NexxtProgressBar } from './components/ProgressBar/ProgressBar.vue'
|