Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

listItems typing fix #3176

Merged
merged 11 commits into from
Dec 12, 2021
85 changes: 51 additions & 34 deletions js/src/common/helpers/listItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,41 @@ import Component, { ComponentAttrs } from '../Component';
import Separator from '../components/Separator';
import classList from '../utils/classList';

export interface ModdedVnodeAttrs {
type ModdedVnodeAttrs = {
itemClassName?: string;
key?: string;
}
};

export type ModdedVnode<Attrs> = Mithril.Vnode<ModdedVnodeAttrs, Component<Attrs> | {}> & {
itemName?: string;
itemClassName?: string;
tag: Mithril.Vnode['tag'] & {
isListItem?: boolean;
isActive?: (attrs: ComponentAttrs) => boolean;
};
type ModdedTag = Mithril.Vnode['tag'] & {
isListItem?: boolean;
isActive?: (attrs: ComponentAttrs) => boolean;
};

function isSeparator<Attrs>(item: ModdedVnode<Attrs>): boolean {
return item.tag === Separator;
type ModdedVnode = Mithril.Vnode<ModdedVnodeAttrs> & { itemName?: string; itemClassName?: string; tag: ModdedTag };

type ModdedChild = ModdedVnode | string | number | boolean | null | undefined;
type ModdedChildArray = ModdedChildren[];
type ModdedChildren = ModdedChild | ModdedChildArray;

/**
* This type represents an element of a list returned by `ItemList.toArray()`,
* coupled with some static properties used on various components.
*/
export type ModdedChildrenWithItemName = ModdedChildren & { itemName?: string };

function isVnode(item: ModdedChildren): item is Mithril.Vnode {
return typeof item === 'object' && item !== null && 'tag' in item;
}

function withoutUnnecessarySeparators<Attrs>(items: ModdedVnode<Attrs>[]): ModdedVnode<Attrs>[] {
const newItems: ModdedVnode<Attrs>[] = [];
let prevItem: ModdedVnode<Attrs>;
function isSeparator(item: ModdedChildren): boolean {
return isVnode(item) && item.tag === Separator;
}

items.filter(Boolean).forEach((item: Mithril.Vnode, i: number) => {
function withoutUnnecessarySeparators(items: ModdedChildrenWithItemName[]): ModdedChildrenWithItemName[] {
const newItems: ModdedChildrenWithItemName[] = [];
let prevItem: ModdedChildren;

items.filter(Boolean).forEach((item, i: number) => {
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
prevItem = item;
newItems.push(item);
Expand All @@ -42,38 +54,43 @@ function withoutUnnecessarySeparators<Attrs>(items: ModdedVnode<Attrs>[]): Modde
* By default, this tag is an `<li>` tag, but this is customisable through the
* second function parameter, `customTag`.
*/
export default function listItems<Attrs extends Record<string, unknown>>(
rawItems: ModdedVnode<Attrs> | ModdedVnode<Attrs>[],
customTag: string | Component<Attrs> = 'li',
export default function listItems<Attrs extends ComponentAttrs>(
rawItems: ModdedChildrenWithItemName[],
customTag: VnodeElementTag<Attrs> = 'li',
attributes: Attrs = {} as Attrs
): Mithril.Vnode[] {
const items = rawItems instanceof Array ? rawItems : [rawItems];
const Tag = customTag;

return withoutUnnecessarySeparators(items).map((item: ModdedVnode<Attrs>) => {
const isListItem = item.tag?.isListItem;
const active = item.tag?.isActive?.(item.attrs);
const className = item.attrs?.itemClassName || item.itemClassName;
return withoutUnnecessarySeparators(items).map((item) => {
const classes = [item.itemName && `item-${item.itemName}`];

if (!isVnode(item)) {
return (
<Tag className={classList(classes)} {...attributes}>
{item}
</Tag>
);
}

if (isListItem) {
if (item.tag.isListItem) {
item.attrs = item.attrs || {};
item.attrs.key = item.attrs.key || item.itemName;
item.key = item.attrs.key;

return item;
}

const node: Mithril.Vnode = isListItem ? (
item
) : (
// @ts-expect-error `Component` does not have any construct or call signatures
<Tag
className={classList([className, item.itemName && `item-${item.itemName}`, active && 'active'])}
key={item?.attrs?.key || item.itemName}
{...attributes}
>
classes.push(item.attrs?.itemClassName || item.itemClassName);

if (item.tag.isActive?.(item.attrs)) {
classes.push('active');
}

return (
<Tag className={classList(classes)} key={item?.attrs?.key || item.itemName} {...attributes}>
{item}
</Tag>
);

return node;
});
}