Skip to content

Commit afff307

Browse files
authored
Merge pull request #759 from maiieul/styled-select
Styled select
2 parents 2855796 + eaeffcc commit afff307

File tree

9 files changed

+324
-5
lines changed

9 files changed

+324
-5
lines changed

.changeset/orange-points-reflect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik-ui/styled': patch
3+
---
4+
5+
FEAT new styled select component

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const statusByComponent: ComponentKitsStatuses = {
2727
Popover: ComponentStatus.Draft,
2828
Progress: ComponentStatus.Draft,
2929
RadioGroup: ComponentStatus.Draft,
30+
Select: ComponentStatus.Draft,
3031
Separator: ComponentStatus.Beta,
3132
Skeleton: ComponentStatus.Beta,
3233
Tabs: ComponentStatus.Beta,

apps/website/src/routes/docs/styled/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- [Popover](/docs/styled/popover)
3030
- [Progress](/docs/styled/progress)
3131
- [RadioGroup](/docs/styled/radio-group)
32+
- [Select](/docs/styled/select)
3233
- [Separator](/docs/styled/separator)
3334
- [Skeleton](/docs/styled/skeleton)
3435
- [Tabs](/docs/styled/tabs)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { LuCheck } from '@qwikest/icons/lucide';
3+
import { Select } from '~/components/ui';
4+
5+
export default component$(() => {
6+
const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby'];
7+
8+
return (
9+
<Select.Root>
10+
<Select.Label>Logged in users</Select.Label>
11+
<Select.Trigger>
12+
<Select.DisplayText placeholder="Select an option" />
13+
</Select.Trigger>
14+
<Select.Popover gutter={8}>
15+
<Select.Listbox>
16+
{users.map((user) => (
17+
<Select.Item key={user}>
18+
<Select.ItemLabel>{user}</Select.ItemLabel>
19+
<Select.ItemIndicator>
20+
<LuCheck class="h-4 w-4" />
21+
</Select.ItemIndicator>
22+
</Select.Item>
23+
))}
24+
</Select.Listbox>
25+
</Select.Popover>
26+
</Select.Root>
27+
);
28+
});

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

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,153 @@ title: Qwik UI | Styled Select Component
44

55
import { statusByComponent } from '~/_state/component-statuses';
66

7+
<StatusBanner status={statusByComponent.styled.Select} />
8+
79
# Select
810

9-
With a gentle click, you unveil a buffet of options; the Qwik UI Styled select is where your choice is the guest of honor, awaiting its moment to shine
11+
Displays a list of options for the user to pick from — triggered by a button.
1012

11-
<StatusBanner status={statusByComponent.styled.Select} />
13+
<Showcase name="hero" />
14+
15+
## Installation
16+
17+
### Run the following cli command or copy/paste the component code into your project
18+
19+
```sh
20+
qwik-ui add select
21+
```
22+
23+
```tsx
24+
import { PropsOf, Slot, component$ } from '@builder.io/qwik';
25+
import { Select as HeadlessSelect } from '@qwik-ui/headless';
26+
import { cn } from '@qwik-ui/utils';
27+
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
28+
29+
const Root = (props: PropsOf<typeof HeadlessSelect.Root>) => (
30+
<HeadlessSelect.Root
31+
{...props}
32+
selectItemComponent={Item}
33+
selectItemLabelComponent={ItemLabel}
34+
selectLabelComponent={Label}
35+
/>
36+
);
37+
38+
const Label = component$<PropsOf<typeof HeadlessSelect.Label>>(({ ...props }) => {
39+
return (
40+
<>
41+
<HeadlessSelect.Label
42+
{...props}
43+
class={cn('px-2 py-1.5 text-sm font-semibold', props.class)}
44+
>
45+
<Slot />
46+
</HeadlessSelect.Label>
47+
</>
48+
);
49+
});
50+
51+
const Trigger = component$<PropsOf<typeof HeadlessSelect.Trigger>>(({ ...props }) => {
52+
return (
53+
<>
54+
<HeadlessSelect.Trigger
55+
{...props}
56+
class={cn(
57+
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
58+
props.class,
59+
)}
60+
>
61+
<Slot />
62+
<LuChevronDown class="h-4 w-4 opacity-50" />
63+
</HeadlessSelect.Trigger>
64+
</>
65+
);
66+
});
67+
68+
const DisplayText = HeadlessSelect.DisplayText;
69+
70+
const Popover = component$<PropsOf<typeof HeadlessSelect.Popover>>(({ ...props }) => {
71+
return (
72+
<>
73+
<HeadlessSelect.Popover
74+
{...props}
75+
class={cn(
76+
'w-full max-w-[15rem] data-[open]:animate-in data-[closing]:animate-out data-[closing]:fade-out-0 data-[open]:fade-in-0 data-[closing]:zoom-out-95 data-[open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
77+
// 'overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
78+
props.class,
79+
)}
80+
>
81+
<Slot />
82+
</HeadlessSelect.Popover>
83+
</>
84+
);
85+
});
86+
87+
type ListboxProps = PropsOf<typeof HeadlessSelect.Listbox>;
88+
const Listbox = component$<ListboxProps>(({ ...props }) => {
89+
return (
90+
<>
91+
<HeadlessSelect.Listbox
92+
{...props}
93+
class={cn(
94+
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
95+
props.class,
96+
)}
97+
>
98+
<Slot />
99+
</HeadlessSelect.Listbox>
100+
</>
101+
);
102+
});
103+
104+
const Group = HeadlessSelect.Group;
105+
106+
const GroupLabel = HeadlessSelect.GroupLabel;
107+
108+
const Item = component$<PropsOf<typeof HeadlessSelect.Item>>(({ ...props }) => {
109+
return (
110+
<HeadlessSelect.Item
111+
{...props}
112+
class={cn(
113+
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
114+
'data-[highlighted]:border-base data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground',
115+
props.class,
116+
)}
117+
>
118+
<Slot />
119+
</HeadlessSelect.Item>
120+
);
121+
});
122+
123+
const ItemIndicator = component$<PropsOf<typeof HeadlessSelect.ItemIndicator>>(
124+
({ ...props }) => {
125+
return (
126+
<span class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
127+
<HeadlessSelect.ItemIndicator {...props}>
128+
<LuCheck class="h-4 w-4" />
129+
</HeadlessSelect.ItemIndicator>
130+
</span>
131+
);
132+
},
133+
);
134+
135+
const ItemLabel = component$<PropsOf<typeof HeadlessSelect.ItemLabel>>(({ ...props }) => {
136+
return (
137+
<HeadlessSelect.ItemLabel {...props}>
138+
<Slot />
139+
</HeadlessSelect.ItemLabel>
140+
);
141+
});
142+
143+
export const Select = {
144+
Root,
145+
Label,
146+
Trigger,
147+
DisplayText,
148+
Popover,
149+
Listbox,
150+
Group,
151+
GroupLabel,
152+
Item,
153+
ItemIndicator,
154+
ItemLabel,
155+
};
156+
```
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Slot, component$, useContext } from '@builder.io/qwik';
1+
import { PropsOf, Slot, component$, useContext } from '@builder.io/qwik';
22
import { selectItemContextId } from './select-context';
33

