apostrophe 4.5.3 → 4.6.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 (55) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/lib/mongodb-connect.js +9 -2
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +6 -1
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +18 -180
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +6 -2
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -1
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +11 -0
  8. package/modules/@apostrophecms/any-page-type/index.js +6 -1
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenu.vue +6 -1
  11. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +63 -11
  12. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +12 -15
  13. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +10 -1
  14. package/modules/@apostrophecms/db/index.js +2 -3
  15. package/modules/@apostrophecms/doc-type/index.js +7 -1
  16. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +183 -109
  17. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocLocalePicker.vue +177 -0
  18. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +4 -0
  19. package/modules/@apostrophecms/i18n/i18n/de.json +1 -0
  20. package/modules/@apostrophecms/i18n/i18n/en.json +7 -1
  21. package/modules/@apostrophecms/i18n/i18n/es.json +1 -0
  22. package/modules/@apostrophecms/i18n/i18n/fr.json +1 -0
  23. package/modules/@apostrophecms/i18n/i18n/it.json +1 -0
  24. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +1 -0
  25. package/modules/@apostrophecms/i18n/i18n/sk.json +1 -0
  26. package/modules/@apostrophecms/i18n/index.js +22 -0
  27. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +1 -0
  28. package/modules/@apostrophecms/i18n/ui/src/index.js +3 -0
  29. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +1 -0
  30. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +22 -30
  31. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +24 -1
  32. package/modules/@apostrophecms/modal/ui/apos/components/TheAposModals.vue +48 -38
  33. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +7 -0
  34. package/modules/@apostrophecms/page/index.js +5 -3
  35. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +1 -0
  36. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +17 -3
  37. package/modules/@apostrophecms/piece-type/index.js +5 -3
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +25 -4
  39. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +1 -1
  40. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +4 -3
  41. package/modules/@apostrophecms/search/index.js +36 -2
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +17 -10
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +36 -23
  44. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +1 -1
  45. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +1 -0
  46. package/modules/@apostrophecms/ui/ui/apos/components/AposLocale.vue +36 -0
  47. package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +283 -0
  48. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +56 -18
  49. package/modules/@apostrophecms/ui/ui/apos/lib/tooltip.js +2 -1
  50. package/modules/@apostrophecms/ui/ui/apos/mixins/AposArchiveMixin.js +4 -2
  51. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +12 -6
  52. package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +10 -1
  53. package/modules/@apostrophecms/util/ui/src/http.js +12 -5
  54. package/package.json +4 -4
  55. package/test/search.js +64 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.6.0 (2024-08-08)
