@studiocms/ui 0.4.11 → 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.
@@ -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
- interface Props {
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
- id={`${name}-container`}
81
- class="sui-select-label"
82
- class:list={[disabled && "disabled", className, fullWidth && "full"]}
83
- data-options={JSON.stringify(options)}
84
- data-id={name}
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
- {label && (
87
- <label class="label" for={`${name}-select-btn`}>
88
- {label}
89
- <span class="req-star">{isRequired && "*"}</span>
90
- </label>
91
- )}
92
- <button
93
- class="sui-select-button"
94
- role="combobox"
95
- aria-controls={`${name}-dropdown`}
96
- aria-expanded="false"
97
- id={`${name}-select-btn`}
98
- type="button"
99
- aria-label={placeholder}
100
- title={placeholder}
101
- >
102
- <span class="sui-select-value-span" id={`${name}-value-span`}>
103
- {
104
- defaultValue
105
- ? options.find((x) => x.value === defaultValue)?.label
106
- : placeholder
107
- }
108
- </span>
109
- <Icon name="chevron-up-down" width={24} height={24} />
110
- </button>
111
- <ul class="sui-select-dropdown" role="listbox" id={`${name}-dropdown`}>
112
- {
113
- options.map((x, i) => (
114
- <li
115
- class="sui-select-option"
116
- role="option"
117
- value={x.value}
118
- class:list={[
119
- defaultValue === x.value && `selected`,
120
- x.disabled && "disabled",
121
- ]}
122
- id={defaultValue === x.value ? `${name}-selected` : ""}
123
- data-option-index={i}
124
- >
125
- {x.label}
126
- </li>
127
- ))
128
- }
129
- </ul>
130
- <select class="sui-hidden-select" id={name} name={name} required={isRequired} hidden tabindex="-1">
131
- <option value={""}> Select </option>
132
- {
133
- options.map((x) => (
134
- <option
135
- value={x.value}
136
- selected={defaultValue === x.value}
137
- disabled={x.disabled}
138
- >
139
- {x.label}
140
- </option>
141
- ))
142
- }
143
- </select>
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 1rem;
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% - 18px + 0.25rem);
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;