@fastkit/vui 0.8.8 → 0.8.11

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.
@@ -1,5 +1,13 @@
1
1
  import './VSelect.scss';
2
- import { ref, VNodeChild, defineComponent, PropType, computed } from 'vue';
2
+ import {
3
+ ref,
4
+ Ref,
5
+ VNodeChild,
6
+ defineComponent,
7
+ PropType,
8
+ computed,
9
+ nextTick,
10
+ } from 'vue';
3
11
  import {
4
12
  createFormSelectorSettings,
5
13
  useFormSelectorControl,
@@ -10,6 +18,7 @@ import {
10
18
  createPropsOptions,
11
19
  VNodeChildOrSlot,
12
20
  resolveVNodeChildOrSlots,
21
+ useKeybord,
13
22
  } from '@fastkit/vue-kit';
14
23
  import { VFormControl } from '../VFormControl';
15
24
  import {
@@ -26,8 +35,19 @@ import {
26
35
  import { VUI_SELECT_SYMBOL, useVui } from '../../injections';
27
36
  import { VIcon } from '../VIcon';
28
37
  import { VMenu } from '../kits';
38
+ import { VOptionGroup } from '../VOptionGroup';
29
39
  import { VOption } from '../VOption';
30
40
  import { VButton } from '..';
41
+ import { VMenuControl } from '@fastkit/vue-stack';
42
+
43
+ export const ARROW_KEY_TYPES = useKeybord.Key(['ArrowUp', 'ArrowDown']);
44
+
45
+ export const CHOICE_KEY_TYPES = useKeybord.Key(['Enter', ' ']);
46
+
47
+ export const KEYBORD_EVENT_TYPES = useKeybord.Key([
48
+ ...ARROW_KEY_TYPES,
49
+ ...CHOICE_KEY_TYPES,
50
+ ]);
31
51
 
32
52
  const { props, emits } = createFormSelectorSettings();
33
53
 
@@ -48,6 +68,7 @@ export const VSelect = defineComponent({
48
68
  },
49
69
  emits,
50
70
  setup(props, ctx) {
71
+ const menuRef: Ref<{ stackMenuControl: VMenuControl } | null> = ref(null);
51
72
  const menuOpened = ref(false);
52
73
  const showMenu = () => {
53
74
  menuOpened.value = true;
@@ -111,9 +132,128 @@ export const VSelect = defineComponent({
111
132
  <span class="v-select__placeholder">{props.placeholder}</span>,
112
133
  );
113
134
  }
135
+
114
136
  return children;
115
137
  };
116
138
 
139
+ const clearKeyFocused = () => {
140
+ const menu = menuRef.value;
141
+ if (!menu) return;
142
+ const bodyEl = menu.stackMenuControl.bodyRef.value;
143
+ if (!bodyEl) return;
144
+
145
+ const els = Array.from(
146
+ bodyEl.querySelectorAll('.v-option'),
147
+ ) as HTMLElement[];
148
+
149
+ els.forEach((el) => el.classList.remove('v-option--key-focused'));
150
+ };
151
+
152
+ const getItemElements = (): HTMLElement[] | void => {
153
+ const menu = menuRef.value;
154
+ if (!menu) return;
155
+
156
+ const bodyEl = menu.stackMenuControl.bodyRef.value;
157
+
158
+ if (!bodyEl) return;
159
+
160
+ const els = (
161
+ Array.from(bodyEl.querySelectorAll('.v-option')) as HTMLElement[]
162
+ ).filter((el) => {
163
+ if (el.tabIndex === -1) return false;
164
+ const disabled = el.getAttribute('disabled');
165
+ const ariaDisabled = el.getAttribute('aria-disabled');
166
+ if (ariaDisabled === 'true') return false;
167
+ return disabled == null || disabled === '';
168
+ });
169
+
170
+ if (!els.length) return;
171
+
172
+ return els;
173
+ };
174
+
175
+ const arrowKeyHandler = (ev: KeyboardEvent) => {
176
+ const { key } = ev;
177
+ if (!menuOpened.value || !ARROW_KEY_TYPES.includes(key as any)) return;
178
+
179
+ const els = getItemElements();
180
+
181
+ if (!els) return;
182
+
183
+ const currentEl = els.find((el) =>
184
+ el.classList.contains('v-option--key-focused'),
185
+ );
186
+ const currentIndex = currentEl && els.indexOf(currentEl);
187
+ let nextIndex: number;
188
+ const { length } = els;
189
+ const isUp = key === 'ArrowUp';
190
+ if (currentIndex == null) {
191
+ nextIndex = isUp ? length - 1 : 0;
192
+ } else {
193
+ const shiftAmount = key === 'ArrowUp' ? -1 : 1;
194
+ nextIndex = currentIndex + shiftAmount;
195
+ if (nextIndex < 0) {
196
+ nextIndex = length - 1;
197
+ } else if (nextIndex >= length) {
198
+ nextIndex = 0;
199
+ }
200
+ }
201
+
202
+ const nextEl = els[nextIndex];
203
+
204
+ if (nextEl) {
205
+ clearKeyFocused();
206
+ nextEl.classList.add('v-option--key-focused');
207
+ nextEl.scrollIntoView({
208
+ block: 'nearest',
209
+ inline: 'nearest',
210
+ behavior: 'smooth',
211
+ });
212
+ ev.preventDefault();
213
+ }
214
+ };
215
+
216
+ const choiceKeyHandler = (ev: KeyboardEvent) => {
217
+ const { key } = ev;
218
+ if (!menuOpened.value || !CHOICE_KEY_TYPES.includes(key as any)) return;
219
+
220
+ const els = getItemElements();
221
+
222
+ if (!els) return;
223
+
224
+ const currentEl = els.find((el) =>
225
+ el.classList.contains('v-option--key-focused'),
226
+ );
227
+
228
+ if (currentEl) {
229
+ const ev = new MouseEvent('click', {
230
+ view: window,
231
+ bubbles: true,
232
+ cancelable: true,
233
+ });
234
+ currentEl.dispatchEvent(ev);
235
+ ev.preventDefault();
236
+ nextTick(() => {
237
+ currentEl.classList.add('v-option--key-focused');
238
+ });
239
+ }
240
+ };
241
+
242
+ const keybordEventHandler = (ev: KeyboardEvent) => {
243
+ if (ARROW_KEY_TYPES.includes(ev.key as any)) return arrowKeyHandler(ev);
244
+ if (CHOICE_KEY_TYPES.includes(ev.key as any)) return choiceKeyHandler(ev);
245
+ };
246
+
247
+ useKeybord(
248
+ [
249
+ {
250
+ key: KEYBORD_EVENT_TYPES,
251
+ handler: keybordEventHandler,
252
+ },
253
+ ],
254
+ { autorun: true },
255
+ );
256
+
117
257
  return {
118
258
  ...inputControl.expose(),
119
259
  ...control,
@@ -124,18 +264,31 @@ export const VSelect = defineComponent({
124
264
  showMenu,
125
265
  closeMenu,
126
266
  renderSelections,
267
+ menuRef: () => menuRef,
268
+ clearKeyFocused,
127
269
  };
128
270
  },
129
271
  render() {
130
272
  const { nodeControl, selectorControl, selectorItems, selectedItems } = this;
131
273
  const children = (this.$slots.default && this.$slots.default()) || [];
132
- const propOptions = this.propItems.map((item) => {
274
+ const propGroups = this.propGroups.map((group) => {
133
275
  return (
134
- <VOption disabled={item.disabled} value={item.value} key={item.value}>
135
- {{
136
- default: () => item.label(selectorControl),
137
- }}
138
- </VOption>
276
+ <VOptionGroup
277
+ key={group.id}
278
+ groupId={group.id}
279
+ label={group.label(selectorControl)}
280
+ disabled={group.disabled}>
281
+ {group.items.map((item) => (
282
+ <VOption
283
+ disabled={item.disabled}
284
+ value={item.value}
285
+ key={item.value}>
286
+ {{
287
+ default: () => item.label(selectorControl),
288
+ }}
289
+ </VOption>
290
+ ))}
291
+ </VOptionGroup>
139
292
  );
140
293
  });
141
294
 
@@ -143,7 +296,13 @@ export const VSelect = defineComponent({
143
296
  <VFormControl
144
297
  nodeControl={nodeControl}
145
298
  // focused={this.nodeControl.focused}
146
- class={['v-select', this.classes]}
299
+ class={[
300
+ 'v-select',
301
+ this.classes,
302
+ {
303
+ 'v-select--multiple': this.multiple,
304
+ },
305
+ ]}
147
306
  label={this.label}
148
307
  hint={this.hint}
149
308
  hinttip={this.hinttip}
@@ -161,6 +320,8 @@ export const VSelect = defineComponent({
161
320
  distance={0}
162
321
  alwaysRender
163
322
  v-model={this.menuOpened}
323
+ ref={this.menuRef()}
324
+ onClose={this.clearKeyFocused}
164
325
  v-slots={{
165
326
  activator: ({ attrs, control }) => [
166
327
  <VControlField
@@ -173,6 +334,7 @@ export const VSelect = defineComponent({
173
334
  // tabindex={this.computedTabindex}
174
335
  size={this.size}
175
336
  focused={this.menuOpened}
337
+ autoHeight={this.multiple}
176
338
  onClick={(ev) => {
177
339
  if (this.canOperation && !control.isActive) {
178
340
  let t = ev.target as HTMLElement;
@@ -261,7 +423,7 @@ export const VSelect = defineComponent({
261
423
  }}>
262
424
  <div class={['v-select__body', this.classes]}>
263
425
  {children}
264
- {propOptions}
426
+ {propGroups}
265
427
  </div>
266
428
  </VMenu>
267
429
  ),
@@ -8,7 +8,7 @@ import {
8
8
  FormControlSlots,
9
9
  useFormSelectorControl,
10
10
  FormSelectorControl,
11
- ResolvedFormSelectorItemData,
11
+ ResolvedFormSelectorItem,
12
12
  FormNodeControl,
13
13
  renderSlotOrEmpty,
14
14
  VNodeChildOrSlot,
@@ -28,7 +28,7 @@ export interface DefineFormSelectorComponentOptions {
28
28
  itemRenderer: (ctx: {
29
29
  control: FormSelectorControl;
30
30
  selected: boolean;
31
- attrs: ResolvedFormSelectorItemData & {
31
+ attrs: ResolvedFormSelectorItem & {
32
32
  modelValue: boolean;
33
33
  key: string | number;
34
34
  };
@@ -70,7 +70,7 @@ export function defineFormSelectorComponent(
70
70
  });
71
71
  const control = useControl(props);
72
72
  const vui = useVui();
73
- const loadingMessageRef = computed(() => {
73
+ const loadingMessageRef = computed<VNodeChild | undefined>(() => {
74
74
  const slot = resolveVNodeChildOrSlots(
75
75
  props.loadingMessage,
76
76
  vui.setting('loadingMessage'),
@@ -135,20 +135,29 @@ export function defineFormSelectorComponent(
135
135
  {this.loadingMessageRef}
136
136
  </div>
137
137
  )}
138
- {this.propItems.map((attrs) => {
139
- const selected = selectorControl.isSelected(attrs.value);
140
- return itemRenderer({
141
- selected,
142
- control: selectorControl,
143
- attrs: {
144
- ...attrs,
145
- modelValue: selected,
146
- key: attrs.value,
147
- },
148
- slots: {
149
- default: () => attrs.label(this.selectorControl),
150
- },
151
- });
138
+ {this.propGroups.map((group) => {
139
+ const { items } = group;
140
+ return (
141
+ <>
142
+ {items.map((attrs) => {
143
+ const selected = selectorControl.isSelected(
144
+ attrs.value,
145
+ );
146
+ return itemRenderer({
147
+ selected,
148
+ control: selectorControl,
149
+ attrs: {
150
+ ...attrs,
151
+ modelValue: selected,
152
+ key: attrs.value,
153
+ },
154
+ slots: {
155
+ default: () => attrs.label(this.selectorControl),
156
+ },
157
+ });
158
+ })}
159
+ </>
160
+ );
152
161
  })}
153
162
  {renderSlotOrEmpty(this.$slots, 'default')}
154
163
  </div>
package/src/plugin.tsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  installVueStackPlugin,
11
11
  VueStackServiceOptions,
12
12
  VueColorSchemePlugin,
13
+ onAppUnmount,
13
14
  } from '@fastkit/vue-kit';
14
15
  import { VButton } from './components/VButton';
15
16
 
@@ -46,14 +47,7 @@ declare module '@vue/runtime-core' {
46
47
  }
47
48
 
48
49
  export class VuiPlugin {
49
- static readonly installedApps = new Set<App>();
50
-
51
50
  static install(app: App, opts: VuiPluginOptions) {
52
- const { installedApps } = this;
53
- if (installedApps.has(app)) return;
54
- const unmountApp = app.unmount;
55
- installedApps.add(app);
56
-
57
51
  const { colorScheme, stack, uiSettings } = opts;
58
52
 
59
53
  // ColorScheme
@@ -88,11 +82,9 @@ export class VuiPlugin {
88
82
  app.provide(VuiInjectionKey, $vui);
89
83
  app.config.globalProperties.$vui = $vui;
90
84
 
91
- app.unmount = function () {
92
- installedApps.delete(app);
85
+ onAppUnmount(app, () => {
93
86
  delete app.config.globalProperties.$vui;
94
- unmountApp();
95
- };
87
+ });
96
88
  }
97
89
  }
98
90