@citizenplane/pimp 18.0.6 → 18.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.0.6",
3
+ "version": "18.1.1",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <button
3
+ :aria-disabled="disabled"
4
+ :aria-pressed="isSelected"
5
+ class="cpToggleButton"
6
+ :class="dynamicClasses"
7
+ :disabled="disabled"
8
+ type="button"
9
+ @click="handleClick"
10
+ >
11
+ <span class="cpToggleButton__leading">
12
+ <slot name="leading">
13
+ <cp-icon size="16" :type="leadingIcon" />
14
+ </slot>
15
+ </span>
16
+ <span v-if="showLabel" class="cpToggleButton__label">
17
+ <slot>
18
+ {{ label }}
19
+ </slot>
20
+ </span>
21
+ </button>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { computed, useSlots } from 'vue'
26
+
27
+ interface Props {
28
+ disabled?: boolean
29
+ isSelected?: boolean
30
+ label?: string
31
+ leadingIcon?: string
32
+ }
33
+
34
+ const props = withDefaults(defineProps<Props>(), {
35
+ isSelected: false,
36
+ disabled: false,
37
+ label: '',
38
+ leadingIcon: 'dashed-circle',
39
+ })
40
+
41
+ const emit = defineEmits<{
42
+ click: [event: MouseEvent]
43
+ }>()
44
+
45
+ const slots = useSlots()
46
+
47
+ const showLabel = computed(() => !!props.label || !!slots.default)
48
+
49
+ const dynamicClasses = computed(() => ({
50
+ 'cpToggleButton--isSelected': props.isSelected,
51
+ }))
52
+
53
+ const handleClick = (event: MouseEvent) => {
54
+ emit('click', event)
55
+ }
56
+ </script>
57
+
58
+ <style lang="scss">
59
+ .cpToggleButton {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: var(--cp-spacing-md);
64
+ padding: var(--cp-spacing-lg);
65
+ border-radius: var(--cp-radius-md);
66
+ box-shadow:
67
+ var(--cp-shadows-3xs-inset),
68
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-soft);
69
+ background-color: var(--cp-background-primary);
70
+ color: var(--cp-text-secondary);
71
+ transition:
72
+ background-color 100ms ease-out,
73
+ box-shadow 100ms ease-out,
74
+ color 100ms ease-out;
75
+
76
+ &:hover:not(:disabled) {
77
+ background-color: var(--cp-background-primary-hover);
78
+ color: var(--cp-text-secondary-hover);
79
+ box-shadow:
80
+ var(--cp-shadows-3xs-inset),
81
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-soft-hover);
82
+ }
83
+
84
+ &:focus-visible {
85
+ box-shadow:
86
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-soft-hover),
87
+ var(--cp-shadow-focus-ring-accent);
88
+ }
89
+
90
+ &__leading {
91
+ display: flex;
92
+ align-items: center;
93
+ }
94
+
95
+ &__label {
96
+ font-size: var(--cp-text-size-sm);
97
+ line-height: var(--cp-line-height-sm);
98
+ font-weight: 500;
99
+ }
100
+
101
+ &__defaultLeading,
102
+ &__defaultLeading * {
103
+ pointer-events: none;
104
+ }
105
+
106
+ &:disabled {
107
+ cursor: not-allowed;
108
+ background-color: var(--cp-background-disabled);
109
+ color: var(--cp-text-disabled);
110
+ box-shadow:
111
+ var(--cp-shadows-3xs-inset),
112
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-disabled);
113
+ }
114
+
115
+ &--isSelected:not(:disabled) {
116
+ background-color: var(--cp-background-accent-secondary);
117
+ color: var(--cp-text-accent-primary);
118
+ box-shadow:
119
+ var(--cp-shadows-3xs-inset),
120
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-accent-solid);
121
+
122
+ &:hover {
123
+ background-color: var(--cp-background-accent-secondary-hover);
124
+ color: var(--cp-text-accent-primary-hover);
125
+ box-shadow:
126
+ var(--cp-shadows-3xs-inset),
127
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-accent-solid);
128
+ }
129
+
130
+ &:focus-visible {
131
+ box-shadow:
132
+ 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-accent-solid),
133
+ var(--cp-shadow-focus-ring-accent);
134
+ }
135
+ }
136
+ }
137
+ </style>
@@ -4,33 +4,36 @@
4
4
  {{ capitalizedLabel }}
