aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/lists/list/list.tsx
blob: 61d2216e9a46cd25285537ceb943d6bcb6c8bdca (plain)
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
import {
  forwardRef,
  type CSSProperties,
  type HTMLAttributes,
  type OlHTMLAttributes,
  type ReactNode,
  type ForwardedRef,
} from 'react';
import type { Spacing } from '../../../../types';
import styles from './list.module.scss';

type OrderedListProps = Omit<OlHTMLAttributes<HTMLOListElement>, 'children'>;

type UnorderedListProps = Omit<HTMLAttributes<HTMLUListElement>, 'children'>;

type BaseListProps<O extends boolean, H extends boolean> = O extends true
  ? OrderedListProps
  : H extends true
    ? OrderedListProps
    : UnorderedListProps;

type AdditionalProps<O extends boolean, H extends boolean> = {
  /**
   * The list items.
   */
  children?: ReactNode;
  /**
   * Should the items marker be hidden?
   *
   * @default false
   */
  hideMarker?: boolean;
  /**
   * Should the list be ordered and hierarchical?
   *
   * @default false
   */
  isHierarchical?: H;
  /**
   * Should the list be inlined?
   *
   * @default false
   */
  isInline?: boolean;
  /**
   * Should the list be ordered?
   *
   * @default false
   */
  isOrdered?: O;
  /**
   * Define the spacing between list items.
   *
   * @default null
   */
  spacing?: Spacing | null;
};

type BuildClassNameConfig<O extends boolean, H extends boolean> = Pick<
  BaseListProps<O, H>,
  'className'
> &
  Pick<
    AdditionalProps<O, H>,
    'hideMarker' | 'isHierarchical' | 'isInline' | 'isOrdered'
  >;

const buildClassName = <O extends boolean, H extends boolean>({
  className = '',
  hideMarker,
  isHierarchical,
  isInline,
  isOrdered,
}: BuildClassNameConfig<O, H>) => {
  const orderedClassName = isHierarchical
    ? 'list--hierarchical'
    : 'list--ordered';
  const classNames: string[] = [
    isHierarchical || isOrdered ? orderedClassName : 'list--unordered',
    isInline ? 'list--inline' : 'list--stack',
    hideMarker ? 'list--no-marker' : 'list--has-marker',
    className,
  ].map((key) => styles[key]);

  if (className) classNames.push(className);

  return classNames.join(' ');
};

export type ListProps<O extends boolean, H extends boolean> = BaseListProps<
  O,
  H
> &
  AdditionalProps<O, H>;

const ListWithRef = <O extends boolean, H extends boolean>(
  {
    className,
    children,
    hideMarker = false,
    isHierarchical,
    isInline = false,
    isOrdered,
    spacing = null,
    style,
    ...props
  }: ListProps<O, H>,
  ref: ForwardedRef<
    O extends true
      ? HTMLOListElement
      : H extends true
        ? HTMLOListElement
        : HTMLUListElement
  >
) => {
  const itemSpacing = spacing === null ? 0 : `var(--spacing-${spacing})`;
  const listClass = buildClassName({
    className,
    hideMarker,
    isHierarchical,
    isInline,
    isOrdered,
  });
  const listStyles = {
    ...style,
    '--itemSpacing': itemSpacing,
  } as CSSProperties;

  return isHierarchical || isOrdered ? (
    <ol
      {...props}
      className={listClass}
      ref={ref as ForwardedRef<HTMLOListElement>}
      style={listStyles}
    >
      {children}
    </ol>
  ) : (
    <ul {...props} className={listClass} ref={ref} style={listStyles}>
      {children}
    </ul>
  );
};

/**
 * List component
 *
 * Render either an ordered or an unordered list.
 */
export const List = forwardRef(ListWithRef);