4
+
5
+ ### Adds
6
+
7
+ * Add a locale switcher in pieces and pages editor modals. This is available for localized documents only, and allows you to switch between locales for the same document.
8
+ The locale can be switche at only one level, meaning that sub documents of a document that already switched locale will not be able to switch locale itself.
9
+ * Adds visual focus states and keyboard handlers for engaging with areas and widgets in-context
10
+
11
+ ### Changes
12
+
13
+ * Add `title` and `_url` to select all projection.
14
+ * Display `Select all` message on all pages in the manager modal.
15
+ * Refresh `checked` in manager modal after archive action.
16
+ * Update `@apostrophecms/emulate-mongo-3-driver` dependency to keep supporting `mongodb@3.x` queries while using `mongodb@6.x`.
17
+ * Updates rich text link tool's keyboard key detection strategy.
18
+ * Buttons that appear on slats (preview, edit crop/relationship, remove) are visually focusable and keyboard accessible.
19
+ * Added tooltip for update button. Thanks to [gkumar9891](https://github.com/gkumar9891) for this addition.
20
+
21
+ ### Fixes
22
+
23
+ * Fixes the rich text link tool's detection and display of the Remove Link button for removing existing links
24
+ * Fixes the rich text link tool's detection and display of Apostrophe Page relationship field.
25
+ * Overriding standard Vue.js components with `editorModal` and `managerModal` are now applied all the time.
26
+ * Accommodate old-style replica set URIs with comma-separated servers by passing any MongoDB URIs that Node.js cannot parse directly to the MongoDB driver, and avoiding unnecessary parsing of the URI in general.
27
+ * Bump `oembetter` dependency to guarantee compatibility with YouTube. YouTube recently deployed broken `link rel="undefined"` tags on some of their video pages.
28
+ * It is now possible to see the right filename and line number when debugging the admin UI build in the browser. This is automatically disabled when `@apostrophecms/security-headers` is installed, because its defaults are incompatible by design.
29
+
30
+ ## 4.5.4 (2024-07-22)
31
+
32
+ ### Fixes
33
+
34
+ * Add a default projection to ancestors of search results in order to load a reasonable amount of data and avoid request timeouts.
35
+
3
36
  ## 4.5.3 (2024-07-17)
4
37
 
5
38
  ### Fixes
@@ -16,8 +16,15 @@ module.exports = async (uri, options) => {
16
16
  useNewUrlParser: true,
17
17
  ...options
18
18
  };
19
- const parsed = new URL(uri);
20
- if ((parsed.protocol !== 'mongodb:') || (parsed.hostname !== 'localhost')) {
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(uri);
22
+ } catch (e) {
23
+ // Parse failed, e.g. old school replica set URI
24
+ // with commas, just let the mongo driver handle it
25
+ return mongo.MongoClient.connect(uri, connectOptions);
26
+ }
27
+ if (!parsed || (parsed.protocol !== 'mongodb:') || (parsed.hostname !== 'localhost')) {
21
28
  return mongo.MongoClient.connect(parsed.toString(), connectOptions);
22
29
  }
23
30
  const records = await dns.promises.lookup('localhost', { all: true });
@@ -5,7 +5,12 @@
5
5
  :class="themeClass"
6
6
  >
7
7
  <div ref="spacer" class="apos-admin-bar-spacer" />
8
- <nav ref="adminBar" class="apos-admin-bar">
8
+ <nav
9
+ ref="adminBar"
10
+ class="apos-admin-bar"
11
+ role="menubar"
12
+ aria-label="Apostrophe Admin Bar"
13
+ >
9
14
  <div class="apos-admin-bar__row">
10
15
  <AposLogoPadless class="apos-admin-bar__logo" />
11
16
  <TheAposAdminBarMenu :items="menuItems" />
@@ -2,63 +2,19 @@
2
2
  <AposContextMenu
3
3
  ref="menu"
4
4
  class="apos-admin-locales"
5
+ identifier="localePickerTrigger"
5
6
  :button="button"
6
7
  :unpadded="true"
7
8
  menu-placement="bottom-end"
8
9
  @open="open"
10
+ @close="isOpen = false"
9
11
  >
10
- <div class="apos-locales-picker">
11
- <div class="apos-input-wrapper">
12
- <input
13
- v-model="search"
14
- type="text"
15
- class="apos-locales-filter"
16
- :placeholder="$t('apostrophe:searchLocalesPlaceholder')"
17
- >
18
- </div>
19
- <ul class="apos-locales">
20
- <li
21
- v-for="locale in filteredLocales"
22
- :key="locale.name"
23
- class="apos-locale-item"
24
- :class="localeClasses(locale)"
25
- @click="switchLocale(locale)"
26
- >
27
- <span class="apos-locale">
28
- <AposIndicator
29
- v-if="isActive(locale)"
30
- icon="check-bold-icon"
31
- fill-color="var(--a-primary)"
32
- class="apos-check"
33
- :icon-size="12"
34
- :title="$t('apostrophe:currentLocale')"
35
- />
36
- {{ locale.label }}
37
- <span class="apos-locale-name">
38
- ({{ locale.name }})
39
- </span>
40
- <span
41
- class="apos-locale-localized"
42
- :class="{ 'apos-state-is-localized': isLocalized(locale) }"
43
- />
44
- </span>
45
- </li>
46
- </ul>
47
- <div class="apos-available-locales">
48
- <p class="apos-available-description">
49
- {{ $t('apostrophe:documentExistsInLocales') }}
50
- </p>
51
- <AposButton
52
- v-for="locale in availableLocales"
53
- :key="locale.name"
54
- class="apos-available-locale"
55
- :label="locale.label"
56
- type="quiet"
57
- :modifiers="['no-motion']"
58
- @click="switchLocale(locale)"
59
- />
60
- </div>
61
- </div>
12
+ <AposLocalePicker
13
+ :current-locale="locale"
14
+ :localized="localized"
15
+ :is-open="isOpen"
16
+ @switch-locale="switchLocale"
17
+ />
62
18
  </AposContextMenu>
63
19
  </template>
64
20
 
@@ -76,10 +32,14 @@ export default {
76
32
  };
77
33
  }