5
5
  </base-input-label>
6
6
  <div class="cpDate__inputs">
7
- <input
8
- v-model="day"
9
- v-maska
10
- :autocomplete="autocompleteFields.day"
11
- class="cpDate__day"
12
- data-maska="##"
13
- :disabled="disabled"
14
- inputmode="numeric"
15
- maxlength="2"
16
- :placeholder="dayInputPlaceholder"
17
- :required="required"
18
- />
19
- <div class="cpDate__divider" />
20
- <div class="cpDate__month" :class="selectDynamicClass">
21
- <select
22
- :id="cpDateId"
23
- v-model="month"
24
- :autocomplete="autocompleteFields.month"
7
+ <template v-for="(field, index) in orderedDateFields" :key="field">
8
+ <input
9
+ v-if="isDayField(field)"
10
+ v-model="day"
11
+ v-maska
12
+ :autocomplete="autocompleteFields.day"
13
+ class="cpDate__day"
14
+ data-maska="##"
25
15
  :disabled="disabled"
16
+ inputmode="numeric"
17
+ maxlength="2"
18
+ :placeholder="dayInputPlaceholder"
26
19
  :required="required"
27
- >
28
- <option value>{{ monthInputPlaceholder }}</option>
29
- <option v-for="(monthItem, index) in months" :key="index" :value="monthItem.value">
30
- {{ monthItem.label }}
31
- </option>
32
- </select>
33
- </div>
20
+ />
21
+ <div v-else class="cpDate__month" :class="selectDynamicClass">
22
+ <select
23
+ :id="cpDateId"
24
+ v-model="month"
25
+ :autocomplete="autocompleteFields.month"
26
+ :disabled="disabled"
27
+ :required="required"
28
+ >
29
+ <option value>{{ monthInputPlaceholder }}</option>
30
+ <option v-for="(monthItem, monthIndex) in months" :key="monthIndex" :value="monthItem.value">
31
+ {{ monthItem.label }}
32
+ </option>
33
+ </select>
34
+ </div>
35
+ <div v-if="shouldShowDivider(index)" class="cpDate__divider" />
36
+ </template>
34
37
  <div class="cpDate__divider" />
35
38
  <input
36
39
  v-model="year"
@@ -71,6 +74,7 @@ interface InputsOptions {
71
74
  monthInputPlaceholder?: string
72
75
  yearInputPlaceholder?: string
73
76
  }
77
+ type DateField = 'day' | 'month'
74
78
 
75
79
  interface Props {
76
80
  autocompleteBirthday?: boolean
@@ -265,6 +269,24 @@ const monthInputPlaceholder = computed(() => {
265
269
  return props.inputsOptions?.monthInputPlaceholder || 'Months'
266
270
  })
267
271
 
272
+ const isDayFirst = computed(() => {
273
+ const parts = new Intl.DateTimeFormat(props.locale, {
274
+ day: '2-digit',
275
+ month: '2-digit',
276
+ }).formatToParts(new Date(2024, 0, 31))
277
+
278
+ const dayIndex = parts.findIndex((part) => part.type === 'day')
279
+ const monthIndex = parts.findIndex((part) => part.type === 'month')
280
+
281
+ if (dayIndex === -1 || monthIndex === -1) return false
282
+
283
+ return dayIndex < monthIndex
284
+ })
285
+
286
+ const orderedDateFields = computed<DateField[]>(() => {
287
+ return isDayFirst.value ? ['day', 'month'] : ['month', 'day']
288
+ })
289
+
268
290
  const yearInputPlaceholder = computed(() => {
269
291
  return props.inputsOptions?.yearInputPlaceholder || 'YYYY'
270
292
  })
@@ -274,6 +296,10 @@ const handleUpdate = (): void => {
274
296
  emit('onValidation', isDateValid.value)
275
297
  }
276
298
 
299
+ const isDayField = (field: DateField): boolean => field === 'day'
300
+
301
+ const shouldShowDivider = (index: number): boolean => index < orderedDateFields.value.length - 1
302
+
277
303
  watch(day, handleUpdate)
278
304
  watch(month, handleUpdate)
279
305
  watch(year, handleUpdate)
@@ -296,6 +322,7 @@ watch(year, handleUpdate)
296
322
  }
297
323
 
