quasar 2.21.4 → 2.22.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.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/dist/api/QUploader.json +1 -1
  3. package/dist/icon-set/bootstrap-icons.umd.prod.js +1 -1
  4. package/dist/icon-set/eva-icons.umd.prod.js +1 -1
  5. package/dist/icon-set/fontawesome-v5-pro.umd.prod.js +1 -1
  6. package/dist/icon-set/fontawesome-v5.umd.prod.js +1 -1
  7. package/dist/icon-set/fontawesome-v6-pro.umd.prod.js +1 -1
  8. package/dist/icon-set/fontawesome-v6.umd.prod.js +1 -1
  9. package/dist/icon-set/fontawesome-v7-pro.umd.prod.js +1 -1
  10. package/dist/icon-set/fontawesome-v7.umd.prod.js +1 -1
  11. package/dist/icon-set/ionicons-v4.umd.prod.js +1 -1
  12. package/dist/icon-set/line-awesome.umd.prod.js +1 -1
  13. package/dist/icon-set/material-icons-outlined.umd.prod.js +1 -1
  14. package/dist/icon-set/material-icons-round.umd.prod.js +1 -1
  15. package/dist/icon-set/material-icons-sharp.umd.prod.js +1 -1
  16. package/dist/icon-set/material-icons.umd.prod.js +1 -1
  17. package/dist/icon-set/material-symbols-outlined.umd.prod.js +1 -1
  18. package/dist/icon-set/material-symbols-rounded.umd.prod.js +1 -1
  19. package/dist/icon-set/material-symbols-sharp.umd.prod.js +1 -1
  20. package/dist/icon-set/mdi-v3.umd.prod.js +1 -1
  21. package/dist/icon-set/mdi-v4.umd.prod.js +1 -1
  22. package/dist/icon-set/mdi-v5.umd.prod.js +1 -1
  23. package/dist/icon-set/mdi-v6.umd.prod.js +1 -1
  24. package/dist/icon-set/mdi-v7.umd.prod.js +1 -1
  25. package/dist/icon-set/svg-bootstrap-icons.umd.prod.js +1 -1
  26. package/dist/icon-set/svg-eva-icons.umd.prod.js +1 -1
  27. package/dist/icon-set/svg-fontawesome-v5.umd.prod.js +1 -1
  28. package/dist/icon-set/svg-fontawesome-v6.umd.prod.js +1 -1
  29. package/dist/icon-set/svg-fontawesome-v7.umd.prod.js +1 -1
  30. package/dist/icon-set/svg-ionicons-v4.umd.prod.js +1 -1
  31. package/dist/icon-set/svg-ionicons-v5.umd.prod.js +1 -1
  32. package/dist/icon-set/svg-ionicons-v6.umd.prod.js +1 -1
  33. package/dist/icon-set/svg-ionicons-v7.umd.prod.js +1 -1
  34. package/dist/icon-set/svg-ionicons-v8.umd.prod.js +1 -1
  35. package/dist/icon-set/svg-line-awesome.umd.prod.js +1 -1
  36. package/dist/icon-set/svg-material-icons-outlined.umd.prod.js +1 -1
  37. package/dist/icon-set/svg-material-icons-round.umd.prod.js +1 -1
  38. package/dist/icon-set/svg-material-icons-sharp.umd.prod.js +1 -1
  39. package/dist/icon-set/svg-material-icons.umd.prod.js +1 -1
  40. package/dist/icon-set/svg-material-symbols-outlined.umd.prod.js +1 -1
  41. package/dist/icon-set/svg-material-symbols-rounded.umd.prod.js +1 -1
  42. package/dist/icon-set/svg-material-symbols-sharp.umd.prod.js +1 -1
  43. package/dist/icon-set/svg-mdi-v6.umd.prod.js +1 -1
  44. package/dist/icon-set/svg-mdi-v7.umd.prod.js +1 -1
  45. package/dist/icon-set/svg-themify.umd.prod.js +1 -1
  46. package/dist/icon-set/themify.umd.prod.js +1 -1
  47. package/dist/lang/ar-TN.umd.prod.js +1 -1
  48. package/dist/lang/ar.umd.prod.js +1 -1
  49. package/dist/lang/az-Latn.umd.prod.js +1 -1
  50. package/dist/lang/bg.umd.prod.js +1 -1
  51. package/dist/lang/bn.umd.prod.js +1 -1
  52. package/dist/lang/bs-BA.umd.prod.js +1 -1
  53. package/dist/lang/ca.umd.prod.js +1 -1
  54. package/dist/lang/cs.umd.prod.js +1 -1
  55. package/dist/lang/da.umd.prod.js +1 -1
  56. package/dist/lang/de-CH.umd.prod.js +1 -1
  57. package/dist/lang/de-DE.umd.prod.js +1 -1
  58. package/dist/lang/de.umd.prod.js +1 -1
  59. package/dist/lang/el.umd.prod.js +1 -1
  60. package/dist/lang/en-GB.umd.prod.js +1 -1
  61. package/dist/lang/en-US.umd.prod.js +1 -1
  62. package/dist/lang/eo.umd.prod.js +1 -1
  63. package/dist/lang/es.umd.prod.js +1 -1
  64. package/dist/lang/et.umd.prod.js +1 -1
  65. package/dist/lang/eu.umd.prod.js +1 -1
  66. package/dist/lang/fa-IR.umd.prod.js +1 -1
  67. package/dist/lang/fa.umd.prod.js +1 -1
  68. package/dist/lang/fi.umd.prod.js +1 -1
  69. package/dist/lang/fr.umd.prod.js +1 -1
  70. package/dist/lang/gn.umd.prod.js +1 -1
  71. package/dist/lang/he.umd.prod.js +1 -1
  72. package/dist/lang/hi.umd.prod.js +1 -1
  73. package/dist/lang/hr.umd.prod.js +1 -1
  74. package/dist/lang/hu.umd.prod.js +1 -1
  75. package/dist/lang/id.umd.prod.js +1 -1
  76. package/dist/lang/is.umd.prod.js +1 -1
  77. package/dist/lang/it.umd.prod.js +1 -1
  78. package/dist/lang/ja.umd.prod.js +1 -1
  79. package/dist/lang/kk.umd.prod.js +1 -1
  80. package/dist/lang/km.umd.prod.js +1 -1
  81. package/dist/lang/ko-KR.umd.prod.js +1 -1
  82. package/dist/lang/kur-CKB.umd.prod.js +1 -1
  83. package/dist/lang/lb.umd.prod.js +1 -1
  84. package/dist/lang/lt.umd.prod.js +1 -1
  85. package/dist/lang/lu.umd.prod.js +1 -1
  86. package/dist/lang/lv.umd.prod.js +1 -1
  87. package/dist/lang/mk.umd.prod.js +1 -1
  88. package/dist/lang/ml.umd.prod.js +1 -1
  89. package/dist/lang/mm.umd.prod.js +1 -1
  90. package/dist/lang/ms-MY.umd.prod.js +1 -1
  91. package/dist/lang/ms.umd.prod.js +1 -1
  92. package/dist/lang/my.umd.prod.js +1 -1
  93. package/dist/lang/nb-NO.umd.prod.js +1 -1
  94. package/dist/lang/nl.umd.prod.js +1 -1
  95. package/dist/lang/pl.umd.prod.js +1 -1
  96. package/dist/lang/pt-BR.umd.prod.js +1 -1
  97. package/dist/lang/pt.umd.prod.js +1 -1
  98. package/dist/lang/ro.umd.prod.js +1 -1
  99. package/dist/lang/ru.umd.prod.js +1 -1
  100. package/dist/lang/sk.umd.prod.js +1 -1
  101. package/dist/lang/sl.umd.prod.js +1 -1
  102. package/dist/lang/sm.umd.prod.js +1 -1
  103. package/dist/lang/sq.umd.prod.js +1 -1
  104. package/dist/lang/sr-CYR.umd.prod.js +1 -1
  105. package/dist/lang/sr.umd.prod.js +1 -1
  106. package/dist/lang/sv.umd.prod.js +1 -1
  107. package/dist/lang/ta.umd.prod.js +1 -1
  108. package/dist/lang/th.umd.prod.js +1 -1
  109. package/dist/lang/tl.umd.prod.js +1 -1
  110. package/dist/lang/tr.umd.prod.js +1 -1
  111. package/dist/lang/ug.umd.prod.js +1 -1
  112. package/dist/lang/uk.umd.prod.js +1 -1
  113. package/dist/lang/ur-PK.umd.prod.js +1 -1
  114. package/dist/lang/uz-Cyrl.umd.prod.js +1 -1
  115. package/dist/lang/uz-Latn.umd.prod.js +1 -1
  116. package/dist/lang/vi.umd.prod.js +1 -1
  117. package/dist/lang/zh-CN.umd.prod.js +1 -1
  118. package/dist/lang/zh-TW.umd.prod.js +1 -1
  119. package/dist/quasar.client.js +572 -187
  120. package/dist/quasar.css +4 -0
  121. package/dist/quasar.prod.css +1 -1
  122. package/dist/quasar.rtl.css +5 -0
  123. package/dist/quasar.rtl.prod.css +1 -1
  124. package/dist/quasar.sass +4 -1
  125. package/dist/quasar.server.prod.cjs +35 -35
  126. package/dist/quasar.server.prod.js +25 -25
  127. package/dist/quasar.umd.js +572 -187
  128. package/dist/quasar.umd.prod.js +30 -30
  129. package/dist/types/composables.d.ts +10 -5
  130. package/dist/types/index.d.ts +4 -4
  131. package/dist/web-types/web-types.json +1 -1
  132. package/package.json +6 -6
  133. package/src/components/carousel/QCarousel.js +2 -0
  134. package/src/components/chip/QChip.js +20 -4
  135. package/src/components/date/QDate.js +9 -2
  136. package/src/components/dialog/QDialog.js +9 -2
  137. package/src/components/editor/QEditor.js +18 -3
  138. package/src/components/expansion-item/QExpansionItem.js +6 -1
  139. package/src/components/input/QInput.js +42 -1
  140. package/src/components/input/QInput.json +7 -0
  141. package/src/components/input/use-mask.js +12 -4
  142. package/src/components/item/QItem.js +10 -1
  143. package/src/components/menu/QMenu.js +9 -2
  144. package/src/components/range/QRange.js +2 -1
  145. package/src/components/rating/QRating.js +27 -8
  146. package/src/components/rating/QRating.sass +4 -0
  147. package/src/components/resize-observer/QResizeObserver.js +14 -3
  148. package/src/components/scroll-area/QScrollArea.js +69 -17
  149. package/src/components/slide-item/QSlideItem.js +3 -2
  150. package/src/components/slider/use-slider.js +7 -3
  151. package/src/components/stepper/StepHeader.js +11 -2
  152. package/src/components/table/QTable.js +11 -9
  153. package/src/components/table/QTh.js +20 -1
  154. package/src/components/table/table-column-selection.js +9 -1
  155. package/src/components/table/table-pagination.js +5 -2
  156. package/src/components/table/table-row-selection.js +10 -10
  157. package/src/components/tabs/QTabs.js +5 -6
  158. package/src/components/time/QTime.js +35 -10
  159. package/src/components/tooltip/QTooltip.js +112 -6
  160. package/src/components/tree/QTree.js +124 -4
  161. package/src/components/uploader/uploader-core.js +19 -4
  162. package/src/components/uploader/xhr-uploader-plugin.js +93 -43
  163. package/src/components/uploader/xhr-uploader-plugin.json +4 -4
  164. package/src/composables/private.use-field/use-field.js +2 -0
  165. package/src/composables/private.use-file/use-file.js +21 -12
  166. package/src/composables/private.use-fullscreen/use-fullscreen.js +8 -3
  167. package/src/composables/private.use-model-toggle/use-model-toggle.js +2 -0
  168. package/src/composables/private.use-panel/use-panel.js +6 -2
  169. package/src/composables/use-interval/use-interval.js +8 -0
  170. package/src/composables/use-render-cache/use-render-cache.js +15 -10
  171. package/src/composables/use-timeout/use-timeout.js +8 -0
  172. package/src/directives/intersection/Intersection.js +2 -2
  173. package/src/directives/touch-repeat/TouchRepeat.js +3 -1
  174. package/src/plugins/cookies/Cookies.js +29 -13
  175. package/src/plugins/meta/Meta.js +57 -9
  176. package/src/utils/extend/extend.js +4 -0
  177. package/src/utils/extend/extend.test.js +29 -0
  178. package/src/utils/scroll/prevent-scroll.js +4 -4
