Skip to content

Commit 24f47f1

Browse files
committed
refactor(list-renderer): use limel-list-item to render the list items
The new private `limel-list-item` component centralizes the styles and features that are required to render list items. It also fixes some styling and interactive details. This commit also removes `limel-list`'s dependency to MDC's styles for rendering lists. BREAKING CHANGE: If the deprecated `iconColor` is defined as a prop for list items, it will have no visible visual effect any longer. The `iconColor` prop however, is not removed yet. Therefore your builds will not fail! But you are highly encouraged to use the `Icon` interface instead, to set the `color` or `backgroundColor` of the icons, as well described in the documentations. The deprecated `iconColor` property will be removed soon.
1 parent 4c689fd commit 24f47f1

File tree

9 files changed

+110
-602
lines changed

9 files changed

+110
-602
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
limel-list {
2-
// Default icon color for the list if not
2+
// Default icon background color for the list if not
33
// set by list items
4-
--icon-background-color: rgb(var(--color-magenta-default));
4+
--icon-background-color: rgb(var(--color-glaucous-dark));
55
}
Lines changed: 29 additions & 259 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
import { ListItem } from './list-item.types';
22
import { ListSeparator } from '../../global/shared-types/separator.types';
3-
import { MenuItem } from '../menu/menu.types';
43
import { h } from '@stencil/core';
5-
import { CheckboxTemplate } from '../checkbox/checkbox.template';
64
import { ListRendererConfig } from './list-renderer-config';
7-
import { RadioButtonTemplate } from '../radio-button-group/radio-button.template';
8-
import {
9-
getIconColor,
10-
getIconName,
11-
getIconTitle,
12-
} from '../icon/get-icon-props';
13-
import { isEmpty } from 'lodash-es';
145