78
34
  ),
79
- localized: {}
35
+ localized: {},
36
+ isOpen: false
80
37
  };
81
38
  },
82
39
  computed: {
40
+ locale() {
41
+ return window.apos.i18n.locale;
42
+ },
83
43
  button() {
84
44
  return {
85
45
  label: {
@@ -91,21 +51,14 @@ export default {
91
51
  type: 'quiet'
92
52
  };
93
53
  },
94
- filteredLocales(input) {
95
- return this.locales.filter(({ name, label }) => {
96
- const matches = term =>
97
- term.toLowerCase().includes(this.search.toLowerCase());
98
- return matches(name) || matches(label);
99
- });
100
- },
101
- availableLocales() {
102
- return this.locales.filter(locale => !!this.localized[locale.name]);
103
- },
104
54
  action() {
105
55
  return apos.modules[apos.adminBar.context.type]?.action;
106
56
  }
107
57
  },
108
58
  methods: {
59
+ close() {
60
+ this.isOpen = false;
61
+ },
109
62
  async open() {
110
63
  if (apos.adminBar.context) {
111
64
  const docs = await apos.http.get(
@@ -120,20 +73,13 @@ export default {
120
73
  .map(doc => [ doc.aposLocale.split(':')[0], doc ])
121
74
  );
122
75
  }
76
+ this.isOpen = true;
123
77
  },
124
78
  isActive(locale) {
125
79
  return window.apos.i18n.locale === locale.name;
126
80
  },
127
81
  isLocalized(locale) {
128
- return !!this.localized[locale.name];
129
- },
130
- localeClasses(locale) {
131
- const classes = {};
132
- if (this.isActive(locale)) {
133
- classes['apos-active'] = true;
134
- }
135
- classes['apos-exists'] = this.localized[locale.name];
136
- return classes;
82
+ return Boolean(this.localized[locale.name]);
137
83
  },
138
84
  async switchLocale(locale) {
139
85
  const { name } = locale;
@@ -221,112 +167,4 @@ export default {
221
167
  letter-spacing: 1px;
222
168
  }
223
169
  }
224
-
225
- .apos-locales-picker {
226
- width: 315px;
227
- }
228
-
229
- .apos-locales-filter {
230
- @include type-large;
231
-
232
- box-sizing: border-box;
233
- width: 100%;
234
- padding: 20px 45px 20px 20px;
235
- border-top: 0;
236
- border-right: 0;
237
- border-bottom: 1px solid var(--a-base-9);
238
- border-left: 0;
239
- border-top-right-radius: var(--a-border-radius);
240
- border-top-left-radius: var(--a-border-radius);
241
-
242
- &::placeholder {
243
- color: var(--a-base-4);
244
- font-style: italic;
245
- }
246
-
247
- &:focus {
248
- outline: none;
249
- background-color: var(--a-base-10);
250
- }
251
- }
252
-
253
- .apos-locales {
254
- margin: $spacing-base 0;
255
- padding-left: 0;
256
- list-style-type: none;
257
- max-height: 350px;
258
- overflow-y: scroll;
259
- font-weight: var(--a-weight-base);
260
- }
261
-
262
- .apos-locale-item {
263
- position: relative;
264
- padding: 12px 35px;
265
- line-height: 1;
266
- cursor: pointer;
267
-
268
- .state {
269
- opacity: 0;
270
- }
271
-
272
- &:hover {
273
- background-color: var(--a-base-10);
274
- }
275
-
276
- .apos-check {
277
- position: absolute;
278
- top: 50%;
279
- left: 18px;
280
- transform: translateY(-50%);
281
- color: var(--a-primary);
282
- stroke: var(--a-primary);
283
- }
284
-
285
- &.apos-active {
286
- .active {
287
- opacity: 1;
288
- }
289
- }
290
-
291
- .apos-locale-localized {
292
- position: relative;
293
- top: -1px;
294
- left: 5px;
295
- display: inline-block;
296
- width: 3px;
297
- height: 3px;
298
- border: 1px solid var(--a-base-5);
299
- border-radius: 50%;
300
-
301
- &.apos-state-is-localized {
302
- background-color: var(--a-success);
303
- border-color: var(--a-success);
304
- }
305
- }
306
- }
307
-
308
- .apos-available-locales {
309
- padding: $spacing-double;
310
- border-top: 1px solid var(--a-base-9);
311
- }
312
-
313
- .apos-available-locale {
314
- display: inline-block;
315
- color: var(--a-primary);
316
- font-size: var(--a-type-small);
317
- }
318
-
319
- .apos-available-locale:not(:last-of-type) {
320
- margin-right: 10px;
321
- margin-bottom: 5px;
322
- }
323
-
324
- .apos-available-description {
325
- margin-top: 0;
326
- }
327
-
328
- .apos-locale-name {
329
- text-transform: uppercase;
330
- }
331
-
332
170
  </style>
@@ -1,11 +1,12 @@
1
1
  <template>
2
- <ul class="apos-admin-bar__items">
2
+ <ol class="apos-admin-bar__items" role="menu">
3
3
  <li v-if="pageTree" class="apos-admin-bar__item">
4
4
  <AposButton
5
5
  type="subtle"
6
6
  label="apostrophe:pages"
7
7
  class="apos-admin-bar__btn"
8
8
  :modifiers="['no-motion']"
9
+ role="menuitem"
9
10
  @click="emitEvent('@apostrophecms/page:manager')"
10
11
  />
11
12
  </li>
@@ -24,6 +25,7 @@
24
25
  class: 'apos-admin-bar__btn',
25
26
  type: 'subtle'
26
27
  }"
28
+ role="menuitem"
27
29
  @item-clicked="emitEvent"
28
30
  />
29
31
  <Component
@@ -33,6 +35,7 @@
33
35
  :label="item.label"
34
36
  :modifiers="['no-motion']"
35
37
  class="apos-admin-bar__btn"
38
+ role="menuitem"
36
39
  @click="emitEvent(item.action)"
37
40
  />
38
41
  </li>
@@ -47,6 +50,7 @@
47
50
  type: 'primary',
48
51
  modifiers: ['round', 'no-motion']
49
52
  }"
53
+ role="menuitem"
50
54
  @item-clicked="emitEvent"
51
55
  />
52
56
  </li>
@@ -76,7 +80,7 @@
76
80
  />
77
81
  </template>
78
82
  </li>
79
- </ul>
83
+ </ol>
80
84
  </template>
81
85
 
82
86
  <script>
@@ -498,7 +498,7 @@ export default {
498
498
  const contextOptions = this.context
499
499
  ? apos.modules[this.context.type]
500
500
  : { contentChangedRefresh: true };
501
- if (contextOptions.contentChangedRefresh) {
501
+ if (!e.localeSwitched && contextOptions.contentChangedRefresh) {
502
502
  await this.refresh({
503
503
  scrollcheck: e.action === 'history'
504
504
  });
@@ -53,6 +53,7 @@
53
53
  v-if="editMode && !isAutopublished"
54
54
  type="primary"
55
55
  :label="publishLabel"
56
+ :tooltip="publishTooltip"
56
57
  :disabled="!readyToPublish"
57
58
  class="apos-admin-bar__btn apos-admin-bar__context-button"
58
59
  :modifiers="['no-motion']"
@@ -133,6 +134,16 @@ export default {
133
134
  }
134
135
  }
135
136
  },
137
+ publishTooltip() {
138
+ if (this.canPublish && this.context.lastPublishedAt && !this.hasBeenPublishedThisPageload) {
139
+ return {
140
+ content: 'apostrophe:updateTooltip',
141
+ placement: 'bottom'
142
+ };
143
+ }
144
+
145
+ return false;
146
+ },
136
147
  isAutopublished() {
137
148
  return this.context._aposAutopublish ?? (window.apos.modules[this.context.type].autopublish || false);
138
149
  },
@@ -120,6 +120,11 @@ module.exports = {
120
120
  continue;
121
121
  }
122
122
  const subquery = self.apos.page.find(req);
123
+
124
+ if (req.aposAncestors === true && req.aposAncestorsApiProjection) {
125
+ subquery.project(req.aposAncestorsApiProjection);
126
+ }
127
+
123
128
  subquery.ancestorPerformanceRestrictions();
124
129
  const parameters = applySubqueryOptions(subquery, options, [ 'depth' ]);
125
130
  const components = page.path.split('/');
@@ -145,7 +150,7 @@ module.exports = {
145
150
  }
146
151
  if (!paths.length) {
147
152
  page._ancestors = [];
148
- return;
153
+ continue;
149
154
  }
150
155
  subquery.and({
151
156
  path: { $in: paths }
@@ -15,6 +15,7 @@
15
15
  label: $t(contextMenuOptions.menu[0].label)
16
16
  }"
17
17
  :disabled="field && field.readOnly"
18
+ :disable-focus="false"
18
19
  type="primary"
19
20
  :icon="icon"
20
21
  @click="add({ index: 0, name: contextMenuOptions.menu[0].name })"
@@ -29,6 +30,7 @@
29
30
  :max-reached="maxReached"
30
31
  :disabled="field && field.readOnly"
31
32
  :widget-options="options.widgets"
33
+ :tabbable="true"
32
34
  @add="add"
33
35
  />
34
36
  </template>
@@ -54,6 +54,10 @@ export default {
54
54
  default: function() {
55
55
  return {};
56
56
  }
57
+ },
58
+ tabbable: {
59
+ type: Boolean,
60
+ default: false
57
61
  }
58
62
  },
59
63
  emits: [ 'add' ],
@@ -64,7 +68,8 @@ export default {
64
68
  icon: 'plus-icon',
65
69
  type: 'primary',
66
70
  modifiers: this.empty ? [] : [ 'round', 'tiny' ],
67
- iconSize: this.empty ? 20 : 11
71
+ iconSize: this.empty ? 20 : 11,
72
+ disableFocus: !this.tabbable
68
73
  };
69
74
  }
70
75
  },
@@ -10,11 +10,15 @@
10
10
  :data-apos-widget-id="widget._id"
11
11
  >
12
12
  <div
13
+ ref="wrapper"
13
14
  class="apos-area-widget-inner"
14
15
  :class="containerClasses"
16
+ tabindex="0"
15
17
  @mouseover="mouseover($event)"
16
18
  @mouseleave="mouseleave"
17
- @click="getFocus($event, widget._id)"
19
+ @click="getFocus($event, widget._id);"
20
+ @focus="attachKeyboardFocusHandler"
21
+ @blur="removeKeyboardFocusHandler"
18
22
  >
19
23
  <div
20
24
  ref="label"
@@ -37,6 +41,7 @@
37
41
  icon="chevron-right-icon"
38
42
  :icon-size="9"
39
43
  :modifiers="['icon-right', 'no-motion']"
44
+ :disable-focus="!(isHovered || isFocused)"
40
45
  @click="getFocus($event, item.id)"
41
46
  />
42
47
  </li>
@@ -50,6 +55,7 @@
50
55
  :tooltip="!isContextual && 'apostrophe:editWidgetForeignTooltip'"
51
56
  :icon-size="11"
52
57
  :modifiers="['no-motion']"
58
+ :disable-focus="!(isHovered || isFocused)"
53
59
  @click="foreign ? $emit('edit', i) : null"
54
60
  @dblclick="(!foreign && !isContextual) ? $emit('edit', i) : null"
55
61
  />
@@ -68,9 +74,14 @@
68
74
  :widget-options="widgets"
69
75
  :options="options"
70
76
  :disabled="disabled"
77
+ :tabbable="isHovered || isFocused"
71
78
  @add="$emit('add', $event);"
72
79
  />
73
80
  </div>
81
+ <div
82
+ class="apos-area-widget-guard"
83
+ :class="{'apos-is-disabled': isFocused}"
84
+ />
74
85
  <div
75
86
  class="apos-area-widget-controls apos-area-widget-controls--modify"
76
87
  :class="controlsClasses"
@@ -83,6 +94,7 @@
83
94
  :foreign="foreign"
84
95
  :disabled="disabled"
85
96
  :max-reached="maxReached"
97
+ :tabbable="isFocused"
86
98
  @up="$emit('up', i);"
87
99
  @remove="$emit('remove', i);"
88
100
  @edit="$emit('edit', i);"
@@ -92,14 +104,6 @@
92
104
  @down="$emit('down', i);"
93
105
  />
94
106
  </div>
95
- <!--
96
- Note: we will not need this guard layer when we implement widget controls outside of the widget DOM
97
- because we will be drawing and fitting a new layer ontop of the widget, which we can use to proxy event handling.
98
- -->
99
- <div
100
- class="apos-area-widget-guard"
101
- :class="{'apos-is-disabled': isFocused}"
102
- />
103
107
  <!-- Still used for contextual editing components -->
104
108
  <component
105
109
  :is="widgetEditorComponent(widget.type)"
@@ -143,6 +147,7 @@
143
147
  :widget-options="widgets"
144
148
  :options="options"
145
149
  :disabled="disabled"
150
+ :tabbable="isHovered || isFocused"
146
151
  @add="$emit('add', $event)"
147
152
  />
148
153
  </div>
@@ -348,6 +353,15 @@ export default {
348
353
  return !!(this.docId && (window.apos.adminBar.contextId !== this.docId));
349
354
  }
350
355
  },
