Skip to content

Commit ae4c746

Browse files
committed
feat(components): add experimental Select component
1 parent 52ed4ed commit ae4c746

File tree

15 files changed

+1147
-1
lines changed

15 files changed

+1147
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
@import url('../../../styles/mixins.css');
2+
3+
.base {
4+
--field-input-outline-width: var(--kbq-size-3xs);
5+
--field-input-color: var(--kbq-foreground-contrast);
6+
--field-input-border-color: var(--kbq-line-contrast-fade);
7+
--field-input-outline-color: var(--kbq-states-line-focus-theme);
8+
--field-input-bg-color: var(--kbq-background-bg);
9+
--field-input-placeholder-color: var(--kbq-foreground-contrast-tertiary);
10+
display: flex;
11+
cursor: pointer;
12+
block-size: 32px;
13+
inline-size: 100%;
14+
align-items: center;
15+
outline-offset: -1px;
16+
box-sizing: border-box;
17+
border-radius: var(--kbq-size-s);
18+
color: var(--field-input-color);
19+
background: var(--field-input-bg-color);
20+
border: 1px solid var(--field-input-border-color);
21+
outline: var(--field-input-outline-width) solid transparent;
22+
padding-block: var(--field-input-padding-block-start)
23+
var(--field-input-padding-block-end);
24+
padding-inline: var(--field-input-padding-inline-start)
25+
var(--field-input-padding-inline-end);
26+
transition:
27+
color var(--kbq-transition-default),
28+
outline-color var(--kbq-transition-default),
29+
background-color var(--kbq-transition-default),
30+
border-color var(--kbq-transition-default);
31+
32+
&:focus,
33+
&[aria-expanded='true'] {
34+
outline-color: var(--field-input-outline-color);
35+
}
36+
}
37+
38+
.content {
39+
display: flex;
40+
overflow: hidden;
41+
align-items: center;
42+
white-space: nowrap;
43+
gap: var(--kbq-size-s);
44+
text-overflow: ellipsis;
45+
46+
@mixin typography text-normal;
47+
}
48+
49+
.error {
50+
--field-input-color: var(--kbq-foreground-error);
51+
--field-input-border-color: var(--kbq-line-error);
52+
--field-input-outline-color: var(--kbq-states-line-focus-error);
53+
--field-input-bg-color: var(--kbq-states-background-error-less);
54+
--field-input-placeholder-color: var(--kbq-foreground-error-tertiary);
55+
}
56+
57+
.disabled {
58+
--field-input-color: var(--kbq-states-foreground-disabled);
59+
--field-input-border-color: var(--kbq-states-line-disabled);
60+
--field-input-bg-color: var(--kbq-states-background-disabled);
61+
cursor: not-allowed;
62+
}
63+
64+
.hasPlaceholder {
65+
--field-input-color: var(--field-input-placeholder-color);
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { forwardRef, type ReactNode } from 'react';
2+
3+
import { clsx, isNotNil } from '@koobiq/react-core';
4+
import { Button } from '@koobiq/react-primitives';
5+
6+
import type { InputPropVariant } from '../../Input';
7+
8+
import s from './FieldSelect.module.css';
9+
10+
export type FieldSelectProps = {
11+
error?: boolean;
12+
disabled?: boolean;
13+
className?: string;
14+
children?: ReactNode;
15+
'data-testid'?: string;
16+
variant?: InputPropVariant;
17+
placeholder?: string | number;
18+
};
19+
20+
export const FieldSelect = forwardRef<HTMLButtonElement, FieldSelectProps>(
21+
(
22+
{
23+
error = false,
24+
disabled = false,
25+
variant = 'filled',
26+
placeholder,
27+
children,
28+
className,
29+
...other
30+
},
31+
ref
32+
) => (
33+
<Button
34+
{...other}
35+
disabled={disabled}
36+
data-slot="select-value"
37+
className={clsx(
38+
s.base,
39+
s[variant],
40+
error && s.error,
41+
disabled && s.disabled,
42+
!isNotNil(children) && s.hasPlaceholder,
43+
className
44+
)}
45+
ref={ref}
46+
>
47+
<span className={s.content}>{children ?? placeholder}</span>
48+
</Button>
49+
)
50+
);
51+
52+
FieldSelect.displayName = 'FieldSelect';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FieldSelect';

packages/components/src/components/FieldComponents/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './FieldControl';
22
export * from './FieldNumberControl';
33
export * from './FieldInput';
4+
export * from './FieldSelect';
45
export * from './FieldLabel';
56
export * from './FieldAddon';
67
export * from './FieldCaption';

packages/components/src/components/List/components/ListItemText/ListItemText.module.css

+4
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
gap: var(--kbq-size-3xs);
66
flex-direction: column;
77
}
8+
9+
[data-slot='select-value'] .caption {
10+
display: none;
11+
}

packages/components/src/components/List/components/ListItemText/ListItemText.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const ListItemText = forwardRef<ListItemTextRef, ListItemTextProps>(
4141
<Typography
4242
as="span"
4343
color="contrast-secondary"
44+
className={s.caption}
4445
variant="text-compact"
4546
{...slotProps?.caption}
4647
>

packages/components/src/components/List/types.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
Ref,
88
} from 'react';
99

10-
import type { AriaListBoxProps } from '@koobiq/react-primitives';
10+
import type { AriaListBoxProps, ListState } from '@koobiq/react-primitives';
1111

