@varialkit/dropdown 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs.md +199 -0
- package/package.json +26 -0
- package/src/Dropdown.scss +249 -0
- package/src/Dropdown.tsx +119 -0
- package/src/Dropdown.types.ts +54 -0
- package/src/index.ts +1 -0
package/docs.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Dropdown
|
|
2
|
+
|
|
3
|
+
The Dropdown component is a select input that allows users to choose a value from a list of options. It is a **controlled component**, which means you must manage its state by providing a `value` and an `onChange` handler.
|
|
4
|
+
|
|
5
|
+
## How to Use
|
|
6
|
+
|
|
7
|
+
To use the Dropdown, import it from the `@solara/dropdown` package and manage its state in your component.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import React, { useState } from 'react';
|
|
11
|
+
import { Dropdown } from "@solara/dropdown";
|
|
12
|
+
|
|
13
|
+
const options = [
|
|
14
|
+
{ label: "Option 1", value: "1" },
|
|
15
|
+
{ label: "Option 2", value: "2" },
|
|
16
|
+
{ label: "Option 3", value: "3" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function MyComponent() {
|
|
20
|
+
const [value, setValue] = useState("1");
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Dropdown
|
|
24
|
+
id="my-dropdown"
|
|
25
|
+
options={options}
|
|
26
|
+
value={value}
|
|
27
|
+
onChange={(e) => setValue(e.target.value)}
|
|
28
|
+
label="Choose an option"
|
|
29
|
+
helperText="This is a helper text."
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Best Practices
|
|
36
|
+
|
|
37
|
+
- **Provide a Label**: Always use the `label` prop to provide a descriptive label for the dropdown. This is crucial for accessibility and usability.
|
|
38
|
+
- **Controlled Component**: Remember that `Dropdown` is a controlled component. You need to manage its `value` and `onChange` event to update the state.
|
|
39
|
+
- **Use Helper Text for Guidance**: The `helperText` prop is useful for providing additional instructions or context below the dropdown.
|
|
40
|
+
- **Provide a Default Value**: It is good practice to have a default value selected to ensure a predictable user experience.
|
|
41
|
+
|
|
42
|
+
## Props
|
|
43
|
+
|
|
44
|
+
The Dropdown component accepts the following props:
|
|
45
|
+
|
|
46
|
+
| Prop | Type | Default | Description |
|
|
47
|
+
| ------------ | ---------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
48
|
+
| `options` | `{ label: string; value: string }[]` | _Required_ | An array of objects, each with a `label` (the text displayed in the dropdown) and a `value` (the value associated with the option). |
|
|
49
|
+
| `size` | `"small" | "medium" | "large"` | `"medium"` | Controls the dropdown's internal padding and font size. Use `small` for tight spaces and `large` for touch-friendly interfaces. |
|
|
50
|
+
| `radius` | `"small" | "medium" | "large" | "full"` | `"medium"` | Controls the dropdown's border radius. |
|
|
51
|
+
| `isInvalid` | `boolean` | `false` | When `true`, applies a style to indicate a validation error. This is useful for form validation. |
|
|
52
|
+
| `isDisabled` | `boolean` | `false` | When `true`, the dropdown is not interactive and has a disabled style. This prevents users from changing the value. |
|
|
53
|
+
| `variant` | `"primary" | "default" | "tertiary" | "ghost"` | `"default"` | The visual style of the dropdown. See [Variants](#variants) for more details. |
|
|
54
|
+
| `label` | `string` | | The label for the dropdown. |
|
|
55
|
+
| `labelPosition` | `"top" | "left"` | `"top"` | The position of the label. |
|
|
56
|
+
| `helperText` | `string` | | The helper text for the dropdown. |
|
|
57
|
+
| `iconLeft` | `SolaraIconName | IconProps` | | Optional leading icon rendered inside the field. |
|
|
58
|
+
| `fullWidth` | `boolean` | `false` | When `true`, the dropdown will take up the full width of its container. |
|
|
59
|
+
|
|
60
|
+
...and all other standard `React.SelectHTMLAttributes<HTMLSelectElement>` props, such as `value`, `onChange`, `id`, etc.
|
|
61
|
+
|
|
62
|
+
## Label and Helper Text
|
|
63
|
+
|
|
64
|
+
The `Dropdown` component now includes built-in support for a `label` and `helperText`.
|
|
65
|
+
|
|
66
|
+
- The `label` prop adds a visible label to the dropdown, which is essential for accessibility.
|
|
67
|
+
- The `helperText` prop provides additional guidance or context below the input.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<Dropdown
|
|
71
|
+
options={options}
|
|
72
|
+
label="Choose an option"
|
|
73
|
+
helperText="This is a helper text."
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Label Position
|
|
78
|
+
|
|
79
|
+
You can control the position of the label using the `labelPosition` prop. It defaults to `top`.
|
|
80
|
+
|
|
81
|
+
### Top (Default)
|
|
82
|
+
|
|
83
|
+
The label is displayed above the dropdown.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<Dropdown options={options} label="Top-aligned Label" labelPosition="top" />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Left
|
|
90
|
+
|
|
91
|
+
The label is displayed to the left of the dropdown.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<Dropdown options={options} label="Left-aligned Label" labelPosition="left" />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Variants
|
|
98
|
+
|
|
99
|
+
The `variant` prop allows you to control the dropdown's visual appearance.
|
|
100
|
+
|
|
101
|
+
### Primary
|
|
102
|
+
|
|
103
|
+
Primary dropdowns are used for the main call-to-action on a page. They should be used sparingly to draw attention to the most important action.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<Dropdown options={options} variant="primary" />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Default
|
|
110
|
+
|
|
111
|
+
Default dropdowns are used for secondary actions. They have a visible border and are less prominent than primary dropdowns.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<Dropdown options={options} variant="default" />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Tertiary
|
|
118
|
+
|
|
119
|
+
Tertiary dropdowns are used for less important actions. They have a transparent background and a border.
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<Dropdown options={options} variant="tertiary" />
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Ghost
|
|
126
|
+
|
|
127
|
+
Ghost dropdowns are used for the least prominent actions. They have a transparent background and no border, making them suitable for clean, minimalist layouts.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<Dropdown options={options} variant="ghost" />
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Size
|
|
134
|
+
|
|
135
|
+
The `size` prop adjusts the dropdown's padding and font size to fit different layout requirements.
|
|
136
|
+
|
|
137
|
+
### Small
|
|
138
|
+
|
|
139
|
+
Small size has reduced padding and a smaller font size, making it suitable for compact interfaces where space is limited.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<Dropdown options={options} size="small" />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Medium
|
|
146
|
+
|
|
147
|
+
Medium size has standard padding and font size. This is the default setting and should be used in most cases.
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<Dropdown options={options} size="medium" />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Large
|
|
154
|
+
|
|
155
|
+
Large size has increased padding and a larger font size, making it more prominent and easier to interact with on touch devices.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<Dropdown options={options} size="large" />
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Invalid State
|
|
162
|
+
|
|
163
|
+
When `isInvalid` is `true`, the dropdown is styled with a red border to indicate a validation error. This is commonly used in forms to show the user which fields need to be corrected.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<Dropdown options={options} isInvalid />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Disabled State
|
|
170
|
+
|
|
171
|
+
When `isDisabled` is `true`, the dropdown is not interactive and has a visually distinct style. This is useful for preventing users from changing the value when it is not appropriate to do so.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<Dropdown options={options} isDisabled />
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Accessibility
|
|
178
|
+
|
|
179
|
+
The `Dropdown` component is designed with accessibility in mind.
|
|
180
|
+
|
|
181
|
+
- **Internal Label**: The component now internally handles the association between the label and the input. By providing the `label` prop, the component ensures the correct `for` and `id` attributes are set, making it accessible to screen readers.
|
|
182
|
+
- **Keyboard Navigation**: The dropdown is focusable and can be navigated and opened using the keyboard.
|
|
183
|
+
|
|
184
|
+
## Icons
|
|
185
|
+
|
|
186
|
+
You can render a leading icon inside the dropdown. Icons inherit the dropdown text color.
|
|
187
|
+
Pass a full `IconProps` object to control size or stroke width.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
<Dropdown options={options} iconLeft="data_spreadsheet_search_24" />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Full Width
|
|
194
|
+
|
|
195
|
+
When `fullWidth` is `true`, the dropdown will take up the full width of its container. This is useful for creating responsive layouts that adapt to different screen sizes.
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<Dropdown options={options} fullWidth />
|
|
199
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varialkit/dropdown",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./examples": "./examples/index.tsx"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@varialkit/icons": "0.1.1"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"docs.md",
|
|
17
|
+
"examples"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "19.0.10",
|
|
24
|
+
"react": "19.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
.solara-dropdown-wrapper {
|
|
2
|
+
--dropdown-padding-y: var(--space-2);
|
|
3
|
+
--dropdown-padding-x: var(--space-3);
|
|
4
|
+
--dropdown-font-size: var(--font-size-body-scaled);
|
|
5
|
+
--dropdown-line-height: var(--line-height-body-scaled);
|
|
6
|
+
--dropdown-label-font-size: var(--font-size-caption-scaled);
|
|
7
|
+
--dropdown-helper-font-size: var(--font-size-footnote-scaled);
|
|
8
|
+
--dropdown-label-gap: var(--space-1);
|
|
9
|
+
--dropdown-helper-gap: var(--space-1);
|
|
10
|
+
display: flex;
|
|
11
|
+
|
|
12
|
+
&.solara-dropdown-wrapper--full-width {
|
|
13
|
+
width: 100%;
|
|
14
|
+
|
|
15
|
+
.solara-dropdown-container,
|
|
16
|
+
.solara-dropdown-field,
|
|
17
|
+
.solara-dropdown,
|
|
18
|
+
.solara-dropdown__select-wrapper {
|
|
19
|
+
width: 100%;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&.solara-dropdown--size-small {
|
|
24
|
+
--dropdown-padding-y: var(--space-1);
|
|
25
|
+
--dropdown-padding-x: var(--space-2);
|
|
26
|
+
--dropdown-font-size: var(--font-size-caption-scaled);
|
|
27
|
+
--dropdown-line-height: var(--line-height-caption-scaled);
|
|
28
|
+
--dropdown-label-font-size: var(--font-size-footnote-scaled);
|
|
29
|
+
--dropdown-helper-font-size: var(--font-size-footnote-scaled);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&.solara-dropdown--size-large {
|
|
33
|
+
--dropdown-padding-y: var(--space-3);
|
|
34
|
+
--dropdown-padding-x: var(--space-4);
|
|
35
|
+
--dropdown-font-size: var(--font-size-h5-scaled);
|
|
36
|
+
--dropdown-line-height: var(--line-height-body-scaled);
|
|
37
|
+
--dropdown-label-font-size: var(--font-size-body-scaled);
|
|
38
|
+
--dropdown-helper-font-size: var(--font-size-caption-scaled);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&--label-position-top {
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
|
|
44
|
+
.solara-dropdown-label {
|
|
45
|
+
margin-bottom: calc(var(--dropdown-label-gap) * var(--spacing-multiplier));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&--label-position-left {
|
|
50
|
+
flex-direction: row;
|
|
51
|
+
align-items: center;
|
|
52
|
+
|
|
53
|
+
.solara-dropdown-label {
|
|
54
|
+
margin-right: calc(var(--dropdown-label-gap) * var(--spacing-multiplier));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.solara-dropdown {
|
|
60
|
+
// Base styles for the dropdown
|
|
61
|
+
--dropdown-border-radius: var(--radius-2);
|
|
62
|
+
--dropdown-border-color: var(--color-surface-400);
|
|
63
|
+
--dropdown-bg-color: var(--color-surface-0);
|
|
64
|
+
--dropdown-hover-border-color: var(--color-primary);
|
|
65
|
+
--dropdown-hover-bg-color: var(--color-surface-200);
|
|
66
|
+
--dropdown-focus-border-color: var(--color-primary);
|
|
67
|
+
--dropdown-focus-halo-color: var(--color-primary-focus);
|
|
68
|
+
--dropdown-active-bg-color: var(--color-surface-300);
|
|
69
|
+
|
|
70
|
+
border: 1px solid var(--dropdown-border-color);
|
|
71
|
+
background-color: var(--dropdown-bg-color);
|
|
72
|
+
border-radius: calc(var(--dropdown-border-radius) * var(--radius-multiplier));
|
|
73
|
+
padding: calc(var(--dropdown-padding-y) * var(--spacing-multiplier))
|
|
74
|
+
calc(var(--dropdown-padding-x) * var(--spacing-multiplier));
|
|
75
|
+
// We add extra padding to the right to make sure the text doesn't overlap with the chevron.
|
|
76
|
+
padding-right: calc(
|
|
77
|
+
(var(--dropdown-padding-x) * var(--spacing-multiplier)) + 1.5rem
|
|
78
|
+
);
|
|
79
|
+
font-size: var(--dropdown-font-size);
|
|
80
|
+
font-family: var(--font-body);
|
|
81
|
+
line-height: var(--dropdown-line-height);
|
|
82
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
|
83
|
+
// We use appearance: none to reset the default browser styles for the select element.
|
|
84
|
+
appearance: none;
|
|
85
|
+
// The following properties are used to truncate the text when it's too long.
|
|
86
|
+
text-overflow: ellipsis;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
|
|
90
|
+
&--radius-small {
|
|
91
|
+
--dropdown-border-radius: var(--radius-1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
&--radius-large {
|
|
95
|
+
--dropdown-border-radius: var(--radius-3);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&--radius-full {
|
|
99
|
+
--dropdown-border-radius: var(--radius-full);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&--with-icon {
|
|
103
|
+
padding-left: calc(
|
|
104
|
+
(var(--dropdown-padding-x) * var(--spacing-multiplier)) +
|
|
105
|
+
(var(--dropdown-icon-size, 1.1em)) +
|
|
106
|
+
(var(--dropdown-icon-gap, var(--space-2)) * var(--spacing-multiplier))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&:hover {
|
|
111
|
+
border-color: var(--dropdown-hover-border-color);
|
|
112
|
+
background-color: var(--dropdown-hover-bg-color);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&:focus {
|
|
116
|
+
outline: none;
|
|
117
|
+
border-color: var(--dropdown-focus-border-color);
|
|
118
|
+
box-shadow: 0 0 0 2px var(--dropdown-focus-halo-color);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&:active {
|
|
122
|
+
background-color: var(--dropdown-active-bg-color);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Disabled state
|
|
126
|
+
&--disabled,
|
|
127
|
+
&:disabled {
|
|
128
|
+
background-color: var(--color-surface-200);
|
|
129
|
+
color: var(--color-on-surface-disabled);
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
border-color: var(--color-surface-300);
|
|
132
|
+
|
|
133
|
+
&:hover {
|
|
134
|
+
border-color: var(--color-surface-300);
|
|
135
|
+
background-color: var(--color-surface-200);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Invalid state
|
|
140
|
+
&--invalid {
|
|
141
|
+
border-color: var(--color-destructive);
|
|
142
|
+
|
|
143
|
+
&:focus {
|
|
144
|
+
box-shadow: 0 0 0 2px var(--color-destructive-focus);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&.primary {
|
|
149
|
+
--dropdown-border-color: var(--color-accent-primary-softer);
|
|
150
|
+
--dropdown-bg-color: var(--color-accent-primary-softer);
|
|
151
|
+
--dropdown-hover-border-color: var(--color-accent-primary-softer);
|
|
152
|
+
--dropdown-hover-bg-color: var(--color-accent-primary-softer);
|
|
153
|
+
--dropdown-focus-border-color: var(--color-accent-primary-softer);
|
|
154
|
+
color: var(--color-on-primary);
|
|
155
|
+
|
|
156
|
+
&:focus {
|
|
157
|
+
--dropdown-focus-halo-color: var(--color-primary-focus);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&.default {
|
|
162
|
+
--dropdown-border-color: var(--color-surface-400);
|
|
163
|
+
--dropdown-bg-color: var(--color-surface-0);
|
|
164
|
+
--dropdown-hover-border-color: var(--color-primary);
|
|
165
|
+
--dropdown-hover-bg-color: var(--color-surface-200);
|
|
166
|
+
--dropdown-focus-border-color: var(--color-primary);
|
|
167
|
+
--dropdown-focus-halo-color: var(--color-primary-focus);
|
|
168
|
+
color: var(--color-on-surface);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&.tertiary {
|
|
172
|
+
--dropdown-border-color: var(--color-surface-400);
|
|
173
|
+
--dropdown-bg-color: transparent;
|
|
174
|
+
--dropdown-hover-border-color: var(--color-primary);
|
|
175
|
+
--dropdown-hover-bg-color: var(--color-surface-100);
|
|
176
|
+
--dropdown-focus-border-color: var(--color-primary);
|
|
177
|
+
--dropdown-focus-halo-color: var(--color-primary-focus);
|
|
178
|
+
color: var(--color-on-surface);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&.ghost {
|
|
182
|
+
--dropdown-border-color: transparent;
|
|
183
|
+
--dropdown-bg-color: transparent;
|
|
184
|
+
--dropdown-hover-border-color: transparent;
|
|
185
|
+
--dropdown-hover-bg-color: var(--color-surface-200);
|
|
186
|
+
--dropdown-focus-border-color: var(--color-primary);
|
|
187
|
+
--dropdown-focus-halo-color: var(--color-primary-focus);
|
|
188
|
+
color: var(--color-on-surface);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.solara-dropdown__select-wrapper {
|
|
193
|
+
// This wrapper is used to position the select element and the chevron icon.
|
|
194
|
+
position: relative;
|
|
195
|
+
display: inline-flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.solara-dropdown__chevron {
|
|
200
|
+
// The chevron icon is positioned absolutely to the right of the dropdown.
|
|
201
|
+
position: absolute;
|
|
202
|
+
right: calc(var(--dropdown-padding-x) * var(--spacing-multiplier));
|
|
203
|
+
display: inline-flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
// We use pointer-events: none to make sure the chevron doesn't block clicks on the select element.
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
color: currentColor;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.solara-dropdown-field {
|
|
212
|
+
position: relative;
|
|
213
|
+
display: inline-flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.solara-dropdown-icon {
|
|
218
|
+
position: absolute;
|
|
219
|
+
left: calc(var(--dropdown-padding-x) * var(--spacing-multiplier));
|
|
220
|
+
display: inline-flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
color: currentColor;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.solara-dropdown-field .solara-icon [stroke]:not([stroke="none"]) {
|
|
228
|
+
stroke: currentColor;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.solara-dropdown-field .solara-icon [fill]:not([fill="none"]) {
|
|
232
|
+
fill: currentColor;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.solara-dropdown-container {
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.solara-dropdown-label {
|
|
241
|
+
font-size: var(--dropdown-label-font-size);
|
|
242
|
+
color: var(--color-text-primary);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.solara-dropdown-helper-text {
|
|
246
|
+
font-size: var(--dropdown-helper-font-size);
|
|
247
|
+
color: var(--color-text-secondary);
|
|
248
|
+
margin-top: calc(var(--dropdown-helper-gap) * var(--spacing-multiplier));
|
|
249
|
+
}
|
package/src/Dropdown.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from '@solara/icons';
|
|
3
|
+
import type { IconProps } from '@solara/icons';
|
|
4
|
+
import { DropdownProps } from './Dropdown.types';
|
|
5
|
+
import './Dropdown.scss';
|
|
6
|
+
|
|
7
|
+
type DropdownIcon = IconProps | IconProps['name'];
|
|
8
|
+
|
|
9
|
+
const normalizeIconProps = (icon: DropdownIcon): IconProps =>
|
|
10
|
+
typeof icon === 'string' ? { name: icon } : icon;
|
|
11
|
+
|
|
12
|
+
const resolveIconProps = (icon: DropdownIcon): IconProps => {
|
|
13
|
+
const iconProps = normalizeIconProps(icon);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...iconProps,
|
|
17
|
+
style: {
|
|
18
|
+
...iconProps.style,
|
|
19
|
+
// Keep dropdown icons aligned with the text color.
|
|
20
|
+
color: 'currentColor'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A standard dropdown select component that allows users to choose a value from a list of options.
|
|
27
|
+
* It is a controlled component, so you must provide a `value` and `onChange` handler.
|
|
28
|
+
* It supports all the standard props of an HTML select element.
|
|
29
|
+
*/
|
|
30
|
+
export const Dropdown: React.FC<DropdownProps> = ({
|
|
31
|
+
options,
|
|
32
|
+
size = 'medium',
|
|
33
|
+
radius = 'medium',
|
|
34
|
+
variant = 'default',
|
|
35
|
+
isInvalid = false,
|
|
36
|
+
isDisabled = false,
|
|
37
|
+
// The label for the dropdown.
|
|
38
|
+
label,
|
|
39
|
+
// The position of the label.
|
|
40
|
+
labelPosition = 'top',
|
|
41
|
+
// The helper text for the dropdown.
|
|
42
|
+
helperText,
|
|
43
|
+
iconLeft,
|
|
44
|
+
fullWidth,
|
|
45
|
+
...props
|
|
46
|
+
}) => {
|
|
47
|
+
// The base class for the component to scope all styles.
|
|
48
|
+
const baseClass = 'solara-dropdown';
|
|
49
|
+
|
|
50
|
+
// The size class modifies the padding and font size of the component.
|
|
51
|
+
const sizeClass = `solara-dropdown--size-${size}`;
|
|
52
|
+
|
|
53
|
+
const radiusClass = `solara-dropdown--radius-${radius}`;
|
|
54
|
+
|
|
55
|
+
// The invalid class is applied when the `isInvalid` prop is true, typically for validation errors.
|
|
56
|
+
const invalidClass = isInvalid ? 'solara-dropdown--invalid' : '';
|
|
57
|
+
|
|
58
|
+
// The disabled class is applied when the `isDisabled` prop is true.
|
|
59
|
+
const disabledClass = isDisabled ? 'solara-dropdown--disabled' : '';
|
|
60
|
+
|
|
61
|
+
// The final classes for the component are composed of the base class and any modifier classes.
|
|
62
|
+
const classes = [
|
|
63
|
+
baseClass,
|
|
64
|
+
sizeClass,
|
|
65
|
+
radiusClass,
|
|
66
|
+
invalidClass,
|
|
67
|
+
disabledClass,
|
|
68
|
+
variant,
|
|
69
|
+
iconLeft ? 'solara-dropdown--with-icon' : '',
|
|
70
|
+
]
|
|
71
|
+
.join(' ')
|
|
72
|
+
.trim();
|
|
73
|
+
|
|
74
|
+
// The dropdown element itself is created here, so it can be wrapped with the label and helper text.
|
|
75
|
+
const dropdown = (
|
|
76
|
+
// This wrapper positions the select element and the chevron icon.
|
|
77
|
+
<div className={`${baseClass}__select-wrapper`}>
|
|
78
|
+
<select className={classes} disabled={isDisabled} {...props}>
|
|
79
|
+
{/* Map over the provided `options` array to create the <option> elements. */}
|
|
80
|
+
{options.map((option) => (
|
|
81
|
+
<option key={option.value} value={option.value}>
|
|
82
|
+
{option.label}
|
|
83
|
+
</option>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
86
|
+
{/* The chevron icon is a separate element to allow for more control over its position. */}
|
|
87
|
+
<div className={`${baseClass}__chevron`}>
|
|
88
|
+
<Icon name="arrow_chevron_down_16" />
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const fullWidthClass = fullWidth ? 'solara-dropdown-wrapper--full-width' : '';
|
|
94
|
+
|
|
95
|
+
// This wrapper handles the positioning of the label and the dropdown (either top or left).
|
|
96
|
+
const wrapperClass = `solara-dropdown-wrapper solara-dropdown-wrapper--label-position-${labelPosition} ${sizeClass} ${fullWidthClass}`.trim();
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className={wrapperClass}>
|
|
100
|
+
{/* The label is rendered only if the `label` prop is provided. */}
|
|
101
|
+
{label && <label className='solara-dropdown-label'>{label}</label>}
|
|
102
|
+
{/* This container groups the dropdown and its helper text. */}
|
|
103
|
+
<div className='solara-dropdown-container'>
|
|
104
|
+
<div className='solara-dropdown-field' data-has-icon={iconLeft ? 'true' : undefined}>
|
|
105
|
+
{iconLeft ? (
|
|
106
|
+
<span className='solara-dropdown-icon'>
|
|
107
|
+
<Icon {...resolveIconProps(iconLeft)} />
|
|
108
|
+
</span>
|
|
109
|
+
) : null}
|
|
110
|
+
{dropdown}
|
|
111
|
+
</div>
|
|
112
|
+
{/* The helper text is rendered only if the `helperText` prop is provided. */}
|
|
113
|
+
{helperText && (
|
|
114
|
+
<p className='solara-dropdown-helper-text'>{helperText}</p>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { IconProps } from '@solara/icons';
|
|
3
|
+
|
|
4
|
+
export type DropdownSize = 'small' | 'medium' | 'large';
|
|
5
|
+
|
|
6
|
+
export type DropdownVariant = 'primary' | 'default' | 'tertiary' | 'ghost';
|
|
7
|
+
|
|
8
|
+
export interface DropdownProps
|
|
9
|
+
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
|
10
|
+
/**
|
|
11
|
+
* The options for the dropdown.
|
|
12
|
+
*/
|
|
13
|
+
options: { label: string; value: string }[];
|
|
14
|
+
/**
|
|
15
|
+
* The size of the dropdown.
|
|
16
|
+
*/
|
|
17
|
+
size?: DropdownSize;
|
|
18
|
+
/**
|
|
19
|
+
* The visual style of the dropdown.
|
|
20
|
+
*/
|
|
21
|
+
variant?: DropdownVariant;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the dropdown is invalid.
|
|
24
|
+
*/
|
|
25
|
+
isInvalid?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the dropdown is disabled.
|
|
28
|
+
*/
|
|
29
|
+
isDisabled?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The label for the dropdown.
|
|
32
|
+
*/
|
|
33
|
+
label?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The position of the label.
|
|
36
|
+
*/
|
|
37
|
+
labelPosition?: 'top' | 'left';
|
|
38
|
+
/**
|
|
39
|
+
* The helper text for the dropdown.
|
|
40
|
+
*/
|
|
41
|
+
helperText?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Optional leading icon to render inside the field.
|
|
44
|
+
*/
|
|
45
|
+
iconLeft?: IconProps | IconProps['name'];
|
|
46
|
+
/**
|
|
47
|
+
* The radius of the dropdown.
|
|
48
|
+
*/
|
|
49
|
+
radius?: 'small' | 'medium' | 'large' | 'full';
|
|
50
|
+
/**
|
|
51
|
+
* Whether the dropdown should take up the full width of its container.
|
|
52
|
+
*/
|
|
53
|
+
fullWidth?: boolean;
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Dropdown';
|