@citizenplane/pimp 14.0.2 → 14.1.0

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": "14.0.2",
3
+ "version": "14.1.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8080",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -10,24 +10,32 @@
10
10
  type="button"
11
11
  @click="handleClick"
12
12
  >
13
- <div class="cpAccordion__title">
14
- <cp-icon v-if="displayLeadingIcon" class="cpAccordion__icon" size="16" :type="dynamicIcon" />
15
- <div class="cpAccordion__leading">
16
- <span>{{ title }}</span>
17
- <slot name="leading-slot" />
13
+ <div class="cpAccordion__headerContent">
14
+ <cp-item-actions
15
+ v-if="hasActions"
16
+ :actions="resolvedActions"
17
+ class="cpAccordion__actions"
18
+ :class="actionsDynamicClasses"
19
+ :quick-options-limit="resolvedQuickOptionsLimit"
20
+ />
21
+ <div class="cpAccordion__title">
22
+ <cp-icon v-if="displayLeadingIcon" class="cpAccordion__icon" size="16" :type="dynamicIcon" />
23
+ <div class="cpAccordion__leading">
24
+ <span class="cpAccordion__titleText">{{ title }}</span>
25
+ <slot name="leading-slot" />
26
+ </div>
27
+ </div>
28
+ <div v-if="displayTrailingSection" class="cpAccordion__trailing">
29
+ <span v-if="hasTrailingSlot" class="cpAccordion__desktopTrailingSlot">
30
+ <slot name="trailing-slot" />
31
+ </span>
32
+ <cp-icon v-if="displayActionTrigger" class="cpAccordion__more" size="16" type="more-horizontal" />
33
+ <cp-icon v-if="displayTrailingIcon" class="cpAccordion__icon" size="16" :type="dynamicIcon" />
18
34
  </div>
19
35
  </div>
20
- <div class="cpAccordion__trailing">
36
+ <span v-if="hasTrailingSlot" class="cpAccordion__mobileTrailingSlot">
21
37
  <slot name="trailing-slot" />
22
- <cp-icon v-if="displayActionTrigger" class="cpAccordion__more" size="16" type="more-horizontal" />
23
- <cp-icon v-if="displayTrailingIcon" class="cpAccordion__icon" size="16" :type="dynamicIcon" />
24
- </div>
25
- <cp-item-actions
26
- v-if="hasActions"
27
- :actions="resolvedActions"
28
- class="cpAccordion__actions"
29
- :quick-options-limit="resolvedQuickOptionsLimit"
30
- />
38
+ </span>
31
39
  </button>
32
40
  <cp-transition-expand>
33
41
  <div v-if="isToggled" :id="accordionContentId" :aria-labelledby="accordionId" class="cpAccordion__content">
@@ -38,7 +46,7 @@
38
46
  </template>
39
47
 
40
48
  <script setup lang="ts">
41
- import { computed, ref, useId } from 'vue'
49
+ import { computed, ref, useId, useSlots } from 'vue'
42
50
 
43
51
  import type { MenuItem } from 'primevue/menuitem'
44
52
 
@@ -64,6 +72,8 @@ export type CpAccordionProps =
64
72
 
65
73
  const props = defineProps<CpAccordionProps>()
66
74
 
75
+ const slots = useSlots()
76
+
67
77
  const isToggled = ref(props.defaultOpenState)
68
78
 
69
79
  const accordionId = useId()