@@ -22,10 +22,15 @@ import useTransition, {
22
22
  } from '../../composables/private.use-transition/use-transition.js'
23
23
  import useTick from '../../composables/use-tick/use-tick.js'
24
24
  import useTimeout from '../../composables/use-timeout/use-timeout.js'
25
+ import useId from '../../composables/use-id/use-id.js'
25
26
 
26
27
  import { createComponent } from '../../utils/private.create/create.js'
27
28
  import { getScrollTarget, scrollTargetProp } from '../../utils/scroll/scroll.js'
28
29
  import { addEvt, cleanEvt, stopAndPrevent } from '../../utils/event/event.js'
30
+ import {
31
+ addEscapeKey,
32
+ removeEscapeKey
33
+ } from '../../utils/private.keyboard/escape-key.js'
29
34
  import { clearSelection } from '../../utils/private.selection/selection.js'
30
35
  import { hSlot } from '../../utils/private.render/render.js'
31
36
  import {
@@ -39,6 +44,8 @@ import {
39
44
  validatePosition
40
45
  } from '../../utils/private.position-engine/position-engine.js'
41
46
 
47
+ let nonSelectableCount = 0
48
+
42
49
  export default createComponent({
43
50
  name: 'QTooltip',
44
51
 
@@ -101,7 +108,11 @@ export default createComponent({
101
108
  emits: [...useModelToggleEmits],
102
109
 
103
110
  setup(props, { slots, emit, attrs }) {
104
- let unwatchPosition, observer
111
+ let unwatchPosition,
112
+ observer,
113
+ removeNonSelectableTimer,
114
+ hasNonSelectable = false,
115
+ describedBy
105
116
 
106
117
  const vm = getCurrentInstance()
107
118
  const {
@@ -110,6 +121,8 @@ export default createComponent({
110
121
 
111
122
  const innerRef = ref(null)
112
123
  const showing = ref(false)
124
+ const targetUid = useId()
125
+ const tooltipId = computed(() => attrs.id || targetUid.value)
113
126
 
114
127
  const anchorOrigin = computed(() =>
115
128
  parsePosition(props.anchor, $q.lang.rtl)
@@ -137,7 +150,7 @@ export default createComponent({
137
150
  processOnMount: true
138
151
  })
139
152
 
140
- Object.assign(anchorEvents, { delayShow, delayHide })
153
+ Object.assign(anchorEvents, { delayShow, delayHide, onFocusin })
141
154
 
142
155
  const { showPortal, hidePortal, renderPortal } = usePortal(
143
156
  vm,
@@ -182,10 +195,26 @@ export default createComponent({
182
195
  onBeforeUnmount(() => {
183
196
  removeClickOutside(clickOutsideProps)
184
197
  })
198
+ } else {
199
+ // dismiss with the ESC key (WCAG 1.4.13) without moving focus;
200
+ // uses the shared escape stack so only the top-most popup reacts
201
+ watch(
202
+ () =>
203
+ // trigger only if it doesn't have external model
204
+ // or else only if the model can be updated (otherwise respect the external model)
205
+ (props.modelValue === null || props['onUpdate:modelValue']) &&
206
+ showing.value === true &&
207
+ props.persistent !== true,
208
+ val => {
209
+ const fn = val === true ? addEscapeKey : removeEscapeKey
210
+ fn(onEscapeKey)
211
+ }
212
+ )
185
213
  }
186
214
 
187
215
  function handleShow(evt) {
188
216
  showPortal()
217
+ addAriaDescription()
189
218
 
190
219
  // should removeTick() if this gets removed
191
220
  registerTick(() => {
@@ -254,7 +283,10 @@ export default createComponent({
254
283
  }
255
284
 
256
285
  unconfigureScrollTarget()
286
+ removeEscapeKey(onEscapeKey)
257
287
  cleanEvt(anchorEvents, 'tooltipTemp')
288
+ removeAriaDescription()
289
+ setNonSelectable(false)
258
290
  }
259
291
 
260
292
  function updatePosition() {
@@ -271,8 +303,13 @@ export default createComponent({
271
303
 
272
304
  function delayShow(evt) {
273
305
  if ($q.platform.is.mobile) {
306
+ if (removeNonSelectableTimer !== void 0) {
307
+ clearTimeout(removeNonSelectableTimer)
308
+ removeNonSelectableTimer = void 0
309
+ }
310
+
274
311
  clearSelection()
275
- document.body.classList.add('non-selectable')
312
+ setNonSelectable(true)
276
313
 
277
314
  const target = anchorEl.value
278
315
  const evts = ['touchmove', 'touchcancel', 'touchend', 'click'].map(
@@ -292,8 +329,9 @@ export default createComponent({
292
329
  cleanEvt(anchorEvents, 'tooltipTemp')
293
330
  clearSelection()
294
331
  // delay needed otherwise selection still occurs
295
- setTimeout(() => {
296
- document.body.classList.remove('non-selectable')
332
+ removeNonSelectableTimer = setTimeout(() => {
333
+ removeNonSelectableTimer = void 0
334
+ setNonSelectable(false)
297
335
  }, 10)
298
336
  }
299
337
 
@@ -303,6 +341,26 @@ export default createComponent({
303
341
  }, props.hideDelay)
304
342
  }
305
343
 
344
+ function onFocusin(evt) {
345
+ const el = evt.target
346
+ if (!el) return
347
+
348
+ // only react to keyboard focus, not to focus coming from a pointer,
349
+ // so the tooltip doesn't pop up when the target is clicked;
350
+ // guard the call for engines that don't support :focus-visible
351
+ try {
352
+ if (el.matches(':focus-visible') === false) return
353
+ } catch {}
354
+
355
+ delayShow(evt)
356
+ }
357
+
358
+ // trigger only if it doesn't have external model
359
+ // or else only if the model can be updated (otherwise respect the external model)
360
+ function onEscapeKey(evt) {
361
+ hide(evt)
362
+ }
363
+
306
364
  function configureAnchorEl() {
307
365
  if (props.noParentEvent || anchorEl.value === null) return
308
366
 
@@ -310,12 +368,59 @@ export default createComponent({
310
368
  ? [[anchorEl.value, 'touchstart', 'delayShow', 'passive']]
311
369
  : [
312
370
  [anchorEl.value, 'mouseenter', 'delayShow', 'passive'],
313
- [anchorEl.value, 'mouseleave', 'delayHide', 'passive']
371
+ [anchorEl.value, 'mouseleave', 'delayHide', 'passive'],
372
+ [anchorEl.value, 'focusin', 'onFocusin', 'passive'],
373
+ [anchorEl.value, 'focusout', 'delayHide', 'passive']
314
374
  ]
315
375
 
316
376
  addEvt(anchorEvents, 'anchor', evts)
317
377
  }
318
378
 
379
+ function setNonSelectable(state) {
380
+ if (hasNonSelectable === state) return
381
+
382
+ hasNonSelectable = state
383
+ nonSelectableCount += state ? 1 : -1
384
+ document.body.classList.toggle('non-selectable', nonSelectableCount > 0)
385
+
386
+ if (!state && removeNonSelectableTimer !== void 0) {
387
+ clearTimeout(removeNonSelectableTimer)
388
+ removeNonSelectableTimer = void 0
389
+ }
390
+ }
391
+
392
+ function addAriaDescription() {
393
+ const el = anchorEl.value,
394
+ id = tooltipId.value
395
+
396
+ if (el === null || id === void 0) return
397
+
398
+ const ids = (el.getAttribute('aria-describedby') || '')
399
+ .split(/\s+/)
400
+ .filter(Boolean)
401
+
402
+ describedBy = { el, id, added: !ids.includes(id) }
403
+
404
+ if (describedBy.added) {
405
+ ids.push(id)
406
+ el.setAttribute('aria-describedby', ids.join(' '))
407
+ }
408
+ }
409
+
410
+ function removeAriaDescription() {
411
+ if (describedBy?.added === true) {
412
+ const { el, id } = describedBy,
413
+ value = (el.getAttribute('aria-describedby') || '')
414
+ .split(/\s+/)
415
+ .filter(entry => entry !== '' && entry !== id)
416
+
417
+ if (value.length === 0) el.removeAttribute('aria-describedby')
418
+ else el.setAttribute('aria-describedby', value.join(' '))
419
+ }
420
+
421
+ describedBy = void 0
422
+ }
423
+
319
424
  function configureScrollTarget() {
320
425
  if (anchorEl.value !== null || props.scrollTarget !== void 0) {
321
426
  localScrollTarget.value = getScrollTarget(
@@ -334,6 +439,7 @@ export default createComponent({
334
439
  'div',
335
440
  {
336
441
  ...attrs,
442
+ id: tooltipId.value,
337
443
  ref: innerRef,
338
444
  class: [
339
445
  'q-tooltip q-tooltip--style q-position-engine no-pointer-events',
@@ -118,11 +118,14 @@ export default createComponent({
118
118
  const lazy = ref({})
119
119
  const innerTicked = ref(props.ticked || [])
120
120
  const innerExpanded = ref(props.expanded || [])
121
+ const focusedKey = ref(null)
121
122
 
122
- let blurTargets = {}
123
+ let blurTargets = {},
124
+ headerTargets = {}
123
125
 
124
126
  onBeforeUpdate(() => {
125
127
  blurTargets = {}
128
+ headerTargets = {}
126
129
  })
127
130
 
128
131
  const classes = computed(
@@ -306,6 +309,38 @@ export default createComponent({
306
309
  return acc
307
310
  })
308
311
 
312
+ const focusableKeys = computed(() => {
313
+ const acc = []
314
+
315
+ function travel(nodes) {
316
+ nodes.forEach(node => {
317
+ const localMeta = meta.value[node[props.nodeKey]]
318
+
319
+ if (props.filter && localMeta.matchesFilter !== true) return
320
+ if (localMeta.link === true) acc.push(localMeta.key)
321
+
322
+ if (
323
+ localMeta.expanded === true &&
324
+ Array.isArray(node[props.childrenKey])
325
+ ) {
326
+ travel(node[props.childrenKey])
327
+ }
328
+ })
329
+ }
330
+
331
+ travel(props.nodes)
332
+ return acc
333
+ })
334
+
335
+ const tabKey = computed(() => {
336
+ const keys = focusableKeys.value
337
+
338
+ if (keys.includes(focusedKey.value)) return focusedKey.value
339
+
340
+ const selected = keys.find(key => meta.value[key].selected === true)
341
+ return selected !== void 0 ? selected : keys[0]
342
+ })
343
+
309
344
  watch(
310
345
  () => props.ticked,
311
346
  val => {
@@ -576,18 +611,36 @@ export default createComponent({
576
611
  : '') +
577
612
  (m.selected === true ? ' q-tree__node--selected' : '') +
578
613
  (m.disabled === true ? ' q-tree__node--disabled' : ''),
579
- tabindex: m.link === true ? 0 : -1,
580
- ariaExpanded: children.length !== 0 ? m.expanded : null,
614
+ ref: el => {
615
+ headerTargets[m.key] = el
616
+ },
617
+ tabindex: m.link === true && m.key === tabKey.value ? 0 : -1,
618
+ 'aria-expanded': isParent
619
+ ? m.expanded
620
+ ? 'true'
621
+ : 'false'
622
+ : null,
623
+ 'aria-selected': m.selectable
624
+ ? m.selected
625
+ ? 'true'
626
+ : 'false'
627
+ : null,
628
+ 'aria-disabled': m.disabled === true ? 'true' : null,
581
629
  role: 'treeitem',
630
+ onFocus() {
631
+ if (m.link === true) focusedKey.value = m.key
632
+ },
582
633
  onClick: e => {
583
634
  onClick(node, m, e)
584
635
  },
585
- onKeypress(e) {
636
+ onKeydown(e) {
586
637
  if (shouldIgnoreKey(e) !== true) {
587
638
  if (e.keyCode === 13) {
588
639
  onClick(node, m, e, true)
589
640
  } else if (e.keyCode === 32) {
590
641
  onExpandClick(node, m, e, true)
642
+ } else if (onNavigationKey(m, e.keyCode)) {
643
+ stopAndPrevent(e)
591
644
  }
592
645
  }
593
646
  }
@@ -722,7 +775,72 @@ export default createComponent({
722
775
  blurTargets[key]?.focus()
723
776
  }
724
777
 
778
+ function focusNode(key) {
779
+ if (key !== void 0) {
780
+ focusedKey.value = key
781
+ headerTargets[key]?.focus()
782
+ }
783
+ }
784
+
785
+ function getFirstFocusableChild(children) {
786
+ for (const child of children) {
787
+ if (props.filter && child.matchesFilter !== true) continue
788
+ if (child.link === true) return child.key
789
+
790
+ if (child.expanded === true) {
791
+ const key = getFirstFocusableChild(child.children)
792
+ if (key !== void 0) return key
793
+ }
794
+ }
795
+ }
796
+
797
+ function onNavigationKey(localMeta, keyCode) {
798
+ const keys = focusableKeys.value,
799
+ index = keys.indexOf(localMeta.key)
800
+
801
+ if (keyCode === 36) {
802
+ focusNode(keys[0])
803
+ return true
804
+ }
805
+ if (keyCode === 35) {
806
+ focusNode(keys.at(-1))
807
+ return true
808
+ }
809
+ if (keyCode === 38) {
810
+ focusNode(keys[index - 1])
811
+ return true
812
+ }
813
+ if (keyCode === 40) {
814
+ focusNode(keys[index + 1])
815
+ return true
816
+ }
817
+ if (keyCode === 39) {
818
+ if (localMeta.isParent !== true && !localMeta.lazy) return true
819
+
820
+ if (localMeta.expanded !== true) {
821
+ setExpanded(localMeta.key, true)
822
+ } else {
823
+ focusNode(getFirstFocusableChild(localMeta.children))
824
+ }
825
+ return true
826
+ }
827
+ if (keyCode === 37) {
828
+ if (localMeta.expanded === true) {
829
+ setExpanded(localMeta.key, false)
830
+ } else {
831
+ let parent = localMeta.parent
832
+ while (parent !== null && parent.link !== true) parent = parent.parent
833
+ focusNode(parent?.key)
834
+ }
835
+ return true
836
+ }
837
+
838
+ return false
839
+ }
840
+
725
841
  function onClick(node, localMeta, e, keyboard) {
842
+ if (localMeta.link === true) focusedKey.value = localMeta.key
843
+
726
844
  if (keyboard !== true && localMeta.selectable !== false) {
727
845
  blur(localMeta.key)
728
846
  }
@@ -749,6 +867,8 @@ export default createComponent({
749
867
  }
750
868
 
751
869
  function onExpandClick(node, localMeta, e, keyboard) {
870
+ if (localMeta.link === true) focusedKey.value = localMeta.key
871
+
752
872
  if (e !== void 0) {
753
873
  stopAndPrevent(e)
754
874
  }
@@ -93,7 +93,11 @@ export function getRenderer(getPlugin, expose) {
93
93
  file.__uploaded = status === 'uploaded' ? file.size : uploadedSize
94
94
 
95
95
  file.__progress =
96
- status === 'uploaded' ? 1 : Math.min(0.9999, file.__uploaded / file.size)
96
+ status === 'uploaded'
97
+ ? 1
98
+ : file.size === 0
99
+ ? 0
100
+ : Math.min(0.9999, file.__uploaded / file.size)
97
101
 
98
102
  file.__progressLabel = getProgressLabel(file.__progress)
99
103
  proxy.$forceUpdate()
@@ -216,7 +220,9 @@ export function getRenderer(getPlugin, expose) {
216
220
 
217
221
  function removeUploadedFiles() {
218
222
  if (!props.disable) {
219
- batchRemoveFiles(['uploaded'], () => {
223
+ batchRemoveFiles(['uploaded'], ({ size }) => {
224
+ state.uploadedSize.value -= size
225
+ uploadSize.value -= size
220
226
  state.uploadedFiles.value = []
221
227
  })
222
228
  }
@@ -258,12 +264,16 @@ export function getRenderer(getPlugin, expose) {
258
264
  function removeFile(file) {
259
265
  if (props.disable) return
260
266
 
267
+ const isUploading = file.__status === 'uploading'
268
+
261
269
  if (file.__status === 'uploaded') {
270
+ state.uploadedSize.value -= file.size
271
+ uploadSize.value -= file.size
262
272
  state.uploadedFiles.value = state.uploadedFiles.value.filter(
263
273
  f => f.__key !== file.__key
264
274
  )
265
- } else if (file.__status === 'uploading') {
266
- file.__abort()
275
+ } else if (isUploading) {
276
+ uploadSize.value -= file.size
267
277
  } else {
268
278
  uploadSize.value -= file.size
269
279
  }
@@ -281,6 +291,11 @@ export function getRenderer(getPlugin, expose) {
281
291
  state.queuedFiles.value = state.queuedFiles.value.filter(
282
292
  f => f.__key !== file.__key
283
293
  )
294
+
295
+ if (isUploading) {
296
+ file.__abort()
297
+ }
298
+
284
299
  emit('removed', [file])
285
300
  }
286
301
 
@@ -31,6 +31,7 @@ function injectPlugin({ props, emit, helpers }) {
31
31
  const xhrs = ref([])
32
32
  const promises = ref([])
33
33
  const workingThreads = ref(0)
34
+ const abortedPromises = new WeakSet()
34
35
 
35
36
  const xhrProps = computed(() => ({
36
37
  url: getFn(props.url),
@@ -46,14 +47,17 @@ function injectPlugin({ props, emit, helpers }) {
46
47
  const isUploading = computed(() => workingThreads.value > 0)
47
48
  const isBusy = computed(() => promises.value.length !== 0)
48
49
 
49
- let abortPromises
50
+ const getLiveFiles = files =>
51
+ files.filter(file => helpers.files.value.includes(file))
50
52
 
51
53
  function abort() {
52
54
  xhrs.value.forEach(x => {
53
55
  x.abort()
54
56
  })
55
57
 
56
- if (promises.value.length !== 0) abortPromises = true
58
+ promises.value.forEach(promise => {
59
+ abortedPromises.add(promise)
60
+ })
57
61
  }
58
62
 
59
63
  function upload() {
@@ -72,54 +76,82 @@ function injectPlugin({ props, emit, helpers }) {
72
76
  function runFactory(files) {
73
77
  workingThreads.value++
74
78
 
79
+ const failed = (err, promise) => {
80
+ if (helpers.isAlive()) {
81
+ if (promise !== void 0) {
82
+ promises.value = promises.value.filter(p => p !== promise)
83
+ }
84
+
85
+ const liveFiles = getLiveFiles(files)
86
+
87
+ if (liveFiles.length !== 0) {
88
+ helpers.queuedFiles.value.push(...liveFiles)
89
+ liveFiles.forEach(f => {
90
+ helpers.updateFileStatus(f, 'failed')
91
+ })
92
+
93
+ emit('factoryFailed', err, liveFiles)
94
+ }
95
+
96
+ workingThreads.value--
97
+ }
98
+ }
99
+
75
100
  if (typeof props.factory !== 'function') {
76
- performUpload(files, {})
101
+ startUpload({})
77
102
  return
78
103
  }
79
104
 
80
- const res = props.factory(files)
105
+ let res
106
+ try {
107
+ res = props.factory(files)
108
+ } catch (err) {
109
+ failed(err)
110
+ return
111
+ }
81
112
 
82
- if (!res) {
83
- emit(
84
- 'factoryFailed',
85
- new Error('QUploader: factory() does not return properly'),
86
- files
87
- )
88
- workingThreads.value--
113
+ if (Object(res) !== res) {
114
+ failed(new Error('QUploader: factory() does not return properly'))
89
115
  } else if (
90
116
  typeof res.catch === 'function' &&
91
117
  typeof res.then === 'function'
92
118
  ) {
93
119
  promises.value.push(res)
94
120
 
95
- const failed = err => {
96
- if (helpers.isAlive()) {
97
- promises.value = promises.value.filter(p => p !== res)
98
-
99
- if (promises.value.length === 0) abortPromises = false
100
-
101
- helpers.queuedFiles.value.push(...files)
102
- files.forEach(f => {
103
- helpers.updateFileStatus(f, 'failed')
104
- })
105
-
106
- emit('factoryFailed', err, files)
107
- workingThreads.value--
108
- }
109
- }
110
-
111
121
  res
112
122
  .then(factory => {
113
- if (abortPromises) {
114
- failed(new Error('Aborted'))
123
+ if (abortedPromises.has(res)) {
124
+ failed(new Error('Aborted'), res)
125
+ } else if (Object(factory) !== factory) {
126
+ failed(
127
+ new Error('QUploader: factory() does not return properly'),
128
+ res
129
+ )
115
130
  } else if (helpers.isAlive()) {
116
131
  promises.value = promises.value.filter(p => p !== res)
117
- performUpload(files, factory)
132
+ startUpload(factory)
118
133
  }
119
134
  })
120
- .catch(failed)
135
+ .catch(err => {
136
+ failed(err, res)
137
+ })
121
138
  } else {
122
- performUpload(files, res || {})
139
+ startUpload(res)
140
+ }
141
+
142
+ function startUpload(factory) {
143
+ const liveFiles = getLiveFiles(files)
144
+
145
+ if (liveFiles.length === 0) {
146
+ workingThreads.value--
147
+ return
148
+ }
149
+
150
+ try {
151
+ performUpload(liveFiles, factory)
152
+ } catch (err) {
153
+ failed(err)
154
+ }
123
155
  }
124
156
  }
125
157
 
@@ -136,6 +168,10 @@ function injectPlugin({ props, emit, helpers }) {
136
168
 
137
169
  if (!url) {
138
170
  console.error('q-uploader: invalid or no URL specified')
171
+ helpers.queuedFiles.value.push(...files)
172
+ files.forEach(f => {
173
+ helpers.updateFileStatus(f, 'failed')
174
+ })
139
175
  workingThreads.value--
140
176
  return
141
177
  }
@@ -166,7 +202,7 @@ function injectPlugin({ props, emit, helpers }) {
166
202
  let size = localUploadedSize - uploadIndexSize
167
203
  for (let i = uploadIndex; size > 0 && i < files.length; i++) {
168
204
  const file = files[i],
169
- uploaded = size > file.size
205
+ uploaded = size >= file.size
170
206
 
171
207
  if (uploaded) {
172
208
  size -= file.size
@@ -186,19 +222,33 @@ function injectPlugin({ props, emit, helpers }) {
186
222
  if (xhr.readyState < 4) return
187
223
 
188
224
  if (xhr.status && xhr.status < 400) {
189
- helpers.uploadedFiles.value.push(...files)
190
- files.forEach(f => {
191
- helpers.updateFileStatus(f, 'uploaded')
192
- })
193
- emit('uploaded', { files, xhr })
225
+ const liveFiles = getLiveFiles(files)
226
+ const liveUploadSize = liveFiles.reduce(
227
+ (total, file) => total + file.size,
228
+ 0
229
+ )
230
+
231
+ helpers.uploadedSize.value += liveUploadSize - localUploadedSize
232
+
233
+ if (liveFiles.length !== 0) {
234
+ helpers.uploadedFiles.value.push(...liveFiles)
235
+ liveFiles.forEach(f => {
236
+ helpers.updateFileStatus(f, 'uploaded')
237
+ })
238
+ emit('uploaded', { files: liveFiles, xhr })
239
+ }
194
240
  } else {
195
241
  aborted = true
196
242
  helpers.uploadedSize.value -= localUploadedSize
197
- helpers.queuedFiles.value.push(...files)
198
- files.forEach(f => {
199
- helpers.updateFileStatus(f, 'failed')
200
- })
201
- emit('failed', { files, xhr })
243
+
244
+ const liveFiles = getLiveFiles(files)
245
+ if (liveFiles.length !== 0) {
246
+ helpers.queuedFiles.value.push(...liveFiles)
247
+ liveFiles.forEach(f => {
248
+ helpers.updateFileStatus(f, 'failed')
249
+ })
250
+ emit('failed', { files: liveFiles, xhr })
251
+ }
202
252
  }
203
253
 
204
254
  workingThreads.value--
@@ -103,7 +103,7 @@
103
103
  }
104
104
  },
105
105
  "returns": {
106
- "type": "String",
106
+ "type": "Array",
107
107
  "desc": "An array consisting of objects with header definitions"
108
108
  },
109
109
  "category": "upload"
@@ -138,8 +138,8 @@
138
138
  }
139
139
  },
140
140
  "returns": {
141
- "type": "String",
142
- "desc": "An array consists of objects with additional fields definitions (used by Form to be uploaded)"
141
+ "type": "Array",
142
+ "desc": "An array consisting of objects with additional field definitions (used by FormData to be uploaded)"
143
143
  },
144
144
  "category": "upload"
145
145
  },
@@ -264,7 +264,7 @@
264
264
  },
265
265
 
266
266
  "factory-failed": {
267
- "desc": "Emitted when factory function is supplied with a Promise which is rejected",
267
+ "desc": "Emitted when the factory function throws, returns an invalid value, or supplies a Promise which is rejected or aborted",
268
268
  "params": {
269
269
  "err": {
270
270
  "type": "Error",
@@ -388,6 +388,8 @@ export default function useField(state) {
388
388
  state.inputRef.value.value = null
389
389
  }
390
390
 
391
+ state.onClear?.()
392
+
391
393
  emit('update:modelValue', null)
392
394
  if (state.changeEvent) emit('change', null)
393
395
  emit('clear', props.modelValue)