apostrophe 4.30.1-beta.1 → 4.31.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 (129) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CHANGELOG.md +22 -2
  3. package/claude-tools/detect-handles.js +46 -0
  4. package/claude-tools/minimal-hang-test.js +28 -0
  5. package/claude-tools/mongo-close-test.js +11 -0
  6. package/claude-tools/stdin-ref-test.js +14 -0
  7. package/eslint.config.js +3 -1
  8. package/modules/@apostrophecms/area/index.js +94 -2
  9. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -40
  10. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +0 -1
  11. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +0 -1
  12. package/modules/@apostrophecms/attachment/index.js +4 -1
  13. package/modules/@apostrophecms/db/index.js +68 -27
  14. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +5 -3
  15. package/modules/@apostrophecms/express/index.js +2 -0
  16. package/modules/@apostrophecms/file/index.js +9 -8
  17. package/modules/@apostrophecms/http/index.js +1 -1
  18. package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
  19. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +2 -2
  20. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +3 -0
  21. package/modules/@apostrophecms/job/index.js +9 -7
  22. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +0 -1
  23. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -1
  24. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +10 -2
  25. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +3 -3
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +52 -23
  27. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +6 -1
  28. package/modules/@apostrophecms/oembed/index.js +2 -1
  29. package/modules/@apostrophecms/piece-type/index.js +2 -1
  30. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +7 -2
  31. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +1 -0
  32. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +21 -4
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +7 -2
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +1 -0
  36. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
  37. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +1 -0
  38. package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +10 -0
  39. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +1 -0
  40. package/modules/@apostrophecms/template/index.js +117 -11
  41. package/modules/@apostrophecms/template/lib/jsxLoader.js +128 -0
  42. package/modules/@apostrophecms/template/lib/jsxRender.js +490 -0
  43. package/modules/@apostrophecms/template/lib/jsxRuntime.js +276 -0
  44. package/modules/@apostrophecms/template/lib/nunjucksLoader.js +11 -36
  45. package/modules/@apostrophecms/template/lib/viewWatcher.js +113 -0
  46. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  47. package/modules/@apostrophecms/ui/ui/apos/components/AposCellLastEdited.vue +1 -1
  48. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +1 -0
  49. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +10 -4
  50. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +6 -1
  51. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +1 -1
  52. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +1 -1
  53. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -0
  54. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
  55. package/modules/@apostrophecms/uploadfs/index.js +3 -0
  56. package/modules/@apostrophecms/util/index.js +20 -3
  57. package/package.json +14 -10
  58. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  59. package/test/add-missing-schema-fields-project/test.js +22 -3
  60. package/test/assets.js +110 -67
  61. package/test/db-tools.js +365 -0
  62. package/test/db.js +24 -15
  63. package/test/default-adapter.js +256 -0
  64. package/test/external-front.js +419 -1
  65. package/test/files.js +28 -0
  66. package/test/job.js +1 -1
  67. package/test/modules/jsx-area-test/index.js +23 -0
  68. package/test/modules/jsx-area-test/views/bad-area.jsx +7 -0
  69. package/test/modules/jsx-area-test/views/with-area-ctx.jsx +13 -0
  70. package/test/modules/jsx-area-test/views/with-area.jsx +7 -0
  71. package/test/modules/jsx-area-test/views/with-widget-ctx.jsx +12 -0
  72. package/test/modules/jsx-area-test/views/with-widget.jsx +7 -0
  73. package/test/modules/jsx-async-widget/index.js +6 -0
  74. package/test/modules/jsx-async-widget/views/widget.jsx +11 -0
  75. package/test/modules/jsx-bridge-test/index.js +1 -0
  76. package/test/modules/jsx-bridge-test/views/cross-module.jsx +7 -0
  77. package/test/modules/jsx-bridge-test/views/disambig-name-only.jsx +7 -0
  78. package/test/modules/jsx-bridge-test/views/disambig-target.jsx +8 -0
  79. package/test/modules/jsx-bridge-test/views/disambig-with-template-name.jsx +7 -0
  80. package/test/modules/jsx-bridge-test/views/include-html.jsx +7 -0
  81. package/test/modules/jsx-bridge-test/views/include-target.html +4 -0
  82. package/test/modules/jsx-bridge-test/views/jsx-extends-via-extend.jsx +9 -0
  83. package/test/modules/jsx-bridge-test/views/jsx-extends.jsx +9 -0
  84. package/test/modules/jsx-bridge-test/views/jsx-layout.jsx +14 -0
  85. package/test/modules/jsx-bridge-test/views/njk-extends.jsx +14 -0
  86. package/test/modules/jsx-bridge-test/views/njk-layout.html +9 -0
  87. package/test/modules/jsx-bridge-test/views/short-form.jsx +7 -0
  88. package/test/modules/jsx-bridge-test/views/short-target.jsx +3 -0
  89. package/test/modules/jsx-component-test/index.js +15 -0
  90. package/test/modules/jsx-component-test/views/greet.html +1 -0
  91. package/test/modules/jsx-component-test/views/uses-component.jsx +8 -0
  92. package/test/modules/jsx-ctx-widget/index.js +6 -0
  93. package/test/modules/jsx-ctx-widget/views/widget.jsx +4 -0
  94. package/test/modules/jsx-mixed-test/index.js +9 -0
  95. package/test/modules/jsx-mixed-test/views/apos-full.jsx +21 -0
  96. package/test/modules/jsx-mixed-test/views/async-list.jsx +12 -0
  97. package/test/modules/jsx-mixed-test/views/lib/format.js +3 -0
  98. package/test/modules/jsx-mixed-test/views/localized.jsx +3 -0
  99. package/test/modules/jsx-mixed-test/views/partial.jsx +3 -0
  100. package/test/modules/jsx-mixed-test/views/safe-helper.jsx +3 -0
  101. package/test/modules/jsx-mixed-test/views/syntax-error.jsx +3 -0
  102. package/test/modules/jsx-mixed-test/views/throws.jsx +5 -0
  103. package/test/modules/jsx-mixed-test/views/uses-import.jsx +5 -0
  104. package/test/modules/jsx-mixed-test/views/uses-require.jsx +5 -0
  105. package/test/modules/jsx-watcher-cross-test/index.js +5 -0
  106. package/test/modules/jsx-watcher-cross-test/views/cross-template.jsx +3 -0
  107. package/test/modules/jsx-watcher-test/index.js +5 -0
  108. package/test/modules/jsx-watcher-test/views/watcher-test.jsx +3 -0
  109. package/test/modules/template-jsx-options-test/index.js +12 -0
  110. package/test/modules/template-jsx-options-test/views/options-test.jsx +9 -0
  111. package/test/modules/template-jsx-subclass-test/index.js +3 -0
  112. package/test/modules/template-jsx-subclass-test/views/override-test.jsx +3 -0
  113. package/test/modules/template-jsx-test/index.js +9 -0
  114. package/test/modules/template-jsx-test/views/boolean-attrs.jsx +11 -0
  115. package/test/modules/template-jsx-test/views/class-and-for.jsx +7 -0
  116. package/test/modules/template-jsx-test/views/dangerously-set.jsx +3 -0
  117. package/test/modules/template-jsx-test/views/escape-attr.jsx +3 -0
  118. package/test/modules/template-jsx-test/views/escape-body.jsx +3 -0
  119. package/test/modules/template-jsx-test/views/inherit-test.jsx +3 -0
  120. package/test/modules/template-jsx-test/views/list.jsx +7 -0
  121. package/test/modules/template-jsx-test/views/override-test.jsx +3 -0
  122. package/test/modules/template-jsx-test/views/svg-attrs.jsx +27 -0
  123. package/test/modules/template-jsx-test/views/test.jsx +3 -0
  124. package/test/modules/template-jsx-test/views/void-elements.jsx +9 -0
  125. package/test/templates-jsx-watcher.js +135 -0
  126. package/test/templates-jsx.js +537 -0
  127. package/test/utils.js +103 -0
  128. package/test-lib/util.js +50 -14
  129. package/lib/mongodb-connect.js +0 -62