@@ -87,15 +97,22 @@ const resolvedHideActionTrigger = computed(() => {
87
97
  const hasActions = computed(() => resolvedActions.value.length > 0)
88
98
  const hasLeadingIcon = computed(() => resolvedIconPosition.value === 'leading')
89
99
  const hasTrailingIcon = computed(() => resolvedIconPosition.value === 'trailing')
100
+ const hasTrailingSlot = computed(() => !!slots['trailing-slot'])
90
101
 
91
102
  const displayActionTrigger = computed(() => !resolvedHideActionTrigger.value && hasActions.value)
92
103
  const displayLeadingIcon = computed(() => hasLeadingIcon.value || hasActions.value)
93
104
  const displayTrailingIcon = computed(() => hasTrailingIcon.value && !hasActions.value)
94
105
 
106
+ const displayTrailingSection = computed(() => {
107
+ return hasTrailingSlot.value || displayActionTrigger.value || displayTrailingIcon.value
108
+ })
109
+
95
110
  const dynamicIcon = computed(() => (isToggled.value ? 'chevron-up' : 'chevron-down'))
96
111
 
97
112
  const dynamicClasses = computed(() => [{ 'cpAccordion--isOpen': isToggled.value }])
98
113
 
114
+ const actionsDynamicClasses = computed(() => ({ 'cpAccordion__actions--hasTrailingSlot': hasTrailingSlot.value }))
115
+
99
116
  const handleClick = () => (isToggled.value = !isToggled.value)
100
117
  </script>
101
118
 
@@ -106,6 +123,14 @@ const handleClick = () => (isToggled.value = !isToggled.value)
106
123
  background-color: var(--cp-background-primary);
107
124
  width: 100%;
108
125
 
126
+ &__headerContent {
127
+ display: flex;
128
+ flex-shrink: 0;
129
+ width: 100%;
130
+ justify-content: space-between;
131
+ gap: var(--cp-spacing-lg);
132
+ }
133
+
109
134
  &__header {
110
135
  @extend %u-focus-outline;
111
136
 
@@ -129,6 +154,7 @@ const handleClick = () => (isToggled.value = !isToggled.value)
129
154
  font-weight: 500;
130
155
  font-size: var(--cp-text-size-sm);
131
156
  line-height: var(--cp-line-height-sm);
157
+ overflow: hidden;
132
158
  }
133
159
 
134
160
  &__icon {
@@ -140,6 +166,12 @@ const handleClick = () => (isToggled.value = !isToggled.value)
140
166
  &__leading {
141
167
  display: flex;
142
168
  flex-direction: column;
169
+ overflow: hidden;
170
+ text-align: left;
171
+ }
172
+
173
+ &__titleText {
174
+ @extend %u-text-ellipsis;
143
175
  }
144
176
 
145
177
  &__trailing {
@@ -149,15 +181,59 @@ const handleClick = () => (isToggled.value = !isToggled.value)
149
181
  position: relative;
150
182
  }
151
183
 
184
+ &__desktopTrailingSlot {
185
+ display: flex;
186
+ }
187
+
188
+ &__mobileTrailingSlot {
189
+ display: none;
190
+ text-align: left;
191
+ padding-left: var(--cp-spacing-3xl);
192
+ }
193
+
152
194
  &__actions {
195
+ right: 0;
196
+ top: 0;
197
+ height: 100%;
153
198
  background: linear-gradient(270deg, var(--cp-utility-neutral-100) 0%, rgba(242, 246, 250, 0) 100%);
154
199
  padding: 0 var(--cp-spacing-md) 0 15%;
200
+ transform: translate3d(calc(var(--cp-dimensions-1) * 1.25), 0%, 0);
155
201
  display: flex;
156
202
  align-items: center;
203
+ z-index: 1;
157
204
  }
158
205
 
159
206
  &__more {
160
207
  color: var(--cp-foreground-primary);
161
208
  }
162
209
  }
210
+
211
+ @media (hover: hover) and (pointer: fine) {
212
+ [cp-item-actions-trigger]:has(.cpItemActions--isDropdownOpen) .cpItemActions,
213
+ [cp-item-actions-trigger]:is(:focus-within, :focus-visible, :hover) .cpItemActions {
214
+ transform: translate3d(0, 0, 0);
215
+ }
216
+ }
217
+
218
+ // var(--cp-breakpoint-sm) = 40rem = 400px
219
+ @include mx.media-query-max(40rem) {
220
+ .cpAccordion {
221
+ &__header {
222
+ flex-wrap: wrap;
223
+ }
224
+
225
+ &__desktopTrailingSlot {
226
+ display: none;
227
+ }
228
+
229
+ &__mobileTrailingSlot {
230
+ display: flex;
231
+ }
232
+
233
+ &__actions--hasTrailingSlot {
234
+ align-items: flex-start;
235
+ top: fn.px-to-rem(8);
236
+ }
237
+ }
238
+ }
163
239
  </style>
@@ -28,7 +28,7 @@
28
28
  :option-label
29
29
  :placeholder
30
30
  :pt="passThroughConfig"
31
- :suggestions="options"
31
+ :suggestions="dynamicOptions"
32
32
  :typeahead
33
33
  @click="handleClick"
34
34
  @complete="handleSearch"
@@ -126,6 +126,7 @@ interface Props {
126
126
  modelValue?: Record<string, unknown>[] | Record<string, unknown> | string[] | null
127
127
  multiple?: boolean
128
128
  name?: string
129
+ onSearch?: (query: string) => void
129
130
  optionDisabled?: string | ((option: unknown) => boolean)
130
131
  optionLabel?: string
131
132
  options?: unknown[]
@@ -141,7 +142,7 @@ const props = withDefaults(defineProps<Props>(), {
141
142
  label: '',
142
143
  name: '',
143
144
  placeholder: '',
144
- optionLabel: undefined,
145
+ optionLabel: 'name',
145
146
  trackBy: undefined,
146
147
  emptyMessage: 'No results found',
147
148
  errorMessage: '',
@@ -149,6 +150,7 @@ const props = withDefaults(defineProps<Props>(), {
149
150
  optionDisabled: 'disabled',
150
151
  options: () => [],
151
152
  size: 'md',
153
+ onSearch: undefined,
152
154
  })
153
155
 
154
156
  const emit = defineEmits<Emits>()
@@ -218,7 +220,27 @@ const displayClearButton = computed(() => {
218
220
  return props.isClearable && !isEmpty(selectModel.value)
219
221
  })
220
222
 
221
- const handleSearch = (event: { query: string }) => emit('search', event.query)
223
+ const hasRegisteredOnSearch = computed(() => props.onSearch !== undefined)
224
+
225
+ const handleSearch = (event: { query: string }) => {
226
+ if (hasRegisteredOnSearch.value) {
227
+ return emit('search', event.query)
228
+ }
229
+
230
+ // If no onSearch is registered, toggle the dropdown (if hidden) to show the options
231
+ if (!isDropdownOpen.value) {
232
+ toggleDropdown()
233
+ }
234
+ }
235
+
236
+ const dynamicOptions = computed(() => {
237
+ if (hasRegisteredOnSearch.value) return props.options
238
+
239
+ return props.options.filter((option: unknown) => {
240
+ const optionLabel = typeof option === 'object' ? (option as Record<string, unknown>)[props.optionLabel] : option
241
+ return (optionLabel as string).toLowerCase().includes(searchQuery.value.toLowerCase())
242
+ })
243
+ })
222
244
  const handleClear = () => (selectModel.value = null)
223
245
  const handleOverlayShown = () => emit('overlayShown')
224
246
  const handleOverlayHidden = () => emit('overlayHidden')
@@ -37,7 +37,7 @@ const defaultStoryActions: MenuItem[] = [
37
37
  ]
38
38
 
39
39
  const meta = {
40
- title: 'Navigation/CpAccordion',
40
+ title: 'Visual/CpAccordion',
41
41
  component: CpAccordion,
42
42
  argTypes: {
43
43
  title: {
@@ -177,22 +177,6 @@ export const Documentation: Story = {
177
177
  <div :style="sampleContentStyle" />
178
178
  </CpAccordion>
179
179
  </div>
180
- <div :style="docCellStyle">
181
- <span :style="docLabelStyle">Hover</span>
182
- <CpAccordion
183
- title="DIDA TRAVEL"
184
- :default-open-state="false"
185
- icon-position="leading"
186
- style="background: var(--cp-background-primary-hover);"
187
- :actions="actions"
188
- :quick-options-limit="3"
189
- >
190
- <template #trailing-slot>
191
- This is a trailing slot
192
- </template>
193
- <div :style="sampleContentStyle" />
194
- </CpAccordion>
195
- </div>
196
180
  <div :style="docCellStyle">
197
181
  <span :style="docLabelStyle">Hide Action Trigger</span>
198
182
  <CpAccordion
@@ -43,7 +43,7 @@ const defaultItemActions: MenuItem[] = [
43
43
  ]
44
44
 
45
45
  const meta = {
46
- title: 'Navigation/CpAccordionGroup',
46
+ title: 'Visual/CpAccordionGroup',
47
47
  component: CpAccordionGroup,
48
48
  argTypes: {
49
49
  variant: {
@@ -113,43 +113,23 @@ const meta = {
113
113
  export default meta
114
114
  type Story = StoryObj<typeof meta>
115
115
 
116
- export const Single: Story = {
116
+ export const DefaultSearchHandler: Story = {
117
117
  args: {
118
118
  placeholder: 'Select a supplier',
119
119
  multiple: false,
120
120
  options: supplierOptions,
121
- optionLabel: 'name',
122
121
  trackBy: 'id',
123
122
  isClearable: true,
124
123
  },
125
124
  render: (args) => ({
126
125
  components: { CpMultiselect },
127
126
  setup() {
128
- const searchQuery = ref('')
129
- const isLoading = ref(false)
130
-
131
- const originalOptions = ref(args.options)
132
- const dynamicOptions = ref(toValue(originalOptions))
133
127
  const selectedSupplier = ref(null)
134
-
135
- const handleSearch = async (query: string) => {
136
- isLoading.value = true
137
- searchQuery.value = query
138
-
139
- await new Promise((resolve) => setTimeout(resolve, 500))
140
-
141
- dynamicOptions.value = originalOptions.value?.filter((option) => {
142
- return (option as IOption).name.toLowerCase().includes(searchQuery.value.toLowerCase())
143
- })
144
-
145
- isLoading.value = false
146
- }
147
-
148
- return { args, selectedSupplier, dynamicOptions, handleSearch, isLoading }
128
+ return { args, selectedSupplier }
149
129
  },
150
130
  template: `
151
131
  <div style="padding: 20px;width: 25rem;">
152
- <CpMultiselect v-model="selectedSupplier" v-bind="args" :options="dynamicOptions" :is-loading="isLoading" @search="handleSearch">
132
+ <CpMultiselect v-model="selectedSupplier" v-bind="args">
153
133
  <template #prefix>
154
134
  <cp-partner-badge type="supplier" size="sm" />
155
135
  </template>