156
export class ListRenderer {
167
private defaultConfig: ListRendererConfig = {
@@ -20,11 +11,6 @@ export class ListRenderer {
2011

2112
private config: ListRendererConfig;
2213

23-
private hasIcons: boolean;
24-
private twoLines: boolean;
25-
private avatarList: boolean;
26-
private commandKey: boolean;
27-
2814
private applyTabIndexToItemAtIndex: number;
2915

3016
public render(
@@ -34,17 +20,6 @@ export class ListRenderer {
3420
items = items || [];
3521
this.config = { ...this.defaultConfig, ...config };
3622

37-
this.twoLines = items.some((item) => {
38-
return 'secondaryText' in item && !!item.secondaryText;
39-
});
40-
41-
this.hasIcons = items.some((item) => {
42-
return 'icon' in item && !!item.icon;
43-
});
44-
45-
this.avatarList = this.config.badgeIcons && this.hasIcons;
46-
const selectableListTypes = ['selectable', 'radio', 'checkbox'];
47-
4823
let role;
4924
switch (this.config.type) {
5025
case 'checkbox': {
@@ -63,19 +38,12 @@ export class ListRenderer {
6338
this.applyTabIndexToItemAtIndex =
6439
this.getIndexForWhichToApplyTabIndex(items);
6540

66-
const classNames = {
67-
'mdc-deprecated-list': true,
68-
'mdc-deprecated-list--two-line': this.twoLines,
69-
selectable: selectableListTypes.includes(this.config.type),
70-
'mdc-deprecated-list--avatar-list': this.avatarList,
71-
'list--compact':
72-
this.twoLines &&
73-
this.commandKey &&
74-
['small', 'x-small'].includes(this.config.iconSize),
75-
};
76-
7741
return (
78-
<ul class={classNames} role={role} aria-orientation="vertical">
42+
<ul
43+
class="mdc-deprecated-list"
44+
role={role}
45+
aria-orientation="vertical"
46+
>
7947
{items.map(this.renderListItem)}
8048
</ul>
8149
);
@@ -140,37 +108,37 @@ export class ListRenderer {
140108
);
141109
}
142110

143-
if (['radio', 'checkbox'].includes(this.config.type)) {
144-
return this.renderVariantListItem(this.config, item, index);
145-
}
146-
147-
const classNames = {
148-
'mdc-deprecated-list-item': true,
149-
'mdc-deprecated-list-item--disabled': item.disabled,
150-
'mdc-deprecated-list-item--selected': item.selected,
151-
'has-primary-component': this.hasPrimaryComponent(item),
152-
};
153-
154111
const attributes: { tabindex?: string } = {};
155112
if (index === this.applyTabIndexToItemAtIndex) {
156113
attributes.tabindex = '0';
157114
}
158115

116+
let itemType: 'radio' | 'checkbox' | 'option' | 'listitem';
117+
if (this.config.type === 'radio' || this.config.type === 'checkbox') {
118+
itemType = this.config.type;
119+
} else if (this.config.type === 'selectable') {
120+
itemType = 'option';
121+
} else {
122+
itemType = 'listitem';
123+
}
124+
159125
return (
160-
<li
161-
class={classNames}
162-
aria-disabled={item.disabled ? 'true' : 'false'}
163-
aria-selected={item.selected ? 'true' : 'false'}
164-
data-index={index}
126+
<limel-list-item
127+
class="mdc-deprecated-list-item" // required for keyboard navigation with arrow keys
165128
{...attributes}
166-
>
167-
{this.renderIcon(this.config, item)}
168-
{this.renderPicture(item)}
169-
{this.getPrimaryComponent(item)}
170-
{this.renderText(item)}
171-
{this.twoLines && this.avatarList ? this.renderDivider() : null}
172-
{this.renderActionMenu(item.actions)}
173-
</li>
129+
data-index={index}
130+
type={itemType}
131+
text={item.text}
132+
secondaryText={item.secondaryText}
133+
icon={item.icon}
134+
image={item.image}
135+
primaryComponent={item.primaryComponent}
136+
badgeIcon={this.config.badgeIcons}
137+
iconSize={this.config.iconSize}
138+
selected={item.selected}
139+
disabled={item.disabled}
140+
actions={item.actions}
141+
/>
174142
);
175143
};
176144

@@ -179,202 +147,4 @@ export class ListRenderer {
179147
return <h2 class="limel-list-divider-title">{item.text}</h2>;
180148
}
181149
};
182-
183-
private getPrimaryComponent(item: ListItem): Element {
184-
if (!this.hasPrimaryComponent(item)) {
185-
return;
186-
}
187-
188-
const PrimaryComponent = item.primaryComponent.name;
189-
const props = item.primaryComponent.props;
190-
191-
return <PrimaryComponent {...props} />;
192-
}
193-
194-
private hasPrimaryComponent = (item: ListItem) => {
195-
return !!item?.primaryComponent?.name;
196-
};
197-
198-
/**
199-
* Render the text of the list item
200-
*
201-
* @param item - the list item
202-
* @returns the text for the list item
203-
*/
204-
private renderText = (item: ListItem) => {
205-
if (this.isSimpleItem(item)) {
206-
return (
207-
<span class="mdc-deprecated-list-item__text">{item.text}</span>
208-
);
209-
}
210-
211-
return (
212-
<div class="mdc-deprecated-list-item__text">
213-
<div class="mdc-deprecated-list-item__primary-command-text">
214-
<div class="mdc-deprecated-list-item__primary-text">
215-
{item.text}
216-
</div>
217-
</div>
218-
<div class="mdc-deprecated-list-item__secondary-text">
219-
{item.secondaryText}
220-
</div>
221-
</div>
222-
);
223-
};
224-
225-
private isSimpleItem = (item: ListItem): boolean => {
226-
return !('secondaryText' in item);
227-
};
228-
229-
/**
230-
* Render an icon for a list item
231-
*
232-
* @param config - the config object, passed on from the `renderListItem` function
233-
* @param item - the list item
234-
* @returns the icon element
235-
*/
236-
private renderIcon = (config: ListRendererConfig, item: ListItem) => {
237-
const style: any = {};
238-
const name = getIconName(item.icon);
239-
if (!name) {
240-
return;
241-
}
242-
243-
const color = getIconColor(item.icon, item.iconColor);
244-
const title = getIconTitle(item.icon);
245-
246-
if (color) {
247-
if (config.badgeIcons) {
248-
style['--icon-background-color'] = color;
249-
} else {
250-
style.color = color;
251-
}
252-
}
253-
254-
return (
255-
<limel-icon
256-
badge={config.badgeIcons}
257-
class="mdc-deprecated-list-item__graphic"
258-
name={name}
259-
style={style}
260-
size={config.iconSize}
261-
aria-label={title}
262-
aria-hidden={title ? null : 'true'}
263-
/>
264-
);
265-
};
266-
267-
private renderPicture(item: ListItem) {
268-
const image = item.image;
269-
if (isEmpty(image)) {
270-
return;
271-
}
272-
273-
return <img src={image.src} alt={image.alt} loading="lazy" />;
274-
}
275-
276-
private renderDivider = () => {
277-
const classes = {
278-
'mdc-deprecated-list-divider': true,
279-
'mdc-deprecated-list-divider--inset': true,
280-
};
281-
if (this.config.iconSize) {
282-
classes[this.config.iconSize] = true;
283-
}
284-
285-
return <hr class={classes} />;
286-
};
287-
288-
private renderActionMenu = (actions: Array<MenuItem | ListSeparator>) => {
289-
if (!actions || actions.length === 0) {
290-
return;
291-
}
292-
293-
return (
294-
<limel-menu
295-
class="mdc-deprecated-list-item__meta"
296-
items={actions}
297-
openDirection="left-start"
298-
>
299-
<limel-icon-button
300-
class="action-menu-trigger"
301-
slot="trigger"
302-
icon="menu_2"
303-
/>
304-
</limel-menu>
305-
);
306-
};
307-
308-
private renderVariantListItem = (
309-
config: ListRendererConfig,
310-
item: ListItem,
311-
index: number
312-
) => {
313-
let itemTemplate;
314-
if (config.type === 'radio') {
315-
itemTemplate = (
316-
<RadioButtonTemplate
317-
id={`c_${index}`}
318-
checked={item.selected}
319-
disabled={item.disabled}
320-
/>
321-
);
322-
} else if (config.type === 'checkbox') {
323-
itemTemplate = (
324-
<CheckboxTemplate
325-
id={`c_${index}`}
326-
checked={item.selected}
327-
disabled={item.disabled}
328-
/>
329-
);
330-
}
331-
332-
const classNames = {
333-
'mdc-deprecated-list-item': true,
334-
'mdc-deprecated-list-item--disabled': item.disabled,
335-
'mdc-deprecated-list-item__text': !item.secondaryText,
336-
'has-primary-component': this.hasPrimaryComponent(item),
337-
};
338-
339-
const attributes: { tabindex?: string } = {};
340-
if (index === this.applyTabIndexToItemAtIndex) {
341-
attributes.tabindex = '0';
342-
}
343-
344-
return (
345-
<li
346-
class={classNames}
347-
role={config.type}
348-
aria-checked={item.selected ? 'true' : 'false'}
349-
aria-disabled={item.disabled ? 'true' : 'false'}
350-
data-index={index}
351-
{...attributes}
352-
>
353-
{this.renderVariantListItemContent(config, item, itemTemplate)}
354-
</li>
355-
);
356-
};
357-
358-
private renderVariantListItemContent = (
359-
config: ListRendererConfig,
360-
item: ListItem,
361-
itemTemplate: any
362-
) => {
363-
if (this.hasIcons) {
364-
return [
365-
item.icon ? this.renderIcon(config, item) : null,
366-
this.getPrimaryComponent(item),
367-
this.renderText(item),
368-
<div class="mdc-deprecated-list-item__meta">
369-
{itemTemplate}
370-
</div>,
371-
];
372-
}
373-
374-
return [
375-
<div class="mdc-deprecated-list-item__graphic">{itemTemplate}</div>,
376-
this.getPrimaryComponent(item),
377-
this.renderText(item),
378-
];
379-
};
380150
}

0 commit comments

Comments
 (0)