This repository was archived by the owner on Oct 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathCheckboxGroup.svelte
256 lines (235 loc) · 7.96 KB
/
CheckboxGroup.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
<script>
import { onMount } from 'svelte';
import Errors from './Errors.svelte';
const uid = Math.floor(Math.random() * 10000000);
// Group Options
// -------------------------------------------------------------------------------------------
let thisGroup;
export let groupLabel = null; // optional label header for input group
export let options = [];
// example content
// options = [
// {
// label: 'Checkbox One',
// checked: false,
// indeterminate: null, // (https://tinyurl.com/inputindeterminate)
// readonly: null,
// disabled: false,
// required: true,
// value: 'one',
// note: "I'm number one.",
// },
// { label: 'Checkbox Two', value: 'two', note: 'I love number two.' },
// // option must include label OR value
// { label: 'Checkbox Three' },
// { value: 'four' },
// ];
export let group = []; // selected items
export let name = `checkboxgroup-${uid}`;
export let tabindex = null;
export let autofocus = null; // boolean, **use thoughtfully** (https://tinyurl.com/inputautofocus)
// Input Options
// -------------------------------------------------------------------------------------------
let thisInput = [];
// Input Style Options – applies to all inputs in group
// -------------------------------------------------------------------------------------------
export let containerClasses = '';
export let inputClasses = '';
export let inheritFontSize = false;
export let displayAsRow = true; // toggles horizontal or vertical display of options
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
// Label & Description Options – applies to all inputs in group
// -------------------------------------------------------------------------------------------
// optional label and note for each input are passed in through 'options' array
export let labelHidden = false;
export let labelWeightNormal = true; // toggles weight from default 'medium' to 'normal'
export let optionLabelRight = true; // toggles which side of checkbox input is displayed on
export let showRequiredHint = true; // toggles display of asterisk next to label for required fields
// Standard Validation Options – applies to all inputs in group
// -------------------------------------------------------------------------------------------
// optional 'required' validation passed in through 'options' array
export let validateOnMount = false; // if true, will validate input and show any errors when component is mounted
let errors = [];
let warnings = [];
// Handlers
// -------------------------------------------------------------------------------------------
function autoFocusFirstChildInput(node) {
const input = node.querySelector('input');
input.focus();
}
function checkValidity(input, i) {
// clear & re-check for current errors or warnings
errors[i] = [];
warnings[i] = [];
input.checkValidity(); // will fire 'invalid' event if any of standard constraints fail
}
onMount(() => {
if (autofocus) autoFocusFirstChildInput(thisGroup);
if (validateOnMount) {
const inputs = Array.from(thisGroup.querySelectorAll('input'));
inputs.forEach(input => input.checkValidity());
}
});
function invalidHandler(input, i) {
// Standard Validation Messages
if (input.validity.valueMissing) errors[i] = ['This is required.'];
}
</script>
<div class={groupLabel ? 'space-y-4' : null} bind:this={thisGroup} {...$$restProps}>
{#if groupLabel}
<p class="font-medium">{groupLabel}</p>
{/if}
<div class="flex {displayAsRow ? 'space-x-6' : 'flex-col space-y-4 items-start'}">
{#each options as checkbox, i}
<div class="optionInputBlock {containerClasses}" class:optionLabelRight>
{#if checkbox.label}
<label
for="{name}-{i + 1}"
id="{name}-{i + 1}-label"
class:hide={labelHidden}
class="block text-gray-700 {labelWeightNormal ? 'font-normal' : 'font-medium'}"
>
{checkbox.label}
{#if checkbox.required && showRequiredHint}
<abbr title="Required" class="font-normal text-gray-500">*</abbr>
{/if}
</label>
{/if}
<input
bind:this={thisInput[i]}
type="checkbox"
{name}
id="{name}-{i + 1}"
{tabindex}
value={checkbox.value ? checkbox.value : checkbox.label ? checkbox.label : null}
bind:group
checked={checkbox.checked ? checkbox.checked : null}
indeterminate={checkbox.indeterminate ? checkbox.indeterminate : null}
disabled={checkbox.disabled ? checkbox.disabled : null}
readonly={checkbox.readonly ? checkbox.readonly : null}
required={checkbox.required ? checkbox.required : null}
class:inheritFontSize
class:addRounding={rounded}
class:addBg={bgFill}
class:addBorder={border}
class:addShadow={shadow && border}
class:hasWarning={warnings[i] && warnings[i].length !== 0}
class:hasError={errors[i] && errors[i].length !== 0}
class="{inputClasses} {!checkbox.label ? 'mt-1' : null}"
aria-required={checkbox.required ? checkbox.required : null}
aria-disabled={checkbox.disabled ? checkbox.disabled : null}
aria-labelledby={checkbox.label ? `${name}-${i + 1}-label` : null}
aria-describedby={checkbox.note ? `${name}-${i + 1}-description` : null}
aria-invalid={errors.length !== 0 ? true : null}
on:input={checkValidity(thisInput[i], i)}
on:blur={checkValidity(thisInput[i], i)}
on:invalid={invalidHandler(thisInput[i], i)}
/>
<!-- using 'add' class names (e.g. 'addShadow') to avoid collisions with Tailwind class names -->
{#if checkbox.note}
<p id="{name}-{i + 1}-description" class="description-block">{checkbox.note}</p>
{/if}
{#if errors[i]?.length || warnings[i]?.length}
<div class="error-block">
<Errors {errors} {warnings} />
</div>
{/if}
</div>
{/each}
</div>
</div>
<style>
.hide {
@apply sr-only;
}
input {
@apply w-4 h-4 border border-transparent text-action focus_ring-action-hover;
}
input.addRounding {
@apply rounded;
}
input.addBg:not(.addBorder):not(:checked) {
@apply bg-gray-200;
}
input.addBg.addBorder:not(:checked) {
@apply bg-gray-100;
}
input.leftPadding {
@apply px-3;
}
input:not(.inheritFontSize) {
@apply text-sm;
}
input.addBorder {
@apply border-gray-300;
}
input:not([readonly], [disabled]) {
@apply focus_ring-action-hover focus_border-action-hover;
}
input[readonly] {
@apply bg-transparent focus_ring-transparent focus_border-transparent cursor-default pointer-events-none;
}
input.addShadow:not([readonly], [disabled]) {
@apply shadow-sm;
}
/* Can enable input styles for warning state if wanted, but since these are
optional UX is preferrable to reserve added visual weight for errors alone
input.hasWarning:not([disabled]) {
@apply focus_ring-yellow-500 focus_border-yellow-500;
}
input.addBorder.hasWarning:not([disabled]) {
@apply border-yellow-500;
} */
input.hasError:not([disabled]) {
@apply focus_ring-red-500 focus_border-red-500;
}
input.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
input:required {}
input:optional {}
input:user-invalid {}
*/
input[disabled],
input[readonly] {
@apply opacity-60;
}
input.addBorder[disabled] {
@apply border-opacity-40;
}
input.addShadow[disabled] {
@apply shadow-none;
}
.optionInputBlock {
@apply inline-grid gap-x-3 gap-y-1;
grid-template-columns: auto auto;
align-content: baseline;
}
input {
align-self: center;
}
.error-block {
grid-column-start: 1;
}
.optionLabelRight label {
grid-column-start: 2;
}
.optionLabelRight input {
grid-column-start: 1;
grid-row-start: 1;
}
.optionLabelRight .description-block {
grid-column-start: 2;
}
.optionLabelRight .error-block {
grid-column-start: 2;
}
.description-block {
@apply text-xs text-gray-500;
}
</style>