@ckeditor/ckeditor5-ui 39.0.2 → 40.1.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 (136) hide show
  1. package/LICENSE.md +3 -3
  2. package/lang/contexts.json +5 -1
  3. package/lang/translations/ar.po +16 -0
  4. package/lang/translations/ast.po +16 -0
  5. package/lang/translations/az.po +16 -0
  6. package/lang/translations/bg.po +16 -0
  7. package/lang/translations/bn.po +16 -0
  8. package/lang/translations/ca.po +16 -0
  9. package/lang/translations/cs.po +16 -0
  10. package/lang/translations/da.po +16 -0
  11. package/lang/translations/de-ch.po +16 -0
  12. package/lang/translations/de.po +16 -0
  13. package/lang/translations/el.po +16 -0
  14. package/lang/translations/en-au.po +16 -0
  15. package/lang/translations/en-gb.po +16 -0
  16. package/lang/translations/en.po +16 -0
  17. package/lang/translations/eo.po +16 -0
  18. package/lang/translations/es.po +16 -0
  19. package/lang/translations/et.po +16 -0
  20. package/lang/translations/eu.po +16 -0
  21. package/lang/translations/fa.po +16 -0
  22. package/lang/translations/fi.po +16 -0
  23. package/lang/translations/fr.po +16 -0
  24. package/lang/translations/gl.po +16 -0
  25. package/lang/translations/he.po +16 -0
  26. package/lang/translations/hi.po +16 -0
  27. package/lang/translations/hr.po +16 -0
  28. package/lang/translations/hu.po +16 -0
  29. package/lang/translations/id.po +16 -0
  30. package/lang/translations/it.po +16 -0
  31. package/lang/translations/ja.po +16 -0
  32. package/lang/translations/km.po +16 -0
  33. package/lang/translations/kn.po +16 -0
  34. package/lang/translations/ko.po +16 -0
  35. package/lang/translations/ku.po +16 -0
  36. package/lang/translations/lt.po +16 -0
  37. package/lang/translations/lv.po +16 -0
  38. package/lang/translations/ms.po +16 -0
  39. package/lang/translations/nb.po +16 -0
  40. package/lang/translations/ne.po +16 -0
  41. package/lang/translations/nl.po +16 -0
  42. package/lang/translations/no.po +16 -0
  43. package/lang/translations/pl.po +16 -0
  44. package/lang/translations/pt-br.po +17 -1
  45. package/lang/translations/pt.po +16 -0
  46. package/lang/translations/ro.po +16 -0
  47. package/lang/translations/ru.po +16 -0
  48. package/lang/translations/sk.po +16 -0
  49. package/lang/translations/sl.po +16 -0
  50. package/lang/translations/sq.po +16 -0
  51. package/lang/translations/sr-latn.po +16 -0
  52. package/lang/translations/sr.po +16 -0
  53. package/lang/translations/sv.po +16 -0
  54. package/lang/translations/th.po +16 -0
  55. package/lang/translations/tk.po +16 -0
  56. package/lang/translations/tr.po +16 -0
  57. package/lang/translations/tt.po +16 -0
  58. package/lang/translations/ug.po +38 -22
  59. package/lang/translations/uk.po +16 -0
  60. package/lang/translations/ur.po +16 -0
  61. package/lang/translations/uz.po +16 -0
  62. package/lang/translations/vi.po +16 -0
  63. package/lang/translations/zh-cn.po +16 -0
  64. package/lang/translations/zh.po +16 -0
  65. package/package.json +3 -3
  66. package/src/arialiveannouncer.d.ts +94 -0
  67. package/src/arialiveannouncer.js +113 -0
  68. package/src/autocomplete/autocompleteview.d.ts +81 -0
  69. package/src/autocomplete/autocompleteview.js +153 -0
  70. package/src/button/button.d.ts +0 -6
  71. package/src/button/buttonlabel.d.ts +34 -0
  72. package/src/button/buttonlabel.js +5 -0
  73. package/src/button/buttonlabelview.d.ts +31 -0
  74. package/src/button/buttonlabelview.js +42 -0
  75. package/src/button/buttonview.d.ts +14 -10
  76. package/src/button/buttonview.js +11 -25
  77. package/src/dropdown/dropdownview.js +5 -4
  78. package/src/dropdown/utils.d.ts +15 -1
  79. package/src/dropdown/utils.js +47 -21
  80. package/src/editorui/editorui.d.ts +6 -0
  81. package/src/editorui/editorui.js +2 -0
  82. package/src/editorui/poweredby.js +14 -37
  83. package/src/focuscycler.d.ts +45 -2
  84. package/src/focuscycler.js +34 -9
  85. package/src/formheader/formheaderview.d.ts +6 -0
  86. package/src/formheader/formheaderview.js +6 -0
  87. package/src/highlightedtext/highlightedtextview.d.ts +38 -0
  88. package/src/highlightedtext/highlightedtextview.js +102 -0
  89. package/src/icon/iconview.d.ts +7 -0
  90. package/src/icon/iconview.js +2 -0
  91. package/src/index.d.ts +12 -2
  92. package/src/index.js +8 -0
  93. package/src/input/inputbase.d.ts +107 -0
  94. package/src/input/inputbase.js +110 -0
  95. package/src/input/inputview.d.ts +4 -89
  96. package/src/input/inputview.js +5 -87
  97. package/src/labeledfield/labeledfieldview.d.ts +7 -2
  98. package/src/labeledfield/labeledfieldview.js +2 -2
  99. package/src/labeledfield/utils.d.ts +34 -4
  100. package/src/labeledfield/utils.js +51 -6
  101. package/src/list/listitemgroupview.d.ts +59 -0
  102. package/src/list/listitemgroupview.js +63 -0
  103. package/src/list/listitemview.d.ts +2 -1
  104. package/src/list/listitemview.js +3 -1
  105. package/src/list/listview.d.ts +59 -2
  106. package/src/list/listview.js +105 -8
  107. package/src/panel/balloon/balloonpanelview.js +26 -4
  108. package/src/panel/sticky/stickypanelview.d.ts +1 -3
  109. package/src/panel/sticky/stickypanelview.js +53 -50
  110. package/src/search/filteredview.d.ts +31 -0
  111. package/src/search/filteredview.js +5 -0
  112. package/src/search/searchinfoview.d.ts +45 -0
  113. package/src/search/searchinfoview.js +59 -0
  114. package/src/search/searchresultsview.d.ts +54 -0
  115. package/src/search/searchresultsview.js +65 -0
  116. package/src/search/text/searchtextqueryview.d.ts +76 -0
  117. package/src/search/text/searchtextqueryview.js +75 -0
  118. package/src/search/text/searchtextview.d.ts +219 -0
  119. package/src/search/text/searchtextview.js +201 -0
  120. package/src/spinner/spinnerview.d.ts +25 -0
  121. package/src/spinner/spinnerview.js +38 -0
  122. package/src/textarea/textareaview.d.ts +88 -0
  123. package/src/textarea/textareaview.js +142 -0
  124. package/src/toolbar/block/blocktoolbar.js +30 -26
  125. package/src/toolbar/normalizetoolbarconfig.d.ts +1 -0
  126. package/src/toolbar/normalizetoolbarconfig.js +9 -8
  127. package/src/toolbar/toolbarview.d.ts +1 -0
  128. package/src/toolbar/toolbarview.js +4 -2
  129. package/theme/components/arialiveannouncer/arialiveannouncer.css +10 -0
  130. package/theme/components/autocomplete/autocomplete.css +22 -0
  131. package/theme/components/button/button.css +9 -1
  132. package/theme/components/formheader/formheader.css +4 -0
  133. package/theme/components/highlightedtext/highlightedtext.css +12 -0
  134. package/theme/components/search/search.css +43 -0
  135. package/theme/components/spinner/spinner.css +23 -0
  136. package/theme/components/textarea/textarea.css +10 -0
