@citizenplane/pimp 18.24.0 → 18.25.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/dist/components/CpDynamicMenu.vue.d.ts +51 -0
- package/dist/components/CpDynamicMenu.vue.d.ts.map +1 -0
- package/dist/components/CpLanguageSwitcher.vue.d.ts +35 -0
- package/dist/components/CpLanguageSwitcher.vue.d.ts.map +1 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/pimp.es.js +1154 -965
- package/dist/pimp.umd.js +29 -29
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/CpDynamicMenu.vue +226 -0
- package/src/components/CpLanguageSwitcher.vue +151 -0
- package/src/components/index.ts +7 -0
- package/src/stories/CpDynamicMenu.stories.ts +279 -0
- package/src/stories/CpLanguageSwitcher.stories.ts +168 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citizenplane/pimp",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.25.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "storybook dev -p 8081",
|
|
6
6
|
"build-storybook": "storybook build --output-dir ./docs",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"@vitest/coverage-v8": "4.1.9",
|
|
76
76
|
"@vue/test-utils": "2.4.11",
|
|
77
77
|
"commitlint-config-gitmoji": "2.3.1",
|
|
78
|
+
"flag-icons": "7.5.0",
|
|
78
79
|
"happy-dom": "20.14.5",
|
|
79
80
|
"husky": "9.1.7",
|
|
80
81
|
"lint-staged": "17.2.0",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cpDynamicMenu">
|
|
3
|
+
<cp-menu v-if="!isMobile" ref="menuRef" v-bind="menuProps" @hide="onMenuHide" @show="onMenuShow">
|
|
4
|
+
<template #trigger>
|
|
5
|
+
<slot name="trigger" />
|
|
6
|
+
</template>
|
|
7
|
+
<slot />
|
|
8
|
+
</cp-menu>
|
|
9
|
+
<template v-else>
|
|
10
|
+
<div
|
|
11
|
+
ref="triggerRef"
|
|
12
|
+
:aria-controls="menuId"
|
|
13
|
+
:aria-expanded="isSheetVisible"
|
|
14
|
+
:aria-haspopup="popupType"
|
|
15
|
+
class="cpDynamicMenu__trigger"
|
|
16
|
+
@click="toggleSheet"
|
|
17
|
+
>
|
|
18
|
+
<slot name="trigger" />
|
|
19
|
+
</div>
|
|
20
|
+
<cp-bottom-sheet v-model="isSheetVisible" v-bind="bottomSheetProps">
|
|
21
|
+
<template v-if="hasSheetTitle" #header>
|
|
22
|
+
<slot name="sheet-title">
|
|
23
|
+
<cp-heading class="cpDynamicMenu__sheetTitle" heading-level="h3">{{ sheetTitle }}</cp-heading>
|
|
24
|
+
</slot>
|
|
25
|
+
</template>
|
|
26
|
+
<cp-menu-list v-if="hasItems" :id="menuId" :items="items" @async-complete="onItemClick" @click="onItemClick" />
|
|
27
|
+
<slot v-else />
|
|
28
|
+
<template #footer>
|
|
29
|
+
<slot name="footer" />
|
|
30
|
+
</template>
|
|
31
|
+
</cp-bottom-sheet>
|
|
32
|
+
</template>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import type { MenuItem } from 'primevue/menuitem'
|
|
38
|
+
import { computed, ref, useId, useSlots, useTemplateRef, watch } from 'vue'
|
|
39
|
+
|
|
40
|
+
import type { CpMenuPlacement } from '@/components/CpMenu.vue'
|
|
41
|
+
|
|
42
|
+
import { useIsBreakpoint } from '@/composables/useIsBreakpoint'
|
|
43
|
+
|
|
44
|
+
import CpBottomSheet from '@/components/CpBottomSheet.vue'
|
|
45
|
+
import CpHeading from '@/components/CpHeading.vue'
|
|
46
|
+
import CpMenu from '@/components/CpMenu.vue'
|
|
47
|
+
import CpMenuList from '@/components/CpMenuList.vue'
|
|
48
|
+
|
|
49
|
+
import { Breakpoints } from '@/constants/layout'
|
|
50
|
+
import { getKeyboardFocusableElements } from '@/helpers/dom'
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
animationDuration?: number
|
|
54
|
+
breakpoint?: number
|
|
55
|
+
class?: string
|
|
56
|
+
contentClass?: string
|
|
57
|
+
expandOnContentDrag?: boolean
|
|
58
|
+
halfAndExpand?: boolean
|
|
59
|
+
items?: MenuItem[]
|
|
60
|
+
keepOpenOnClick?: boolean
|
|
61
|
+
placement?: CpMenuPlacement
|
|
62
|
+
preventClose?: boolean
|
|
63
|
+
sheetTitle?: string
|
|
64
|
+
snapPoints?: Array<number | `${number}%`>
|
|
65
|
+
swipeCloseThreshold?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const props = defineProps<Props>()
|
|
69
|
+
|
|
70
|
+
const emits = defineEmits<{
|
|
71
|
+
hide: []
|
|
72
|
+
show: []
|
|
73
|
+
}>()
|
|
74
|
+
|
|
75
|
+
const slots = useSlots()
|
|
76
|
+
|
|
77
|
+
const isMobile = useIsBreakpoint(props.breakpoint ?? Breakpoints.MOBILE)
|
|
78
|
+
|
|
79
|
+
const menuRef = useTemplateRef('menuRef')
|
|
80
|
+
const triggerRef = useTemplateRef<HTMLElement>('triggerRef')
|
|
81
|
+
|
|
82
|
+
const isMenuOpen = ref(false)
|
|
83
|
+
const isSheetVisible = ref(false)
|
|
84
|
+
const menuId = useId()
|
|
85
|
+
|
|
86
|
+
const hasItems = computed(() => Boolean(props.items?.length))
|
|
87
|
+
const hasSheetTitle = computed(() => Boolean(props.sheetTitle || slots['sheet-title']))
|
|
88
|
+
|
|
89
|
+
const popupType = computed(() => {
|
|
90
|
+
if (hasItems.value) return 'menu'
|
|
91
|
+
return 'dialog'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const menuProps = computed(() => ({
|
|
95
|
+
class: props.class,
|
|
96
|
+
forcePopover: true,
|
|
97
|
+
items: props.items,
|
|
98
|
+
keepOpenOnClick: props.keepOpenOnClick,
|
|
99
|
+
placement: props.placement,
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
const sheetContentClass = computed(() => {
|
|
103
|
+
if (!hasItems.value) return props.contentClass
|
|
104
|
+
if (!props.contentClass) return 'cpDynamicMenu__menuListContent'
|
|
105
|
+
return `cpDynamicMenu__menuListContent ${props.contentClass}`
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const sheetBaseClass = computed(() => {
|
|
109
|
+
if (hasSheetTitle.value) return 'cpDynamicMenu__sheet'
|
|
110
|
+
return 'cpDynamicMenu__sheet cpDynamicMenu__sheet--hasNoTitle'
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const sheetClass = computed(() => {
|
|
114
|
+
if (!props.class) return sheetBaseClass.value
|
|
115
|
+
return `${sheetBaseClass.value} ${props.class}`
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const bottomSheetProps = computed(() => ({
|
|
119
|
+
animationDuration: props.animationDuration,
|
|
120
|
+
canBackdropClose: !props.preventClose,
|
|
121
|
+
canSwipeClose: !props.preventClose,
|
|
122
|
+
class: sheetClass.value,
|
|
123
|
+
contentClass: sheetContentClass.value,
|
|
124
|
+
expandOnContentDrag: props.expandOnContentDrag,
|
|
125
|
+
halfAndExpand: props.halfAndExpand,
|
|
126
|
+
preventClose: props.preventClose,
|
|
127
|
+
snapPoints: props.snapPoints,
|
|
128
|
+
swipeCloseThreshold: props.swipeCloseThreshold,
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
const focusTrigger = () => {
|
|
132
|
+
if (!triggerRef.value) return
|
|
133
|
+
const [firstFocusableElement] = getKeyboardFocusableElements(triggerRef.value) as HTMLElement[]
|
|
134
|
+
firstFocusableElement?.focus()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// CpMenu emits show/hide twice when driven imperatively, so only real state changes are relayed.
|
|
138
|
+
const onMenuShow = () => {
|
|
139
|
+
if (isMenuOpen.value) return
|
|
140
|
+
isMenuOpen.value = true
|
|
141
|
+
emits('show')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const onMenuHide = () => {
|
|
145
|
+
if (!isMenuOpen.value) return
|
|
146
|
+
isMenuOpen.value = false
|
|
147
|
+
emits('hide')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toggleSheet = () => {
|
|
151
|
+
isSheetVisible.value = !isSheetVisible.value
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const onItemClick = () => {
|
|
155
|
+
if (props.keepOpenOnClick) return
|
|
156
|
+
isSheetVisible.value = false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const show = (event: Event) => {
|
|
160
|
+
if (isMobile.value) {
|
|
161
|
+
isSheetVisible.value = true
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
menuRef.value?.show(event)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hide = () => {
|
|
168
|
+
if (isMobile.value) {
|
|
169
|
+
isSheetVisible.value = false
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
menuRef.value?.hide()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const toggle = (event: Event) => {
|
|
176
|
+
if (isMobile.value) {
|
|
177
|
+
toggleSheet()
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
menuRef.value?.toggle(event)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
watch(isSheetVisible, (isVisible) => {
|
|
184
|
+
if (isVisible) {
|
|
185
|
+
emits('show')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
emits('hide')
|
|
189
|
+
focusTrigger()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
watch(isMobile, (isMobileViewport) => {
|
|
193
|
+
if (!isMobileViewport) {
|
|
194
|
+
isSheetVisible.value = false
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
if (isMenuOpen.value) menuRef.value?.hide()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
defineExpose({ show, hide, toggle })
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<style lang="scss">
|
|
204
|
+
.cpDynamicMenu {
|
|
205
|
+
display: contents;
|
|
206
|
+
|
|
207
|
+
&__trigger {
|
|
208
|
+
display: inline-flex;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
&__sheetTitle {
|
|
213
|
+
font-size: var(--cp-text-size-2xl);
|
|
214
|
+
font-weight: 700;
|
|
215
|
+
line-height: var(--cp-line-height-2xl);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
&__menuListContent.cpBottomSheet__content {
|
|
219
|
+
padding: var(--cp-spacing-sm) 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
&__sheet--hasNoTitle .cpBottomSheet__header {
|
|
223
|
+
padding-bottom: 0;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<cp-dynamic-menu
|
|
3
|
+
ref="dynamicMenuRef"
|
|
4
|
+
:breakpoint="breakpoint"
|
|
5
|
+
:class="props.class"
|
|
6
|
+
content-class="cpLanguageSwitcher__content"
|
|
7
|
+
:placement="placement"
|
|
8
|
+
>
|
|
9
|
+
<template #trigger>
|
|
10
|
+
<slot name="trigger">
|
|
11
|
+
<cp-button appearance="tertiary" color="accent" size="sm">
|
|
12
|
+
<template #leading-icon>
|
|
13
|
+
<cp-icon type="globe" />
|
|
14
|
+
</template>
|
|
15
|
+
{{ uppercasedCurrentCode }}
|
|
16
|
+
</cp-button>
|
|
17
|
+
</slot>
|
|
18
|
+
</template>
|
|
19
|
+
<ul class="cpLanguageSwitcher__list">
|
|
20
|
+
<li v-for="lang in langs" :key="lang.code">
|
|
21
|
+
<button
|
|
22
|
+
:aria-current="getAriaCurrent(lang.code)"
|
|
23
|
+
class="cpLanguageSwitcher__lang"
|
|
24
|
+
:class="getLangClass(lang.code)"
|
|
25
|
+
type="button"
|
|
26
|
+
@click="selectLanguage(lang.code)"
|
|
27
|
+
>
|
|
28
|
+
<span class="cpLanguageSwitcher__left">
|
|
29
|
+
<span v-if="lang.flag" class="cpLanguageSwitcher__flag" :class="getFlagClass(lang.flag)" />
|
|
30
|
+
<span class="cpLanguageSwitcher__name">{{ lang.name }}</span>
|
|
31
|
+
</span>
|
|
32
|
+
<cp-icon v-if="isCurrentLanguage(lang.code)" class="cpLanguageSwitcher__check" size="20" type="check" />
|
|
33
|
+
</button>
|
|
34
|
+
</li>
|
|
35
|
+
</ul>
|
|
36
|
+
</cp-dynamic-menu>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed, useTemplateRef } from 'vue'
|
|
41
|
+
|
|
42
|
+
import type { CpMenuPlacement } from '@/components/CpMenu.vue'
|
|
43
|
+
|
|
44
|
+
import CpButton from '@/components/CpButton.vue'
|
|
45
|
+
import CpDynamicMenu from '@/components/CpDynamicMenu.vue'
|
|
46
|
+
import CpIcon from '@/components/CpIcon.vue'
|
|
47
|
+
|
|
48
|
+
export interface LanguageOption {
|
|
49
|
+
code: string
|
|
50
|
+
flag?: string
|
|
51
|
+
name: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Props {
|
|
55
|
+
breakpoint?: number
|
|
56
|
+
class?: string
|
|
57
|
+
langs: LanguageOption[]
|
|
58
|
+
placement?: CpMenuPlacement
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const props = defineProps<Props>()
|
|
62
|
+
|
|
63
|
+
const currentCode = defineModel<string>()
|
|
64
|
+
|
|
65
|
+
const dynamicMenuRef = useTemplateRef('dynamicMenuRef')
|
|
66
|
+
|
|
67
|
+
const uppercasedCurrentCode = computed(() => {
|
|
68
|
+
if (!currentCode.value) return ''
|
|
69
|
+
return currentCode.value.toUpperCase()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const isCurrentLanguage = (code: string) => code === currentCode.value
|
|
73
|
+
|
|
74
|
+
const getLangClass = (code: string) => ({ 'cpLanguageSwitcher__lang--isSelected': isCurrentLanguage(code) })
|
|
75
|
+
|
|
76
|
+
// flag-icons is a devDependency only: the consumer app loads flag-icons.min.css itself.
|
|
77
|
+
const getFlagClass = (flag: string) => `fi fi-${flag.toLowerCase()}`
|
|
78
|
+
|
|
79
|
+
const getAriaCurrent = (code: string) => {
|
|
80
|
+
if (!isCurrentLanguage(code)) return undefined
|
|
81
|
+
return 'true'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const selectLanguage = (code: string) => {
|
|
85
|
+
if (!isCurrentLanguage(code)) currentCode.value = code
|
|
86
|
+
dynamicMenuRef.value?.hide()
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style lang="scss">
|
|
91
|
+
.cpLanguageSwitcher {
|
|
92
|
+
&__content.cpBottomSheet__content {
|
|
93
|
+
padding: var(--cp-spacing-sm) 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&__list {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
padding: 0 var(--cp-spacing-sm);
|
|
100
|
+
margin: 0;
|
|
101
|
+
gap: var(--cp-spacing-sm);
|
|
102
|
+
list-style: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&__lang {
|
|
106
|
+
@extend %u-focus-outline;
|
|
107
|
+
|
|
108
|
+
display: flex;
|
|
109
|
+
width: 100%;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
padding: var(--cp-spacing-lg) var(--cp-spacing-xl) var(--cp-spacing-lg) var(--cp-spacing-lg);
|
|
113
|
+
border: none;
|
|
114
|
+
border-radius: var(--cp-radius-md);
|
|
115
|
+
background-color: transparent;
|
|
116
|
+
color: var(--cp-text-primary);
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
gap: var(--cp-spacing-md);
|
|
119
|
+
|
|
120
|
+
&:hover {
|
|
121
|
+
background-color: var(--cp-background-primary-hover);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&--isSelected {
|
|
125
|
+
background-color: var(--cp-background-accent-primary);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&__left {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: var(--cp-spacing-lg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&__flag {
|
|
136
|
+
flex: none;
|
|
137
|
+
border-radius: var(--cp-radius-sm);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&__name {
|
|
141
|
+
font-size: var(--cp-text-size-md);
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
line-height: var(--cp-line-height-md);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
&__check {
|
|
147
|
+
flex: none;
|
|
148
|
+
color: var(--cp-text-accent-primary);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -36,12 +36,14 @@ import CpDate from './CpDate.vue'
|
|
|
36
36
|
import CpDatepicker from './CpDatepicker.vue'
|
|
37
37
|
import CpDialog from './CpDialog.vue'
|
|
38
38
|
import CpDynamicDialog from './CpDynamicDialog.vue'
|
|
39
|
+
import CpDynamicMenu from './CpDynamicMenu.vue'
|
|
39
40
|
import CpFlightTabs from './CpFlightTabs.vue'
|
|
40
41
|
import CpHeading from './CpHeading.vue'
|
|
41
42
|
import CpIcon from './CpIcon.vue'
|
|
42
43
|
import CpIllustration from './CpIllustration.vue'
|
|
43
44
|
import CpInput from './CpInput.vue'
|
|
44
45
|
import CpItemActions from './CpItemActions.vue'
|
|
46
|
+
import CpLanguageSwitcher from './CpLanguageSwitcher.vue'
|
|
45
47
|
import CpLoader from './CpLoader.vue'
|
|
46
48
|
import CpMenu from './CpMenu.vue'
|
|
47
49
|
import CpMenuItem from './CpMenuItem.vue'
|
|
@@ -112,11 +114,13 @@ const Components = {
|
|
|
112
114
|
CpButtonGroup,
|
|
113
115
|
CpDialog,
|
|
114
116
|
CpDynamicDialog,
|
|
117
|
+
CpDynamicMenu,
|
|
115
118
|
CpDate,
|
|
116
119
|
CpContextualMenu,
|
|
117
120
|
CpMenu,
|
|
118
121
|
CpMenuItem,
|
|
119
122
|
CpMenuList,
|
|
123
|
+
CpLanguageSwitcher,
|
|
120
124
|
CpItemActions,
|
|
121
125
|
CpCoreDatepicker,
|
|
122
126
|
CpDatepicker,
|
|
@@ -203,6 +207,7 @@ const Pimp: Plugin = {
|
|
|
203
207
|
},
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
export type { LanguageOption } from '@/components/CpLanguageSwitcher.vue'
|
|
206
211
|
export type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
|
|
207
212
|
export type { CpBasketCategory, CpBasketGroup, CpBasketRow } from '@/constants/CpBasketTypes'
|
|
208
213
|
export type { CpFlightTabsFlight } from '@/constants/CpFlightTabsTypes'
|
|
@@ -266,12 +271,14 @@ export {
|
|
|
266
271
|
CpDatepicker,
|
|
267
272
|
CpDialog,
|
|
268
273
|
CpDynamicDialog,
|
|
274
|
+
CpDynamicMenu,
|
|
269
275
|
CpFlightTabs,
|
|
270
276
|
CpHeading,
|
|
271
277
|
CpIcon,
|
|
272
278
|
CpIllustration,
|
|
273
279
|
CpInput,
|
|
274
280
|
CpItemActions,
|
|
281
|
+
CpLanguageSwitcher,
|
|
275
282
|
CpLoader,
|
|
276
283
|
CpMenu,
|
|
277
284
|
CpMenuItem,
|