356
+ watch: {
357
+ isFocused(newVal) {
358
+ if (newVal) {
359
+ this.$refs.wrapper.addEventListener('keydown', this.handleKeyboardUnfocus);
360
+ } else {
361
+ this.$refs.wrapper.removeEventListener('keydown', this.handleKeyboardUnfocus);
362
+ }
363
+ }
364
+ },
351
365
  created() {
352
366
  if (this.options.groups) {
353
367
  for (const group of Object.keys(this.options.groups)) {
@@ -390,6 +404,14 @@ export default {
390
404
  return offsetTop - labelHeight < adminBarHeight;
391
405
  },
392
406
 
407
+ attachKeyboardFocusHandler() {
408
+ this.$refs.wrapper.addEventListener('keydown', this.handleKeyboardFocus);
409
+ },
410
+
411
+ removeKeyboardFocusHandler() {
412
+ this.$refs.wrapper.removeEventListener('keydown', this.handleKeyboardFocus);
413
+ },
414
+
393
415
  // Focus parent, useful for obtrusive UI
394
416
  focusParent() {
395
417
  // Something above us asked the focused widget to try and focus its parent
@@ -439,6 +461,22 @@ export default {
439
461
  }
440
462
  },
441
463
 
464
+ handleKeyboardFocus($event) {
465
+ if ($event.key === 'Enter' || $event.code === 'Space') {
466
+ $event.preventDefault();
467
+ this.getFocus($event, this.widget._id);
468
+ this.$refs.wrapper.removeEventListener('keydown', this.handleKeyboardFocus);
469
+ }
470
+ },
471
+
472
+ handleKeyboardUnfocus($event) {
473
+ if ($event.key === 'Escape') {
474
+ this.getFocus($event, null);
475
+ document.activeElement.blur();
476
+ this.$refs.wrapper.focus();
477
+ }
478
+ },
479
+
442
480
  getParent() {
443
481
  if (!this.mounted) {
444
482
  return false;
@@ -500,6 +538,12 @@ export default {
500
538
  outline: 1px solid transparent;
501
539
  transition: outline 200ms ease;
502
540
 
541
+ &:focus {
542
+ box-shadow: 0 0 11px 1px var(--a-primary-transparent-25);
543
+ outline: 1px dashed var(--a-primary-transparent-50);
544
+ outline-offset: 2px;
545
+ }
546
+
503
547
  &.apos-is-highlighted {
504
548
  outline: 1px dashed var(--a-primary-transparent-50);
505
549
  }
@@ -514,6 +558,7 @@ export default {
514
558
 
515
559
  &.apos-is-ui-adjusted {
516
560
  .apos-area-widget-controls--modify {
561
+ top: 0;
517
562
  transform: translate3d(-10px, 50px, 0);
518
563
  }
519
564
 
@@ -571,8 +616,9 @@ export default {
571
616
  }
572
617
 
573
618
  .apos-area-widget-controls--modify {
619
+ top: 50%;
574
620
  right: 0;
575
- transform: translate3d(-10px, 30px, 0);
621
+ transform: translate3d(-10px, -50%, 0);
576
622
 
577
623
  :deep(.apos-button-group__inner) {
578
624
  border: 1px solid var(--a-primary-transparent-25);
@@ -597,6 +643,10 @@ export default {
597
643
  color: var(--a-primary);
598
644
  }
599
645
 
646
+ &:focus:not([disabled])::after {
647
+ background-color: transparent;
648
+ }
649
+
600
650
  &[disabled] {
601
651
  color: var(--a-base-6);
602
652
  }
@@ -735,7 +785,9 @@ export default {
735
785
  color: var(--a-primary-dark-10);
736
786
 
737
787
  &:hover, &:active, &:focus {
738
- text-decoration: none;
788
+ .apos-button__content {
789
+ color: var(--a-primary);
790
+ }
739
791
  }
740
792
  }
741
793