Skip to content

Commit 21a6eed

Browse files
Merge pull request #380 from thejackshelton/autocomplete-improvements
refactor(accordion & autocomplete): autocomplete preventdefault, acco…
2 parents 37dbb74 + 598a22c commit 21a6eed

File tree

14 files changed

+199
-153
lines changed

14 files changed

+199
-153
lines changed

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { component$, Slot } from '@builder.io/qwik';
22
import {
3-
AutocompleteButton,
4-
AutocompleteInput,
53
AutocompleteLabel,
6-
AutocompleteListbox,
7-
AutocompleteOption,
84
AutocompleteRoot,
9-
AutocompleteTrigger
5+
AutocompleteControl,
6+
AutocompleteInput,
7+
AutocompleteTrigger,
8+
AutocompleteListbox,
9+
AutocompleteOption
1010
} from '@qwik-ui/headless';
11+
1112
import { PreviewCodeExample } from '../../../_components/preview-code-example/preview-code-example';
1213

1314
const trainers = [
@@ -31,9 +32,9 @@ export const Example01 = component$(() => {
3132
<AutocompleteLabel class=" font-semibold dark:text-white text-[#333333]">
3233
Personal Trainers ⚡
3334
</AutocompleteLabel>
34-
<AutocompleteTrigger class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
35+
<AutocompleteControl class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
3536
<AutocompleteInput class="w-44 bg-inherit px-2 pr-6 text-white" />
36-
<AutocompleteButton class="w-6 h-6 group absolute right-0">
37+
<AutocompleteTrigger class="w-6 h-6 group absolute right-0">
3738
<svg
3839
xmlns="http://www.w3.org/2000/svg"
3940
viewBox="0 0 24 24"
@@ -45,8 +46,8 @@ export const Example01 = component$(() => {
4546
>
4647
<polyline points="6 9 12 15 18 9"></polyline>
4748
</svg>
48-
</AutocompleteButton>
49-
</AutocompleteTrigger>
49+
</AutocompleteTrigger>
50+
</AutocompleteControl>
5051
<AutocompleteListbox class="text-white w-full bg-[#1f2532] px-4 py-2 mt-2 rounded-sm border-[#7d95b3] border-[1px]">
5152
{trainers.map((trainer, index) => (
5253
<AutocompleteOption

apps/website/src/routes/docs/headless/(components)/autocomplete/index.mdx

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
AutocompleteLabel,
88
AutocompleteTrigger,
99
AutocompleteInput,
10-
AutocompleteButton,
1110
AutocompleteListbox,
1211
AutocompleteOption,
1312
} from '@qwik-ui/headless';
@@ -32,9 +31,9 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
3231
<AutocompleteLabel class="text-inherit font-semibold">
3332
Personal Trainers ⚡
3433
</AutocompleteLabel>
35-
<AutocompleteTrigger class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
34+
<AutocompleteControl class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
3635
<AutocompleteInput class="w-44 bg-inherit px-2 pr-6" />
37-
<AutocompleteButton class="w-6 h-6 group absolute right-0">
36+
<AutocompleteTrigger class="w-6 h-6 group absolute right-0">
3837
<svg
3938
xmlns="http://www.w3.org/2000/svg"
4039
viewBox="0 0 24 24"
@@ -46,8 +45,8 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
4645
>
4746
<polyline points="6 9 12 15 18 9"></polyline>
4847
</svg>
49-
</AutocompleteButton>
50-
</AutocompleteTrigger>
48+
</AutocompleteTrigger>
49+
</AutocompleteControl>
5150
<AutocompleteListbox class="w-full bg-[#1f2532] px-4 py-2 mt-2 rounded-sm border-[#7d95b3] border-[1px]">
5251
{trainers.map((trainer, index) => (
5352
<AutocompleteOption
@@ -68,22 +67,24 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
6867
<CodeExample>
6968
```tsx
7069
import { component$ } from '@builder.io/qwik';
71-
import { AutocompleteRoot, AutocompleteLabel, AutocompleteTrigger, AutocompleteInput, AutocompleteButton, AutocompleteListbox, AutocompleteOption } from '@qwik-ui/headless';
72-
73-
export default component$(() => (
74-
<AutocompleteRoot>
75-
<AutocompleteLabel>Label</AutocompleteLabel>
76-
<AutocompleteTrigger>
77-
<AutocompleteInput />
78-
<AutocompleteButton>
79-
Button Marker
80-
</AutocompleteButton>
81-
</AutocompleteTrigger>
82-
<AutocompleteListbox>
83-
<AutocompleteOption />
84-
</AutocompleteListbox>
85-
</AutocompleteRoot>
86-
))
70+
import { AutocompleteRoot, AutocompleteLabel, AutocompleteControl, AutocompleteInput, AutocompleteTrigger, AutocompleteListbox, AutocompleteOption } from '@qwik-ui/headless';
71+
72+
export default component$(() => {
73+
return (
74+
<AutocompleteRoot>
75+
<AutocompleteLabel>Label</AutocompleteLabel>
76+
<AutocompleteControl>
77+
<AutocompleteInput />
78+
<AutocompleteTrigger>
79+
Button
80+
</AutocompleteTrigger>
81+
</AutocompleteControl>
82+
<AutocompleteListbox>
83+
<AutocompleteOption>Option</AutocompleteOption>
84+
</AutocompleteListbox>
85+
</AutocompleteRoot>
86+
)
87+
})
8788
```
8889

8990
</CodeExample>

packages/kit-headless/src/components/accordion/accordion-trigger.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { accordionItemContextId, accordionRootContextId } from './accordion-cont
1414

1515
import { KeyCode } from '../../utils/key-code.type';
1616

17-
export const accordionPreventedKeys = [
17+
const accordionPreventedKeys = [
1818
KeyCode.Home,
1919
KeyCode.End,
2020
KeyCode.PageDown,
@@ -23,7 +23,9 @@ export const accordionPreventedKeys = [
2323
KeyCode.ArrowUp
2424
];
2525

26-
export type AccordionTriggerProps = QwikIntrinsicElements['button'];
26+
export type AccordionTriggerProps = {
27+
disabled?: boolean;
28+
} & QwikIntrinsicElements['button'];
2729

2830
export const AccordionTrigger = component$(
2931
({ disabled, ...props }: AccordionTriggerProps) => {

packages/kit-headless/src/components/autocomplete/autocomplete-button.tsx

Lines changed: 0 additions & 38 deletions
This file was deleted.

packages/kit-headless/src/components/autocomplete/autocomplete-context.type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface AutocompleteContext {
1010
labelRef: Signal<HTMLElement | undefined>;
1111
listBoxId: string;
1212
inputId: string;
13-
buttonId: string;
13+
triggerId: string;
1414
activeOptionId: Signal<string | null>;
1515
inputValue: Signal<string>;
1616
focusInput$: QRL<(inputId: string) => void>;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
component$,
3+
useSignal,
4+
useContext,
5+
Slot,
6+
type QwikIntrinsicElements
7+
} from '@builder.io/qwik';
8+
import AutocompleteContextId from './autocomplete-context-id';
9+
10+
export type AutocompleteControlProps = QwikIntrinsicElements['div'];
11+
12+
export const AutocompleteControl = component$((props: AutocompleteControlProps) => {
13+
const ref = useSignal<HTMLElement>();
14+
const contextService = useContext(AutocompleteContextId);
15+
contextService.triggerRef = ref;
16+
17+
return (
18+
<div ref={ref} {...props}>
19+
<Slot />
20+
</div>
21+
);
22+
});

packages/kit-headless/src/components/autocomplete/autocomplete-input.tsx

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@ import {
66
$,
77
type Signal,
88
type QwikKeyboardEvent,
9-
type QwikIntrinsicElements,
9+
type QwikIntrinsicElements
1010
} from '@builder.io/qwik';
1111
import AutocompleteContextId from './autocomplete-context-id';
1212

13+
import { KeyCode } from '../../utils/key-code.type';
14+
15+
const autocompletePreventedKeys = [
16+
KeyCode.Home,
17+
KeyCode.End,
18+
KeyCode.PageDown,
19+
KeyCode.PageUp,
20+
KeyCode.ArrowDown,
21+
KeyCode.ArrowUp
22+
];
23+
1324
export type InputProps = QwikIntrinsicElements['input'];
1425

1526
// Add required context here
@@ -18,37 +29,53 @@ export const AutocompleteInput = component$((props: InputProps) => {
1829
const contextService = useContext(AutocompleteContextId);
1930
const listboxId = contextService.listBoxId;
2031
const labelRef = contextService.labelRef;
32+
const inputElement = ref.value;
33+
34+
useVisibleTask$(function preventDefaultTask({ cleanup }) {
35+
function keyHandler(e: KeyboardEvent) {
36+
if (autocompletePreventedKeys.includes(e.key as KeyCode)) {
37+
e.preventDefault();
38+
}
39+
}
40+
41+
inputElement?.addEventListener('keydown', keyHandler);
42+
cleanup(() => {
43+
inputElement?.removeEventListener('keydown', keyHandler);
44+
});
45+
});
2146

2247
/*
2348
previously had useTask here, but noticed whenever it first renders,
2449
it won't focus the first option when hitting the down arrow key to open the listbox
2550
Also, all of our tests break on useTask, BUT it seems to work fine in the browser with useTask.
2651
Very odd.
2752
*/
28-
useVisibleTask$(({ track }) => {
53+
useVisibleTask$(async ({ track }) => {
2954
track(() => contextService.inputValue.value);
3055

31-
contextService.filteredOptions = contextService.options.filter(
32-
(option: Signal) => {
33-
const optionValue = option.value.getAttribute('optionValue');
34-
const inputValue = contextService.inputValue.value;
56+
contextService.filteredOptions = contextService.options.filter((option: Signal) => {
57+
const optionValue = option.value.getAttribute('optionValue');
58+
const inputValue = contextService.inputValue.value;
3559

36-
if (
37-
contextService.inputValue.value.length >= 0 &&
38-
document.activeElement === ref.value
39-
) {
40-
if (optionValue === inputValue) {
41-
contextService.isExpanded.value = false;
42-
} else if (optionValue.match(new RegExp(inputValue, 'i'))) {
43-
contextService.isExpanded.value = true;
44-
}
45-
} else {
60+
const defaultFilterRegex = '[0-9]*';
61+
const defaultFilterPattern = inputValue + defaultFilterRegex;
62+
const defaultFilter = new RegExp(defaultFilterPattern, 'i');
63+
64+
if (
65+
contextService.inputValue.value.length >= 0 &&
66+
document.activeElement === ref.value
67+
) {
68+
if (optionValue === inputValue) {
4669
contextService.isExpanded.value = false;
70+
} else if (optionValue.match(defaultFilter)) {
71+
contextService.isExpanded.value = true;
4772
}
48-
49-
return optionValue.match(new RegExp(inputValue, 'i'));
73+
} else {
74+
contextService.isExpanded.value = false;
5075
}
51-
);
76+
77+
return optionValue.match(defaultFilter);
78+
});
5279

5380
// Probably better to refactor Signal type later
5481
contextService.options.map((option: Signal) => {
@@ -86,7 +113,7 @@ export const AutocompleteInput = component$((props: InputProps) => {
86113
contextService.filteredOptions[0]?.value?.focus();
87114
}
88115
}),
89-
props.onKeyDown$,
116+
props.onKeyDown$
90117
]}
91118
{...props}
92119
/>

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@ import {
66
Slot,
77
type QwikIntrinsicElements,
88
type QwikKeyboardEvent,
9+
useVisibleTask$
910
} from '@builder.io/qwik';
1011
import AutocompleteContextId from './autocomplete-context-id';
1112

13+
import { KeyCode } from '../../utils/key-code.type';
14+
15+
const preventedKeys = [
16+
KeyCode.Home,
17+
KeyCode.End,
18+
KeyCode.PageDown,
19+
KeyCode.PageUp,
20+
KeyCode.ArrowDown,
21+
KeyCode.ArrowUp
22+
];
23+
1224
export type ListboxProps = {
1325
isExpanded?: boolean;
1426
} & QwikIntrinsicElements['ul'];
@@ -17,7 +29,20 @@ export const AutocompleteListbox = component$((props: ListboxProps) => {
1729
const ref = useSignal<HTMLElement>();
1830
const contextService = useContext(AutocompleteContextId);
1931
const listboxId = contextService.listBoxId;
20-
contextService.listBoxRef = ref;
32+
const listboxElement = ref.value;
33+
34+
useVisibleTask$(function preventDefaultTask({ cleanup }) {
35+
function keyHandler(e: KeyboardEvent) {
36+
if (preventedKeys.includes(e.key as KeyCode)) {
37+
e.preventDefault();
38+
}
39+
}
40+
41+
listboxElement?.addEventListener('keydown', keyHandler);
42+
cleanup(() => {
43+
listboxElement?.removeEventListener('keydown', keyHandler);
44+
});
45+
});
2146

2247
return (
2348
<ul
@@ -64,7 +89,7 @@ export const AutocompleteListbox = component$((props: ListboxProps) => {
6489
availableOptions[availableOptions.length - 1]?.focus();
6590
}
6691
}),
67-
props.onKeyDown$,
92+
props.onKeyDown$
6893
]}
6994
>
7095
<Slot />

packages/kit-headless/src/components/autocomplete/autocomplete-option.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {
44
useContext,
55
component$,
66
$,
7+
useVisibleTask$,
78
type QwikIntrinsicElements,
8-
type QwikKeyboardEvent,
9+
type QwikKeyboardEvent
910
} from '@builder.io/qwik';
1011
import AutocompleteContextId from './autocomplete-context-id';
1112

@@ -17,6 +18,7 @@ export type OptionProps = {
1718
export const AutocompleteOption = component$((props: OptionProps) => {
1819
const ref = useSignal<HTMLElement>();
1920
const contextService = useContext(AutocompleteContextId);
21+
const optionElement = ref.value;
2022

2123
contextService.options = [...contextService.options, ref];
2224

@@ -33,7 +35,7 @@ export const AutocompleteOption = component$((props: OptionProps) => {
3335
contextService.isExpanded.value = false;
3436
}
3537
}),
36-
props.onClick$,
38+
props.onClick$
3739
]}
3840
onKeyDown$={[
3941
$((e: QwikKeyboardEvent) => {
@@ -43,7 +45,7 @@ export const AutocompleteOption = component$((props: OptionProps) => {
4345
contextService.focusInput$(contextService.inputId);
4446
}
4547
}),
46-
props.onKeyDown$,
48+
props.onKeyDown$
4749
]}
4850
{...props}
4951
>

0 commit comments

Comments
 (0)