This repository has been archived by the owner on Oct 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathSelect.svelte
270 lines (248 loc) · 8.21 KB
/
Select.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<script>
import { onMount } from 'svelte';
import Errors from './Errors.svelte';
// Select Options
// -------------------------------------------------------------------------------------------
let thisSelect;
const uid = Math.floor(Math.random() * 10000000);
export let name = `select-${uid}`;
export let id = `${name}-${uid}`;
export let tabindex = null;
export let options = ['One', 'Two', 'Three'];
export let selected = null;
export let multiple = false;
export let size = 0;
export let defaultToNullValue = !selected && !multiple ? true : false;
export let nullValueText = 'Select';
export let disabled = null;
export let autofocus = null; // boolean, **use thoughtfully** (https://tinyurl.com/inputautofocus)
export let autocomplete = null;
// Select Style Options
// -------------------------------------------------------------------------------------------
export let containerClasses = '';
export let selectClasses = '';
export let inheritFontSize = false;
export let width = 'full'; // resets to auto if use any keyword other than 'full'
export let leftPadding = true;
export let rounded = true;
export let border = true;
export let bgFill = !border ? true : false;
export let shadow = true; // won't be applied, irrespective of value, if border is false
if (width === 'full') containerClasses += ` flex flex-col`;
if (width === 'full') selectClasses += ` block w-full`;
// Label & Description Options
// -------------------------------------------------------------------------------------------
export let label = null;
export let labelHidden = false;
export let showRequiredHint = true; // toggles display of asterisk next to label for required fields
export let note = null;
// Standard Validation Options
// -------------------------------------------------------------------------------------------
// By default, validation and display of error messages will occur after user interacts
// with select first time; can optionally be set to validate on page load
export let validateOnMount = false; // if true, will validate select and show any errors when component is mounted
export let required = false;
let errors = [];
let warnings = [];
// Optional Custom Validation Options
// -------------------------------------------------------------------------------------------
// - expects an object with 'errors' and/or 'warnings' arrays of rules (as objects)
// - first property 'pattern' is regex to evaluate textarea's value against
// - second property 'messageIfMatch' is a boolean that determines if message is displayed when pattern matches or misses
// - third property 'message' is error message displayed when pattern & messageIfMatch align
// - if multiple errors exists they will be displayed one at a time, in the order added to the arrays
// - if errors and warnings are both present, errors will be shown first
export let customValidation = {};
// example
// customValidation = {
// errors: [
// {
// pattern: 'One',
// messageIfMatch: true,
// message: "Believe us - it's lonely at the top.",
// },
// ],
// warnings: [
// {
// pattern: 'Two',
// messageIfMatch: true,
// message: 'Number two? Bold choice.',
// },
// ],
// };
// Handlers
// -------------------------------------------------------------------------------------------
function checkValidity() {
// clear & re-check for current errors or warnings
errors = [];
warnings = [];
thisSelect.checkValidity(); // will fire 'invalid' event if any of standard constraints fail
if (customValidation?.errors?.length || customValidation?.warnings?.length) {
checkCustomValidation(); // will test custom rules and populate any related errors/warnings
}
}
function checkCustomValidation() {
customValidation?.errors?.forEach((rule) => {
const { pattern, messageIfMatch, message } = rule;
const regex = new RegExp(pattern, 'g');
const validity = regex.test(selected); // 'selected' is Svelte variable for current value
if ((validity && messageIfMatch) || (!validity && !messageIfMatch)) errors.push(message);
});
customValidation?.warnings?.forEach((rule) => {
const { pattern, messageIfMatch, message } = rule;
const regex = new RegExp(pattern, 'g');
const validity = regex.test(selected);
if ((validity && messageIfMatch) || (!validity && !messageIfMatch)) warnings.push(message);
});
}
onMount(() => {
if (validateOnMount) checkValidity();
});
function inputHandler(e) {
const currentlySelected = e.target.selectedOptions;
selected = [...currentlySelected].map(option => option.value);
checkValidity();
}
function invalidHandler() {
// Standard Validation Messages
if (thisSelect.validity.valueMissing) errors = ['This field is required.'];
}
</script>
<div class={containerClasses}>
{#if label}
<label
for={id}
id="{id}-label"
class:required
class:hide={labelHidden}
class="block font-medium text-gray-700 mb-3"
>
{label}
{#if required && showRequiredHint}
<abbr title="Required" class="font-normal text-gray-500">*</abbr>
{/if}
</label>
{/if}
<!-- svelte-ignore a11y-autofocus -->
<select
bind:this='{thisSelect}'
{name}
{id}
{tabindex}
{disabled}
{autofocus}
{autocomplete}
{multiple}
{size}
{required}
value={selected}
class:inheritFontSize
class:leftPadding
class:addRounding={rounded}
class:addBorder={border}
class:addBg={bgFill}
class:addShadow={shadow}
class:hasWarning={warnings.length !== 0}
class:hasError={errors.length !== 0}
class={selectClasses}
aria-required={required ? true : null}
aria-disabled={disabled ? true : null}
aria-labelledby={label ? `${id}-label` : null}
aria-describedby={note ? `${id}-description` : null}
aria-invalid={errors.length !== 0 ? true : null}
on:input={inputHandler}
on:blur={inputHandler}
on:invalid={invalidHandler}
>
<!-- using 'add' class names (e.g. 'addShadow') to avoid collisions with Tailwind class names -->
{#if defaultToNullValue && !multiple}
{#if nullValueText}
<option value disabled selected>{nullValueText}</option>
{:else}
<option value disabled selected></option>
{/if}
{/if}
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
{#if note}
<p id="{id}-description" class="description-block">{note}</p>
{/if}
{#if errors?.length || warnings?.length}
<div class="error-block">
<Errors {errors} {warnings} />
</div>
{/if}
</div>
<style>
.hide {
@apply sr-only;
}
select {
@apply pr-10 py-2 text-gray-700 border border-transparent focus_outline-none focus_ring-action-hover focus_border-action-hover;
}
select:not(.inheritFontSize) {
@apply text-sm;
}
select.addRounding {
@apply rounded-md;
}
select.leftPadding {
@apply pl-3;
}
select.addBg {
@apply bg-gray-100 focus_bg-transparent;
}
select.addBorder {
@apply border-gray-300 focus_border-action-hover;
}
select.addShadow:not([disabled]) {
@apply shadow-sm;
}
select[multiple] {
@apply px-2;
}
/* TODO: sort out how to apply custom styles to selected option background color
select[multiple] option:checked {
@apply bg-action-hover;
} */
/* Can enable select styles for warning state if wanted, but since these are
optional UX is preferrable to reserve added visual weight for errors alone
select.hasWarning:not([disabled]) {
@apply text-yellow-700 placeholder-yellow-400 focus_ring-yellow-500 focus_border-yellow-500;
}
select.addBorder.hasWarning:not([disabled]) {
@apply border-yellow-500;
} */
select.hasError:not([disabled]) {
@apply text-red-700 placeholder-red-400 focus_ring-red-500 focus_border-red-500;
}
select.addBorder.hasError:not([disabled]) {
@apply border-red-500;
}
/* additional helpful psuedo classes that can be styled
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#ui_pseudo-classes
select:required {}
select:optional {}
select:user-invalid {}
*/
select.addBorder[disabled] {
@apply border-opacity-40;
}
select.addShadow[disabled] {
@apply shadow-none;
}
.description-block {
@apply text-xs text-gray-500;
}
:not(.optionInputGroup) .description-block {
@apply mt-2;
}
:not(.optionInputGroup) .error-block {
@apply mt-2;
}
:not(.optionInputGroup) .description-block + .error-block {
@apply mt-1;
}
</style>