@citizenplane/pimp 9.7.5 → 9.7.6
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/pimp.es.js +1219 -1215
- package/dist/pimp.umd.js +14 -14
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CpCheckbox.vue +53 -35
- package/src/stories/CpCheckbox.stories.ts +72 -13
package/package.json
CHANGED
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
<div class="cpCheckbox__wrapper">
|
|
4
4
|
<input
|
|
5
5
|
:id="checkboxUniqueId"
|
|
6
|
-
|
|
6
|
+
ref="inputRef"
|
|
7
7
|
:autofocus="autofocus"
|
|
8
|
+
:checked="isChecked"
|
|
8
9
|
:disabled="isDisabled"
|
|
9
10
|
:name="groupName"
|
|
10
11
|
type="checkbox"
|
|
11
12
|
:value="checkboxValue"
|
|
12
|
-
@change="onChange
|
|
13
|
+
@change="onChange"
|
|
13
14
|
/>
|
|
14
|
-
<cp-icon type="
|
|
15
|
+
<cp-icon :type="icon" />
|
|
15
16
|
</div>
|
|
16
17
|
<div class="cpCheckbox__content">
|
|
17
18
|
<slot>
|
|
18
|
-
<span v-if="checkboxLabel" class="cpCheckbox__label"
|
|
19
|
+
<span v-if="checkboxLabel" class="cpCheckbox__label">
|
|
19
20
|
{{ checkboxLabel }}
|
|
20
21
|
</span>
|
|
21
22
|
</slot>
|
|
@@ -27,19 +28,23 @@
|
|
|
27
28
|
</template>
|
|
28
29
|
|
|
29
30
|
<script setup lang="ts">
|
|
30
|
-
import { computed, ref, useSlots, useId } from 'vue'
|
|
31
|
+
import { computed, ref, useSlots, useId, watch, nextTick } from 'vue'
|
|
31
32
|
|
|
32
33
|
import { ToggleColors } from '@/constants'
|
|
33
34
|
import { capitalizeFirstLetter } from '@/helpers'
|
|
34
35
|
|
|
36
|
+
type EmitType = {
|
|
37
|
+
(e: 'update:modelValue', value: boolean | unknown[]): void
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
interface Props {
|
|
36
41
|
autofocus?: boolean
|
|
37
|
-
capitalizeLabel?: boolean
|
|
38
42
|
checkboxLabel?: string
|
|
39
43
|
checkboxValue?: string | number
|
|
40
44
|
color?: string
|
|
41
45
|
groupName?: string
|
|
42
46
|
helper?: string
|
|
47
|
+
indeterminate?: boolean
|
|
43
48
|
isDisabled?: boolean
|
|
44
49
|
modelValue?: boolean | unknown[]
|
|
45
50
|
reverseLabel?: boolean
|
|
@@ -49,34 +54,42 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
49
54
|
modelValue: false,
|
|
50
55
|
checkboxValue: '',
|
|
51
56
|
checkboxLabel: '',
|
|
52
|
-
isDisabled: false,
|
|
53
57
|
groupName: '',
|
|
54
|
-
// eslint-disable-next-line vue/no-boolean-default
|
|
55
|
-
capitalizeLabel: true,
|
|
56
58
|
color: ToggleColors.BLUE,
|
|
57
|
-
reverseLabel: false,
|
|
58
|
-
autofocus: false,
|
|
59
59
|
helper: '',
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
const emit = defineEmits(
|
|
62
|
+
const emit = defineEmits<EmitType>()
|
|
63
63
|
|
|
64
|
-
const checkedProxy = ref(false)
|
|
65
64
|
const checkboxUniqueId = useId()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
const inputRef = ref<HTMLInputElement>()
|
|
66
|
+
|
|
67
|
+
// Synchronize native checkbox state with our props
|
|
68
|
+
// as it will not reactively update otherwise
|
|
69
|
+
watch(
|
|
70
|
+
() => ({ checked: props.modelValue, indeterminate: props.indeterminate }),
|
|
71
|
+
({ checked, indeterminate }) => {
|
|
72
|
+
nextTick(() => {
|
|
73
|
+
if (inputRef.value) {
|
|
74
|
+
inputRef.value.indeterminate = indeterminate
|
|
75
|
+
if (!Array.isArray(checked)) {
|
|
76
|
+
inputRef.value.checked = checked as boolean
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
76
80
|
},
|
|
81
|
+
{ immediate: true },
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const isChecked = computed(() => {
|
|
85
|
+
if (Array.isArray(props.modelValue)) {
|
|
86
|
+
return props.modelValue.includes(props.checkboxValue)
|
|
87
|
+
}
|
|
88
|
+
return props.modelValue
|
|
77
89
|
})
|
|
78
90
|
|
|
79
91
|
const capitalizedColor = computed(() => capitalizeFirstLetter(props.color))
|
|
92
|
+
const icon = computed(() => (props.indeterminate ? 'minus' : 'check'))
|
|
80
93
|
|
|
81
94
|
const slots = useSlots()
|
|
82
95
|
const hasDefaultSlot = computed(() => !!slots.default)
|
|
@@ -89,36 +102,39 @@ const computedClasses = computed(() => {
|
|
|
89
102
|
'cpCheckbox--isEmpty': isEmpty.value,
|
|
90
103
|
'cpCheckbox--isDisabled': props.isDisabled,
|
|
91
104
|
'cpCheckbox--isReversed': props.reverseLabel,
|
|
105
|
+
'cpCheckbox--isIndeterminate': props.indeterminate,
|
|
92
106
|
},
|
|
93
107
|
`cpCheckbox--is${capitalizedColor.value}`,
|
|
94
108
|
]
|
|
95
109
|
})
|
|
96
110
|
|
|
97
|
-
const
|
|
98
|
-
return { 'cpCheckbox__label--isCapitalized': props.capitalizeLabel }
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const onChange = (value: string | number) => {
|
|
111
|
+
const onChange = () => {
|
|
102
112
|
if (Array.isArray(props.modelValue)) {
|
|
103
113
|
const currentValues = [...props.modelValue]
|
|
104
|
-
const valueIndex = currentValues.indexOf(
|
|
114
|
+
const valueIndex = currentValues.indexOf(props.checkboxValue)
|
|
105
115
|
|
|
106
116
|
if (valueIndex > -1) {
|
|
107
117
|
currentValues.splice(valueIndex, 1)
|
|
108
118
|
} else {
|
|
109
|
-
currentValues.push(
|
|
119
|
+
currentValues.push(props.checkboxValue)
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
emit('update:modelValue', currentValues)
|
|
113
123
|
} else {
|
|
114
|
-
|
|
124
|
+
// Always uncheck when previously indeterminate
|
|
125
|
+
if (props.indeterminate) {
|
|
126
|
+
emit('update:modelValue', false)
|
|
127
|
+
} else {
|
|
128
|
+
emit('update:modelValue', !props.modelValue)
|
|
129
|
+
}
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
132
|
</script>
|
|
118
133
|
|
|
119
134
|
<style lang="scss">
|
|
120
135
|
@mixin cp-checkbox-style($color, $className) {
|
|
121
|
-
&--is#{$className} input:checked
|
|
136
|
+
&--is#{$className} input:checked,
|
|
137
|
+
&--is#{$className} input:indeterminate {
|
|
122
138
|
background-color: $color;
|
|
123
139
|
border-color: $color;
|
|
124
140
|
}
|
|
@@ -127,7 +143,8 @@ const onChange = (value: string | number) => {
|
|
|
127
143
|
background-color: color.scale($color, $lightness: 95%);
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
&--is#{$className}:hover input:checked
|
|
146
|
+
&--is#{$className}:hover input:checked,
|
|
147
|
+
&--is#{$className}:hover input:indeterminate {
|
|
131
148
|
background-color: color.adjust($color, $lightness: -10%);
|
|
132
149
|
}
|
|
133
150
|
|
|
@@ -192,7 +209,8 @@ const onChange = (value: string | number) => {
|
|
|
192
209
|
stroke-width: 3;
|
|
193
210
|
}
|
|
194
211
|
|
|
195
|
-
&:checked + i
|
|
212
|
+
&:checked + i,
|
|
213
|
+
&:indeterminate + i {
|
|
196
214
|
visibility: visible;
|
|
197
215
|
opacity: 1;
|
|
198
216
|
}
|
|
@@ -241,7 +259,7 @@ const onChange = (value: string | number) => {
|
|
|
241
259
|
&__label {
|
|
242
260
|
font-weight: 500;
|
|
243
261
|
|
|
244
|
-
|
|
262
|
+
&::first-letter {
|
|
245
263
|
text-transform: capitalize;
|
|
246
264
|
}
|
|
247
265
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref } from 'vue'
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
2
|
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
4
4
|
|
|
@@ -28,10 +28,6 @@ const meta = {
|
|
|
28
28
|
control: 'text',
|
|
29
29
|
description: 'Name attribute for checkbox group',
|
|
30
30
|
},
|
|
31
|
-
capitalizeLabel: {
|
|
32
|
-
control: 'boolean',
|
|
33
|
-
description: 'Whether to capitalize the first letter of the label',
|
|
34
|
-
},
|
|
35
31
|
color: {
|
|
36
32
|
control: 'select',
|
|
37
33
|
options: ['blue', 'purple'],
|
|
@@ -49,6 +45,10 @@ const meta = {
|
|
|
49
45
|
control: 'text',
|
|
50
46
|
description: 'Helper text to display below the label',
|
|
51
47
|
},
|
|
48
|
+
indeterminate: {
|
|
49
|
+
control: 'boolean',
|
|
50
|
+
description: 'Whether the checkbox is in an indeterminate state',
|
|
51
|
+
},
|
|
52
52
|
},
|
|
53
53
|
} satisfies Meta<typeof CpCheckbox>
|
|
54
54
|
|
|
@@ -57,14 +57,14 @@ type Story = StoryObj<typeof meta>
|
|
|
57
57
|
|
|
58
58
|
export const Default: Story = {
|
|
59
59
|
args: {
|
|
60
|
-
checkboxLabel: '
|
|
60
|
+
checkboxLabel: 'checkbox label',
|
|
61
61
|
modelValue: false,
|
|
62
62
|
isDisabled: false,
|
|
63
|
-
capitalizeLabel: true,
|
|
64
63
|
color: 'blue',
|
|
65
64
|
reverseLabel: false,
|
|
66
65
|
autofocus: false,
|
|
67
66
|
helper: '',
|
|
67
|
+
indeterminate: false,
|
|
68
68
|
},
|
|
69
69
|
render: (args) => ({
|
|
70
70
|
components: { CpCheckbox },
|
|
@@ -118,12 +118,71 @@ export const Reversed: Story = {
|
|
|
118
118
|
},
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
export const Indeterminate: Story = {
|
|
122
|
+
render: () => ({
|
|
123
|
+
components: { CpCheckbox },
|
|
124
|
+
setup() {
|
|
125
|
+
const childOptions = ref([
|
|
126
|
+
{ id: 'email', label: 'Email notifications', checked: false },
|
|
127
|
+
{ id: 'push', label: 'Push notifications', checked: true },
|
|
128
|
+
{ id: 'sms', label: 'SMS notifications', checked: false },
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
const parentState = computed(() => {
|
|
132
|
+
const checkedCount = childOptions.value.filter((option) => option.checked).length
|
|
133
|
+
const totalCount = childOptions.value.length
|
|
134
|
+
|
|
135
|
+
if (checkedCount === 0) return { checked: false, indeterminate: false }
|
|
136
|
+
if (checkedCount === totalCount) return { checked: true, indeterminate: false }
|
|
137
|
+
return { checked: false, indeterminate: true }
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const handleParentChange = (value: boolean) => childOptions.value.forEach((option) => (option.checked = value))
|
|
141
|
+
|
|
142
|
+
const handleChildChange = (childId: string) => {
|
|
143
|
+
const child = childOptions.value.find((option) => option.id === childId)
|
|
144
|
+
if (child) {
|
|
145
|
+
child.checked = !child.checked
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
childOptions,
|
|
151
|
+
parentState,
|
|
152
|
+
handleParentChange,
|
|
153
|
+
handleChildChange,
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
template: `
|
|
157
|
+
<div style="padding: 20px; display: flex; flex-direction: column; gap: 16px;">
|
|
158
|
+
<div style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">
|
|
159
|
+
Parent-Child Checkbox Example
|
|
160
|
+
</div>
|
|
161
|
+
<CpCheckbox
|
|
162
|
+
:model-value="parentState.checked"
|
|
163
|
+
:indeterminate="parentState.indeterminate"
|
|
164
|
+
checkbox-label="All notifications"
|
|
165
|
+
@update:model-value="handleParentChange"
|
|
166
|
+
style="font-weight: 500; border-bottom: 1px solid #e0e0e0; padding-bottom: 12px;"
|
|
167
|
+
/>
|
|
168
|
+
<div style="margin-left: 24px; display: flex; flex-direction: column; gap: 8px;">
|
|
169
|
+
<CpCheckbox
|
|
170
|
+
v-for="option in childOptions"
|
|
171
|
+
:key="option.id"
|
|
172
|
+
:model-value="option.checked"
|
|
173
|
+
:checkbox-label="option.label"
|
|
174
|
+
@update:model-value="() => handleChildChange(option.id)"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
<div style="margin-top: 16px; padding: 12px; background: #f5f5f5; border-radius: 4px; font-size: 14px;">
|
|
178
|
+
<strong>Current state: </strong>
|
|
179
|
+
<span v-if="parentState.indeterminate" style="color: #ff9800;">Indeterminate (some selected)</span>
|
|
180
|
+
<span v-else-if="parentState.checked" style="color: #4caf50;">All selected</span>
|
|
181
|
+
<span v-else style="color: #757575;">None selected</span>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
`,
|
|
185
|
+
}),
|
|
127
186
|
}
|
|
128
187
|
|
|
129
188
|
export const CheckboxGroup: Story = {
|