4-
export const HSelectItemIndicator = component$(() => {
4+
export const HSelectItemIndicator = component$<PropsOf<'span'>>(() => {
55
const selectContext = useContext(selectItemContextId);
66

7-
return <>{selectContext.isSelectedSig.value && <Slot />}</>;
7+
return <span>{selectContext.isSelectedSig.value && <Slot />}</span>;
88
});

packages/kit-styled/components-registry.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
"componentFolder": "radio-group",
8686
"files": ["radio-group.tsx"]
8787
},
88+
{
89+
"displayName": "Select",
90+
"type": "select",
91+
"componentFolder": "select",
92+
"files": ["select.tsx"]
93+
},
8894
{
8995
"displayName": "Separator",
9096
"type": "separator",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { PropsOf, Slot, component$ } from '@builder.io/qwik';
2+
import { Select as HeadlessSelect } from '@qwik-ui/headless';
3+
import { cn } from '@qwik-ui/utils';
4+
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
5+
6+
const Root = (props: PropsOf<typeof HeadlessSelect.Root>) => (
7+
<HeadlessSelect.Root
8+
{...props}
9+
selectItemComponent={Item}
10+
selectItemLabelComponent={ItemLabel}
11+
selectLabelComponent={Label}
12+
/>
13+
);
14+
15+
const Label = component$<PropsOf<typeof HeadlessSelect.Label>>(({ ...props }) => {
16+
return (
17+
<>
18+
<HeadlessSelect.Label
19+
{...props}
20+
class={cn('px-2 py-1.5 text-sm font-semibold', props.class)}
21+
>
22+
<Slot />
23+
</HeadlessSelect.Label>
24+
</>
25+
);
26+
});
27+
28+
const Trigger = component$<PropsOf<typeof HeadlessSelect.Trigger>>(({ ...props }) => {
29+
return (
30+
<>
31+
<HeadlessSelect.Trigger
32+
{...props}
33+
class={cn(
34+
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
35+
props.class,
36+
)}
37+
>
38+
<Slot />
39+
<LuChevronDown class="h-4 w-4 opacity-50" />
40+
</HeadlessSelect.Trigger>
41+
</>
42+
);
43+
});
44+
45+
const DisplayText = HeadlessSelect.DisplayText;
46+
47+
const Popover = component$<PropsOf<typeof HeadlessSelect.Popover>>(({ ...props }) => {
48+
return (
49+
<>
50+
<HeadlessSelect.Popover
51+
{...props}
52+
class={cn(
53+
'w-full max-w-[15rem] data-[open]:animate-in data-[closing]:animate-out data-[closing]:fade-out-0 data-[open]:fade-in-0 data-[closing]:zoom-out-95 data-[open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
54+
// 'overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
55+
props.class,
56+
)}
57+
>
58+
<Slot />
59+
</HeadlessSelect.Popover>
60+
</>
61+
);
62+
});
63+
64+
type ListboxProps = PropsOf<typeof HeadlessSelect.Listbox>;
65+
const Listbox = component$<ListboxProps>(({ ...props }) => {
66+
return (
67+
<>
68+
<HeadlessSelect.Listbox
69+
{...props}
70+
class={cn(
71+
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
72+
props.class,
73+
)}
74+
>
75+
<Slot />
76+
</HeadlessSelect.Listbox>
77+
</>
78+
);
79+
});
80+
81+
const Group = HeadlessSelect.Group;
82+
83+
const GroupLabel = HeadlessSelect.GroupLabel;
84+
85+
const Item = component$<PropsOf<typeof HeadlessSelect.Item>>(({ ...props }) => {
86+
return (
87+
<HeadlessSelect.Item
88+
{...props}
89+
class={cn(
90+
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
91+
'data-[highlighted]:border-base data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground',
92+
props.class,
93+
)}
94+
>
95+
<Slot />
96+
</HeadlessSelect.Item>
97+
);
98+
});
99+
100+
const ItemIndicator = component$<PropsOf<typeof HeadlessSelect.ItemIndicator>>(
101+
({ ...props }) => {
102+
return (
103+
<span class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
104+
<HeadlessSelect.ItemIndicator {...props}>
105+
<LuCheck class="h-4 w-4" />
106+
</HeadlessSelect.ItemIndicator>
107+
</span>
108+
);
109+
},
110+
);
111+
112+
const ItemLabel = component$<PropsOf<typeof HeadlessSelect.ItemLabel>>(({ ...props }) => {
113+
return (
114+
<HeadlessSelect.ItemLabel {...props}>
115+
<Slot />
116+
</HeadlessSelect.ItemLabel>
117+
);
118+
});
119+
120+
export const Select = {
121+
Root,
122+
Label,
123+
Trigger,
124+
DisplayText,
125+
Popover,
126+
Listbox,
127+
Group,
128+
GroupLabel,
129+
Item,
130+
ItemIndicator,
131+
ItemLabel,
132+
};

packages/kit-styled/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from './components/popover/popover';
1414
export * from './components/progress/progress';
1515
export * from './components/radio-group/radio-group';
1616
export * from './components/separator/separator';
17+
export * from './components/select/select';
1718
export * from './components/skeleton/skeleton';
1819
export * from './components/tabs/tabs';
1920
export * from './components/textarea/textarea';

0 commit comments

Comments
 (0)