Skip to content

Commit 6a4eb32

Browse files
feat(select): options now correctly skip disabled elements
1 parent e0ce73b commit 6a4eb32

File tree

8 files changed

+74
-10
lines changed

8 files changed

+74
-10
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { component$, useSignal } from '@builder.io/qwik';
2+
import { Select, SelectListbox, SelectOption, SelectTrigger } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
6+
7+
return (
8+
<Select class="relative min-w-40">
9+
<p>This one is the disabled</p>
10+
<SelectTrigger class="w-full border-2 border-dashed border-red-400" />
11+
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
12+
{usersSig.value.map((user, index) => (
13+
<SelectOption
14+
class="border-dashed border-blue-400 data-[highlighted]:border-2"
15+
key={user}
16+
disabled={index === 0 ? true : false}
17+
>
18+
{user}
19+
</SelectOption>
20+
))}
21+
</SelectListbox>
22+
</Select>
23+
);
24+
});

apps/website/src/routes/docs/headless/select/index.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ This element is used to create a drop-down list, it's often used in a form, to c
1414
<Showcase name="hero" />
1515
</div>
1616

17+
<div data-testid="select-disabled-test">
18+
<Showcase name="disabled" />
19+
</div>
20+
1721
## Building blocks
1822

1923
<CodeSnippet name="building-blocks" />

apps/website/src/routes/docs/headless/select/select.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,40 @@ test.describe('Keyboard Behavior', () => {
237237
await expect(options[1]).toHaveAttribute('data-highlighted');
238238
});
239239
});
240+
241+
test.describe('disabled', () => {
242+
test(`GIVEN an open hero select with the first option disabled
243+
WHEN clicking the disabled option
244+
It should be disabled`, async ({ page }) => {
245+
const { getTrigger, getListbox, getOptions } = await setup(
246+
page,
247+
'select-disabled-test',
248+
);
249+
250+
await getTrigger().focus();
251+
await getTrigger().press('Enter');
252+
// should be open initially
253+
await expect(getListbox()).toBeVisible();
254+
255+
await getTrigger().focus();
256+
await getTrigger().press('ArrowDown');
257+
const options = await getOptions();
258+
await expect(options[0]).toBeDisabled();
259+
});
260+
261+
test(`GIVEN an open hero select by the enter key
262+
WHEN first option is disabled
263+
THEN the second option should have data-highlighted`, async ({ page }) => {
264+
const { getTrigger, getListbox, getOptions } = await setup(
265+
page,
266+
'select-disabled-test',
267+
);
268+
269+
await getTrigger().focus();
270+
await getTrigger().press('ArrowDown');
271+
// should be open initially
272+
await expect(getListbox()).toBeVisible();
273+
const options = await getOptions();
274+
await expect(options[1]).toHaveAttribute('data-highlighted');
275+
});
276+
});

packages/kit-headless/src/components/select/select-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type SelectContext = {
1515
selectedOptionRef: Signal<HTMLLIElement | null>;
1616

1717
// core state
18-
highlightedIndexSig: Signal<number>;
18+
highlightedIndexSig: Signal<number | null>;
1919
isListboxOpenSig: Signal<boolean>;
2020
selectedIndexSig: Signal<number | null>;
2121
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const SelectOption = component$<SelectOptionProps>((props) => {
5656
ref={optionRef}
5757
tabIndex={-1}
5858
aria-selected={isSelected}
59+
aria-disabled={disabled === true ? 'true' : 'false'}
5960
data-selected={isSelected ? '' : undefined}
6061
data-highlighted={isHighlighted ? '' : undefined}
6162
data-disabled={disabled ? '' : undefined}

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
1313
const context = useContext(SelectContextId);
1414
const openKeys = ['ArrowUp', 'ArrowDown'];
1515
const closedKeys = [`Escape`];
16-
// const initialIndex = context.highlightedIndexSig.value === -1;
1716

1817
// Both the space and enter keys run with handleClick$
1918
const handleClick$ = $(() => {
@@ -36,7 +35,7 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
3635
throw new Error('Qwik UI: internal select option is undefined');
3736
}
3837

39-
const isDisabled = option.value.hasAttribute('disabled');
38+
const isDisabled = option.value.ariaDisabled === 'true';
4039

4140
return { element: option.value, isDisabled };
4241
});
@@ -51,17 +50,16 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
5150

5251
if (e.key === 'Home') {
5352
context.highlightedIndexSig.value = 0;
54-
console.log('Highlighted index: ', context.highlightedIndexSig.value);
55-
return;
5653
}
5754

5855
if (e.key === 'End') {
5956
context.highlightedIndexSig.value = context.optionRefsArray.value.length - 1;
60-
return;
6157
}
6258

63-
if (context.highlightedIndexSig.value === -1) {
64-
context.highlightedIndexSig.value++;
59+
/** When initially opening the listbox, we want to grab the first enabled option index */
60+
if (context.highlightedIndexSig.value === null) {
61+
context.highlightedIndexSig.value = getNextEnabledOptionIndex(-1, options);
62+
console.log(context.highlightedIndexSig.value);
6563
return;
6664
}
6765

packages/kit-headless/src/components/select/select.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const SelectImpl = component$<SelectProps>((props) => {
2525
const selectedIndexSig = useSignal<number | null>(null);
2626
const selectedOptionRef = useSignal<HTMLLIElement | null>(null);
2727
const isListboxOpenSig = useSignal<boolean>(false);
28-
const highlightedIndexSig = useSignal<number>(-1);
28+
const highlightedIndexSig = useSignal<number | null>(null);
2929

3030
useTask$(function deriveSelectedRef({ track }) {
3131
track(() => selectedIndexSig.value);

packages/kit-headless/src/components/select/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const getNextEnabledOptionIndex = (index: number, options: OptionsType) =
66
const opts = options;
77
const len = opts.length;
88

9-
while (opts[(currentIndex + offset) % len]?.isDisabled) {
9+
while (opts[(currentIndex + offset) % len].isDisabled) {
1010
offset++;
1111
if (offset + currentIndex > len - 1) {
1212
currentIndex = 0;

0 commit comments

Comments
 (0)