Skip to content

Commit d054c61

Browse files
committed
Merge branch 'main' into pr-tabs-final-selectedIndex-attempt
2 parents ebee7c1 + 8fc83c0 commit d054c61

File tree

18 files changed

+667
-537
lines changed

18 files changed

+667
-537
lines changed

apps/website/src/_state/component-statuses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const componentsStatuses: ComponentKitsStatuses = {
4040
},
4141
headless: {
4242
Accordion: 'Planned',
43+
Autocomplete: 'Draft',
4344
Carousel: 'Planned',
4445
Popover: 'Planned',
4546
Select: 'Draft',

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ export const Example01 = component$(() => {
2727
return (
2828
<PreviewCodeExample>
2929
<div q:slot="actualComponent">
30-
<AutocompleteRoot class="relative text-white">
31-
<AutocompleteLabel class="text-inherit font-semibold">
30+
<AutocompleteRoot class="relative">
31+
<AutocompleteLabel class=" font-semibold dark:text-white text-[#333333]">
3232
Personal Trainers ⚡
3333
</AutocompleteLabel>
3434
<AutocompleteTrigger class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
35-
<AutocompleteInput class="w-44 bg-inherit px-2 pr-6" />
35+
<AutocompleteInput class="w-44 bg-inherit px-2 pr-6 text-white" />
3636
<AutocompleteButton class="w-6 h-6 group absolute right-0">
3737
<svg
3838
xmlns="http://www.w3.org/2000/svg"
@@ -47,7 +47,7 @@ export const Example01 = component$(() => {
4747
</svg>
4848
</AutocompleteButton>
4949
</AutocompleteTrigger>
50-
<AutocompleteListbox class="w-full bg-[#1f2532] px-4 py-2 mt-2 rounded-sm border-[#7d95b3] border-[1px]">
50+
<AutocompleteListbox class="text-white w-full bg-[#1f2532] px-4 py-2 mt-2 rounded-sm border-[#7d95b3] border-[1px]">
5151
{trainers.map((trainer, index) => (
5252
<AutocompleteOption
5353
optionValue={trainer}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export const Example01 = component$(() => {
1515
return (
1616
<PreviewCodeExample>
1717
<div q:slot="actualComponent">
18-
<SelectRoot class="dark:bg-gray-700">
19-
<SelectLabel class="text-white font-semibold ml-2">
18+
<SelectRoot>
19+
<SelectLabel class=" font-semibold ml-2 text-[#333333] dark:text-white">
2020
Qwik Fruits
2121
</SelectLabel>
2222
<SelectTrigger class="flex justify-between items-center px-8 bg-[#1f2532] border-[#7d95b3] border-[1px] rounded-md p-4 group peer">
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {
2+
component$,
3+
$,
4+
Slot,
5+
useContext,
6+
type QwikIntrinsicElements,
7+
} from '@builder.io/qwik';
8+
import AutocompleteContextId from './autocomplete-context-id';
9+
10+
export type ButtonProps = QwikIntrinsicElements['button'];
11+
12+
export const AutocompleteButton = component$((props: ButtonProps) => {
13+
const contextService = useContext(AutocompleteContextId);
14+
15+
return (
16+
<button
17+
{...props}
18+
aria-expanded={contextService.isExpanded.value}
19+
// add their own custom onClick with our onClick functionality
20+
onClick$={[
21+
$(
22+
() =>
23+
(contextService.isExpanded.value = !contextService.isExpanded.value)
24+
),
25+
props.onClick$,
26+
]}
27+
tabIndex={-1}
28+
>
29+
<Slot />
30+
</button>
31+
);
32+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createContextId } from '@builder.io/qwik';
2+
import { AutocompleteContext } from './autocomplete-context.type';
3+
4+
const AutocompleteContextId =
5+
createContextId<AutocompleteContext>('autocomplete-root');
6+
7+
export default AutocompleteContextId;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Signal, QRL } from '@builder.io/qwik';
2+
3+
export interface AutocompleteContext {
4+
options: Signal<HTMLElement | undefined>[];
5+
filteredOptions: Signal<HTMLElement | undefined>[];
6+
selectedOption: Signal<string>;
7+
isExpanded: Signal<boolean>;
8+
triggerRef: Signal<HTMLElement | undefined>;
9+
listBoxRef: Signal<HTMLElement | undefined>;
10+
labelRef: Signal<HTMLElement | undefined>;
11+
listBoxId: string;
12+
inputId: string;
13+
activeOptionId: Signal<string | null>;
14+
inputValue: Signal<string>;
15+
focusInput$: QRL<(inputId: string) => void>;
16+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
component$,
3+
useSignal,
4+
useContext,
5+
useVisibleTask$,
6+
$,
7+
type Signal,
8+
type QwikKeyboardEvent,
9+
type QwikIntrinsicElements,
10+
} from '@builder.io/qwik';
11+
import AutocompleteContextId from './autocomplete-context-id';
12+
13+
export type InputProps = QwikIntrinsicElements['input'];
14+
15+
// Add required context here
16+
export const AutocompleteInput = component$((props: InputProps) => {
17+
const ref = useSignal<HTMLElement>();
18+
const contextService = useContext(AutocompleteContextId);
19+
20+
/*
21+
previously had useTask here, but noticed whenever it first renders,
22+
it won't focus the first option when hitting the down arrow key to open the listbox
23+
Also, all of our tests break on useTask, BUT it seems to work fine in the browser with useTask.
24+
Very odd.
25+
*/
26+
useVisibleTask$(({ track }) => {
27+
track(() => contextService.inputValue.value);
28+
29+
contextService.filteredOptions = contextService.options.filter(
30+
(option: Signal) => {
31+
const optionValue = option.value.getAttribute('optionValue');
32+
const inputValue = contextService.inputValue.value;
33+
34+
if (
35+
contextService.inputValue.value.length >= 0 &&
36+
document.activeElement === ref.value
37+
) {
38+
if (optionValue === inputValue) {
39+
contextService.isExpanded.value = false;
40+
} else if (optionValue.match(new RegExp(inputValue, 'i'))) {
41+
contextService.isExpanded.value = true;
42+
}
43+
} else {
44+
contextService.isExpanded.value = false;
45+
}
46+
47+
return optionValue.match(new RegExp(inputValue, 'i'));
48+
}
49+
);
50+
51+
// Probably better to refactor Signal type later
52+
contextService.options.map((option: Signal) => {
53+
if (
54+
!option.value
55+
.getAttribute('optionValue')
56+
.match(new RegExp(contextService.inputValue.value, 'i'))
57+
) {
58+
option.value.style.display = 'none';
59+
} else {
60+
option.value.style.display = '';
61+
}
62+
});
63+
});
64+
65+
return (
66+
<input
67+
data-autocomplete-input-id={contextService.inputId}
68+
ref={ref}
69+
role="combobox"
70+
id={contextService.inputId}
71+
aria-autocomplete="list"
72+
aria-controls={contextService.listBoxId}
73+
bind:value={contextService.inputValue}
74+
onKeyDown$={[
75+
$((e: QwikKeyboardEvent) => {
76+
if (e.key === 'ArrowDown') {
77+
contextService.isExpanded.value = true;
78+
contextService.filteredOptions[0]?.value?.focus();
79+
}
80+
}),
81+
props.onKeyDown$,
82+
]}
83+
{...props}
84+
/>
85+
);
86+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
Slot,
3+
useSignal,
4+
useContext,
5+
component$,
6+
type QwikIntrinsicElements,
7+
} from '@builder.io/qwik';
8+
import AutocompleteContextId from './autocomplete-context-id';
9+
10+
export type AutocompleteLabelProps = QwikIntrinsicElements['label'];
11+
12+
export const AutocompleteLabel = component$((props: AutocompleteLabelProps) => {
13+
const ref = useSignal<HTMLElement>();
14+
const contextService = useContext(AutocompleteContextId);
15+
contextService.labelRef = ref;
16+
17+
return (
18+
<label {...props} for={contextService.inputId}>
19+
<Slot />
20+
</label>
21+
);
22+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
$,
3+
useSignal,
4+
useContext,
5+
component$,
6+
Slot,
7+
type QwikIntrinsicElements,
8+
type QwikKeyboardEvent,
9+
} from '@builder.io/qwik';
10+
import AutocompleteContextId from './autocomplete-context-id';
11+
12+
export type ListboxProps = {
13+
isExpanded?: boolean;
14+
} & QwikIntrinsicElements['ul'];
15+
16+
export const AutocompleteListbox = component$((props: ListboxProps) => {
17+
const ref = useSignal<HTMLElement>();
18+
const contextService = useContext(AutocompleteContextId);
19+
contextService.listBoxRef = ref;
20+
21+
return (
22+
<ul
23+
id={contextService.listBoxId}
24+
ref={ref}
25+
style={`
26+
display: ${
27+
contextService.isExpanded.value ? 'block' : 'none'
28+
}; position: absolute; z-index: 1; ${props.style}
29+
`}
30+
role="listbox"
31+
{...props}
32+
// aria-label={!contextService.labelRef.value ? contextService.inputValue.value : undefined}
33+
onKeyDown$={[
34+
$((e: QwikKeyboardEvent) => {
35+
const availableOptions = contextService.filteredOptions.map(
36+
(option) => option.value
37+
);
38+
39+
const target = e.target as HTMLElement;
40+
const currentIndex = availableOptions.indexOf(target);
41+
42+
if (e.key === 'ArrowDown') {
43+
if (currentIndex === availableOptions.length - 1) {
44+
availableOptions[0]?.focus();
45+
} else {
46+
availableOptions[currentIndex + 1]?.focus();
47+
}
48+
}
49+
50+
if (e.key === 'ArrowUp') {
51+
if (currentIndex <= 0) {
52+
availableOptions[availableOptions.length - 1]?.focus();
53+
} else {
54+
availableOptions[currentIndex - 1]?.focus();
55+
}
56+
}
57+
58+
if (e.key === 'Home') {
59+
availableOptions[0]?.focus();
60+
}
61+
62+
if (e.key === 'End') {
63+
availableOptions[availableOptions.length - 1]?.focus();
64+
}
65+
}),
66+
props.onKeyDown$,
67+
]}
68+
>
69+
<Slot />
70+
</ul>
71+
);
72+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
Slot,
3+
useSignal,
4+
useContext,
5+
component$,
6+
$,
7+
type QwikIntrinsicElements,
8+
type QwikKeyboardEvent,
9+
} from '@builder.io/qwik';
10+
import AutocompleteContextId from './autocomplete-context-id';
11+
12+
export type OptionProps = {
13+
optionValue: string;
14+
disabled?: boolean;
15+
} & QwikIntrinsicElements['li'];
16+
17+
export const AutocompleteOption = component$((props: OptionProps) => {
18+
const ref = useSignal<HTMLElement>();
19+
const contextService = useContext(AutocompleteContextId);
20+
21+
contextService.options = [...contextService.options, ref];
22+
23+
return (
24+
<li
25+
ref={ref}
26+
role="option"
27+
tabIndex={props.disabled ? -1 : 0}
28+
aria-disabled={props.disabled}
29+
onClick$={[
30+
$(() => {
31+
if (!props.disabled) {
32+
contextService.inputValue.value = props.optionValue;
33+
contextService.isExpanded.value = false;
34+
}
35+
}),
36+
props.onClick$,
37+
]}
38+
onKeyDown$={[
39+
$((e: QwikKeyboardEvent) => {
40+
if ((e.key === 'Enter' || e.key === ' ') && !props.disabled) {
41+
contextService.inputValue.value = props.optionValue;
42+
contextService.isExpanded.value = false;
43+
contextService.focusInput$(contextService.inputId);
44+
}
45+
}),
46+
props.onKeyDown$,
47+
]}
48+
{...props}
49+
>
50+
<Slot />
51+
</li>
52+
);
53+
});

0 commit comments

Comments
 (0)