@@ -7,8 +7,9 @@
7
7
  */
8
8
  import InputTextView from '../inputtext/inputtextview';
9
9
  import InputNumberView from '../inputnumber/inputnumberview';
10
- import type LabeledFieldView from './labeledfieldview';
10
+ import TextareaView from '../textarea/textareaview';
11
11
  import type DropdownView from '../dropdown/dropdownview';
12
+ import type { LabeledFieldViewCreator } from './labeledfieldview';
12
13
  /**
13
14
  * A helper for creating labeled inputs.
14
15
  *
@@ -36,7 +37,7 @@ import type DropdownView from '../dropdown/dropdownview';
36
37
  * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
37
38
  * @returns The input text view instance.
38
39
  */
39
- export declare function createLabeledInputText(labeledFieldView: LabeledFieldView, viewUid: string, statusUid: string): InputTextView;
40
+ declare const createLabeledInputText: LabeledFieldViewCreator<InputTextView>;
40
41
  /**
41
42
  * A helper for creating labeled number inputs.
42
43
  *
@@ -64,7 +65,35 @@ export declare function createLabeledInputText(labeledFieldView: LabeledFieldVie
64
65
  * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
65
66
  * @returns The input number view instance.
66
67
  */
67
- export declare function createLabeledInputNumber(labeledFieldView: LabeledFieldView, viewUid: string, statusUid: string): InputNumberView;
68
+ declare const createLabeledInputNumber: LabeledFieldViewCreator<InputNumberView>;
69
+ /**
70
+ * A helper for creating labeled textarea.
71
+ *
72
+ * It creates an instance of a {@link module:ui/textarea/textareaview~TextareaView textarea} that is
73
+ * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in DOM.
74
+ *
75
+ * The helper does the following:
76
+ *
77
+ * * It sets textarea's `id` and `ariaDescribedById` attributes.
78
+ * * It binds textarea's `isReadOnly` to the labeled view.
79
+ * * It binds textarea's `hasError` to the labeled view.
80
+ * * It enables a logic that cleans up the error when user starts typing in the textarea.
81
+ *
82
+ * Usage:
83
+ *
84
+ * ```ts
85
+ * const labeledTextarea = new LabeledFieldView( locale, createLabeledTextarea );
86
+ * console.log( labeledTextarea.fieldView ); // A textarea instance.
87
+ * ```
88
+ *
89
+ * @param labeledFieldView The instance of the labeled field view.
90
+ * @param viewUid An UID string that allows DOM logical connection between the
91
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view's label} and the textarea.
92
+ * @param statusUid An UID string that allows DOM logical connection between the
93
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the textarea.
94
+ * @returns The textarea view instance.
95
+ */
96
+ declare const createLabeledTextarea: LabeledFieldViewCreator<TextareaView>;
68
97
  /**
69
98
  * A helper for creating labeled dropdowns.
70
99
  *
@@ -90,4 +119,5 @@ export declare function createLabeledInputNumber(labeledFieldView: LabeledFieldV
90
119
  * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view status} and the dropdown.
91
120
  * @returns The dropdown view instance.
92
121
  */
