@vaadin/avatar-group 24.6.0-alpha8 → 24.6.0-alpha9
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.
- package/package.json +13 -12
- package/src/vaadin-avatar-group-mixin.d.ts +99 -0
- package/src/vaadin-avatar-group-mixin.js +556 -0
- package/src/vaadin-avatar-group-styles.d.ts +8 -0
- package/src/vaadin-avatar-group-styles.js +51 -0
- package/src/vaadin-avatar-group.d.ts +3 -87
- package/src/vaadin-avatar-group.js +9 -597
- package/src/vaadin-lit-avatar-group-menu-item.js +60 -0
- package/src/vaadin-lit-avatar-group-menu.js +87 -0
- package/src/vaadin-lit-avatar-group-overlay.js +64 -0
- package/src/vaadin-lit-avatar-group.d.ts +1 -0
- package/src/vaadin-lit-avatar-group.js +64 -0
- package/theme/lumo/vaadin-lit-avatar-group.d.ts +3 -0
- package/theme/lumo/vaadin-lit-avatar-group.js +3 -0
- package/theme/material/vaadin-lit-avatar-group.d.ts +3 -0
- package/theme/material/vaadin-lit-avatar-group.js +3 -0
- package/vaadin-lit-avatar-group.d.ts +1 -0
- package/vaadin-lit-avatar-group.js +2 -0
- package/web-types.json +3 -3
- package/web-types.lit.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/avatar-group",
|
|
3
|
-
"version": "24.6.0-
|
|
3
|
+
"version": "24.6.0-alpha9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -36,20 +36,21 @@
|
|
|
36
36
|
"polymer"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
39
40
|
"@polymer/polymer": "^3.0.0",
|
|
40
|
-
"@vaadin/a11y-base": "24.6.0-
|
|
41
|
-
"@vaadin/avatar": "24.6.0-
|
|
42
|
-
"@vaadin/component-base": "24.6.0-
|
|
43
|
-
"@vaadin/item": "24.6.0-
|
|
44
|
-
"@vaadin/list-box": "24.6.0-
|
|
45
|
-
"@vaadin/overlay": "24.6.0-
|
|
46
|
-
"@vaadin/vaadin-lumo-styles": "24.6.0-
|
|
47
|
-
"@vaadin/vaadin-material-styles": "24.6.0-
|
|
48
|
-
"@vaadin/vaadin-themable-mixin": "24.6.0-
|
|
41
|
+
"@vaadin/a11y-base": "24.6.0-alpha9",
|
|
42
|
+
"@vaadin/avatar": "24.6.0-alpha9",
|
|
43
|
+
"@vaadin/component-base": "24.6.0-alpha9",
|
|
44
|
+
"@vaadin/item": "24.6.0-alpha9",
|
|
45
|
+
"@vaadin/list-box": "24.6.0-alpha9",
|
|
46
|
+
"@vaadin/overlay": "24.6.0-alpha9",
|
|
47
|
+
"@vaadin/vaadin-lumo-styles": "24.6.0-alpha9",
|
|
48
|
+
"@vaadin/vaadin-material-styles": "24.6.0-alpha9",
|
|
49
|
+
"@vaadin/vaadin-themable-mixin": "24.6.0-alpha9",
|
|
49
50
|
"lit": "^3.0.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@vaadin/chai-plugins": "24.6.0-
|
|
53
|
+
"@vaadin/chai-plugins": "24.6.0-alpha9",
|
|
53
54
|
"@vaadin/testing-helpers": "^1.0.0",
|
|
54
55
|
"sinon": "^18.0.0"
|
|
55
56
|
},
|
|
@@ -57,5 +58,5 @@
|
|
|
57
58
|
"web-types.json",
|
|
58
59
|
"web-types.lit.json"
|
|
59
60
|
],
|
|
60
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "e303d77ba20c3089c9998be9a318733d9ec5b53c"
|
|
61
62
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2020 - 2024 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { AvatarI18n } from '@vaadin/avatar/src/vaadin-avatar.js';
|
|
8
|
+
import type { OverlayClassMixinClass } from '@vaadin/component-base/src/overlay-class-mixin.js';
|
|
9
|
+
import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js';
|
|
10
|
+
|
|
11
|
+
export interface AvatarGroupI18n extends AvatarI18n {
|
|
12
|
+
activeUsers: {
|
|
13
|
+
one: string;
|
|
14
|
+
many: string;
|
|
15
|
+
};
|
|
16
|
+
joined: string;
|
|
17
|
+
left: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AvatarGroupItem {
|
|
21
|
+
name?: string;
|
|
22
|
+
abbr?: string;
|
|
23
|
+
img?: string;
|
|
24
|
+
colorIndex?: number;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A mixin providing common avatar functionality.
|
|
30
|
+
*/
|
|
31
|
+
export declare function AvatarGroupMixin<T extends Constructor<HTMLElement>>(
|
|
32
|
+
base: T,
|
|
33
|
+
): Constructor<AvatarGroupMixinClass> & Constructor<OverlayClassMixinClass> & Constructor<ResizeMixinClass> & T;
|
|
34
|
+
|
|
35
|
+
export declare class AvatarGroupMixinClass {
|
|
36
|
+
/**
|
|
37
|
+
* An array containing the items which will be stamped as avatars.
|
|
38
|
+
*
|
|
39
|
+
* The items objects allow to configure [`name`](#/elements/vaadin-avatar#property-name),
|
|
40
|
+
* [`abbr`](#/elements/vaadin-avatar#property-abbr), [`img`](#/elements/vaadin-avatar#property-img)
|
|
41
|
+
* and [`colorIndex`](#/elements/vaadin-avatar#property-colorIndex) properties on the
|
|
42
|
+
* stamped avatars, and set `className` to provide CSS class names.
|
|
43
|
+
*
|
|
44
|
+
* #### Example
|
|
45
|
+
*
|
|
46
|
+
* ```js
|
|
47
|
+
* group.items = [
|
|
48
|
+
* {
|
|
49
|
+
* name: 'User name',
|
|
50
|
+
* img: 'url-to-image.png',
|
|
51
|
+
* className: 'even'
|
|
52
|
+
* },
|
|
53
|
+
* {
|
|
54
|
+
* abbr: 'JD',
|
|
55
|
+
* colorIndex: 1,
|
|
56
|
+
* className: 'odd'
|
|
57
|
+
* },
|
|
58
|
+
* ];
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
items: AvatarGroupItem[] | undefined;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The maximum number of avatars to display. By default, all the avatars are displayed.
|
|
65
|
+
* When _maxItemsVisible_ is set, the overflowing avatars are grouped into one avatar with
|
|
66
|
+
* a dropdown. Setting 0 or 1 has no effect so there are always at least two avatars visible.
|
|
67
|
+
* @attr {number} max-items-visible
|
|
68
|
+
*/
|
|
69
|
+
maxItemsVisible: number | null | undefined;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The object used to localize this component.
|
|
73
|
+
* To change the default localization, replace the entire
|
|
74
|
+
* _i18n_ object or just the property you want to modify.
|
|
75
|
+
*
|
|
76
|
+
* The object has the following JSON structure and default values:
|
|
77
|
+
* ```
|
|
78
|
+
* {
|
|
79
|
+
* // Translation of the anonymous user avatar tooltip.
|
|
80
|
+
* anonymous: 'anonymous',
|
|
81
|
+
* // Translation of the avatar group accessible label.
|
|
82
|
+
* // {count} is replaced with the actual count of users.
|
|
83
|
+
* activeUsers: {
|
|
84
|
+
* one: 'Currently one active user',
|
|
85
|
+
* many: 'Currently {count} active users'
|
|
86
|
+
* },
|
|
87
|
+
* // Screen reader announcement when user joins group.
|
|
88
|
+
* // {user} is replaced with the name or abbreviation.
|
|
89
|
+
* // When neither is set, "anonymous" is used instead.
|
|
90
|
+
* joined: '{user} joined',
|
|
91
|
+
* // Screen reader announcement when user leaves group.
|
|
92
|
+
* // {user} is replaced with the name or abbreviation.
|
|
93
|
+
* // When neither is set, "anonymous" is used instead.
|
|
94
|
+
* left: '{user} left'
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
i18n: AvatarGroupI18n;
|
|
99
|
+
}
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2020 - 2024 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
|
7
|
+
import { html, render } from 'lit';
|
|
8
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
9
|
+
import { announce } from '@vaadin/a11y-base/src/announce.js';
|
|
10
|
+
import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
|
|
11
|
+
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
|
|
12
|
+
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
13
|
+
|
|
14
|
+
const MINIMUM_DISPLAYED_AVATARS = 2;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A mixin providing common avatar group functionality.
|
|
18
|
+
*
|
|
19
|
+
* @polymerMixin
|
|
20
|
+
* @mixes ResizeMixin
|
|
21
|
+
* @mixes OverlayClassMixin
|
|
22
|
+
*/
|
|
23
|
+
export const AvatarGroupMixin = (superClass) =>
|
|
24
|
+
class AvatarGroupMixinClass extends ResizeMixin(OverlayClassMixin(superClass)) {
|
|
25
|
+
static get properties() {
|
|
26
|
+
return {
|
|
27
|
+
/**
|
|
28
|
+
* An array containing the items which will be stamped as avatars.
|
|
29
|
+
*
|
|
30
|
+
* The items objects allow to configure [`name`](#/elements/vaadin-avatar#property-name),
|
|
31
|
+
* [`abbr`](#/elements/vaadin-avatar#property-abbr), [`img`](#/elements/vaadin-avatar#property-img)
|
|
32
|
+
* and [`colorIndex`](#/elements/vaadin-avatar#property-colorIndex) properties on the
|
|
33
|
+
* stamped avatars, and set `className` to provide CSS class names.
|
|
34
|
+
*
|
|
35
|
+
* #### Example
|
|
36
|
+
*
|
|
37
|
+
* ```js
|
|
38
|
+
* group.items = [
|
|
39
|
+
* {
|
|
40
|
+
* name: 'User name',
|
|
41
|
+
* img: 'url-to-image.png',
|
|
42
|
+
* className: 'even'
|
|
43
|
+
* },
|
|
44
|
+
* {
|
|
45
|
+
* abbr: 'JD',
|
|
46
|
+
* colorIndex: 1,
|
|
47
|
+
* className: 'odd'
|
|
48
|
+
* },
|
|
49
|
+
* ];
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @type {!Array<!AvatarGroupItem> | undefined}
|
|
53
|
+
*/
|
|
54
|
+
items: {
|
|
55
|
+
type: Array,
|
|
56
|
+
observer: '__itemsChanged',
|
|
57
|
+
sync: true,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The maximum number of avatars to display. By default, all the avatars are displayed.
|
|
62
|
+
* When _maxItemsVisible_ is set, the overflowing avatars are grouped into one avatar with
|
|
63
|
+
* a dropdown. Setting 0 or 1 has no effect so there are always at least two avatars visible.
|
|
64
|
+
* @attr {number} max-items-visible
|
|
65
|
+
*/
|
|
66
|
+
maxItemsVisible: {
|
|
67
|
+
type: Number,
|
|
68
|
+
sync: true,
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The object used to localize this component.
|
|
73
|
+
* To change the default localization, replace the entire
|
|
74
|
+
* _i18n_ object or just the property you want to modify.
|
|
75
|
+
*
|
|
76
|
+
* The object has the following JSON structure and default values:
|
|
77
|
+
* ```
|
|
78
|
+
* {
|
|
79
|
+
* // Translation of the anonymous user avatar tooltip.
|
|
80
|
+
* anonymous: 'anonymous',
|
|
81
|
+
* // Translation of the avatar group accessible label.
|
|
82
|
+
* // {count} is replaced with the actual count of users.
|
|
83
|
+
* activeUsers: {
|
|
84
|
+
* one: 'Currently one active user',
|
|
85
|
+
* many: 'Currently {count} active users'
|
|
86
|
+
* },
|
|
87
|
+
* // Screen reader announcement when user joins group.
|
|
88
|
+
* // {user} is replaced with the name or abbreviation.
|
|
89
|
+
* // When neither is set, "anonymous" is used instead.
|
|
90
|
+
* joined: '{user} joined',
|
|
91
|
+
* // Screen reader announcement when user leaves group.
|
|
92
|
+
* // {user} is replaced with the name or abbreviation.
|
|
93
|
+
* // When neither is set, "anonymous" is used instead.
|
|
94
|
+
* left: '{user} left'
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
* @type {!AvatarGroupI18n}
|
|
98
|
+
* @default {English/US}
|
|
99
|
+
*/
|
|
100
|
+
i18n: {
|
|
101
|
+
type: Object,
|
|
102
|
+
sync: true,
|
|
103
|
+
value: () => {
|
|
104
|
+
return {
|
|
105
|
+
anonymous: 'anonymous',
|
|
106
|
+
activeUsers: {
|
|
107
|
+
one: 'Currently one active user',
|
|
108
|
+
many: 'Currently {count} active users',
|
|
109
|
+
},
|
|
110
|
+
joined: '{user} joined',
|
|
111
|
+
left: '{user} left',
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/** @private */
|
|
117
|
+
_avatars: {
|
|
118
|
+
type: Array,
|
|
119
|
+
value: () => [],
|
|
120
|
+
sync: true,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/** @private */
|
|
124
|
+
__itemsInView: {
|
|
125
|
+
type: Number,
|
|
126
|
+
value: null,
|
|
127
|
+
sync: true,
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/** @private */
|
|
131
|
+
_overflow: {
|
|
132
|
+
type: Object,
|
|
133
|
+
sync: true,
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/** @private */
|
|
137
|
+
_overflowItems: {
|
|
138
|
+
type: Array,
|
|
139
|
+
observer: '__overflowItemsChanged',
|
|
140
|
+
computed: '__computeOverflowItems(items, __itemsInView, maxItemsVisible)',
|
|
141
|
+
sync: true,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/** @private */
|
|
145
|
+
_overflowTooltip: {
|
|
146
|
+
type: Object,
|
|
147
|
+
sync: true,
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** @private */
|
|
151
|
+
_opened: {
|
|
152
|
+
type: Boolean,
|
|
153
|
+
sync: true,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static get observers() {
|
|
159
|
+
return [
|
|
160
|
+
'__i18nItemsChanged(i18n, items)',
|
|
161
|
+
'__openedChanged(_opened, _overflow)',
|
|
162
|
+
'__updateAvatarsTheme(_overflow, _avatars, _theme)',
|
|
163
|
+
'__updateAvatars(items, __itemsInView, maxItemsVisible, _overflow, i18n)',
|
|
164
|
+
'__updateOverflowAvatar(_overflow, items, __itemsInView, maxItemsVisible)',
|
|
165
|
+
'__updateOverflowTooltip(_overflowTooltip, items, __itemsInView, maxItemsVisible)',
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @protected */
|
|
170
|
+
ready() {
|
|
171
|
+
super.ready();
|
|
172
|
+
|
|
173
|
+
this._overflowController = new SlotController(this, 'overflow', 'vaadin-avatar', {
|
|
174
|
+
initializer: (overflow) => {
|
|
175
|
+
overflow.setAttribute('aria-haspopup', 'menu');
|
|
176
|
+
overflow.setAttribute('aria-expanded', 'false');
|
|
177
|
+
overflow.addEventListener('click', (e) => this._onOverflowClick(e));
|
|
178
|
+
overflow.addEventListener('keydown', (e) => this._onOverflowKeyDown(e));
|
|
179
|
+
|
|
180
|
+
const tooltip = document.createElement('vaadin-tooltip');
|
|
181
|
+
tooltip.setAttribute('slot', 'tooltip');
|
|
182
|
+
overflow.appendChild(tooltip);
|
|
183
|
+
|
|
184
|
+
this._overflow = overflow;
|
|
185
|
+
this._overflowTooltip = tooltip;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
this.addController(this._overflowController);
|
|
189
|
+
|
|
190
|
+
const overlay = this.$.overlay;
|
|
191
|
+
overlay.renderer = this.__overlayRenderer.bind(this);
|
|
192
|
+
this._overlayElement = overlay;
|
|
193
|
+
|
|
194
|
+
afterNextRender(this, () => {
|
|
195
|
+
this.__setItemsInView();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @protected */
|
|
200
|
+
disconnectedCallback() {
|
|
201
|
+
super.disconnectedCallback();
|
|
202
|
+
|
|
203
|
+
this._opened = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @private */
|
|
207
|
+
__getMessage(user, action) {
|
|
208
|
+
return action.replace('{user}', user.name || user.abbr || this.i18n.anonymous);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Renders items when they are provided by the `items` property and clears the content otherwise.
|
|
213
|
+
* @param {!HTMLElement} root
|
|
214
|
+
* @param {!Select} _select
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
__overlayRenderer(root) {
|
|
218
|
+
let menu = root.firstElementChild;
|
|
219
|
+
if (!menu) {
|
|
220
|
+
menu = document.createElement('vaadin-avatar-group-menu');
|
|
221
|
+
menu.addEventListener('keydown', (event) => this._onListKeyDown(event));
|
|
222
|
+
root.appendChild(menu);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
menu.textContent = '';
|
|
226
|
+
|
|
227
|
+
if (!this._overflowItems) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this._overflowItems.forEach((item) => {
|
|
232
|
+
menu.appendChild(this.__createItemElement(item));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @private */
|
|
237
|
+
__createItemElement(item) {
|
|
238
|
+
const itemElement = document.createElement('vaadin-avatar-group-menu-item');
|
|
239
|
+
|
|
240
|
+
const avatar = document.createElement('vaadin-avatar');
|
|
241
|
+
itemElement.appendChild(avatar);
|
|
242
|
+
|
|
243
|
+
avatar.setAttribute('aria-hidden', 'true');
|
|
244
|
+
avatar.setAttribute('tabindex', '-1');
|
|
245
|
+
avatar.i18n = this.i18n;
|
|
246
|
+
|
|
247
|
+
if (this._theme) {
|
|
248
|
+
avatar.setAttribute('theme', this._theme);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
avatar.name = item.name;
|
|
252
|
+
avatar.abbr = item.abbr;
|
|
253
|
+
avatar.img = item.img;
|
|
254
|
+
avatar.colorIndex = item.colorIndex;
|
|
255
|
+
if (item.className) {
|
|
256
|
+
avatar.className = item.className;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (item.name) {
|
|
260
|
+
const text = document.createTextNode(item.name);
|
|
261
|
+
itemElement.appendChild(text);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return itemElement;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @private */
|
|
268
|
+
_onOverflowClick(e) {
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
if (this._opened) {
|
|
271
|
+
this.$.overlay.close();
|
|
272
|
+
} else if (!e.defaultPrevented) {
|
|
273
|
+
this._opened = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** @private */
|
|
278
|
+
_onOverflowKeyDown(e) {
|
|
279
|
+
if (!this._opened) {
|
|
280
|
+
if (/^(Enter|SpaceBar|\s)$/u.test(e.key)) {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
this._opened = true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** @private */
|
|
288
|
+
_onListKeyDown(event) {
|
|
289
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
290
|
+
this._opened = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @protected
|
|
296
|
+
* @override
|
|
297
|
+
*/
|
|
298
|
+
_onResize() {
|
|
299
|
+
this.__setItemsInView();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @private */
|
|
303
|
+
_onVaadinOverlayClose(e) {
|
|
304
|
+
if (e.detail.sourceEvent && e.detail.sourceEvent.composedPath().includes(this)) {
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @private */
|
|
310
|
+
_onVaadinOverlayOpen() {
|
|
311
|
+
if (this._menuElement) {
|
|
312
|
+
this._menuElement.focus();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @private */
|
|
317
|
+
__renderAvatars(items) {
|
|
318
|
+
render(
|
|
319
|
+
html`
|
|
320
|
+
${items.map(
|
|
321
|
+
(item) => html`
|
|
322
|
+
<vaadin-avatar
|
|
323
|
+
.name="${item.name}"
|
|
324
|
+
.abbr="${item.abbr}"
|
|
325
|
+
.img="${item.img}"
|
|
326
|
+
.colorIndex="${item.colorIndex}"
|
|
327
|
+
.i18n="${this.i18n}"
|
|
328
|
+
class="${ifDefined(item.className)}"
|
|
329
|
+
with-tooltip
|
|
330
|
+
></vaadin-avatar>
|
|
331
|
+
`,
|
|
332
|
+
)}
|
|
333
|
+
`,
|
|
334
|
+
this,
|
|
335
|
+
{ renderBefore: this._overflow },
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** @private */
|
|
340
|
+
__updateAvatars(items, itemsInView, maxItemsVisible, overflow) {
|
|
341
|
+
if (!overflow || !Array.isArray(items)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const limit = this.__getLimit(items.length, itemsInView, maxItemsVisible);
|
|
346
|
+
|
|
347
|
+
this.__renderAvatars(limit ? items.slice(0, limit) : items);
|
|
348
|
+
|
|
349
|
+
this._avatars = [...this.querySelectorAll('vaadin-avatar')];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @private */
|
|
353
|
+
__computeOverflowItems(items, itemsInView, maxItemsVisible) {
|
|
354
|
+
const count = Array.isArray(items) ? items.length : 0;
|
|
355
|
+
const limit = this.__getLimit(count, itemsInView, maxItemsVisible);
|
|
356
|
+
return limit ? items.slice(limit) : [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @private */
|
|
360
|
+
__updateOverflowAvatar(overflow, items, itemsInView, maxItemsVisible) {
|
|
361
|
+
if (overflow) {
|
|
362
|
+
const count = Array.isArray(items) ? items.length : 0;
|
|
363
|
+
const maxReached = maxItemsVisible != null && count > this.__getMax(maxItemsVisible);
|
|
364
|
+
|
|
365
|
+
overflow.abbr = `+${count - this.__getLimit(count, itemsInView, maxItemsVisible)}`;
|
|
366
|
+
overflow.toggleAttribute('hidden', !maxReached && !(itemsInView && itemsInView < count));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** @private */
|
|
371
|
+
__updateAvatarsTheme(overflow, avatars, theme) {
|
|
372
|
+
if (overflow) {
|
|
373
|
+
[overflow, ...avatars].forEach((avatar) => {
|
|
374
|
+
if (theme) {
|
|
375
|
+
avatar.setAttribute('theme', theme);
|
|
376
|
+
} else {
|
|
377
|
+
avatar.removeAttribute('theme');
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** @private */
|
|
384
|
+
__updateOverflowTooltip(tooltip, items, itemsInView, maxItemsVisible) {
|
|
385
|
+
if (!tooltip || !Array.isArray(items)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const limit = this.__getLimit(items.length, itemsInView, maxItemsVisible);
|
|
390
|
+
if (limit == null) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const result = [];
|
|
395
|
+
for (let i = limit; i < items.length; i++) {
|
|
396
|
+
const item = items[i];
|
|
397
|
+
if (item) {
|
|
398
|
+
result.push(item.name || item.abbr || 'anonymous');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
tooltip.text = result.join('\n');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** @private */
|
|
406
|
+
__getLimit(items, itemsInView, maxItemsVisible) {
|
|
407
|
+
let limit = null;
|
|
408
|
+
// Handle max set to 0 or 1
|
|
409
|
+
const adjustedMax = this.__getMax(maxItemsVisible);
|
|
410
|
+
if (maxItemsVisible != null && adjustedMax < items) {
|
|
411
|
+
limit = adjustedMax - 1;
|
|
412
|
+
} else if (itemsInView && itemsInView < items) {
|
|
413
|
+
limit = itemsInView;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return Math.min(limit, this.__calculateAvatarsFitWidth());
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** @private */
|
|
420
|
+
__getMax(maxItemsVisible) {
|
|
421
|
+
return Math.max(maxItemsVisible, MINIMUM_DISPLAYED_AVATARS);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** @private */
|
|
425
|
+
__itemsChanged(items, oldItems) {
|
|
426
|
+
this.__setItemsInView();
|
|
427
|
+
|
|
428
|
+
let added = [];
|
|
429
|
+
let removed = [];
|
|
430
|
+
|
|
431
|
+
const hasNewItems = Array.isArray(items);
|
|
432
|
+
const hasOldItems = Array.isArray(oldItems);
|
|
433
|
+
|
|
434
|
+
if (hasOldItems) {
|
|
435
|
+
removed = oldItems.filter((item) => hasNewItems && !items.includes(item));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (hasNewItems) {
|
|
439
|
+
added = items.filter((item) => hasOldItems && !oldItems.includes(item));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.__announceItemsChange(added, removed);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** @private */
|
|
446
|
+
__announceItemsChange(added, removed) {
|
|
447
|
+
let addedMsg = [];
|
|
448
|
+
let removedMsg = [];
|
|
449
|
+
if (added) {
|
|
450
|
+
addedMsg = added.map((user) => this.__getMessage(user, this.i18n.joined || '{user} joined'));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (removed) {
|
|
454
|
+
removedMsg = removed.map((user) => this.__getMessage(user, this.i18n.left || '{user} left'));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const messages = removedMsg.concat(addedMsg);
|
|
458
|
+
if (messages.length > 0) {
|
|
459
|
+
announce(messages.join(', '));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** @private */
|
|
464
|
+
__i18nItemsChanged(i18n, items) {
|
|
465
|
+
if (i18n && i18n.activeUsers) {
|
|
466
|
+
const count = Array.isArray(items) ? items.length : 0;
|
|
467
|
+
const field = count === 1 ? 'one' : 'many';
|
|
468
|
+
if (i18n.activeUsers[field]) {
|
|
469
|
+
this.setAttribute('aria-label', i18n.activeUsers[field].replace('{count}', count || 0));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this._avatars.forEach((avatar) => {
|
|
473
|
+
avatar.i18n = i18n;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @private */
|
|
479
|
+
__openedChanged(opened, overflow) {
|
|
480
|
+
if (!overflow) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (opened) {
|
|
485
|
+
if (!this._menuElement) {
|
|
486
|
+
this._menuElement = this.$.overlay.querySelector('vaadin-avatar-group-menu');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this._openedWithFocusRing = overflow.hasAttribute('focus-ring');
|
|
490
|
+
} else if (this.__oldOpened) {
|
|
491
|
+
overflow.focus();
|
|
492
|
+
if (this._openedWithFocusRing) {
|
|
493
|
+
overflow.setAttribute('focus-ring', '');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
overflow.setAttribute('aria-expanded', opened === true);
|
|
498
|
+
this.__oldOpened = opened;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** @private */
|
|
502
|
+
__overflowItemsChanged(items, oldItems) {
|
|
503
|
+
if (items || oldItems) {
|
|
504
|
+
this.$.overlay.requestContentUpdate();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** @private */
|
|
509
|
+
__setItemsInView() {
|
|
510
|
+
const avatars = this._avatars;
|
|
511
|
+
const items = this.items;
|
|
512
|
+
|
|
513
|
+
// Always show at least two avatars
|
|
514
|
+
if (!items || !avatars || avatars.length < 3) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let result = this.__calculateAvatarsFitWidth();
|
|
519
|
+
|
|
520
|
+
// Only show overlay if two or more avatars don't fit
|
|
521
|
+
if (result === items.length - 1) {
|
|
522
|
+
result = items.length;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Close overlay if all avatars become visible
|
|
526
|
+
if (result >= items.length && this._opened) {
|
|
527
|
+
this.$.overlay.close();
|
|
528
|
+
// FIXME: hack to avoid jump before closing
|
|
529
|
+
this.$.overlay._flushAnimation('closing');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Reserve space for overflow avatar
|
|
533
|
+
this.__itemsInView = result;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** @private */
|
|
537
|
+
__calculateAvatarsFitWidth() {
|
|
538
|
+
if (!this.shadowRoot || this._avatars.length < MINIMUM_DISPLAYED_AVATARS) {
|
|
539
|
+
return MINIMUM_DISPLAYED_AVATARS;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const avatars = this._avatars;
|
|
543
|
+
|
|
544
|
+
// Assume all the avatars have the same width
|
|
545
|
+
const avatarWidth = avatars[0].clientWidth;
|
|
546
|
+
|
|
547
|
+
// Take negative margin into account
|
|
548
|
+
const { marginLeft, marginRight } = getComputedStyle(avatars[1]);
|
|
549
|
+
|
|
550
|
+
const offset = this.__isRTL
|
|
551
|
+
? parseInt(marginRight, 0) - parseInt(marginLeft, 0)
|
|
552
|
+
: parseInt(marginLeft, 0) - parseInt(marginRight, 0);
|
|
553
|
+
|
|
554
|
+
return Math.floor((this.$.container.offsetWidth - avatarWidth) / (avatarWidth + offset));
|
|
555
|
+
}
|
|
556
|
+
};
|