@flux-ui/components 3.1.2 → 3.1.3

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.
Files changed (61) hide show
  1. package/dist/component/FluxAvatarGroup.vue.d.ts +17 -0
  2. package/dist/component/FluxContextMenu.vue.d.ts +26 -0
  3. package/dist/component/FluxDataTable.vue.d.ts +20 -10
  4. package/dist/component/FluxDescriptionItem.vue.d.ts +19 -0
  5. package/dist/component/FluxDescriptionList.vue.d.ts +17 -0
  6. package/dist/component/FluxFormCombobox.vue.d.ts +20 -0
  7. package/dist/component/FluxFormRating.vue.d.ts +21 -0
  8. package/dist/component/FluxFormTagsInput.vue.d.ts +27 -0
  9. package/dist/component/FluxFormTextArea.vue.d.ts +6 -1
  10. package/dist/component/FluxInlineEdit.vue.d.ts +41 -0
  11. package/dist/component/FluxMenu.vue.d.ts +1 -0
  12. package/dist/component/FluxMenuFlyout.vue.d.ts +22 -0
  13. package/dist/component/FluxTableCell.vue.d.ts +1 -0
  14. package/dist/component/FluxTour.vue.d.ts +35 -0
  15. package/dist/component/FluxTourItem.vue.d.ts +18 -0
  16. package/dist/component/FluxVirtualScroller.vue.d.ts +27 -0
  17. package/dist/component/index.d.ts +12 -0
  18. package/dist/component/primitive/AnchorPopup.vue.d.ts +7 -1
  19. package/dist/component/primitive/SelectBase.vue.d.ts +3 -0
  20. package/dist/composable/private/index.d.ts +1 -0
  21. package/dist/composable/private/useMenuFlyout.d.ts +42 -0
  22. package/dist/data/di.d.ts +35 -0
  23. package/dist/data/i18n.d.ts +7 -0
  24. package/dist/index.css +441 -1
  25. package/dist/index.js +2018 -407
  26. package/dist/index.js.map +1 -1
  27. package/package.json +7 -7
  28. package/src/component/FluxAvatarGroup.vue +52 -0
  29. package/src/component/FluxContextMenu.vue +134 -0
  30. package/src/component/FluxDataTable.vue +113 -32
  31. package/src/component/FluxDescriptionItem.vue +43 -0
  32. package/src/component/FluxDescriptionList.vue +37 -0
  33. package/src/component/FluxFormCombobox.vue +98 -0
  34. package/src/component/FluxFormRating.vue +172 -0
  35. package/src/component/FluxFormTagsInput.vue +249 -0
  36. package/src/component/FluxFormTextArea.vue +16 -1
  37. package/src/component/FluxInlineEdit.vue +176 -0
  38. package/src/component/FluxMenu.vue +13 -3
  39. package/src/component/FluxMenuFlyout.vue +118 -0
  40. package/src/component/FluxTableCell.vue +2 -0
  41. package/src/component/FluxTour.vue +332 -0
  42. package/src/component/FluxTourItem.vue +27 -0
  43. package/src/component/FluxVirtualScroller.vue +96 -0
  44. package/src/component/index.ts +12 -0
  45. package/src/component/primitive/AnchorPopup.vue +27 -0
  46. package/src/component/primitive/SelectBase.vue +37 -2
  47. package/src/composable/private/index.ts +1 -0
  48. package/src/composable/private/useMenuFlyout.ts +417 -0
  49. package/src/css/component/AvatarGroup.module.scss +22 -0
  50. package/src/css/component/ContextMenu.module.scss +17 -0
  51. package/src/css/component/DescriptionList.module.scss +98 -0
  52. package/src/css/component/Form.module.scss +51 -0
  53. package/src/css/component/FormRating.module.scss +47 -0
  54. package/src/css/component/InlineEdit.module.scss +45 -0
  55. package/src/css/component/Menu.module.scss +4 -1
  56. package/src/css/component/MenuFlyout.module.scss +38 -0
  57. package/src/css/component/Table.module.scss +16 -0
  58. package/src/css/component/Tour.module.scss +108 -0
  59. package/src/css/component/VirtualScroller.module.scss +17 -0
  60. package/src/data/di.ts +40 -0
  61. package/src/data/i18n.ts +7 -0
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <div
3
+ :class="clsx(
4
+ $style.formRating,
5
+ disabled && $style.isDisabled,
6
+ error && $style.isInvalid
7
+ )"
8
+ :id="id"
9
+ :style="{
10
+ fontSize: size && `${size}px`
11
+ }"
12
+ role="slider"
13
+ :aria-disabled="disabled ? true : undefined"
14
+ :aria-invalid="error ? true : undefined"
15
+ :aria-readonly="isReadonly ? true : undefined"
16
+ :aria-valuemin="0"
17
+ :aria-valuemax="count"
18
+ :aria-valuenow="modelValue ?? 0"
19
+ :aria-valuetext="`${modelValue ?? 0} / ${count}`"
20
+ :tabindex="isInteractive ? 0 : undefined"
21
+ @keydown="onKeyDown"
22
+ @mouseleave="hoverValue = null">
23
+ <input
24
+ v-if="name"
25
+ type="hidden"
26
+ :name="name"
27
+ :value="modelValue ?? ''">
28
+
29
+ <button
30
+ v-for="star of count"
31
+ :key="star"
32
+ :class="$style.formRatingStar"
33
+ type="button"
34
+ tabindex="-1"
35
+ aria-hidden="true"
36
+ :disabled="!isInteractive"
37
+ :style="{
38
+ '--fill': fillFor(star)
39
+ }"
40
+ @click="onClick(star, $event)"
41
+ @mousemove="onMouseMove(star, $event)">
42
+ <FluxIcon
43
+ :class="$style.formRatingStarEmpty"
44
+ :name="icon"/>
45
+
46
+ <FluxIcon
47
+ :class="$style.formRatingStarFull"
48
+ :name="icon"/>
49
+ </button>
50
+ </div>
51
+ </template>
52
+
53
+ <script
54
+ lang="ts"
55
+ setup>
56
+ import type { FluxFormInputBaseProps, FluxIconName } from '@flux-ui/types';
57
+ import { clsx } from 'clsx';
58
+ import { computed, ref, toRef } from 'vue';
59
+ import { useDisabled, useFormFieldInjection } from '~flux/components/composable';
60
+ import FluxIcon from './FluxIcon.vue';
61
+ import $style from '~flux/components/css/component/FormRating.module.scss';
62
+
63
+ const modelValue = defineModel<number | null>({
64
+ default: null
65
+ });
66
+
67
+ const {
68
+ allowHalf = false,
69
+ clearable = false,
70
+ count = 5,
71
+ disabled: componentDisabled,
72
+ icon = 'star',
73
+ isReadonly
74
+ } = defineProps<Pick<FluxFormInputBaseProps, 'disabled' | 'error' | 'isReadonly' | 'name'> & {
75
+ readonly allowHalf?: boolean;
76
+ readonly clearable?: boolean;
77
+ readonly count?: number;
78
+ readonly icon?: FluxIconName;
79
+ readonly size?: number;
80
+ }>();
81
+
82
+ const emit = defineEmits<{
83
+ change: [number | null];
84
+ }>();
85
+
86
+ const disabled = useDisabled(toRef(() => componentDisabled));
87
+ const {id} = useFormFieldInjection();
88
+
89
+ const hoverValue = ref<number | null>(null);
90
+
91
+ const isInteractive = computed(() => !disabled.value && !isReadonly);
92
+ const displayValue = computed(() => hoverValue.value ?? modelValue.value ?? 0);
93
+
94
+ function fillFor(star: number): number {
95
+ return Math.min(1, Math.max(0, displayValue.value - (star - 1)));
96
+ }
97
+
98
+ function resolveStarValue(star: number, evt: MouseEvent): number {
99
+ if (!allowHalf) {
100
+ return star;
101
+ }
102
+
103
+ const {left, width} = (evt.currentTarget as HTMLElement).getBoundingClientRect();
104
+ return evt.clientX - left < width / 2 ? star - 0.5 : star;
105
+ }
106
+
107
+ function commit(value: number | null): void {
108
+ modelValue.value = value;
109
+ emit('change', value);
110
+ }
111
+
112
+ function onMouseMove(star: number, evt: MouseEvent): void {
113
+ if (!isInteractive.value) {
114
+ return;
115
+ }
116
+
117
+ hoverValue.value = resolveStarValue(star, evt);
118
+ }
119
+
120
+ function onClick(star: number, evt: MouseEvent): void {
121
+ if (!isInteractive.value) {
122
+ return;
123
+ }
124
+
125
+ const value = resolveStarValue(star, evt);
126
+ commit(clearable && modelValue.value === value ? null : value);
127
+ }
128
+
129
+ function onKeyDown(evt: KeyboardEvent): void {
130
+ if (!isInteractive.value) {
131
+ return;
132
+ }
133
+
134
+ const step = allowHalf ? 0.5 : 1;
135
+ const current = modelValue.value ?? 0;
136
+
137
+ switch (evt.key) {
138
+ case 'ArrowRight':
139
+ case 'ArrowUp':
140
+ evt.preventDefault();
141
+ commit(Math.min(count, current + step));
142
+ break;
143
+
144
+ case 'ArrowLeft':
145
+ case 'ArrowDown':
146
+ evt.preventDefault();
147
+ commit(Math.max(0, current - step));
148
+ break;
149
+
150
+ case 'Home':
151
+ evt.preventDefault();
152
+ commit(0);
153
+ break;
154
+
155
+ case 'End':
156
+ evt.preventDefault();
157
+ commit(count);
158
+ break;
159
+
160
+ default:
161
+ if (/^[0-9]$/.test(evt.key)) {
162
+ const digit = Number(evt.key);
163
+
164
+ if (digit <= count) {
165
+ evt.preventDefault();
166
+ commit(digit);
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ }
172
+ </script>
@@ -0,0 +1,249 @@
1
+ <template>
2
+ <Anchor
3
+ ref="anchor"
4
+ :class="clsx(
5
+ disabled ? $style.formTagsInputDisabled : $style.formTagsInputEnabled,
6
+ isCondensed && $style.isCondensed,
7
+ isSecondary && $style.isSecondary,
8
+ error && $style.isInvalid
9
+ )"
10
+ :id="id"
11
+ role="group"
12
+ tag-name="div"
13
+ @click="focusInput">
14
+ <FluxTag
15
+ v-for="(tag, index) of modelValue"
16
+ :key="`${tag}-${index}`"
17
+ :color="tagColor"
18
+ :label="tag"
19
+ is-deletable
20
+ @delete="removeAt(index)"/>
21
+
22
+ <input
23
+ ref="input"
24
+ v-model="query"
25
+ :class="$style.formTagsInputField"
26
+ :name="name"
27
+ autocomplete="off"
28
+ :disabled="disabled"
29
+ :placeholder="modelValue.length === 0 ? placeholder : undefined"
30
+ :readonly="isReadonly"
31
+ type="text"
32
+ @input="onInput"
33
+ @keydown="onKeyDown"
34
+ @paste="onPaste">
35
+ </Anchor>
36
+
37
+ <Teleport to="body">
38
+ <FluxFadeTransition>
39
+ <AnchorPopup
40
+ v-if="isOpen && filteredSuggestions.length > 0"
41
+ ref="popup"
42
+ :class="$style.formTagsInputPopup"
43
+ :anchor="anchorRef"
44
+ direction="vertical"
45
+ use-anchor-width>
46
+ <FluxMenu>
47
+ <FluxMenuItem
48
+ v-for="(suggestion, index) of filteredSuggestions"
49
+ :key="suggestion.value ?? index"
50
+ :icon-leading="suggestion.icon"
51
+ :is-highlighted="highlightedIndex === index"
52
+ :label="suggestion.label"
53
+ type="button"
54
+ @click="addSuggestion(suggestion)"/>
55
+ </FluxMenu>
56
+ </AnchorPopup>
57
+ </FluxFadeTransition>
58
+ </Teleport>
59
+ </template>
60
+
61
+ <script
62
+ lang="ts"
63
+ setup>
64
+ import { useClickOutside } from '@basmilius/common';
65
+ import type { FluxColor, FluxFormInputBaseProps, FluxFormSelectOption } from '@flux-ui/types';
66
+ import { clsx } from 'clsx';
67
+ import { type ComponentPublicInstance, computed, nextTick, ref, toRef, useTemplateRef } from 'vue';
68
+ import { useDisabled, useFormFieldInjection } from '~flux/components/composable';
69
+ import { FluxFadeTransition } from '~flux/components/transition';
70
+ import { Anchor, AnchorPopup } from '~flux/components/component/primitive';
71
+ import FluxMenu from './FluxMenu.vue';
72
+ import FluxMenuItem from './FluxMenuItem.vue';
73
+ import FluxTag from './FluxTag.vue';
74
+ import $style from '~flux/components/css/component/Form.module.scss';
75
+
76
+ const modelValue = defineModel<string[]>({
77
+ default: () => []
78
+ });
79
+
80
+ const query = defineModel<string>('searchQuery', {
81
+ default: ''
82
+ });
83
+
84
+ const {
85
+ allowDuplicates,
86
+ delimiters = ['Enter', ','],
87
+ disabled: componentDisabled,
88
+ max,
89
+ suggestions,
90
+ tagColor,
91
+ validate
92
+ } = defineProps<Pick<FluxFormInputBaseProps, 'disabled' | 'error' | 'isCondensed' | 'isReadonly' | 'isSecondary' | 'name' | 'placeholder'> & {
93
+ readonly allowDuplicates?: boolean;
94
+ readonly delimiters?: string[];
95
+ readonly max?: number;
96
+ readonly suggestions?: FluxFormSelectOption[];
97
+ readonly tagColor?: FluxColor;
98
+ readonly validate?: (value: string) => boolean;
99
+ }>();
100
+
101
+ const emit = defineEmits<{
102
+ add: [string];
103
+ remove: [string];
104
+ }>();
105
+
106
+ const disabled = useDisabled(toRef(() => componentDisabled));
107
+ const {id} = useFormFieldInjection();
108
+
109
+ const anchorRef = useTemplateRef<ComponentPublicInstance>('anchor');
110
+ const popupRef = useTemplateRef<ComponentPublicInstance>('popup');
111
+ const inputElementRef = useTemplateRef<HTMLInputElement>('input');
112
+
113
+ const isOpen = ref(false);
114
+ const highlightedIndex = ref(-1);
115
+
116
+ const isMaxReached = computed(() => max !== undefined && modelValue.value.length >= max);
117
+ const filteredSuggestions = computed(() => {
118
+ if (!suggestions) {
119
+ return [];
120
+ }
121
+
122
+ const search = query.value.trim().toLowerCase();
123
+
124
+ return suggestions.filter(suggestion =>
125
+ !modelValue.value.includes(suggestion.label) &&
126
+ (search === '' || suggestion.label.toLowerCase().includes(search)));
127
+ });
128
+
129
+ function focusInput(): void {
130
+ inputElementRef.value?.focus();
131
+ }
132
+
133
+ function addTag(rawValue: string): void {
134
+ const value = rawValue.trim();
135
+
136
+ if (value === '' || isMaxReached.value) {
137
+ return;
138
+ }
139
+
140
+ if (!allowDuplicates && modelValue.value.includes(value)) {
141
+ query.value = '';
142
+ return;
143
+ }
144
+
145
+ if (validate && !validate(value)) {
146
+ return;
147
+ }
148
+
149
+ modelValue.value = [...modelValue.value, value];
150
+ emit('add', value);
151
+
152
+ query.value = '';
153
+ highlightedIndex.value = -1;
154
+ isOpen.value = false;
155
+ }
156
+
157
+ function removeAt(index: number): void {
158
+ const removed = modelValue.value[index];
159
+ modelValue.value = modelValue.value.filter((_, i) => i !== index);
160
+ emit('remove', removed);
161
+ }
162
+
163
+ function addSuggestion(suggestion: FluxFormSelectOption): void {
164
+ addTag(suggestion.label);
165
+ nextTick(() => focusInput());
166
+ }
167
+
168
+ function onInput(): void {
169
+ isOpen.value = query.value.trim() !== '' && filteredSuggestions.value.length > 0;
170
+ highlightedIndex.value = -1;
171
+ }
172
+
173
+ function onKeyDown(evt: KeyboardEvent): void {
174
+ if (evt.key === 'Escape' && isOpen.value) {
175
+ evt.preventDefault();
176
+ isOpen.value = false;
177
+ return;
178
+ }
179
+
180
+ if (isOpen.value && (evt.key === 'ArrowDown' || evt.key === 'ArrowUp')) {
181
+ evt.preventDefault();
182
+
183
+ const count = filteredSuggestions.value.length;
184
+
185
+ if (count === 0) {
186
+ return;
187
+ }
188
+
189
+ highlightedIndex.value = evt.key === 'ArrowDown'
190
+ ? (highlightedIndex.value + 1) % count
191
+ : (highlightedIndex.value - 1 + count) % count;
192
+
193
+ return;
194
+ }
195
+
196
+ if (evt.key === 'Enter') {
197
+ if (isOpen.value && highlightedIndex.value >= 0) {
198
+ evt.preventDefault();
199
+ addSuggestion(filteredSuggestions.value[highlightedIndex.value]);
200
+ return;
201
+ }
202
+
203
+ if (delimiters.includes('Enter')) {
204
+ evt.preventDefault();
205
+ addTag(query.value);
206
+ }
207
+
208
+ return;
209
+ }
210
+
211
+ if (evt.key.length === 1 && delimiters.includes(evt.key)) {
212
+ evt.preventDefault();
213
+ addTag(query.value);
214
+ return;
215
+ }
216
+
217
+ if (evt.key === 'Backspace' && query.value === '' && modelValue.value.length > 0) {
218
+ removeAt(modelValue.value.length - 1);
219
+ }
220
+ }
221
+
222
+ function onPaste(evt: ClipboardEvent): void {
223
+ if (!evt.clipboardData) {
224
+ return;
225
+ }
226
+
227
+ const separators = [...delimiters.filter(delimiter => delimiter !== 'Enter'), '\n'];
228
+ const text = evt.clipboardData.getData('text');
229
+
230
+ let parts = [text];
231
+
232
+ for (const separator of separators) {
233
+ parts = parts.flatMap(part => part.split(separator));
234
+ }
235
+
236
+ parts = parts.map(part => part.trim()).filter(part => part !== '');
237
+
238
+ if (parts.length <= 1) {
239
+ return;
240
+ }
241
+
242
+ evt.preventDefault();
243
+ parts.forEach(addTag);
244
+ }
245
+
246
+ if (typeof window !== 'undefined') {
247
+ useClickOutside([anchorRef, popupRef], isOpen, () => isOpen.value = false);
248
+ }
249
+ </script>
@@ -29,9 +29,10 @@
29
29
  <script
30
30
  lang="ts"
31
31
  setup>
32
+ import { unrefTemplateElement } from '@flux-ui/internals';
32
33
  import type { FluxAutoCompleteType, FluxFormInputBaseProps } from '@flux-ui/types';
33
34
  import { clsx } from 'clsx';
34
- import { toRef } from 'vue';
35
+ import { toRef, useTemplateRef } from 'vue';
35
36
  import { useDisabled, useFormFieldInjection } from '~flux/components/composable';
36
37
  import $style from '~flux/components/css/component/Form.module.scss';
37
38
 
@@ -55,5 +56,19 @@
55
56
  }>();
56
57
 
57
58
  const disabled = useDisabled(toRef(() => componentDisabled));
59
+ const inputRef = useTemplateRef<HTMLTextAreaElement>('input');
58
60
  const {id} = useFormFieldInjection();
61
+
62
+ function blur(): void {
63
+ unrefTemplateElement(inputRef)?.blur();
64
+ }
65
+
66
+ function focus(): void {
67
+ unrefTemplateElement(inputRef)?.focus();
68
+ }
69
+
70
+ defineExpose({
71
+ blur,
72
+ focus
73
+ });
59
74
  </script>
@@ -0,0 +1,176 @@
1
+ <template>
2
+ <div :class="$style.inlineEdit">
3
+ <template v-if="isEditing">
4
+ <FluxFormTextArea
5
+ v-if="multiline"
6
+ v-model="draft"
7
+ ref="field"
8
+ :class="$style.inlineEditField"
9
+ :error="error"
10
+ :placeholder="placeholder"
11
+ @blur="onBlur"
12
+ @keydown="onKeyDown"/>
13
+
14
+ <FluxFormInput
15
+ v-else
16
+ v-model="draft"
17
+ ref="field"
18
+ :class="$style.inlineEditField"
19
+ :error="error"
20
+ :placeholder="placeholder"
21
+ @blur="onBlur"
22
+ @keydown="onKeyDown"/>
23
+
24
+ <div
25
+ :class="$style.inlineEditActions"
26
+ @mousedown.prevent>
27
+ <slot
28
+ name="actions"
29
+ :save="save"
30
+ :cancel="cancel">
31
+ <FluxSecondaryButton
32
+ icon-leading="check"
33
+ @click="save"/>
34
+
35
+ <FluxSecondaryButton
36
+ icon-leading="xmark"
37
+ @click="cancel"/>
38
+ </slot>
39
+ </div>
40
+ </template>
41
+
42
+ <div
43
+ v-else
44
+ ref="display"
45
+ :class="clsx(
46
+ $style.inlineEditDisplay,
47
+ isInteractive && $style.isInteractive,
48
+ !modelValue && $style.isPlaceholder
49
+ )"
50
+ :role="isInteractive ? 'button' : undefined"
51
+ :tabindex="isInteractive ? 0 : undefined"
52
+ @click="edit"
53
+ @keydown="onDisplayKeyDown">
54
+ <slot
55
+ :value="modelValue"
56
+ :edit="edit">
57
+ {{ modelValue || placeholder }}
58
+ </slot>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <script
64
+ lang="ts"
65
+ setup>
66
+ import { clsx } from 'clsx';
67
+ import { computed, nextTick, ref, toRef, useTemplateRef, type VNode } from 'vue';
68
+ import { useDisabled } from '~flux/components/composable';
69
+ import FluxFormInput from './FluxFormInput.vue';
70
+ import FluxFormTextArea from './FluxFormTextArea.vue';
71
+ import FluxSecondaryButton from './FluxSecondaryButton.vue';
72
+ import $style from '~flux/components/css/component/InlineEdit.module.scss';
73
+
74
+ const modelValue = defineModel<string>({
75
+ default: ''
76
+ });
77
+
78
+ const {
79
+ disabled: componentDisabled,
80
+ isReadonly,
81
+ multiline = false,
82
+ saveOnBlur = true
83
+ } = defineProps<{
84
+ readonly disabled?: boolean;
85
+ readonly error?: string | null;
86
+ readonly isReadonly?: boolean;
87
+ readonly multiline?: boolean;
88
+ readonly placeholder?: string;
89
+ readonly saveOnBlur?: boolean;
90
+ }>();
91
+
92
+ const emit = defineEmits<{
93
+ cancel: [];
94
+ edit: [];
95
+ save: [string];
96
+ }>();
97
+
98
+ defineSlots<{
99
+ actions?(props: {save(): void; cancel(): void}): VNode[];
100
+ default?(props: {value: string; edit(): void}): VNode[];
101
+ }>();
102
+
103
+ const disabled = useDisabled(toRef(() => componentDisabled));
104
+ const displayRef = useTemplateRef<HTMLElement>('display');
105
+ const fieldRef = useTemplateRef<{focus(): void}>('field');
106
+
107
+ const isEditing = ref(false);
108
+ const draft = ref('');
109
+
110
+ const isInteractive = computed(() => !disabled.value && !isReadonly);
111
+
112
+ function edit(): void {
113
+ if (!isInteractive.value) {
114
+ return;
115
+ }
116
+
117
+ draft.value = modelValue.value;
118
+ isEditing.value = true;
119
+ emit('edit');
120
+
121
+ nextTick(() => fieldRef.value?.focus());
122
+ }
123
+
124
+ function close(): void {
125
+ isEditing.value = false;
126
+ nextTick(() => displayRef.value?.focus());
127
+ }
128
+
129
+ function save(): void {
130
+ modelValue.value = draft.value;
131
+ emit('save', draft.value);
132
+ close();
133
+ }
134
+
135
+ function cancel(): void {
136
+ emit('cancel');
137
+ close();
138
+ }
139
+
140
+ function onBlur(): void {
141
+ if (saveOnBlur && isEditing.value) {
142
+ save();
143
+ }
144
+ }
145
+
146
+ function onKeyDown(evt: KeyboardEvent): void {
147
+ if (evt.key === 'Escape') {
148
+ evt.preventDefault();
149
+ cancel();
150
+ return;
151
+ }
152
+
153
+ if (evt.key === 'Enter') {
154
+ if (multiline) {
155
+ if (evt.metaKey || evt.ctrlKey) {
156
+ evt.preventDefault();
157
+ save();
158
+ }
159
+ } else {
160
+ evt.preventDefault();
161
+ save();
162
+ }
163
+ }
164
+ }
165
+
166
+ function onDisplayKeyDown(evt: KeyboardEvent): void {
167
+ if (!isInteractive.value) {
168
+ return;
169
+ }
170
+
171
+ if (evt.key === 'Enter' || evt.key === ' ') {
172
+ evt.preventDefault();
173
+ edit();
174
+ }
175
+ }
176
+ </script>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <nav
3
3
  ref="element"
