-
Notifications
You must be signed in to change notification settings - Fork 147
/
Copy pathcombobox-inline.tsx
173 lines (143 loc) · 5.15 KB
/
combobox-inline.tsx
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
import { type JSXNode, Component } from '@builder.io/qwik';
import { HComboboxRootImpl, HComboboxRootImplProps } from './combobox-root';
import { HComboboxItem as InternalComboboxItem } from './combobox-item';
import { HComboboxItemLabel as InternalComboboxItemLabel } from './combobox-item-label';
import { HComboboxEmpty as InternalComboboxEmpty } from './combobox-empty';
import { HComboboxErrorMessage } from './combobox-error-message';
export type TItemsMap = Map<
number,
{ value: string; displayValue: string; disabled: boolean }
>;
export type InternalComboboxProps = {
/** When a value is passed, we check if it's an actual item value, and get its index at pre-render time.
**/
_valuePropIndex?: number | null;
_value?: string;
/** Checks if the consumer added the label in their JSX */
_label?: boolean;
/** Our source of truth for the items. We get this at pre-render time in the inline component, that way we do not need to call native methods such as textContent.
**/
_itemsMap?: TItemsMap;
comboboxItemComponent?: typeof InternalComboboxItem;
comboboxItemLabelComponent?: typeof InternalComboboxItemLabel;
};
/*
This is an inline component. An example use case of an inline component to get the proper indexes with CSR. See issue #4757
for more information.
*/
export const HComboboxRoot: Component<InternalComboboxProps & HComboboxRootImplProps> = (
props: InternalComboboxProps & HComboboxRootImplProps,
) => {
const {
children: myChildren,
comboboxItemComponent: UserItem,
comboboxItemLabelComponent: UserItemLabel,
...rest
} = props;
const HComboboxItem = UserItem ?? InternalComboboxItem;
const HComboboxItemLabel = UserItemLabel ?? InternalComboboxItemLabel;
const HComboboxEmpty = InternalComboboxEmpty;
// recursively extracts the markup (ex: styling characters based on search)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getInnerText(node: any): string {
if (typeof node === 'string') return node;
if (Array.isArray(node)) return node.map(getInnerText).join('');
if (node && typeof node === 'object') {
if ('props' in node && 'children' in node.props) {
return getInnerText(node.props.children);
} else if ('children' in node) {
return getInnerText(node.children);
}
}
return '';
}
// source of truth
const itemsMap = new Map();
let currItemIndex = 0;
let isItemDisabled = false;
let givenItemValue = null;
let valuePropIndex = null;
let _value;
let hasEmptyComp = false;
let hasErrorComp = false;
const childrenToProcess = (
Array.isArray(myChildren) ? [...myChildren] : [myChildren]
) as Array<JSXNode>;
while (childrenToProcess.length) {
const child = childrenToProcess.shift();
if (!child) {
continue;
}
if (Array.isArray(child)) {
childrenToProcess.unshift(...child);
continue;
}
switch (child.type) {
case HComboboxItem: {
// get the index of the current option
child.props._index = currItemIndex;
isItemDisabled = child.props.disabled === true;
if (child.props.value) {
givenItemValue = child.props.value;
}
// the default case isn't handled here, so we need to process the children to get to the label component
if (child.props.children) {
const childChildren = Array.isArray(child.props.children)
? [...child.props.children]
: [child.props.children];
childrenToProcess.unshift(...childChildren);
}
break;
}
case HComboboxItemLabel: {
const displayValue = getInnerText(child.props.children);
// distinct value, or the display value is the same as the value
const value = (givenItemValue !== null ? givenItemValue : displayValue) as string;
itemsMap.set(currItemIndex, { value, displayValue, disabled: isItemDisabled });
if (props.value && props.multiple) {
throw new Error(
`Qwik UI: When in multiple selection mode, the value prop is disabled. Use the bind:value prop's initial signal value instead.`,
);
}
// if the current option value is equal to the initial value
if (value === props.value) {
// minus one because it is incremented already in SelectOption
valuePropIndex = currItemIndex;
_value = value;
}
// increment after processing children
currItemIndex++;
break;
}
case HComboboxEmpty: {
hasEmptyComp = true;
break;
}
case HComboboxErrorMessage: {
hasErrorComp = true;
break;
}
default: {
if (child) {
const anyChildren = Array.isArray(child.children)
? [...child.children]
: [child.children];
childrenToProcess.unshift(...(anyChildren as JSXNode[]));
}
break;
}
}
}
return (
<HComboboxRootImpl
{...rest}
_valuePropIndex={valuePropIndex}
_value={_value}
_itemsMap={itemsMap}
hasEmptyComp={hasEmptyComp}
hasErrorComp={hasErrorComp}
>
{props.children}
</HComboboxRootImpl>
);
};