-
Notifications
You must be signed in to change notification settings - Fork 18
/
index.ts
292 lines (262 loc) · 8.03 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
import {
ControlElement,
css,
CSSResultGroup,
html,
PropertyValues,
TemplateResult
} from '@refinitiv-ui/core';
import { customElement } from '@refinitiv-ui/core/decorators/custom-element.js';
import { property } from '@refinitiv-ui/core/decorators/property.js';
import { query } from '@refinitiv-ui/core/decorators/query.js';
import { VERSION } from '../version.js';
import '../icon/index.js';
import '../checkbox/index.js';
import type { ItemType, ItemText, ItemHeader, ItemDivider, ItemData } from './helpers/types';
export type { ItemType, ItemText, ItemHeader, ItemDivider, ItemData };
const isAllWhitespaceTextNode = (node: Node): boolean =>
node.nodeType === document.TEXT_NODE
&& !node.textContent?.trim();
/**
* Used as a basic building block to compose complex custom elements,
* additionally it can be used by applications
* to create simple menus or navigation panels.
*
* @attr {string} value - The content of this attribute represents the value of the item.
* @prop {string} [value=""] - The content of this attribute represents the value of the item.
*
* @attr {boolean} disabled - Set disabled state.
* @prop {boolean} [disabled=false] - Set disabled state.
*
* @slot left - Used to render the content on the left of the label.
* @slot right - Used to render the content on the right of the label.
*/
@customElement('ef-item', {
alias: 'coral-item'
})
export class Item extends ControlElement {
/**
* Element version number
* @returns version number
*/
static get version (): string {
return VERSION;
}
/**
* `CSSResultGroup` that will be used to style the host,
* slotted children and the internal template of the element.
* @returns CSS template
*/
static get styles (): CSSResultGroup {
return css`
:host {
display: flex;
align-items: center;
}
[part=checkbox] {
pointer-events: none;
}
[part=left],
[part=right] {
display: flex;
align-items: center;
}
[part=center] {
flex: 1;
}
:host([type=divider]) > * {
display: none;
}
`;
}
/**
* The text for the label indicating the meaning of the item.
* By having both `label` and content, `label` always takes priority
*/
@property({ type: String })
public label: string | null = null;
/**
* If defined value can be `text`, `header` or `divider`
* @type {ItemType | null}
*/
@property({ type: String, reflect: true })
public type: ItemType | null = null;
/**
* Set the icon name from the ef-icon list
*/
@property({ type: String, reflect: true })
public icon: string | null = null;
/**
* Indicates that the item is selected
*/
@property({ type: Boolean, reflect: true })
public selected = false;
/**
* Is the item part of a multiple selection
*/
@property({ type: Boolean, reflect: true })
public multiple = false;
/**
* Highlight the item
*/
@property({ type: Boolean, reflect: true })
public highlighted = false;
/**
* The`subLabel` property represents the text beneath the label.
*/
@property({ type: String, reflect: true, attribute: 'sub-label' })
public subLabel: string | null = null;
/**
* Specifies which element an item is bound to
*/
@property({ type: String, reflect: true })
public for: string | null = null;
/**
* Cache label element
*/
@query('#label')
private labelEl?: HTMLElement;
/**
* True, if there is no slotted content
*/
private isSlotEmpty = true;
/**
* @param node that should be checked
* @returns whether node can be ignored.
*/
private isIgnorable (node: Node): boolean {
return node.nodeType === document.COMMENT_NODE
|| isAllWhitespaceTextNode(node);
}
/**
* Checks slotted children nodes and updates component to refresh label and sub-label templates.
* @param event slotchange
* @returns {void}
*/
private checkSlotChildren = (event: Event): void => {
const slot = event.target as HTMLSlotElement;
this.isSlotEmpty = !slot.assignedNodes().filter(node => !this.isIgnorable(node)).length;
this.requestUpdate();
};
/**
* Handles aria-selected or aria-checked when toggle between single and multiple selection mode
* @returns {void}
**/
private multipleChanged (): void {
this.removeAttribute(this.multiple ? 'aria-selected' : 'aria-checked');
this.selectedChanged();
}
/**
* Handles aria when selected state changes
* @returns {void}
*/
private selectedChanged (): void {
this.setAttribute(this.multiple ? 'aria-checked' : 'aria-selected', String(this.selected));
}
/**
* Control State behaviour will update tabindex based on the property
* @returns {void}
*/
private typeChanged (): void {
const noInteraction = this.type === 'header' || this.type === 'divider' || this.disabled;
if (noInteraction) {
this.disableFocus();
}
else if (!this.disabled) {
this.enableFocus();
}
}
/**
* Invoked before update() to compute values needed during the update.
* @param changedProperties changed properties
* @returns {void}
*/
protected willUpdate (changedProperties: PropertyValues): void {
if (changedProperties.has('type')) {
this.typeChanged();
}
if (changedProperties.has('multiple')) {
this.multipleChanged();
}
else if (changedProperties.has('selected')) {
this.selectedChanged();
}
}
/**
* Get icon template if icon attribute is defined
*/
private get iconTemplate (): TemplateResult | undefined {
return this.icon !== null && this.icon !== undefined ? html`<ef-icon part="icon" .icon="${this.icon}"></ef-icon>` : undefined;
}
/**
* Get subLabel template if it is defined and no slot content present
*/
private get subLabelTemplate (): TemplateResult | undefined {
return this.subLabel && this.isSlotEmpty ? html`<div part="sub-label">${this.subLabel}</div>` : undefined;
}
/**
* Get label template if it is defined and no slot content present
*/
private get labelTemplate (): TemplateResult | undefined {
return this.label && this.isSlotEmpty ? html`${this.label}` : undefined;
}
/**
* Get template for `for` attribute.
* This is usually used with menus when an item needs to reference an element
*/
private get forTemplate (): TemplateResult | undefined {
return this.for ? html`<ef-icon icon="right"></ef-icon>` : undefined;
}
/**
* Get template for `multiple` attribute.
* This is usually used with lists, when an item can be part of a multiple selection
*/
private get multipleTemplate (): TemplateResult | undefined {
const multiple = this.multiple && (!this.type || this.type === 'text');
return multiple ? html`<ef-checkbox part="checkbox" .checked="${this.selected}" tabindex="-1"></ef-checkbox>` : undefined;
}
/**
* Return true if the item can be highlighted. True if not disabled and type is Text
* @prop {boolean} highlightable
* @returns whether element is highlightable
*/
public get highlightable (): boolean {
return !this.disabled && this.type !== 'header' && this.type !== 'divider';
}
/**
* Getter returning if the label is truncated
* @prop {boolean} isTruncated
* @returns whether element is truncated or not
*/
public get isTruncated (): boolean {
return !!(this.labelEl && (this.labelEl.offsetWidth < this.labelEl.scrollWidth));
}
/**
* A `TemplateResult` that will be used
* to render the updated internal template.
* @returns Render template
*/
protected render (): TemplateResult {
return html`
<div part="left">
${this.iconTemplate}
${this.multipleTemplate}
<slot name="left"></slot>
</div>
<div part="center" id="label">
${this.labelTemplate}
<slot @slotchange="${this.checkSlotChildren}"></slot>
${this.subLabelTemplate}
</div>
<div part="right">
<slot name="right"></slot>
${this.forTemplate}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'ef-item': Item;
}
}