Skip to content

Commit edaa979

Browse files
feat(combobox): adding floating UI feature parity for the listbox
1 parent 742ccfd commit edaa979

File tree

4 files changed

+92
-69
lines changed

4 files changed

+92
-69
lines changed

apps/website/src/routes/docs/headless/(components)/combobox/examples.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ export const HeroExample = component$(() => {
9797
</ComboboxControl>
9898
<ComboboxPortal>
9999
<ComboboxListbox
100-
placement="bottom"
101-
setOffset={8}
100+
flip={true}
101+
offset={8}
102102
class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]"
103103
/>
104104
</ComboboxPortal>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@commitlint/config-conventional": "^17.7.0",
2828
"@cypress/code-coverage": "^3.11.0",
2929
"@cypress/vite-dev-server": "^5.0.6",
30+
"@floating-ui/core": "1.4.1",
3031
"@floating-ui/dom": "^1.5.1",
3132
"@frsource/cypress-plugin-visual-regression-diff": "^3.3.10",
3233
"@jscutlery/semver": "^3.1.0",

packages/kit-headless/src/components/combobox/combobox-listbox.tsx

Lines changed: 84 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,100 +4,127 @@ import {
44
useVisibleTask$,
55
type QwikIntrinsicElements,
66
} from '@builder.io/qwik';
7+
78
import {
89
ReferenceElement,
9-
arrow,
1010
autoUpdate,
1111
computePosition,
12-
flip,
13-
offset,
14-
shift,
12+
offset as _offset,
13+
flip as _flip,
14+
shift as _shift,
15+
arrow as _arrow,
16+
size as _size,
17+
autoPlacement as _autoPlacement,
18+
hide as _hide,
19+
inline as _inline,
20+
type Placement,
21+
type DetectOverflowOptions,
22+
type ComputePositionReturn,
1523
} from '@floating-ui/dom';
24+
25+
import type {
26+
ShiftOptions,
27+
OffsetOptions,
28+
ArrowOptions,
29+
FlipOptions,
30+
SizeOptions,
31+
AutoPlacementOptions,
32+
HideOptions,
33+
InlineOptions,
34+
Platform,
35+
} from '@floating-ui/core';
36+
1637
import ComboboxContextId from './combobox-context-id';
1738
import type { ComboboxContext, Option } from './combobox-context.type';
1839

19-
// type ArrowData = { element: HTMLElement; padding?: number | undefined };
20-
2140
export type ComboboxListboxProps = {
22-
// come back to shift later
23-
arrowData?: { element: HTMLElement; padding?: number | undefined };
24-
setArrow?: boolean;
25-
setShift?: {
26-
mainAxis?: boolean;
27-
crossAxis?: boolean;
28-
limiter?: {
29-
fn: (state: unknown) => unknown;
30-
options?: unknown;
31-
};
32-
};
33-
setOffset?:
34-
| number
35-
| {
36-
mainAxis?: number;
37-
crossAxis?: number;
38-
alignmentAxis?: number | null;
39-
};
40-
setFlip?: boolean;
41-
placement?:
42-
| 'top'
43-
| 'top-start'
44-
| 'top-end'
45-
| 'right'
46-
| 'right-start'
47-
| 'right-end'
48-
| 'bottom'
49-
| 'bottom-start'
50-
| 'bottom-end'
51-
| 'left'
52-
| 'left-start'
53-
| 'left-end';
41+
// main floating UI props
42+
placement?: Placement;
5443
ancestorScroll?: boolean;
5544
ancestorResize?: boolean;
5645
elementResize?: boolean;
5746
layoutShift?: boolean;
5847
animationFrame?: boolean;
48+
49+
// middleware
50+
offset?: OffsetOptions;
51+
shift?: Partial<ShiftOptions & DetectOverflowOptions> | boolean;
52+
flip?: FlipOptions | boolean;
53+
arrow?: ArrowOptions;
54+
size?: SizeOptions;
55+
autoPlacement?: AutoPlacementOptions | boolean;
56+
hide?: HideOptions | boolean;
57+
inline?: InlineOptions | boolean;
58+
onPositionComputed?: (resolvedData: ComputePositionReturn) => void;
59+
60+
// misc
61+
transform: string;
62+
platform: Platform;
5963
} & QwikIntrinsicElements['ul'];
6064

6165
export const ComboboxListbox = component$(
6266
<O extends Option = Option>({
63-
setOffset,
64-
setFlip = true,
67+
offset,
68+
flip = true,
6569
placement = 'bottom',
66-
setShift,
67-
setArrow,
68-
arrowData,
70+
shift,
71+
arrow,
72+
size,
73+
hide,
74+
inline,
75+
autoPlacement = false,
6976
ancestorScroll = true,
7077
ancestorResize = true,
7178
elementResize = true,
7279
animationFrame = false,
80+
onPositionComputed,
81+
transform,
82+
platform,
7383
...props
7484
}: ComboboxListboxProps) => {
7585
const context = useContext<ComboboxContext<O>>(ComboboxContextId);
7686
const listboxId = `${context.localId}-listbox`;
7787

78-
useVisibleTask$(function setListboxPosition({ cleanup }) {
79-
// Our settings from Floating UI
88+
useVisibleTask$(function setFloatingUIConfig({ cleanup }) {
8089
function updatePosition() {
81-
const middleware = [offset(setOffset), setFlip && flip(), setShift && shift()];
90+
const middleware = [_offset(offset), arrow && _arrow(arrow), size && _size(size)];
8291

83-
if (setArrow && arrowData) {
84-
middleware.push(arrow(arrowData));
85-
}
92+
// offers a bool to turn on or off default config, or customize it.
93+
const middlewareFunctions = [_flip, _shift, _autoPlacement, _hide, _inline];
94+
const middlewareProps = [flip, shift, autoPlacement, hide, inline];
95+
96+
middlewareFunctions.forEach((func, index) => {
97+
const isMiddlewareEnabled = middlewareProps[index];
98+
99+
if (isMiddlewareEnabled) {
100+
const middlewareConfig =
101+
isMiddlewareEnabled === true ? undefined : isMiddlewareEnabled;
102+
middleware.push(func(middlewareConfig));
103+
}
104+
});
86105

87106
computePosition(
88107
context.inputRef.value as ReferenceElement,
89108
context.listboxRef.value as HTMLElement,
90109
{
91-
placement: placement,
92-
middleware: middleware,
110+
placement,
111+
middleware,
112+
platform,
93113
},
94-
).then(({ x, y }) => {
114+
).then((resolvedData) => {
115+
const { x, y } = resolvedData;
95116
if (context.listboxRef.value) {
96117
Object.assign(context.listboxRef.value.style, {
97118
left: `${x}px`,
98119
top: `${y}px`,
120+
transform,
99121
});
100122
}
123+
124+
// user-provided resolved code
125+
if (onPositionComputed) {
126+
onPositionComputed(resolvedData);
127+
}
101128
});
102129
}
103130

@@ -109,10 +136,10 @@ export const ComboboxListbox = component$(
109136
context.listboxRef.value,
110137
updatePosition,
111138
{
112-
ancestorScroll: ancestorScroll,
113-
ancestorResize: ancestorResize,
114-
elementResize: elementResize,
115-
animationFrame: animationFrame,
139+
ancestorScroll,
140+
ancestorResize,
141+
elementResize,
142+
animationFrame,
116143
},
117144
);
118145

@@ -128,9 +155,7 @@ export const ComboboxListbox = component$(
128155
id={listboxId}
129156
ref={context.listboxRef}
130157
aria-label={
131-
context.labelRef.value
132-
? context.labelRef.value?.innerText
133-
: context.inputRef.value?.value
158+
context.labelRef.value ? context.labelRef.value?.innerText : 'Suggestions'
134159
}
135160
role="listbox"
136161
hidden={!context.isListboxOpenSig.value}

pnpm-lock.yaml

Lines changed: 5 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)