@citizenplane/pimp 10.9.3 → 11.0.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": "10.9.3",
3
+ "version": "11.0.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8080",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -7,6 +7,15 @@
7
7
  overflow: hidden;
8
8
  }
9
9
 
10
+ .u-layout-box-padding,
11
+ %u-layout-box-padding {
12
+ padding: var(--cp-spacing-2xl);
13
+
14
+ @media (max-width: 650px) {
15
+ padding: var(--cp-spacing-xl);
16
+ }
17
+ }
18
+
10
19
  .u-asterisk {
11
20
  position: relative;
12
21
  top: fn.px-to-rem(-3);
@@ -1,19 +1,35 @@
1
1
  <template>
2
2
  <div class="cpDialog">
3
- <dialog ref="dialogElement" class="cpDialog__dialog" @keydown.esc.stop.prevent="handleClose">
4
- <div class="cpDialog__overlay" />
3
+ <dialog
4
+ ref="dialogElement"
5
+ :aria-describedby="ariaDescribedby"
6
+ :aria-labelledby="titleId"
7
+ aria-modal="true"
8
+ class="cpDialog__dialog"
9
+ @keydown.esc.stop.prevent="handleClose"
10
+ >
11
+ <div aria-hidden="true" class="cpDialog__overlay" />
5
12
  <main ref="dialogContainer" class="cpDialog__container" :style="dynamicStyle" @keydown.tab="trapFocus">
6
- <header v-if="hasHeaderSlot" class="cpDialog__header">
7
- <slot name="header" />
8
- <button class="cpDialog__close" type="button" @click="handleClose">
9
- <cp-icon type="x" />
13
+ <header class="cpDialog__header">
14
+ <div class="cpDialog__left">
15
+ <div class="cpDialog__title">
16
+ <slot name="title">
17
+ <h2 :id="titleId">{{ title }}</h2>
18
+ </slot>
19
+ </div>
20
+ <div v-if="hasSubtitle" :id="subtitleId" class="cpDialog__subtitle">
21
+ <slot name="subtitle">
22
+ <p>{{ subtitle }}</p>
23
+ </slot>
24
+ </div>
25
+ </div>
26
+ <button aria-label="Close dialog" class="cpDialog__close" type="button" @click="handleClose">
27
+ <cp-icon aria-hidden="true" type="x" />
10
28
  </button>
11
29
  </header>
12
- <slot>
13
- <section class="cpDialog__content">
14
- <slot name="content" />
15
- </section>
16
- </slot>
30
+ <section class="cpDialog__content">
31
+ <slot />
32
+ </section>
17
33
  <footer v-if="hasFooterSlot" class="cpDialog__footer">
18
34
  <slot name="footer" />
19
35
  </footer>
@@ -23,12 +39,14 @@
23
39
  </template>
24
40
 
25
41
  <script setup lang="ts">
26
- import { computed, ref, useSlots, onMounted, nextTick, onBeforeUnmount } from 'vue'
42
+ import { computed, ref, useSlots, onMounted, nextTick, onBeforeUnmount, useId } from 'vue'
27
43
 
28
44
  import { getKeyboardFocusableElements, handleTrapFocus } from '@/helpers/dom'
29
45
 
30
46
  interface Props {
31
47
  maxWidth?: number
48
+ subtitle?: string
49
+ title: string
32
50
  }
33
51
 
34
52
  interface Emits {
@@ -37,10 +55,17 @@ interface Emits {
37
55
 
38
56
  const props = withDefaults(defineProps<Props>(), {
39
57
  maxWidth: 600,
58
+ subtitle: '',
40
59
  })
41
60
 
42
61
  const emit = defineEmits<Emits>()
43
62
 
63
+ const dialogId = useId()
64
+ const titleId = computed(() => `${dialogId}-title`)
65
+ const subtitleId = computed(() => `${dialogId}-subtitle`)
66
+
67
+ const ariaDescribedby = computed(() => (hasSubtitle.value ? subtitleId.value : undefined))
68
+
44
69
  const slots = useSlots()
45
70
 
46
71
  const dialogElement = ref<HTMLDialogElement | null>(null)
@@ -48,8 +73,8 @@ const dialogContainer = ref<HTMLElement | null>(null)
48
73
 
49
74
  const dynamicStyle = computed(() => ({ maxWidth: `${props.maxWidth}px` }))
50
75
 
51
- const hasHeaderSlot = computed(() => !!slots.header)
52
-
76
+ const hasSubtitleSlot = computed(() => !!slots.subtitle)
77
+ const hasSubtitle = computed(() => !!props.subtitle || hasSubtitleSlot.value)
53
78
  const hasFooterSlot = computed(() => !!slots.footer)
54
79
 
55
80
  const handleClose = () => emit('close')
@@ -151,64 +176,81 @@ $dialog-breakpoint: 550px;
151
176
 
152
177
  &__header,
153
178
  &__footer {
154
- display: flex;
155
- align-items: center;
156
- justify-content: space-between;
179
+ padding: var(--cp-spacing-xl) var(--cp-spacing-2xl);
157
180
  }
158
181
 
159
182
  &__header {
160
- position: relative;
183
+ display: flex;
184
+ align-items: flex-start;
185
+ justify-content: space-between;
186
+ gap: var(--cp-spacing-md);
161
187
  border-bottom: 1px solid var(--cp-border-soft);
162
188
  }
163
189
 
190
+ &__left {
191
+ display: flex;
192
+ flex-direction: column;
193
+ min-width: 0;
194
+ }
195
+
196
+ &__title > * {
197
+ @extend %u-text-ellipsis;
198
+ }
199
+
200
+ &__title,
201
+ &__title > h2 {
202
+ font-size: var(--cp-text-size-xl);
203
+ font-weight: 600;
204
+ line-height: var(--cp-line-height-xl);
205
+ }
206
+
207
+ &__subtitle,
208
+ &__subtitle > p {
209
+ font-size: var(--cp-text-size-sm);
210
+ font-weight: 400;
211
+ line-height: var(--cp-line-height-md);
212
+ color: var(--cp-text-secondary);
213
+ }
214
+
164
215
  &__close {
165
- position: absolute;
166
- top: var(--cp-spacing-2xl);
167
- right: var(--cp-spacing-2xl);
168
- display: inline-flex;
216
+ display: flex;
169
217
  align-items: center;
170
218
  justify-content: center;
171
219
  padding: var(--cp-spacing-sm);
172
220
  border-radius: var(--cp-radius-md);
173
221
  color: var(--cp-foreground-secondary);
174
- transition: 0.2s ease-in-out;
175
- transform: translate(var(--cp-dimensions-1), calc(var(--cp-dimensions-1) * -1));
222
+ transition: 200ms ease-in-out;
176
223
  transition-property: color, background-color;
177
224
 
178
225
  &:hover {
179
226
  color: var(--cp-foreground-primary);
227
+ background-color: var(--cp-background-secondary);
180
228
  }
181
229
 
182
230
  &:focus-visible {
183
- outline: calc(var(--cp-dimensions-0_5) * 1.5) solid var(--cp-focus);
231
+ outline: fn.px-to-rem(2) solid var(--cp-focus);
184
232
  }
185
233
  }
186
234
 
187
235
  &__content {
236
+ @extend %u-layout-box-padding;
237
+
188
238
  overflow: auto;
189
239
  min-height: 0;
190
240
  flex: 1;
191
241
  }
192
242
 
193
243
  &__footer {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: space-between;
247
+
194
248
  &:not(:empty) {
195
249
  border-top: 1px solid var(--cp-border-soft);
196
- padding: var(--cp-spacing-xl) var(--cp-spacing-2xl);
197
- }
198
-
199
- &--noBorder {
200
- border-top: none;
201
250
  }
202
251
  }
203
252
  }
204
253
 
205
- @media screen and (max-width: $dialog-breakpoint) {
206
- .cpDialog__close {
207
- top: var(--cp-spacing-xl);
208
- right: var(--cp-spacing-xl);
209
- }
210
- }
211
-
212
254
  @media (max-width: 650px) {
213
255
  .cpDialog__footer {
214
256
  padding: var(--cp-spacing-xl);
@@ -14,13 +14,6 @@ const meta = {
14
14
  },
15
15
  onClose: { action: 'closed' },
16
16
  },
17
- parameters: {
18
- styles: {
19
- '.header': {
20
- color: 'red',
21
- },
22
- },
23
- },
24
17
  } satisfies Meta<typeof CpDialog>
25
18
 
26
19
  export default meta
@@ -29,6 +22,8 @@ type Story = StoryObj<typeof meta>
29
22
  export const Default: Story = {
30
23
  args: {
31
24
  maxWidth: 600,
25
+ title: 'Dialog title',
26
+ subtitle: 'Dialog subtitle',
32
27
  },
33
28
  render: (args) => ({
34
29
  setup() {
@@ -39,14 +34,76 @@ export const Default: Story = {
39
34
  <CpButton @click="isOpen = true">Open Dialog</CpButton>
40
35
  <CpTransitionDialog>
41
36
  <CpDialog v-bind="args" v-if="isOpen" @close="isOpen = false">
42
- <template #header>
43
- <h1 style="padding: 16px 24px">Header slot</h1>
44
- </template>
45
- <p style="padding: 16px 24px">This is the default slot content. You can put any content here.</p>
46
- <template #footer>
47
- <CpButton @click="isOpen = false">Cancel</CpButton>
48
- This is the footer slot
49
- </template>
37
+ <template #title>Header slot</template>
38
+ <template #subtitle>Subtitle</template>
39
+ <p>This is the default slot content. You can put any content here.</p>
40
+ <template #footer>
41
+ <CpButton @click="isOpen = false">Cancel</CpButton>
42
+ This is the footer slot
43
+ </template>
44
+ </CpDialog>
45
+ </CpTransitionDialog>
46
+ `,
47
+ }),
48
+ }
49
+
50
+ export const TitleSubtitleWithProps: Story = {
51
+ args: {
52
+ maxWidth: 600,
53
+ title: 'Dialog title',
54
+ },
55
+ render: (args) => ({
56
+ setup() {
57
+ const isOpen = ref(false)
58
+ return { args, isOpen }
59
+ },
60
+ template: `
61
+ <CpButton @click="isOpen = true">Open Dialog with string title/subtitle</CpButton>
62
+ <CpTransitionDialog>
63
+ <CpDialog v-bind="args" v-if="isOpen" @close="isOpen = false">
64
+ <p>This is the default slot content. You can put any content here.</p>
65
+ <template #footer>
66
+ <CpButton @click="isOpen = false">Cancel</CpButton>
67
+ This is the footer slot
68
+ </template>
69
+ </CpDialog>
70
+ </CpTransitionDialog>
71
+ `,
72
+ }),
73
+ }
74
+
75
+ export const TitleSubtitleWithSlots: Story = {
76
+ args: {
77
+ maxWidth: 560,
78
+ titleTag: 'div',
79
+ subtitleTag: 'div',
80
+ },
81
+ render: (args) => ({
82
+ setup() {
83
+ const isOpen = ref(false)
84
+ return { args, isOpen }
85
+ },
86
+ template: `
87
+ <CpButton @click="isOpen = true">Open Dialog (flex title/subtitle)</CpButton>
88
+ <CpTransitionDialog>
89
+ <CpDialog v-bind="args" v-if="isOpen" @close="isOpen = false" title-tag="div" subtitle-tag="div">
90
+ <template #title>
91
+ <div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
92
+ <cp-icon type="info" style="flex-shrink: 0;" />
93
+ <span>Dialog with flex layout</span>
94
+ </div>
95
+ </template>
96
+ <template #subtitle>
97
+ <div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
98
+ <span>Optional info</span>
99
+ <span style="color: var(--cp-text-tertiary);">•</span>
100
+ <span>Last updated today</span>
101
+ </div>
102
+ </template>
103
+ <p>Body content. Title and subtitle above are divs with flex layout.</p>
104
+ <template #footer>
105
+ <CpButton @click="isOpen = false">Close</CpButton>
106
+ </template>
50
107
  </CpDialog>
51
108
  </CpTransitionDialog>
52
109
  `,