93
- export declare function createLabeledDropdown(labeledFieldView: LabeledFieldView, viewUid: string, statusUid: string): DropdownView;
122
+ declare const createLabeledDropdown: LabeledFieldViewCreator<DropdownView>;
123
+ export { createLabeledInputNumber, createLabeledInputText, createLabeledTextarea, createLabeledDropdown };
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import InputTextView from '../inputtext/inputtextview';
9
9
  import InputNumberView from '../inputnumber/inputnumberview';
10
+ import TextareaView from '../textarea/textareaview';
10
11
  import { createDropdown } from '../dropdown/utils';
11
12
  /**
12
13
  * A helper for creating labeled inputs.
@@ -35,7 +36,7 @@ import { createDropdown } from '../dropdown/utils';
35
36
  * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
36
37
  * @returns The input text view instance.
37
38
  */
38
- export function createLabeledInputText(labeledFieldView, viewUid, statusUid) {
39
+ const createLabeledInputText = (labeledFieldView, viewUid, statusUid) => {
39
40
  const inputView = new InputTextView(labeledFieldView.locale);
40
41
  inputView.set({
41
42
  id: viewUid,
@@ -50,7 +51,7 @@ export function createLabeledInputText(labeledFieldView, viewUid, statusUid) {
50
51
  });
51
52
  labeledFieldView.bind('isEmpty', 'isFocused', 'placeholder').to(inputView);
52
53
  return inputView;
53
- }
54
+ };
54
55
  /**
55
56
  * A helper for creating labeled number inputs.
56
57
  *
@@ -78,7 +79,7 @@ export function createLabeledInputText(labeledFieldView, viewUid, statusUid) {
78
79
  * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
79
80
  * @returns The input number view instance.
80
81
  */
81
- export function createLabeledInputNumber(labeledFieldView, viewUid, statusUid) {
82
+ const createLabeledInputNumber = (labeledFieldView, viewUid, statusUid) => {
82
83
  const inputView = new InputNumberView(labeledFieldView.locale);
83
84
  inputView.set({
84
85
  id: viewUid,
@@ -94,7 +95,50 @@ export function createLabeledInputNumber(labeledFieldView, viewUid, statusUid) {
94
95
  });
95
96
  labeledFieldView.bind('isEmpty', 'isFocused', 'placeholder').to(inputView);
96
97
  return inputView;
97
- }
98
+ };
99
+ /**
100
+ * A helper for creating labeled textarea.
101
+ *
102
+ * It creates an instance of a {@link module:ui/textarea/textareaview~TextareaView textarea} that is
103
+ * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in DOM.
104
+ *
105
+ * The helper does the following:
106
+ *
107
+ * * It sets textarea's `id` and `ariaDescribedById` attributes.
108
+ * * It binds textarea's `isReadOnly` to the labeled view.
109
+ * * It binds textarea's `hasError` to the labeled view.
110
+ * * It enables a logic that cleans up the error when user starts typing in the textarea.
111
+ *
112
+ * Usage:
113
+ *
114
+ * ```ts
115
+ * const labeledTextarea = new LabeledFieldView( locale, createLabeledTextarea );
116
+ * console.log( labeledTextarea.fieldView ); // A textarea instance.
117
+ * ```
118
+ *
119
+ * @param labeledFieldView The instance of the labeled field view.
120
+ * @param viewUid An UID string that allows DOM logical connection between the
121
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view's label} and the textarea.
122
+ * @param statusUid An UID string that allows DOM logical connection between the
123
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the textarea.
124
+ * @returns The textarea view instance.
125
+ */
126
+ const createLabeledTextarea = (labeledFieldView, viewUid, statusUid) => {
127
+ const textareaView = new TextareaView(labeledFieldView.locale);
128
+ textareaView.set({
129
+ id: viewUid,
130
+ ariaDescribedById: statusUid
131
+ });
132
+ textareaView.bind('isReadOnly').to(labeledFieldView, 'isEnabled', value => !value);
133
+ textareaView.bind('hasError').to(labeledFieldView, 'errorText', value => !!value);
134
+ textareaView.on('input', () => {
135
+ // UX: Make the error text disappear and disable the error indicator as the user
136
+ // starts fixing the errors.
137
+ labeledFieldView.errorText = null;
138
+ });
139
+ labeledFieldView.bind('isEmpty', 'isFocused', 'placeholder').to(textareaView);
140
+ return textareaView;
141
+ };
98
142
  /**
99
143
  * A helper for creating labeled dropdowns.
100
144
  *
@@ -120,7 +164,7 @@ export function createLabeledInputNumber(labeledFieldView, viewUid, statusUid) {
120
164
  * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view status} and the dropdown.
121
165
  * @returns The dropdown view instance.
122
166
  */
123
- export function createLabeledDropdown(labeledFieldView, viewUid, statusUid) {
167
+ const createLabeledDropdown = (labeledFieldView, viewUid, statusUid) => {
124
168
  const dropdownView = createDropdown(labeledFieldView.locale);
125
169
  dropdownView.set({
126
170
  id: viewUid,
@@ -128,4 +172,5 @@ export function createLabeledDropdown(labeledFieldView, viewUid, statusUid) {
128
172
  });
129
173
  dropdownView.bind('isEnabled').to(labeledFieldView);
130
174
  return dropdownView;
131
- }
175
+ };
176
+ export { createLabeledInputNumber, createLabeledInputText, createLabeledTextarea, createLabeledDropdown };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/list/listitemgroupview
7
+ */
8
+ import View from '../view';
9
+ import type ViewCollection from '../viewcollection';
10
+ import ListView from './listview';
11
+ import LabelView from '../label/labelview';
12
+ import { type Locale } from '@ckeditor/ckeditor5-utils';
13
+ /**
14
+ * The list item group view class.
15
+ */
16
+ export default class ListItemGroupView extends View {
17
+ /**
18
+ * The visible label of the group.
19
+ *
20
+ * @observable
21
+ * @default ''
22
+ */
23
+ label: string;
24
+ /**
25
+ * Label of the group view. Its text is configurable using the {@link #label label attribute}.
26
+ *
27
+ * If a custom label view is not passed in `ListItemGroupView` constructor, the label is an instance
28
+ * of {@link module:ui/label/labelview~LabelView}.
29
+ */
30
+ readonly labelView: LabelView;
31
+ /**
32
+ * Collection of the child list items inside this group.
33
+ */
34
+ readonly items: ListView['items'];
35
+ /**
36
+ * Collection of the child elements of the group.
37
+ */
38
+ readonly children: ViewCollection;
39
+ /**
40
+ * Controls whether the item view is visible. Visible by default, list items are hidden
41
+ * using a CSS class.
42
+ *
43
+ * @observable
44
+ * @default true
45
+ */
46
+ isVisible: boolean;
47
+ /**
48
+ * Creates an instance of the list item group view class.
49
+ *
50
+ * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
51
+ * @param labelView The instance of the group's label. If not provided, an instance of
52
+ * {@link module:ui/label/labelview~LabelView} is used.
53
+ */
54
+ constructor(locale?: Locale, labelView?: LabelView);
55
+ /**
56
+ * Focuses the list item.
57
+ */
58
+ focus(): void;
59
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/list/listitemgroupview
7
+ */
8
+ import View from '../view';
9
+ import ListView from './listview';
10
+ import LabelView from '../label/labelview';
11
+ /**
12
+ * The list item group view class.
13
+ */
14
+ export default class ListItemGroupView extends View {
15
+ /**
16
+ * Creates an instance of the list item group view class.
17
+ *
18
+ * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
19
+ * @param labelView The instance of the group's label. If not provided, an instance of
20
+ * {@link module:ui/label/labelview~LabelView} is used.
21
+ */
22
+ constructor(locale, labelView = new LabelView()) {
23
+ super(locale);
24
+ const bind = this.bindTemplate;
25
+ const nestedList = new ListView(locale);
26
+ this.set({
27
+ label: '',
28
+ isVisible: true
29
+ });
30
+ this.labelView = labelView;
31
+ this.labelView.bind('text').to(this, 'label');
32
+ this.children = this.createCollection();
33
+ this.children.addMany([this.labelView, nestedList]);
34
+ nestedList.set({
35
+ role: 'group',
36
+ ariaLabelledBy: labelView.id
37
+ });
38
+ // Disable focus tracking and accessible navigation in the child list.
39
+ nestedList.focusTracker.destroy();
40
+ nestedList.keystrokes.destroy();
41
+ this.items = nestedList.items;
42
+ this.setTemplate({
43
+ tag: 'li',
44
+ attributes: {
45
+ role: 'presentation',
46
+ class: [
47
+ 'ck',
48
+ 'ck-list__group',
49
+ bind.if('isVisible', 'ck-hidden', value => !value)
50
+ ]
51
+ },
52
+ children: this.children
53
+ });
54
+ }
55
+ /**
56
+ * Focuses the list item.
57
+ */
58
+ focus() {
59
+ if (this.items.first) {
60
+ this.items.first.focus();
61
+ }
62
+ }
63
+ }
@@ -6,6 +6,7 @@
6
6
  * @module ui/list/listitemview
7
7
  */
8
8
  import View from '../view';
9
+ import type { FocusableView } from '../focuscycler';
9
10
  import type ViewCollection from '../viewcollection';
10
11
  import type { Locale } from '@ckeditor/ckeditor5-utils';
11
12
  /**
@@ -15,7 +16,7 @@ export default class ListItemView extends View {
15
16
  /**
16
17
  * Collection of the child views inside of the list item {@link #element}.
17
18
  */
18
- readonly children: ViewCollection;
19
+ readonly children: ViewCollection<FocusableView>;
19
20
  /**
20
21
  * Controls whether the item view is visible. Visible by default, list items are hidden
21
22
  * using a CSS class.
@@ -35,6 +35,8 @@ export default class ListItemView extends View {
35
35
  * Focuses the list item.
36
36
  */
37
37
  focus() {
38
- this.children.first.focus();
38
+ if (this.children.first) {
39
+ this.children.first.focus();
40
+ }
39
41
  }
40
42
  }
@@ -6,18 +6,26 @@
6
6
  * @module ui/list/listview
7
7
  */
8
8
  import View from '../view';
9
+ import type ListItemView from './listitemview';
10
+ import ListItemGroupView from './listitemgroupview';
9
11
  import type DropdownPanelFocusable from '../dropdown/dropdownpanelfocusable';
10
- import type ViewCollection from '../viewcollection';
12
+ import ViewCollection from '../viewcollection';
11
13
  import { FocusTracker, KeystrokeHandler, type Locale } from '@ckeditor/ckeditor5-utils';
12
14
  import '../../theme/components/list/list.css';
13
15
  /**
14
16
  * The list view class.
15
17
  */
16
18
  export default class ListView extends View<HTMLUListElement> implements DropdownPanelFocusable {
19
+ /**
20
+ * The collection of focusable views in the list. It is used to determine accessible navigation
21
+ * between the {@link module:ui/list/listitemview~ListItemView list items} and
22
+ * {@link module:ui/list/listitemgroupview~ListItemGroupView list groups}.
23
+ */
24
+ readonly focusables: ViewCollection;
17
25
  /**
18
26
  * Collection of the child list views.
19
27
  */
20
- readonly items: ViewCollection;
28
+ readonly items: ViewCollection<ListItemView | ListItemGroupView>;
21
29
  /**
22
30
  * Tracks information about DOM focus in the list.
23
31
  */
@@ -32,6 +40,12 @@ export default class ListView extends View<HTMLUListElement> implements Dropdown
32
40
  * @observable
33
41
  */
34
42
  ariaLabel: string | undefined;
43
+ /**
44
+ * (Optional) The ARIA property reflected by the `aria-ariaLabelledBy` DOM attribute used by assistive technologies.
45
+ *
46
+ * @observable
47
+ */
48
+ ariaLabelledBy?: string | undefined;
35
49
  /**
36
50
  * The property reflected by the `role` DOM attribute to be used by assistive technologies.
37
51
  *
@@ -42,6 +56,11 @@ export default class ListView extends View<HTMLUListElement> implements Dropdown
42
56
  * Helps cycling over focusable {@link #items} in the list.
43
57
  */
44
58
  private readonly _focusCycler;
59
+ /**
60
+ * A cached map of {@link module:ui/list/listitemgroupview~ListItemGroupView} to `change` event listeners for their `items`.
61
+ * Used for accessibility and keyboard navigation purposes.
62
+ */
63
+ private readonly _listItemGroupToChangeListeners;
45
64
  /**
46
65
  * @inheritDoc
47
66
  */
@@ -58,8 +77,46 @@ export default class ListView extends View<HTMLUListElement> implements Dropdown
58
77
  * Focuses the first focusable in {@link #items}.
59
78
  */
60
79
  focus(): void;
80
+ /**
81
+ * Focuses the first focusable in {@link #items}.
82
+ */
83
+ focusFirst(): void;
61
84
  /**
62
85
  * Focuses the last focusable in {@link #items}.
63
86
  */
64
87
  focusLast(): void;
88
+ /**
89
+ * Registers a list item view in the focus tracker.
90
+ *
91
+ * @param item The list item view to be registered.
92
+ * @param index Index of the list item view in the {@link #items} collection. If not specified, the item will be added at the end.
93
+ */
94
+ private _registerFocusableListItem;
95
+ /**
96
+ * Removes a list item view from the focus tracker.
97
+ *
98
+ * @param item The list item view to be removed.
99
+ */
100
+ private _deregisterFocusableListItem;
101
+ /**
102
+ * Gets a callback that will be called when the `items` collection of a {@link module:ui/list/listitemgroupview~ListItemGroupView}
103
+ * change.
104
+ *
105
+ * @param groupView The group view for which the callback will be created.
106
+ * @returns The callback function to be used for the items `change` event listener in a group.
107
+ */
108
+ private _getOnGroupItemsChangeCallback;
109
+ /**
110
+ * Registers a list item group view (and its children) in the focus tracker.
111
+ *
112
+ * @param groupView A group view to be registered.
113
+ * @param groupIndex Index of the group view in the {@link #items} collection. If not specified, the group will be added at the end.
114
+ */
115
+ private _registerFocusableItemsGroup;
116
+ /**
117
+ * Removes a list item group view (and its children) from the focus tracker.
118
+ *
119
+ * @param groupView The group view to be removed.
120
+ */
121
+ private _deregisterFocusableItemsGroup;
65
122
  }
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import View from '../view';
9
9
  import FocusCycler from '../focuscycler';
10
+ import ListItemGroupView from './listitemgroupview';
11
+ import ViewCollection from '../viewcollection';
10
12
  import { FocusTracker, KeystrokeHandler } from '@ckeditor/ckeditor5-utils';
11
13
  import '../../theme/components/list/list.css';
12
14
  /**
@@ -18,12 +20,18 @@ export default class ListView extends View {
18
20
  */
19
21
  constructor(locale) {
20
22
  super(locale);
23
+ /**
24
+ * A cached map of {@link module:ui/list/listitemgroupview~ListItemGroupView} to `change` event listeners for their `items`.
25
+ * Used for accessibility and keyboard navigation purposes.
26
+ */
27
+ this._listItemGroupToChangeListeners = new WeakMap();
21
28
  const bind = this.bindTemplate;
29
+ this.focusables = new ViewCollection();
22
30
  this.items = this.createCollection();
23
31
  this.focusTracker = new FocusTracker();
24
32
  this.keystrokes = new KeystrokeHandler();
25
33
  this._focusCycler = new FocusCycler({
26
- focusables: this.items,
34
+ focusables: this.focusables,
27
35
  focusTracker: this.focusTracker,
28
36
  keystrokeHandler: this.keystrokes,
29
37
  actions: {
@@ -34,6 +42,7 @@ export default class ListView extends View {
34
42
  }
35
43
  });
36
44
  this.set('ariaLabel', undefined);
45
+ this.set('ariaLabelledBy', undefined);
37
46
  this.set('role', undefined);
38
47
  this.setTemplate({
39
48
  tag: 'ul',
@@ -44,7 +53,8 @@ export default class ListView extends View {
44
53
  'ck-list'
45
54
  ],
46
55
  role: bind.to('role'),
47
- 'aria-label': bind.to('ariaLabel')
56
+ 'aria-label': bind.to('ariaLabel'),
57
+ 'aria-labelledby': bind.to('ariaLabelledBy')
48
58
  },
49
59
  children: this.items
50
60
  });
@@ -56,13 +66,30 @@ export default class ListView extends View {
56
66
  super.render();
57
67
  // Items added before rendering should be known to the #focusTracker.
58
68
  for (const item of this.items) {
59
- this.focusTracker.add(item.element);
69
+ if (item instanceof ListItemGroupView) {
70
+ this._registerFocusableItemsGroup(item);
71
+ }
72
+ else {
73
+ this._registerFocusableListItem(item);
74
+ }
60
75
  }
61
- this.items.on('add', (evt, item) => {
62
- this.focusTracker.add(item.element);
63
- });
64
- this.items.on('remove', (evt, item) => {
65
- this.focusTracker.remove(item.element);
76
+ this.items.on('change', (evt, data) => {
77
+ for (const removed of data.removed) {
78
+ if (removed instanceof ListItemGroupView) {
79
+ this._deregisterFocusableItemsGroup(removed);
80
+ }
81
+ else {
82
+ this._deregisterFocusableListItem(removed);
83
+ }
84
+ }
85
+ for (const added of Array.from(data.added).reverse()) {
86
+ if (added instanceof ListItemGroupView) {
87
+ this._registerFocusableItemsGroup(added, data.index);
88
+ }
89
+ else {
90
+ this._registerFocusableListItem(added, data.index);
91
+ }
92
+ }
66
93
  });
67
94
  // Start listening for the keystrokes coming from #element.
68
95
  this.keystrokes.listenTo(this.element);
@@ -81,10 +108,80 @@ export default class ListView extends View {
81
108
  focus() {
82
109
  this._focusCycler.focusFirst();
83
110
  }
111
+ /**
112
+ * Focuses the first focusable in {@link #items}.
113
+ */
114
+ focusFirst() {
115
+ this._focusCycler.focusFirst();
116
+ }
84
117
  /**
85
118
  * Focuses the last focusable in {@link #items}.
86
119
  */
87
120
  focusLast() {
88
121
  this._focusCycler.focusLast();
89
122
  }
123
+ /**
124
+ * Registers a list item view in the focus tracker.
125
+ *
126
+ * @param item The list item view to be registered.
127
+ * @param index Index of the list item view in the {@link #items} collection. If not specified, the item will be added at the end.
128
+ */
129
+ _registerFocusableListItem(item, index) {
130
+ this.focusTracker.add(item.element);
131
+ this.focusables.add(item, index);
132
+ }
133
+ /**
134
+ * Removes a list item view from the focus tracker.
135
+ *
136
+ * @param item The list item view to be removed.
137
+ */
138
+ _deregisterFocusableListItem(item) {
139
+ this.focusTracker.remove(item.element);
140
+ this.focusables.remove(item);
141
+ }
142
+ /**
143
+ * Gets a callback that will be called when the `items` collection of a {@link module:ui/list/listitemgroupview~ListItemGroupView}
144
+ * change.
145
+ *
146
+ * @param groupView The group view for which the callback will be created.
147
+ * @returns The callback function to be used for the items `change` event listener in a group.
148
+ */
149
+ _getOnGroupItemsChangeCallback(groupView) {
150
+ return (evt, data) => {
151
+ for (const removed of data.removed) {
152
+ this._deregisterFocusableListItem(removed);
153
+ }
154
+ for (const added of Array.from(data.added).reverse()) {
155
+ this._registerFocusableListItem(added, this.items.getIndex(groupView) + data.index);
156
+ }
157
+ };
158
+ }
159
+ /**
160
+ * Registers a list item group view (and its children) in the focus tracker.
161
+ *
162
+ * @param groupView A group view to be registered.
163
+ * @param groupIndex Index of the group view in the {@link #items} collection. If not specified, the group will be added at the end.
164
+ */
165
+ _registerFocusableItemsGroup(groupView, groupIndex) {
166
+ Array.from(groupView.items).forEach((child, childIndex) => {
167
+ const registeredChildIndex = typeof groupIndex !== 'undefined' ? groupIndex + childIndex : undefined;
168
+ this._registerFocusableListItem(child, registeredChildIndex);
169
+ });
170
+ const groupItemsChangeCallback = this._getOnGroupItemsChangeCallback(groupView);
171
+ // Cache the reference to the callback in case the group is removed (see _deregisterFocusableItemsGroup()).
172
+ this._listItemGroupToChangeListeners.set(groupView, groupItemsChangeCallback);
173
+ groupView.items.on('change', groupItemsChangeCallback);
174
+ }
175
+ /**
176
+ * Removes a list item group view (and its children) from the focus tracker.
177
+ *
178
+ * @param groupView The group view to be removed.
179
+ */
180
+ _deregisterFocusableItemsGroup(groupView) {
181
+ for (const child of groupView.items) {
182
+ this._deregisterFocusableListItem(child);
183
+ }
184
+ groupView.items.off('change', this._listItemGroupToChangeListeners.get(groupView));
185
+ this._listItemGroupToChangeListeners.delete(groupView);
186
+ }
90
187
  }
@@ -11,6 +11,22 @@ import { isElement } from 'lodash-es';
11
11
  import '../../../theme/components/panel/balloonpanel.css';
12
12
  const toPx = toUnit('px');
13
13
  const defaultLimiterElement = global.document.body;
14
+ // A static balloon panel positioning function that moves the balloon far off the viewport.
15
+ // It is used as a fallback when there is no way to position the balloon using provided
16
+ // positioning functions (see: `getOptimalPosition()`), for instance, when the target the
17
+ // balloon should be attached to gets obscured by scrollable containers or the viewport.
18
+ //
19
+ // It prevents the balloon from being attached to the void and possible degradation of the UX.
20
+ // At the same time, it keeps the balloon physically visible in the DOM so the focus remains
21
+ // uninterrupted.
22
+ const POSITION_OFF_SCREEN = {
23
+ top: -99999,
24
+ left: -99999,
25
+ name: 'arrowless',
26
+ config: {
27
+ withArrow: false
28
+ }
29
+ };
14
30
  /**
15
31
  * The balloon panel view class.
16
32
  *
@@ -153,7 +169,7 @@ export default class BalloonPanelView extends View {
153
169
  limiter: defaultLimiterElement,
154
170
  fitInViewport: true
155
171
  }, options);
156
- const optimalPosition = BalloonPanelView._getOptimalPosition(positionOptions);
172
+ const optimalPosition = BalloonPanelView._getOptimalPosition(positionOptions) || POSITION_OFF_SCREEN;
157
173
  // Usually browsers make some problems with super accurate values like 104.345px
158
174
  // so it is better to use int values.
159
175
  const left = parseInt(optimalPosition.left);
@@ -953,12 +969,18 @@ export function generatePositions(options = {}) {
953
969
  ...(config && { config })
954
970
  }),
955
971
  // ------- Sticky
956
- viewportStickyNorth: (targetRect, balloonRect, viewportRect) => {
957
- if (!targetRect.getIntersection(viewportRect)) {
972
+ viewportStickyNorth: (targetRect, balloonRect, viewportRect, limiterRect) => {
973
+ const boundaryRect = limiterRect || viewportRect;
974
+ if (!targetRect.getIntersection(boundaryRect)) {
975
+ return null;
976
+ }
977
+ // Engage when the target top and bottom edges are close or off the boundary.
978
+ // By close, it means there's not enough space for the balloon arrow (offset).
979
+ if (boundaryRect.height - targetRect.height > stickyVerticalOffset) {
958
980
  return null;
959
981
  }
960
982
  return {
961
- top: viewportRect.top + stickyVerticalOffset,
983
+ top: boundaryRect.top + stickyVerticalOffset,
962
984
  left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
963
985
  name: 'arrowless',
964
986
  config: {
@@ -125,10 +125,8 @@ export default class StickyPanelView extends View {
125
125
  /**
126
126
  * Analyzes the environment to decide whether the panel should be sticky or not.
127
127
  * Then handles the positioning of the panel.
128
- *
129
- * @param [scrollTarget] The element which is being scrolled.
130
128
  */
131
- checkIfShouldBeSticky(scrollTarget?: HTMLElement | Document): void;
129
+ checkIfShouldBeSticky(): void;
132
130
  /**
133
131
  * Sticks the panel at the given CSS `top` offset.
134
132
  *