@@ -244,7 +244,9 @@ module.exports = {
244
244
  },
245
245
  setTotal (n) {
246
246
  total = n;
247
- return self.setTotal(job, n);
247
+ const result = self.setTotal(job, n);
248
+ promises.push(result);
249
+ return result;
248
250
  },
249
251
  setResults (_results) {
250
252
  results = _results;
@@ -412,12 +414,12 @@ module.exports = {
412
414
  //
413
415
  // No promise is returned as this method just updates
414
416
  // the job tracking information in the background.
415
- setTotal(job, total) {
416
- self.db.updateOne({ _id: job._id }, { $set: { total } }, function (err) {
417
- if (err) {
418
- self.apos.util.error(err);
419
- }
420
- });
417
+ async setTotal(job, total) {
418
+ try {
419
+ await self.db.updateOne({ _id: job._id }, { $set: { total } });
420
+ } catch (err) {
421
+ self.apos.util.error(err);
422
+ }
421
423
  },
422
424
  // Mark the given job as ended. If `success`
423
425
  // is true the job is reported as an overall
@@ -6,7 +6,6 @@
6
6
  :style="itemStyles"
7
7
  :class="widgetStyles.classes"
8
8
  class="apos-layout__item"
9
- role="gridcell"
10
9
  data-apos-test="aposLayoutItem"
11
10
  :data-id="item._id"
12
11
  :data-tablet-full="props.tabletFullItems[item._id] || false"
@@ -14,7 +14,6 @@
14
14
  :key="item._id"
15
15
  ref="items"
16
16
  class="apos-layout__item"
17
- role="gridcell"
18
17
  data-apos-test="aposManageLayoutItem"
19
18
  :data-id="item._id"
20
19
  :style="{
@@ -10,6 +10,7 @@
10
10
  <div
11
11
  v-if="showNav"
12
12
  class="apos-login__nav"
13
+ role="navigation"
13
14
  >
14
15
  <a
15
16
  href="#"
@@ -22,7 +23,13 @@
22
23
  >{{ $t('apostrophe:loginHome') }}</a>
23
24
  </div>
24
25
  </transition>
25
- <div class="apos-login__wrapper">
26
+ <div
27
+ class="apos-login__wrapper"
28
+ role="main"
29
+ >
30
+ <h1 class="apos-sr-only">
31
+ {{ $t('apostrophe:login') }}
32
+ </h1>
26
33
  <transition
27
34
  name="fade-body"
28
35
  mode="out-in"
@@ -55,6 +62,7 @@
55
62
  <div
56
63
  v-show="loaded"
57
64
  class="apos-login__footer"
65
+ role="contentinfo"
58
66
  >
59
67
  <AposLogo class="apos-login__logo" />
60
68
  <label
@@ -234,7 +242,7 @@ export default {
234
242
  overflow: hidden;
235
243
  margin-right: 0;
236
244
  margin-left: auto;
237
- color: var(--a-base-5);
245
+ color: var(--a-base-2);
238
246
  text-overflow: clip;
239
247
  white-space: nowrap;
240
248
  }
@@ -112,16 +112,16 @@ export default {
112
112
  text-transform: capitalize;
113
113
  padding: 6px 12px;
114
114
  color: var(--a-white);
115
- background: var(--a-success);
115
+ background: var(--a-success-dark);
116
116
  border-radius: 5px;
117
117
  }
118
118
 
119
119
  &--development {
120
- background: var(--a-danger);
120
+ background: var(--a-danger-button-hover);
121
121
  }
122
122
 
123
123
  &--success, &--staging {
124
- background: var(--a-warning);
124
+ background: var(--a-warning-dark);
125
125
  }
126
126
  }
127
127
 
@@ -16,7 +16,7 @@
16
16
  :data-apos-graph-key="props.graphKey || undefined"
17
17
  tabindex="0"
18
18
  @focus.capture="captureFocus"
19
- @keyup.tab="onKeyup"
19
+ @keydown.tab="onKeydownTab"
20
20
  @keyup.esc="onKeyup"
21
21
  >
22
22
  <transition :name="transitionType">
@@ -67,6 +67,7 @@
67
67
  <header
68
68
  v-if="!modal.disableHeader"
69
69
  class="apos-modal__header"
70
+ role="none"
70
71
  >
71
72
  <div class="apos-modal__header__main">
72
73
  <div
@@ -133,6 +134,7 @@
133
134
  <footer
134
135
  v-if="hasSlot('footer')"
135
136
  class="apos-modal__footer"
137
+ role="none"
136
138
  >
137
139
  <div class="apos-modal__footer__inner">
138
140
  <slot name="footer" />
@@ -216,6 +218,22 @@ const nonDraggableElements = [
216
218
  '.apos-input-array-inline-table'
217
219
  ];
218
220
 
221
+ // Selector for focusable elements inside the modal. Used both at trap setup
222
+ // and on every Tab keydown to refresh the cycle list, so that elements that
223
+ // became disabled/hidden (e.g. Save when validation fails) or newly visible
224
+ // are reflected.
225
+ const focusableSelector = [
226
+ '[tabindex]',
227
+ '[href]',
228
+ 'input',
229
+ 'select',
230
+ 'textarea',
231
+ 'button',
232
+ '[data-apos-focus-priority]'
233
+ ]
234
+ .map(s => `${s}:not([tabindex="-1"]):not([disabled]):not([type="hidden"]):not([aria-hidden]):not(.apos-sr-only)`)
235
+ .join(', ');
236
+
219
237
  const resizeSides = [
220
238
  {
221
239
  edge: 'top',
@@ -476,13 +494,35 @@ onUnmounted(() => {
476
494
  }
477
495
  });
478
496
 
479
- function onKeyup(event) {
497
+ // Handle Tab on keydown — before the browser moves focus.
498
+ // Handling Tab on keyup is too late: the browser has already moved focus,
499
+ // so the cycling logic sees the wrong activeElement.
500
+ //
501
+ // We also recompute the focusable list here on every Tab, scoped to
502
+ // modalEl, instead of relying on the snapshot taken at trapFocus() time.
503
+ function onKeydownTab(event) {
504
+ if (!shouldTrapFocus.value) {
505
+ return;
506
+ }
480
507
  if (!store.isOnTop(modalEl.value)) {
481
508
  return;
482
509
  }
510
+ if (event.target?.nodeName?.toLowerCase() === 'textarea') {
511
+ return;
512
+ }
513
+ // Skip visually hidden elements when cycling — but only here, not in
514
+ // trapFocus. Initial focus runs while `renderingElements` is still true,
515
+ // which puts the content under display:none and makes every candidate's
516
+ // offsetParent null.
517
+ const elements = getFocusableElements(modalEl.value)
518
+ .filter(el => el.offsetParent !== null);
519
+ // Keep the store snapshot consistent for other consumers.
520
+ store.updateModalData(props.modalData.id, { elementsToFocus: elements });
521
+ cycleElementsToFocus(event, elements);
522
+ }
483
523
 
484
- if (event.key === 'Tab') {
485
- cycleElementsToFocus(event, props.modalData.elementsToFocus);
524
+ function onKeyup(event) {
525
+ if (!store.isOnTop(modalEl.value)) {
486
526
  return;
487
527
  }
488
528
 
@@ -517,23 +557,16 @@ function captureFocus(e) {
517
557
  store.updateModalData(props.modalData.id, { focusedElement: e.target });
518
558
  }
519
559
 
560
+ function getFocusableElements(rootEl) {
561
+ if (!rootEl) {
562
+ return [];
563
+ }
564
+ return [ ...rootEl.querySelectorAll(focusableSelector) ];
565
+ }
566
+
520
567
  async function trapFocus() {
521
568
  if (modalEl?.value) {
522
- const elementSelectors = [
523
- '[tabindex]',
524
- '[href]',
525
- 'input',
526
- 'select',
527
- 'textarea',
528
- 'button',
529
- '[data-apos-focus-priority]'
530
- ];
531
-
532
- const selector = elementSelectors
533
- .map(addExcludingAttributes)
534
- .join(', ');
535
-
536
- const elementsToFocus = [ ...modalEl.value.querySelectorAll(selector) ];
569
+ const elementsToFocus = getFocusableElements(modalEl.value);
537
570
 
538
571
  store.updateModalData(props.modalData.id, { elementsToFocus });
539
572
 
@@ -556,10 +589,6 @@ async function trapFocus() {
556
589
  await nextTick();
557
590
  focusElement(props.modalData.focusedElement, firstElementToFocus);
558
591
  }
559
-
560
- function addExcludingAttributes(element) {
561
- return `${element}:not([tabindex="-1"]):not([disabled]):not([type="hidden"]):not([aria-hidden]):not(.apos-sr-only)`;
562
- }
563
592
  }
564
593
 
565
594
  function close() {
@@ -1,15 +1,20 @@
1
1
  <template>
2
2
  <div class="apos-modal-tabs">
3
- <ul class="apos-modal-tabs__tabs">
3
+ <ul
4
+ class="apos-modal-tabs__tabs"
5
+ role="tablist"
6
+ >
4
7
  <li
5
8
  v-for="tab in tabs"
6
9
  v-show="tab.isVisible !== false"
7
10
  :key="tab.name"
8
11
  class="apos-modal-tabs__tab"
12
+ role="presentation"
9
13
  >
10
14
  <button
11
15
  :id="tab.name"
12
16
  class="apos-modal-tabs__btn"
17
+ role="tab"
13
18
  :aria-selected="tab.name === current ? true : false"
14
19
  @click="selectTab"
15
20
  >
@@ -54,7 +54,8 @@ module.exports = {
54
54
 
55
55
  self.oembetter.allowlist(minimumAllowlist.concat(self.options.allowlist || []));
56
56
 
57
- const minimumEndpoints = self.options.minimumEndpoints || self.oembetter.suggestedEndpoints;
57
+ const minimumEndpoints = self.options.minimumEndpoints ||
58
+ self.oembetter.suggestedEndpoints;
58
59
  self.oembetter.endpoints(
59
60
  minimumEndpoints.concat(self.options.endpoints || [])
60
61
  );
@@ -73,7 +73,8 @@ module.exports = {
73
73
  },
74
74
  labels: {
75
75
  name: 'labels',
76
- label: '',
76
+ label: 'apostrophe:status',
77
+ labelSrOnly: true,
77
78
  component: 'AposCellLabels'
78
79
  },
79
80
  updatedAt: {
@@ -4,8 +4,11 @@
4
4
  <tr>
5
5
  <th
6
6
  v-if="hasBatchOperations"
7
+ scope="col"
7
8
  class="apos-table__header"
8
- />
9
+ >
10
+ <span class="apos-sr-only">{{ $t('apostrophe:selectAll') }}</span>
11
+ </th>
9
12
  <th
10
13
  v-for="header in headers"
11
14
  :key="header.label"
@@ -16,6 +19,7 @@
16
19
  <component
17
20
  :is="getEl(header)"
18
21
  class="apos-table__header-label"
22
+ :class="{ 'apos-sr-only': header.labelSrOnly }"
19
23
  >
20
24
  <component
21
25
  :is="icons[header.labelIcon]"
@@ -28,11 +32,12 @@
28
32
  </th>
29
33
  <th
30
34
  key="contextMenu"
35
+ scope="col"
31
36
  class="apos-table__header"
32
37
  >
33
38
  <component
34
39
  :is="getEl({})"
35
- class="apos-table__header-label apos-is-hidden"
40
+ class="apos-table__header-label apos-sr-only"
36
41
  >
37
42
  {{ $t('apostrophe:moreOperations') }}
38
43
  </component>
@@ -15,6 +15,7 @@
15
15
  :href="item._url"
16
16
  class="apos-recently-edited__cell-link"
17
17
  data-apos-test="recently-edited-cell-link"
18
+ :aria-label="$t('apostrophe:preview')"
18
19
  @click.stop
19
20
  >
20
21
  <AposIndicator
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <div
3
3
  :id="widgetId"
4
- :aria-controls="`insert-menu-${modelValue._id}`"
5
4
  :style="widgetStyles.inline"
6
5
  :class="widgetStyles.classes"
7
6
  @keyup="handleUIKeyup"
@@ -43,7 +42,12 @@
43
42
  ref="insertMenu"
44
43
  plugin-key="insertMenu"
45
44
  :class="insertMenuClasses"
46
- :tippy-options="{ duration: 100, zIndex: 999, placement: 'bottom-start' }"
45
+ :tippy-options="{
46
+ duration: 100,
47
+ zIndex: 999,
48
+ placement: 'bottom-start',
49
+ aria: { content: null, expanded: false }
50
+ }"
47
51
  :should-show="showFloatingMenu"
48
52
  :editor="editor"
49
53
  role="listbox"
@@ -229,7 +233,11 @@ export default {
229
233
  inertia: true,
230
234
  placement: 'bottom',
231
235
  hideOnClick: false,
232
- onHide: this.onBubbleHide
236
+ onHide: this.onBubbleHide,
237
+ aria: {
238
+ content: null,
239
+ expanded: false
240
+ }
233
241
  };
234
242
  },
235
243
  // Note that context menu class-list expects a string
@@ -264,7 +272,11 @@ export default {
264
272
  placement: 'top',
265
273
  offset: [ 0, 35 ],
266
274
  moveTransition: 'transform 0s ease-out',
267
- appendTo: document.body
275
+ appendTo: document.body,
276
+ aria: {
277
+ content: null,
278
+ expanded: false
279
+ }
268
280
  };
269
281
  },
270
282
  moduleOptions() {
@@ -468,6 +480,11 @@ export default {
468
480
  autofocus: this.autofocus,
469
481
  onUpdate: this.editorUpdate,
470
482
  extensions,
483
+ editorProps: {
484
+ attributes: {
485
+ 'aria-label': this.$t('apostrophe:richTextEditor')
486
+ }
487
+ },
471
488
 
472
489
  // The following events are triggered:
473
490
  // - before the placeholder configuration function, when loading the page
@@ -58,6 +58,7 @@
58
58
  class="apos-modal-array-items__items"
59
59
  :selected="currentId"
60
60
  :model-value="withLabels(next)"
61
+ :draggable="field.draggable !== false"
61
62
  @update:model-value="update"
62
63
  @select="select"
63
64
  />
@@ -15,13 +15,17 @@
15
15
  @toggle="toggle"
16
16
  />
17
17
  <input
18
+ :id="uid"
18
19
  v-model="date"
19
20
  class="apos-input apos-input--date"
20
21
  :class="classes"
21
22
  type="date"
22
23
  @change="setDateAndTime"
23
24
  >
24
- <span class="apos-input--label">
25
+ <span
26
+ :id="`${uid}-at`"
27
+ class="apos-input--label"
28
+ >
25
29
  {{ $t('apostrophe:at') }}
26
30
  </span>
27
31
  <input
@@ -29,6 +33,7 @@
29
33
  class="apos-input apos-input--time"
30
34
  :class="classes"
31
35
  type="time"
36
+ :aria-labelledby="`${uid} ${uid}-at`"
32
37
  @change="setDateAndTime"
33
38
  >
34
39
  </div>
@@ -60,7 +65,7 @@ export default {
60
65
  &--disabled {
61
66
  background-color: var(--a-white);
62
67
  border-color: var(--a-base-8);
63
- color: var(--a-base-4);
68
+ color: var(--a-base-2);
64
69
  }
65
70
 
66
71
  &--label {
@@ -8,6 +8,7 @@
8
8
  >
9
9
  <template #body>
10
10
  <AposSelect
11
+ :uid="uid"
11
12
  :icon="icon"
12
13
  :choices="choices"
13
14
  :classes="classes"
@@ -211,7 +211,7 @@ export default {
211
211
  & {
212
212
  margin: 0 0 $spacing-base;
213
213
  line-height: var(--a-line-tall);
214
- color: var(--a-base-3);
214
+ color: var(--a-base-2);
215
215
  }
216
216
  }
217
217
 
@@ -74,6 +74,7 @@
74
74
  </div>
75
75
  <button
76
76
  class="apos-subform__preview-trigger"
77
+ :aria-label="editSubformLabel"
77
78
  @click="toggleExpanded"
78
79
  @mouseenter="triggerHover = true"
79
80
  @mouseleave="triggerHover = false"
@@ -67,6 +67,16 @@ export default {
67
67
  },
68
68
  serverError() {
69
69
  return this.error || !!this.serverErrors;
70
+ },
71
+ subformLabel() {
72
+ return this.$t(
73
+ this.subform.label ||
74
+ this.subform.schema?.[0]?.label ||
75
+ 'apostrophe:notAvailable'
76
+ );
77
+ },
78
+ editSubformLabel() {
79
+ return this.$t('apostrophe:editSubform', { label: this.subformLabel });
70
80
  }
71
81
  },
72
82
 
@@ -19,6 +19,7 @@
19
19
  v-if="currentPath.length"
20
20
  key="btn"
21
21
  class="apos-styles__header-navigate-btn"
22
+ :aria-label="$t('apostrophe:back')"
22
23
  @mousedown.stop=""
23
24
  @click="navigateLeft"
24
25
  >