298
324
  input[type='number'] {
325
+ appearance: textfield;
299
326
  -moz-appearance: textfield;
300
327
  }
301
328
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <label class="cpSwitch" :class="computedClasses" :for="switchUniqueId">
2
+ <label class="cpSwitch" :class="computedClasses" :for="switchUniqueId" v-bind="inertProps">
3
3
  <span class="cpSwitch__switch">
4
4
  <input
5
5
  :id="switchUniqueId"
@@ -46,6 +46,7 @@ interface Props {
46
46
  enableHaptics?: boolean
47
47
  groupName?: string
48
48
  helper?: string
49
+ inert?: boolean
49
50
  isRequired?: boolean
50
51
  label?: string
51
52
  modelValue?: boolean
@@ -68,6 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
68
69
  autofocus: false,
69
70
  isRequired: false,
70
71
  tooltip: '',
72
+ inert: false,
71
73
  })
72
74
 
73
75
  const emit = defineEmits<Emits>()
@@ -80,6 +82,16 @@ const capitalizedColor = computed(() => {
80
82
  return capitalizeFirstLetter(props.color)
81
83
  })
82
84
 
85
+ const inertProps = computed(() => {
86
+ if (!props.inert) return
87
+
88
+ return {
89
+ 'aria-hidden': true,
90
+ tabindex: -1,
91
+ inert: true,
92
+ }
93
+ })
94
+
83
95
  const computedClasses = computed(() => {
84
96
  return [
85
97
  {
@@ -22,6 +22,7 @@ import CpAlert from './CpAlert.vue'
22
22
  import CpBadge from './CpBadge.vue'
23
23
  import CpButton from './CpButton.vue'
24
24
  import CpButtonGroup from './CpButtonGroup.vue'
25
+ import CpButtonToggle from './CpButtonToggle.vue'
25
26
  import CpCalendar from './CpCalendar.vue'
26
27
  import CpCheckbox from './CpCheckbox.vue'
27
28
  import CpContextualMenu from './CpContextualMenu.vue'
@@ -73,6 +74,7 @@ const Components = {
73
74
  CpAccordion,
74
75
  CpAccordionGroup,
75
76
  CpToast,
77
+ CpButtonToggle,
76
78
  CpBadge,
77
79
  CpTabs,
78
80
  CpHeading,
@@ -148,6 +150,7 @@ export {
148
150
  CpAccordion,
149
151
  CpAccordionGroup,
150
152
  CpToast,
153
+ CpButtonToggle,
151
154
  CpBadge,
152
155
  CpTabs,
153
156
  CpHeading,
@@ -0,0 +1,137 @@
1
+ import { ref } from 'vue'
2
+
3
+ import type { Args, Meta, StoryObj } from '@storybook/vue3-vite'
4
+
5
+ import CpButtonToggle from '@/components/CpButtonToggle.vue'
6
+ import CpSwitch from '@/components/CpSwitch.vue'
7
+
8
+ import { docCellStyle, docLabelStyle, docRowWrapStyle } from '@/stories/documentationStyles'
9
+
10
+ const meta = {
11
+ title: 'Atoms/CpButtonToggle',
12
+ component: CpButtonToggle,
13
+ argTypes: {
14
+ isSelected: {
15
+ control: 'boolean',
16
+ description: 'Whether the toggle button is selected.',
17
+ },
18
+ disabled: {
19
+ control: 'boolean',
20
+ description: 'Whether interactions are disabled.',
21
+ },
22
+ label: {
23
+ control: 'text',
24
+ description: 'Text displayed in the button.',
25
+ },
26
+ leadingIcon: {
27
+ control: 'text',
28
+ description: 'Icon displayed in the button.',
29
+ },
30
+ },
31
+ } satisfies Meta<typeof CpButtonToggle>
32
+
33
+ export default meta
34
+ type Story = StoryObj<typeof meta>
35
+
36
+ const defaultRender = (args: Args) => ({
37
+ components: { CpButtonToggle },
38
+ setup() {
39
+ return { args }
40
+ },
41
+ template: `
42
+ <CpButtonToggle v-bind="args" />
43
+ `,
44
+ })
45
+
46
+ export const Default: Story = {
47
+ args: {
48
+ label: 'Title',
49
+ isSelected: false,
50
+ disabled: false,
51
+ leadingIcon: 'dashed-circle',
52
+ },
53
+ render: defaultRender,
54
+ }
55
+
56
+ export const States: Story = {
57
+ parameters: { controls: { disable: true } },
58
+ render: () => ({
59
+ components: { CpButtonToggle },
60
+ setup() {
61
+ return { docCellStyle, docLabelStyle, docRowWrapStyle }
62
+ },
63
+ template: `
64
+ <div :style="docRowWrapStyle">
65
+ <div :style="docCellStyle">
66
+ <span :style="docLabelStyle">Default</span>
67
+ <CpButtonToggle label="Title" />
68
+ </div>
69
+ <div :style="docCellStyle">
70
+ <span :style="docLabelStyle">Selected</span>
71
+ <CpButtonToggle label="Title" :is-selected="true" />
72
+ </div>
73
+ <div :style="docCellStyle">
74
+ <span :style="docLabelStyle">Disabled</span>
75
+ <CpButtonToggle label="Title" :disabled="true" />
76
+ </div>
77
+ <div :style="docCellStyle">
78
+ <span :style="docLabelStyle">Disabled selected</span>
79
+ <CpButtonToggle label="Title" :is-selected="true" :disabled="true" />
80
+ </div>
81
+ </div>
82
+ `,
83
+ }),
84
+ }
85
+
86
+ export const withInertSwitch: Story = {
87
+ args: {
88
+ ...Default.args,
89
+ isSelected: undefined,
90
+ },
91
+ render: (args: Args) => ({
92
+ components: { CpButtonToggle, CpSwitch },
93
+ setup() {
94
+ const isSelected = ref(false)
95
+ return { docCellStyle, docLabelStyle, docRowWrapStyle, isSelected, args }
96
+ },
97
+ template: `
98
+ <div :style="docRowWrapStyle">
99
+ <div :style="docCellStyle">
100
+ <span :style="docLabelStyle">Selected</span>
101
+ <CpButtonToggle v-bind="args" :is-selected="isSelected" @click="isSelected = !isSelected">
102
+ <template #leading>
103
+ <CpSwitch v-model="isSelected" inert :disabled="args.disabled" />
104
+ </template>
105
+ </CpButtonToggle>
106
+ </div>
107
+ </div>
108
+ `,
109
+ }),
110
+ }
111
+
112
+ export const CustomLeadingSlot: Story = {
113
+ parameters: { controls: { disable: true } },
114
+ render: () => ({
115
+ components: { CpButtonToggle },
116
+ template: `
117
+ <div>
118
+ <CpButtonToggle label="Title" :is-selected="true">
119
+ <template #leading>
120
+ <span style="
121
+ width: 16px;
122
+ height: 16px;
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ border-radius: 4px;
127
+ background: var(--cp-background-accent-solid);
128
+ color: var(--cp-foreground-white);
129
+ font-size: 11px;
130
+ line-height: 1;
131
+ ">✓</span>
132
+ </template>
133
+ </CpButtonToggle>
134
+ </div>
135
+ `,
136
+ }),
137
+ }
@@ -140,6 +140,33 @@ export const States: Story = {
140
140
  }),
141
141
  }
142
142
 
143
+ /**
144
+ * Compares input ordering by locale (day/month in French, month/day in US English).
145
+ */
146
+ export const LocaleOrdering: Story = {
147
+ parameters: { controls: { disable: true } },
148
+ render: () => ({
149
+ components: { CpDate },
150
+ setup() {
151
+ const frenchDate = ref('')
152
+ const usDate = ref('')
153
+ return { frenchDate, usDate, docRowColumnStyle, dateStackStyle, docLabelStyle }
154
+ },
155
+ template: `
156
+ <div :style="docRowColumnStyle">
157
+ <div :style="dateStackStyle">
158
+ <span :style="docLabelStyle">fr-FR (day / month / year)</span>
159
+ <CpDate v-model="frenchDate" label="Date" locale="fr-FR" />
160
+ </div>
161
+ <div :style="dateStackStyle">
162
+ <span :style="docLabelStyle">en-US (month / day / year)</span>
163
+ <CpDate v-model="usDate" label="Date" locale="en-US" />
164
+ </div>
165
+ </div>
166
+ `,
167
+ }),
168
+ }
169
+
143
170
  /**
144
171
  * Combines a date input with a text input on the same line.
145
172
  */