4
- :class="isLarge ? $style.menuLarge : $style.menuNormal"
4
+ :class="[isLarge ? $style.menuLarge : $style.menuNormal, coneActive && $style.menuConeActive]"
5
5
  role="menu"
6
6
  aria-orientation="vertical">
7
7
  <slot/>
@@ -12,10 +12,14 @@
12
12
  lang="ts"
13
13
  setup>
14
14
  import { useFocusZone } from '@flux-ui/internals';
15
- import { useTemplateRef, type VNode } from 'vue';
15
+ import { computed, toRef, useTemplateRef, type VNode } from 'vue';
16
+ import { useMenuFlyoutContext } from '~flux/components/composable/private';
16
17
  import $style from '~flux/components/css/component/Menu.module.scss';
17
18
 
18
- defineProps<{
19
+ const {
20
+ debugCone = false
21
+ } = defineProps<{
22
+ readonly debugCone?: boolean;
19
23
  readonly isLarge?: boolean;
20
24
  }>();
21
25
 
@@ -28,4 +32,10 @@
28
32
  useFocusZone(elementRef, {
29
33
  direction: 'vertical'
30
34
  });
35
+
36
+ const menuFlyout = useMenuFlyoutContext({
37
+ debugCone: toRef(() => debugCone)
38
+ });
39
+
40
+ const coneActive = computed(() => !!menuFlyout.activeCone.value);
31
41
  </script>