@studiocms/ui 0.4.12 → 0.4.13
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/dist/components/Badge/Badge.astro +16 -2
- package/dist/components/SearchSelect/SearchSelect.astro +137 -64
- package/dist/components/SearchSelect/searchselect.css +64 -1
- package/dist/components/SearchSelect/searchselect.d.ts +15 -2
- package/dist/components/SearchSelect/searchselect.js +347 -146
- package/dist/components/Select/Select.astro +133 -69
- package/dist/components/Select/select.css +76 -3
- package/dist/components/Select/select.d.ts +17 -0
- package/dist/components/Select/select.js +336 -123
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { generateID } from '../../utils/generateID.js';
|
|
3
|
+
import Badge from '../Badge/Badge.astro';
|
|
3
4
|
import Icon from '../Icon/Icon.astro';
|
|
4
5
|
import './select.css';
|
|
5
6
|
|
|
@@ -21,18 +22,36 @@ interface Option {
|
|
|
21
22
|
disabled?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
type NonMultipleProps = {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the select accepts multiple options. Defaults to `false`.
|
|
28
|
+
*/
|
|
29
|
+
multiple: false;
|
|
30
|
+
/**
|
|
31
|
+
* The default value of the select.
|
|
32
|
+
*/
|
|
33
|
+
defaultValue: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type MultipleProps = {
|
|
37
|
+
/**
|
|
38
|
+
* Whether the select accepts multiple options. Defaults to `false`.
|
|
39
|
+
*/
|
|
40
|
+
multiple: true;
|
|
41
|
+
/**
|
|
42
|
+
* The default value of the select.
|
|
43
|
+
*/
|
|
44
|
+
defaultValue: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
24
47
|
/**
|
|
25
48
|
* The props for the select component.
|
|
26
49
|
*/
|
|
27
|
-
|
|
50
|
+
type BaseProps = {
|
|
28
51
|
/**
|
|
29
52
|
* The label of the select.
|
|
30
53
|
*/
|
|
31
54
|
label?: string;
|
|
32
|
-
/**
|
|
33
|
-
* The default value of the select.
|
|
34
|
-
*/
|
|
35
|
-
defaultValue?: string;
|
|
36
55
|
/**
|
|
37
56
|
* Additional classes to apply to the select.
|
|
38
57
|
*/
|
|
@@ -61,7 +80,13 @@ interface Props {
|
|
|
61
80
|
* The placeholder of the select. Defaults to `Select`.
|
|
62
81
|
*/
|
|
63
82
|
placeholder?: string;
|
|
64
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The maximum number of options that can be selected. Defaults to `undefined`.
|
|
85
|
+
*/
|
|
86
|
+
max?: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type Props = BaseProps & (MultipleProps | NonMultipleProps);
|
|
65
90
|
|
|
66
91
|
const {
|
|
67
92
|
label,
|
|
@@ -73,74 +98,113 @@ const {
|
|
|
73
98
|
disabled,
|
|
74
99
|
fullWidth,
|
|
75
100
|
placeholder = 'Select',
|
|
101
|
+
multiple = false,
|
|
102
|
+
max = undefined,
|
|
76
103
|
} = Astro.props;
|
|
104
|
+
|
|
105
|
+
let selected: Option | (Option | undefined)[] | undefined;
|
|
106
|
+
|
|
107
|
+
if (multiple && Array.isArray(defaultValue)) {
|
|
108
|
+
selected = defaultValue.map((x) => options.find((y) => y.value === x));
|
|
109
|
+
} else {
|
|
110
|
+
selected = options.find((x) => x.value === defaultValue);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const defaultLabel = selected
|
|
114
|
+
? Array.isArray(selected)
|
|
115
|
+
? selected.map((x) => x?.label)
|
|
116
|
+
: selected.label
|
|
117
|
+
: placeholder;
|
|
77
118
|
---
|
|
78
119
|
|
|
79
120
|
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
id={`${name}-container`}
|
|
122
|
+
class="sui-select-label"
|
|
123
|
+
class:list={[disabled && "disabled", className, fullWidth && "full"]}
|
|
124
|
+
data-options={JSON.stringify(options)}
|
|
125
|
+
data-multiple={multiple ? "true" : undefined}
|
|
126
|
+
data-multiple-max={multiple && max !== undefined ? max : undefined}
|
|
127
|
+
data-id={name}
|
|
85
128
|
>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
129
|
+
{label && (
|
|
130
|
+
<label class="label" for={`${name}-select-btn`}>
|
|
131
|
+
{label}
|
|
132
|
+
<span class="req-star">{isRequired && "*"}</span>
|
|
133
|
+
</label>
|
|
134
|
+
)}
|
|
135
|
+
<div class="sui-select-dropdown-container">
|
|
136
|
+
<button
|
|
137
|
+
class="sui-select-button"
|
|
138
|
+
role="combobox"
|
|
139
|
+
aria-controls={`${name}-dropdown`}
|
|
140
|
+
aria-expanded="false"
|
|
141
|
+
id={`${name}-select-btn`}
|
|
142
|
+
type="button"
|
|
143
|
+
aria-label={placeholder}
|
|
144
|
+
title={placeholder}
|
|
145
|
+
>
|
|
146
|
+
<span class="sui-select-value-span" id={`${name}-value-span`}>
|
|
147
|
+
{Array.isArray(selected)
|
|
148
|
+
? <div class="sui-select-badge-container">{selected.map((s) => s && <Badge class="sui-select-badge" data-value={s.value} size="sm" label={s.label} iconPosition="right" icon="x-mark" />)}</div>
|
|
149
|
+
: selected?.label ?? defaultLabel
|
|
150
|
+
}
|
|
151
|
+
</span>
|
|
152
|
+
<Icon name="chevron-up-down" width={24} height={24} class="sui-select-chevron" />
|
|
153
|
+
</button>
|
|
154
|
+
<ul class="sui-select-dropdown" role="listbox" id={`${name}-dropdown`}>
|
|
155
|
+
{
|
|
156
|
+
options.map((x, i) => {
|
|
157
|
+
const isSelected = Array.isArray(selected)
|
|
158
|
+
? selected.map((y) => y && y.value).includes(x.value)
|
|
159
|
+
: selected?.value === x.value;
|
|
160
|
+
return (
|
|
161
|
+
<li
|
|
162
|
+
class="sui-select-option"
|
|
163
|
+
role="option"
|
|
164
|
+
value={x.value}
|
|
165
|
+
class:list={[
|
|
166
|
+
isSelected && `selected`,
|
|
167
|
+
x.disabled && "disabled",
|
|
168
|
+
]}
|
|
169
|
+
id={isSelected ? `${name}-selected` : ""}
|
|
170
|
+
data-option-index={i}
|
|
171
|
+
>
|
|
172
|
+
{x.label}
|
|
173
|
+
</li>
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
</ul>
|
|
178
|
+
</div>
|
|
179
|
+
<select class="sui-hidden-select" id={name} name={name} required={isRequired} multiple={multiple ? "" : undefined} hidden tabindex="-1">
|
|
180
|
+
<option value={""}> Select </option>
|
|
181
|
+
{
|
|
182
|
+
options.map((x) => {
|
|
183
|
+
const isSelected = Array.isArray(selected)
|
|
184
|
+
? selected.map((y) => y && y.value).includes(x.value)
|
|
185
|
+
: selected?.value === x.value;
|
|
186
|
+
(
|
|
187
|
+
<option
|
|
188
|
+
value={x.value}
|
|
189
|
+
selected={isSelected}
|
|
190
|
+
disabled={x.disabled}
|
|
191
|
+
>
|
|
192
|
+
{x.label}
|
|
193
|
+
</option>
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
</select>
|
|
198
|
+
{multiple && max !== undefined && (
|
|
199
|
+
<span class="sui-select-max-span">
|
|
200
|
+
<span class="sui-select-select-count">0</span> / {max} selected
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
{
|
|
204
|
+
multiple && Array.isArray(selected ?? []) && (
|
|
205
|
+
<div class="sui-select-badge-container-below"></div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
144
208
|
</div>
|
|
145
209
|
<script>
|
|
146
210
|
import "studiocms:ui/scripts/select"
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
font-weight: 700;
|
|
25
25
|
}
|
|
26
26
|
.sui-select-button {
|
|
27
|
-
padding: 0.5rem 0.75rem 0.5rem
|
|
27
|
+
padding: 0.5rem 0.75rem 0.5rem 0.75rem;
|
|
28
28
|
border-radius: var(--radius-md);
|
|
29
29
|
border: 1px solid hsl(var(--border));
|
|
30
30
|
background: hsl(var(--background-step-2));
|
|
31
31
|
color: hsl(var(--text-normal));
|
|
32
|
-
transition: background border 0.15s ease;
|
|
32
|
+
transition: background, border 0.15s ease;
|
|
33
33
|
display: flex;
|
|
34
34
|
flex-direction: row;
|
|
35
35
|
align-items: center;
|
|
@@ -50,6 +50,23 @@
|
|
|
50
50
|
border: 1px solid hsl(var(--primary-base));
|
|
51
51
|
background: hsl(var(--background-step-2));
|
|
52
52
|
}
|
|
53
|
+
.sui-select-button:has(.sui-select-badge-container) {
|
|
54
|
+
padding: 0.5rem 0.75rem 0.5rem 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
.sui-select-value-span {
|
|
57
|
+
position: relative;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: flex-start;
|
|
60
|
+
gap: 0.25rem;
|
|
61
|
+
}
|
|
62
|
+
.sui-select-dropdown-container {
|
|
63
|
+
width: 100%;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: 0.25rem;
|
|
67
|
+
position: relative;
|
|
68
|
+
height: fit-content;
|
|
69
|
+
}
|
|
53
70
|
.sui-select-dropdown {
|
|
54
71
|
position: absolute;
|
|
55
72
|
width: 100%;
|
|
@@ -72,7 +89,7 @@
|
|
|
72
89
|
}
|
|
73
90
|
.sui-select-dropdown.above {
|
|
74
91
|
top: auto;
|
|
75
|
-
bottom: calc(100%
|
|
92
|
+
bottom: calc(100% + 0.25rem);
|
|
76
93
|
}
|
|
77
94
|
.sui-select-option {
|
|
78
95
|
padding: 0.5rem;
|
|
@@ -89,6 +106,11 @@
|
|
|
89
106
|
.sui-select-option.focused {
|
|
90
107
|
background-color: hsl(var(--background-step-3));
|
|
91
108
|
}
|
|
109
|
+
.sui-select-label[data-multiple=true] .sui-select-option.selected:hover,
|
|
110
|
+
.sui-select-label[data-multiple=true] .sui-select-option.selected:focus,
|
|
111
|
+
.sui-select-label[data-multiple=true] .sui-select-option.selected.focused {
|
|
112
|
+
background-color: hsl(var(--primary-hover));
|
|
113
|
+
}
|
|
92
114
|
.sui-select-option:focus {
|
|
93
115
|
outline: none;
|
|
94
116
|
border: none;
|
|
@@ -98,6 +120,9 @@
|
|
|
98
120
|
color: hsl(var(--text-inverted));
|
|
99
121
|
cursor: default;
|
|
100
122
|
}
|
|
123
|
+
.sui-select-label[data-multiple=true] .sui-select-option.selected {
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
101
126
|
.sui-hidden-select {
|
|
102
127
|
height: 0;
|
|
103
128
|
width: 0;
|
|
@@ -108,3 +133,51 @@
|
|
|
108
133
|
pointer-events: none;
|
|
109
134
|
opacity: 0;
|
|
110
135
|
}
|
|
136
|
+
.sui-select-badge-container {
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: row;
|
|
139
|
+
gap: 0.25rem;
|
|
140
|
+
width: fit-content;
|
|
141
|
+
}
|
|
142
|
+
.sui-select-badge-container-below:has(.sui-select-badge-container) .sui-select-badge-container {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: row;
|
|
145
|
+
width: 100%;
|
|
146
|
+
margin-top: 0.5rem;
|
|
147
|
+
gap: 0.5rem;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
}
|
|
150
|
+
.sui-select-label:has(.sui-select-max-span) .sui-select-badge-container {
|
|
151
|
+
margin-top: 0rem;
|
|
152
|
+
}
|
|
153
|
+
.sui-select-badge-container .sui-select-badge.sui-badge {
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
cursor: default;
|
|
156
|
+
position: relative;
|
|
157
|
+
padding-right: 2rem;
|
|
158
|
+
border-block: 1.5px solid rgba(0, 0, 0, 0);
|
|
159
|
+
}
|
|
160
|
+
.sui-select-badge-container .sui-select-badge.sui-badge svg {
|
|
161
|
+
position: absolute;
|
|
162
|
+
right: 0.25rem;
|
|
163
|
+
padding: 4px;
|
|
164
|
+
height: 20px;
|
|
165
|
+
width: 20px;
|
|
166
|
+
aspect-ratio: 1 / 1;
|
|
167
|
+
border-radius: 999px;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
outline: 1px solid hsla(var(--border), 0);
|
|
170
|
+
transition: background-color 0.15s ease, outline 0.15s ease;
|
|
171
|
+
}
|
|
172
|
+
.sui-select-badge-container .sui-select-badge.sui-badge svg:hover {
|
|
173
|
+
background-color: hsla(100, 100%, 95%, 0.2);
|
|
174
|
+
}
|
|
175
|
+
.sui-select-badge-container .sui-select-badge.sui-badge svg:active,
|
|
176
|
+
.sui-select-badge-container .sui-select-badge.sui-badge svg:focus {
|
|
177
|
+
background-color: hsla(100, 100%, 95%, 0.2);
|
|
178
|
+
outline: 1px solid hsl(var(--border));
|
|
179
|
+
}
|
|
180
|
+
.sui-select-max-span {
|
|
181
|
+
font-size: 0.875em;
|
|
182
|
+
color: hsl(var(--text-muted));
|
|
183
|
+
}
|
|
@@ -1 +1,18 @@
|
|
|
1
|
+
type SelectOption = {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type SelectContainer = HTMLDivElement & {
|
|
7
|
+
button: HTMLButtonElement | null;
|
|
8
|
+
dropdown: HTMLUListElement | null;
|
|
9
|
+
select: HTMLSelectElement | null;
|
|
10
|
+
};
|
|
11
|
+
type SelectState = {
|
|
12
|
+
optionsMap: Record<string, SelectOption[]>;
|
|
13
|
+
isMultipleMap: Record<string, boolean>;
|
|
14
|
+
focusIndex: number;
|
|
15
|
+
placeholder: string;
|
|
16
|
+
};
|
|
17
|
+
type ResizeCallback = (width: number, height: number, element: Element) => void;
|
|
1
18
|
declare function loadSelects(): void;
|