1212
import type { TypographyProps } from '../Typography';
1313

@@ -80,6 +80,11 @@ export type ListProps<T extends object> = ListBaseProps<T>;
8080

8181
export type ListRef = ComponentRef<'ul'>;
8282

83+
export type ListInnerProps<T extends object> = {
84+
state: ListState<T>;
85+
listRef?: Ref<HTMLUListElement>;
86+
} & Omit<ListBaseProps<T>, 'ref'>;
87+
8388
export type ListComponent = <T extends object>(
8489
props: ListProps<T>
8590
) => ReactElement | null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
Meta,
3+
Story,
4+
Props,
5+
Status,
6+
} from '../../../../../.storybook/components';
7+
8+
import * as Stories from './Select.stories';
9+
10+
<Meta of={Stories} />
11+
12+
# Select
13+
14+
<Status variant="experimental" />
15+
16+
A select displays a collapsible list of options and allows a user to select one of them.
17+
18+
```tsx
19+
import {
20+
Select,
21+
ListItem,
22+
ListSection,
23+
ListItemText,
24+
} from '@koobiq/react-components';
25+
```
26+
27+
<Story of={Stories.Base} />
28+
29+
## Props
30+
31+
<Props />
32+
33+
## Content
34+
35+
Select accepts static and dynamic collections.The examples above show static collections,
36+
which can be used when the full list of options is known ahead of time.
37+
Dynamic collections, as shown below, can be used when the options come from an external
38+
data source such as an API call, or update over time.
39+
40+
As seen below, an iterable list of options is passed to the `Select` using the `items` prop.
41+
Each item accepts an `key` prop, which is passed to the `onSelectionChange` handler to identify
42+
the selected item. Alternatively, if the item objects contain an key property, as shown
43+
in the example below, then this is used automatically and an `key` prop is not required.
44+
45+
<Story of={Stories.Content} />
46+
47+
## Selection
48+
49+
Setting a selected option can be done by using the `defaultSelectedKey` or `selectedKey` prop.
50+
The selected key corresponds to the `key` prop of an item.
51+
52+
<Story of={Stories.Selection} />
53+
54+
## Error
55+
56+
The `error` prop toggles the error state. The `errorMessage` shows a message to explain the error to the user.
57+
58+
<Story of={Stories.Error} />
59+
60+
## Disabled
61+
62+
When the select component is disabled, it cannot be interacted with.
63+
64+
<Story of={Stories.Disabled} />
65+
66+
## Disabled options
67+
68+
Select supports marking items as disabled using the `disabledKeys` prop.
69+
Each key in this list corresponds with the `key` prop passed to the `ListItem` component.
70+
71+
<Story of={Stories.DisabledOptions} />
72+
73+
## Required
74+
75+
To make a select required, add the `required` prop.
76+
If the field has a label, a required indicator will appear next to it.
77+
78+
<Story of={Stories.Required} />
79+
80+
## Full width
81+
82+
The `fullWidth` prop will make a select fit to its parent width.
83+
84+
<Story of={Stories.FullWidth} />
85+
86+
## Open
87+
88+
### Default open
89+
90+
Select isn't opened by default. The `defaultOpen` prop can be used to set the default state.
91+
92+
### Controlled open
93+
94+
The `open` prop can be used to make the opened state controlled. The `onOpenChange` event is fired when the select's open state changes.
95+
96+
<Story of={Stories.Open} />
97+
98+
## Section
99+
100+
A select component can display items grouped together in sections.
101+
102+
<Story of={Stories.Section} />
103+
104+
## With icons
105+
106+
See example below how to use [icons](?path=/docs/icons--docs) in items of component.
107+
108+
<Story of={Stories.WithIcons} />
109+
110+
## With item details
111+
112+
See example below for using details in items of component.
113+
114+
<Story of={Stories.WithItemDetails} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.base {
2+
--field-input-padding-inline-start: var(--kbq-size-m);
3+
--field-input-padding-inline-end: var(--kbq-size-m);
4+
--field-input-padding-block-start: var(--kbq-size-xs);
5+
--field-input-padding-block-end: var(--kbq-size-xs);
6+
position: relative;
7+
display: inline-flex;
8+
justify-content: center;
9+
flex-direction: column;
10+
align-items: flex-start;
11+
}
12+
13+
.fullWidth {
14+
inline-size: 100%;
15+
}
16+
17+
/* addons */
18+
.addon {
19+
pointer-events: none;
20+
}
21+
22+
/* popover */
23+
.popover {
24+
border-radius: var(--kbq-size-s);
25+
opacity: 0;
26+
transform: translateY(-8px);
27+
}
28+
29+
/* list */
30+
.list {
31+
inline-size: 100%;
32+
padding: var(--kbq-size-xxs);
33+
}
34+
35+
.popover[data-transition='entering'] {
36+
opacity: 1;
37+
transform: translateY(0);
38+
}
39+
40+
.popover[data-transition='entered'] {
41+
opacity: 1;
42+
transform: translateY(0);
43+
}
44+
45+
.popover[data-transition='exiting'] {
46+
opacity: 0;
47+
transform: translateY(-8px);
48+
}
49+
50+
.popover[data-transition='exited'] {
51+
opacity: 0;
52+
transform: translateY(-8px);
53+
}

0 commit comments

Comments
 (0)