fds-vue-core 2.1.4 → 2.1.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.
Files changed (121) hide show
  1. package/components.d.ts +8 -0
  2. package/configs/tsconfig.base.json +2 -1
  3. package/dist/fds-vue-core.cjs.js +35 -15
  4. package/dist/fds-vue-core.cjs.js.map +1 -1
  5. package/dist/fds-vue-core.es.js +35 -15
  6. package/dist/fds-vue-core.es.js.map +1 -1
  7. package/dist/global-components.d.ts +35 -33
  8. package/package.json +23 -21
  9. package/src/.DS_Store +0 -0
  10. package/src/App.vue +133 -0
  11. package/src/apply.css +60 -0
  12. package/src/assets/icons.ts +517 -0
  13. package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.stories.ts +94 -0
  14. package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.vue +112 -0
  15. package/src/components/Blocks/FdsBlockAlert/types.ts +12 -0
  16. package/src/components/Blocks/FdsBlockContent/FdsBlockContent.stories.ts +110 -0
  17. package/src/components/Blocks/FdsBlockContent/FdsBlockContent.vue +66 -0
  18. package/src/components/Blocks/FdsBlockContent/types.ts +6 -0
  19. package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.stories.ts +123 -0
  20. package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.vue +87 -0
  21. package/src/components/Blocks/FdsBlockExpander/types.ts +8 -0
  22. package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.stories.ts +110 -0
  23. package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.vue +75 -0
  24. package/src/components/Blocks/FdsBlockInfo/types.ts +9 -0
  25. package/src/components/Blocks/FdsBlockLink/FdsBlockLink.css +9 -0
  26. package/src/components/Blocks/FdsBlockLink/FdsBlockLink.stories.ts +179 -0
  27. package/src/components/Blocks/FdsBlockLink/FdsBlockLink.vue +149 -0
  28. package/src/components/Blocks/FdsBlockLink/types.ts +14 -0
  29. package/src/components/Buttons/ButtonBaseProps.ts +18 -0
  30. package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.stories.ts +53 -0
  31. package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.vue +87 -0
  32. package/src/components/Buttons/FdsButtonCopy/types.ts +8 -0
  33. package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.stories.ts +111 -0
  34. package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.vue +187 -0
  35. package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.stories.ts +55 -0
  36. package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue +57 -0
  37. package/src/components/Buttons/FdsButtonIcon/types.ts +12 -0
  38. package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.stories.ts +68 -0
  39. package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.vue +126 -0
  40. package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.stories.ts +86 -0
  41. package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.vue +107 -0
  42. package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.stories.ts +68 -0
  43. package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.vue +107 -0
  44. package/src/components/FdsIcon/FdsIcon.stories.ts +69 -0
  45. package/src/components/FdsIcon/FdsIcon.vue +34 -0
  46. package/src/components/FdsIcon/types.ts +9 -0
  47. package/src/components/FdsModal/FdsModal.stories.ts +241 -0
  48. package/src/components/FdsModal/FdsModal.vue +269 -0
  49. package/src/components/FdsModal/types.ts +12 -0
  50. package/src/components/FdsPagination/FdsPagination.stories.ts +109 -0
  51. package/src/components/FdsPagination/FdsPagination.vue +193 -0
  52. package/src/components/FdsPagination/types.ts +6 -0
  53. package/src/components/FdsSearchSelect/FdsSearchSelect.stories.ts +428 -0
  54. package/src/components/FdsSearchSelect/FdsSearchSelect.vue +621 -0
  55. package/src/components/FdsSearchSelect/types.ts +25 -0
  56. package/src/components/FdsSpinner/FdsSpinner.stories.ts +31 -0
  57. package/src/components/FdsSpinner/FdsSpinner.vue +90 -0
  58. package/src/components/FdsSpinner/types.ts +6 -0
  59. package/src/components/FdsSticker/FdsSticker.stories.ts +148 -0
  60. package/src/components/FdsSticker/FdsSticker.vue +44 -0
  61. package/src/components/FdsSticker/types.ts +4 -0
  62. package/src/components/FdsTreeView/FdsTreeView.stories.ts +136 -0
  63. package/src/components/FdsTreeView/FdsTreeView.vue +162 -0
  64. package/src/components/FdsTreeView/TreeNode.vue +383 -0
  65. package/src/components/FdsTreeView/types.ts +141 -0
  66. package/src/components/FdsTreeView/useTreeState.ts +607 -0
  67. package/src/components/FdsTreeView/utils.ts +69 -0
  68. package/src/components/FdsTruncatedText/FdsTruncatedText.stories.ts +78 -0
  69. package/src/components/FdsTruncatedText/FdsTruncatedText.vue +85 -0
  70. package/src/components/FdsTruncatedText/types.ts +6 -0
  71. package/src/components/Form/FdsCheckbox/FdsCheckbox.stories.ts +275 -0
  72. package/src/components/Form/FdsCheckbox/FdsCheckbox.vue +155 -0
  73. package/src/components/Form/FdsCheckbox/types.ts +10 -0
  74. package/src/components/Form/FdsInput/FdsInput.stories.ts +319 -0
  75. package/src/components/Form/FdsInput/FdsInput.vue +233 -0
  76. package/src/components/Form/FdsInput/types.ts +25 -0
  77. package/src/components/Form/FdsRadio/FdsRadio.stories.ts +63 -0
  78. package/src/components/Form/FdsRadio/FdsRadio.vue +88 -0
  79. package/src/components/Form/FdsRadio/types.ts +12 -0
  80. package/src/components/Form/FdsSelect/FdsSelect.stories.ts +78 -0
  81. package/src/components/Form/FdsSelect/FdsSelect.vue +136 -0
  82. package/src/components/Form/FdsSelect/types.ts +13 -0
  83. package/src/components/Form/FdsTextarea/FdsTextarea.stories.ts +52 -0
  84. package/src/components/Form/FdsTextarea/FdsTextarea.vue +110 -0
  85. package/src/components/Form/FdsTextarea/types.ts +12 -0
  86. package/src/components/Table/FdsTable/FdsTable.stories.ts +221 -0
  87. package/src/components/Table/FdsTable/FdsTable.vue +25 -0
  88. package/src/components/Table/FdsTable/types.ts +4 -0
  89. package/src/components/Table/FdsTableHead/FdsTableHead.stories.ts +151 -0
  90. package/src/components/Table/FdsTableHead/FdsTableHead.vue +54 -0
  91. package/src/components/Table/FdsTableHead/types.ts +5 -0
  92. package/src/components/Tabs/FdsTabs/FdsTabs.stories.ts +247 -0
  93. package/src/components/Tabs/FdsTabs/FdsTabs.vue +27 -0
  94. package/src/components/Tabs/FdsTabs/types.ts +4 -0
  95. package/src/components/Tabs/FdsTabsItem/FdsTabsItem.vue +125 -0
  96. package/src/components/Tabs/FdsTabsItem/types.ts +16 -0
  97. package/src/components/Typography/FdsHeading/FdsHeading.stories.ts +93 -0
  98. package/src/components/Typography/FdsHeading/FdsHeading.vue +51 -0
  99. package/src/components/Typography/FdsHeading/types.ts +5 -0
  100. package/src/components/Typography/FdsListHeading/FdsListHeading.stories.ts +58 -0
  101. package/src/components/Typography/FdsListHeading/FdsListHeading.vue +62 -0
  102. package/src/components/Typography/FdsListHeading/types.ts +8 -0
  103. package/src/components/Typography/FdsSeparator/FdsSeparator.stories.ts +31 -0
  104. package/src/components/Typography/FdsSeparator/FdsSeparator.vue +5 -0
  105. package/src/components/Typography/FdsText/FdsText.stories.ts +66 -0
  106. package/src/components/Typography/FdsText/FdsText.vue +28 -0
  107. package/src/components/Typography/FdsText/types.ts +3 -0
  108. package/src/composables/useBoldQuery.ts +29 -0
  109. package/src/composables/useElementFinalSize.ts +24 -0
  110. package/src/composables/useHasSlots.ts +17 -0
  111. package/src/composables/useIsPid.ts +48 -0
  112. package/src/docs/Start/Start.mdx +12 -0
  113. package/src/docs/Usage.md +117 -0
  114. package/src/fonts.css +28 -0
  115. package/src/global-components.ts +75 -0
  116. package/src/index.ts +180 -0
  117. package/src/main.ts +7 -0
  118. package/src/slot-styles.css +93 -0
  119. package/src/style.css +89 -0
  120. package/src/tokens.css +252 -0
  121. package/dist/index.d.ts +0 -2
@@ -0,0 +1,241 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import FdsButtonPrimary from '@/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.vue'
4
+ import FdsButtonSecondary from '@/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.vue'
5
+ import FdsModal from './FdsModal.vue'
6
+
7
+ const meta: Meta<typeof FdsModal> = {
8
+ title: 'FDS/FdsModal',
9
+ component: FdsModal,
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ heading: { control: { type: 'text' } },
13
+ size: { control: { type: 'select' }, options: ['sm', 'md', 'lg', 'xl'] },
14
+ alignTop: { control: { type: 'boolean' } },
15
+ alignLeftFooter: { control: { type: 'boolean' } },
16
+ stickyFooter: { control: { type: 'boolean' } },
17
+ strict: { control: { type: 'boolean' } },
18
+ lockScroll: { control: { type: 'boolean' } },
19
+ info: { control: { type: 'select' }, options: ['valid', 'invalid', 'warning'] },
20
+ locale: { control: { type: 'select' }, options: ['sv', 'en'] },
21
+ open: { control: { type: 'boolean' } },
22
+ },
23
+ args: {
24
+ heading: 'Modal heading',
25
+ size: 'md',
26
+ alignTop: false,
27
+ alignLeftFooter: false,
28
+ stickyFooter: false,
29
+ strict: false,
30
+ lockScroll: false,
31
+ info: undefined,
32
+ locale: 'sv',
33
+ open: false,
34
+ },
35
+ }
36
+
37
+ export default meta
38
+ type Story = StoryObj<typeof meta>
39
+
40
+ // Helper to create modal story setup with reactive open state
41
+ const createModalStory = (args: any) => {
42
+ const open = ref(args.open ?? false)
43
+ const handleClose = () => {
44
+ open.value = false
45
+ }
46
+ const handleOpen = () => {
47
+ open.value = true
48
+ }
49
+ return { args, open, handleClose, handleOpen }
50
+ }
51
+
52
+ const modalTransform = (storyContext: {
53
+ args?: {
54
+ heading?: string
55
+ size?: string
56
+ open?: boolean
57
+ lockScroll?: boolean
58
+ info?: string
59
+ locale?: string
60
+ strict?: boolean
61
+ }
62
+ }) => {
63
+ const args = storyContext?.args || {}
64
+ const attrs = []
65
+
66
+ if (args.heading) attrs.push(`heading="${args.heading}"`)
67
+ if (args.size && args.size !== 'md') attrs.push(`size="${args.size}"`)
68
+ if (args.open) attrs.push(':open="true"')
69
+ if (args.lockScroll) attrs.push('lockScroll')
70
+ if (args.info) attrs.push(`info="${args.info}"`)
71
+ if (args.locale && args.locale !== 'sv') attrs.push(`locale="${args.locale}"`)
72
+ if (args.strict) attrs.push('strict')
73
+ const attrsStr = attrs.length ? ` ${attrs.join(' ')}` : ''
74
+
75
+ return `<FdsModal${attrsStr} @close="handleClose">
76
+ <p>This is the modal content. You can add any content here.</p>
77
+ <template #modal-footer>
78
+ <FdsButtonSecondary text="Cancel" @click="handleClose" />
79
+ <FdsButtonPrimary text="Confirm" />
80
+ </template>
81
+ </FdsModal>`
82
+ }
83
+
84
+ export const Default: Story = {
85
+ render: (args) => ({
86
+ components: { FdsModal, FdsButtonPrimary, FdsButtonSecondary },
87
+ setup: () => createModalStory(args),
88
+ template: `
89
+ <div>
90
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
91
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
92
+ <p>This is the modal content. You can add any content here.</p>
93
+ <template #modal-footer>
94
+ <FdsButtonSecondary text="Cancel" @click="handleClose" />
95
+ <FdsButtonPrimary text="Confirm" />
96
+ </template>
97
+ </FdsModal>
98
+ </div>
99
+ `,
100
+ }),
101
+ parameters: {
102
+ docs: {
103
+ source: {
104
+ transform: modalTransform,
105
+ },
106
+ },
107
+ },
108
+ }
109
+
110
+ export const WithFooter: Story = {
111
+ render: (args) => ({
112
+ components: { FdsModal, FdsButtonPrimary, FdsButtonSecondary },
113
+ setup: () => createModalStory(args),
114
+ template: `
115
+ <div>
116
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
117
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
118
+ <p>This is the modal content with a footer.</p>
119
+ <template #modal-footer>
120
+ <FdsButtonSecondary text="Cancel" @click="handleClose" />
121
+ <FdsButtonPrimary text="Confirm" />
122
+ </template>
123
+ </FdsModal>
124
+ </div>
125
+ `,
126
+ }),
127
+ }
128
+
129
+ export const Strict: Story = {
130
+ render: (args) => ({
131
+ components: { FdsModal, FdsButtonPrimary, FdsButtonSecondary },
132
+ setup: () => createModalStory(args),
133
+ template: `
134
+ <div>
135
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
136
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
137
+ <p>This is a strict modal. You cannot close it by clicking the backdrop or pressing Escape.</p>
138
+ <template #modal-footer>
139
+ <FdsButtonSecondary text="Close" @click="handleClose" />
140
+ </template>
141
+ </FdsModal>
142
+ </div>
143
+ `,
144
+ }),
145
+ args: {
146
+ strict: true,
147
+ },
148
+ }
149
+
150
+ export const ValidInfo: Story = {
151
+ render: (args) => ({
152
+ components: { FdsModal, FdsButtonPrimary },
153
+ setup: () => createModalStory(args),
154
+ template: `
155
+ <div>
156
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
157
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
158
+ <p>Your changes have been saved successfully.</p>
159
+ </FdsModal>
160
+ </div>
161
+ `,
162
+ }),
163
+ args: {
164
+ info: 'valid',
165
+ },
166
+ }
167
+
168
+ export const WarningInfo: Story = {
169
+ render: (args) => ({
170
+ components: { FdsModal, FdsButtonPrimary, FdsButtonSecondary },
171
+ setup: () => createModalStory(args),
172
+ template: `
173
+ <div>
174
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
175
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
176
+ <p>Are you sure you want to continue? This action cannot be undone.</p>
177
+ <template #modal-footer>
178
+ <FdsButtonSecondary text="Cancel" @click="handleClose" />
179
+ <FdsButtonPrimary text="Confirm" />
180
+ </template>
181
+ </FdsModal>
182
+ </div>
183
+ `,
184
+ }),
185
+ args: {
186
+ info: 'warning',
187
+ },
188
+ }
189
+
190
+ export const LargeSize: Story = {
191
+ render: (args) => ({
192
+ components: { FdsModal, FdsButtonPrimary },
193
+ setup: () => createModalStory(args),
194
+ template: `
195
+ <div>
196
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
197
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
198
+ <p>This is a large modal with more space for content.</p>
199
+ <p>You can add multiple paragraphs and other content here.</p>
200
+ </FdsModal>
201
+ </div>
202
+ `,
203
+ }),
204
+ args: {
205
+ size: 'xl',
206
+ },
207
+ }
208
+
209
+ export const LongContentWithStickyFooter: Story = {
210
+ render: (args) => ({
211
+ components: { FdsModal, FdsButtonPrimary, FdsButtonSecondary },
212
+ setup: () => createModalStory(args),
213
+ template: `
214
+ <div>
215
+ <FdsButtonPrimary text="Open Modal" @click="handleOpen" />
216
+ <FdsModal v-bind="args" :open="open" @close="handleClose">
217
+ <p>This is the first paragraph of content.</p>
218
+ <p>This is the second paragraph with more information about the topic at hand.</p>
219
+ <p>Here is a third paragraph that provides additional context and details.</p>
220
+ <p>And yet another paragraph to demonstrate the scrolling behavior.</p>
221
+ <p>This paragraph continues to show how the content flows in the modal.</p>
222
+ <p>More content is added here to make the modal longer and more scrollable.</p>
223
+ <p>Another paragraph to show the sticky footer functionality in action.</p>
224
+ <p>This content should be long enough to require scrolling.</p>
225
+ <p>The footer should remain sticky at the bottom as you scroll through the content.</p>
226
+ <p>This is the final paragraph to complete the long content example.</p>
227
+ <template #modal-footer>
228
+ <FdsButtonSecondary text="Cancel" @click="handleClose" />
229
+ <FdsButtonPrimary text="Save Changes" />
230
+ </template>
231
+ </FdsModal>
232
+ </div>
233
+ `,
234
+ }),
235
+ args: {
236
+ heading: 'Long Content Modal',
237
+ size: 'md',
238
+ stickyFooter: true,
239
+ lockScroll: true,
240
+ },
241
+ }
@@ -0,0 +1,269 @@
1
+ <script setup lang="ts">
2
+ import FdsButtonIcon from '@/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue'
3
+ import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
4
+ import { useHasSlot } from '@/composables/useHasSlots'
5
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
6
+ import type { FdsModalProps } from './types'
7
+
8
+ const props = withDefaults(defineProps<FdsModalProps>(), {
9
+ open: false,
10
+ heading: '',
11
+ size: 'md',
12
+ alignTop: false,
13
+ alignLeftFooter: false,
14
+ stickyFooter: false,
15
+ strict: false,
16
+ lockScroll: false,
17
+ info: undefined,
18
+ locale: 'sv',
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ (e: 'close'): void
23
+ (e: 'update:open', value: boolean): void
24
+ }>()
25
+
26
+ const isOpen = ref(props.open)
27
+ let cleanupFocusTrap: (() => void) | null = null
28
+ const previouslyFocusedElement = ref<HTMLElement | null>(null)
29
+
30
+ const hasFooterSlot = useHasSlot('modal-footer')
31
+
32
+ const modalOuterClasses = computed(() => ['fixed top-0 left-0 w-full h-full overflow-auto z-100'])
33
+
34
+ const modalInnerClasses = computed(() => [
35
+ 'relative min-w-[288px] w-[calc(100%-5rem)] bg-white rounded-lg overflow-auto z-[99999] shadow-lg px-6 pt-6.5 left-1/2',
36
+ 'max-h-[80%] top-1/2 translate-x-[-50%]',
37
+ props.alignTop ? 'top-[10rem]' : 'translate-y-[-50%]',
38
+ props.stickyFooter ? 'pb-0' : 'pb-8',
39
+ {
40
+ 'max-w-[480px]': props.size === 'sm',
41
+ 'max-w-[588px]': props.size === 'md',
42
+ 'max-w-[768px]': props.size === 'lg',
43
+ 'max-w-[1200px]': props.size === 'xl',
44
+ },
45
+ ])
46
+
47
+ const headerTitleClasses = computed(() => ['m-0'])
48
+
49
+ const iconName = computed(() => {
50
+ if (props.info === 'valid') return 'check'
51
+ if (props.info === 'invalid' || props.info === 'warning') return 'alert'
52
+ return null
53
+ })
54
+
55
+ const iconClasses = computed(() => [
56
+ 'inline mr-2 float-left',
57
+ {
58
+ 'fill-green-600': props.info === 'valid',
59
+ 'fill-red-600': props.info === 'invalid',
60
+ 'fill-yellow-600': props.info === 'warning',
61
+ },
62
+ ])
63
+
64
+ const closeLabel = computed(() => (props.locale === 'sv' ? 'Stäng dialogruta' : 'Close modal window'))
65
+
66
+ const footerClasses = computed(() => [
67
+ 'mt-6 flex flex-col gap-4 left-0 right-0',
68
+ {
69
+ 'sm:flex-row sm:justify-end': !props.alignLeftFooter,
70
+ 'sm:flex-row sm:justify-start': props.alignLeftFooter,
71
+ 'sticky bg-white bottom-0 shadow-[0px_-8px_16px_0px_rgba(255,255,255,0.59)] z-[2] -mx-6 px-6 pb-6':
72
+ props.stickyFooter,
73
+ },
74
+ ])
75
+
76
+ const setHtmlOverflow = (value: 'auto' | 'hidden') => {
77
+ if (typeof document === 'undefined') return
78
+ const htmlEl = document.documentElement
79
+ if (htmlEl) {
80
+ htmlEl.style.overflow = value
81
+ }
82
+ }
83
+
84
+ const handleBackdropClick = () => {
85
+ handleClose('backdrop')
86
+ }
87
+
88
+ const handleClose = (context: 'backdrop' | 'icon' | 'keydown') => {
89
+ if (props.strict && context === 'icon') {
90
+ return
91
+ }
92
+ isOpen.value = false
93
+ setHtmlOverflow('auto')
94
+
95
+ // Restore focus to the element that opened the modal
96
+ nextTick(() => {
97
+ if (previouslyFocusedElement.value) {
98
+ previouslyFocusedElement.value.focus()
99
+ previouslyFocusedElement.value = null
100
+ }
101
+ })
102
+
103
+ emit('close')
104
+ emit('update:open', false)
105
+ }
106
+
107
+ const handleKeyDown = (e: KeyboardEvent) => {
108
+ if (isOpen.value && e.code === 'Escape') {
109
+ handleClose('keydown')
110
+ }
111
+ }
112
+
113
+ const getTabList = (modal: Element) =>
114
+ modal.querySelectorAll(
115
+ 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])',
116
+ )
117
+
118
+ const trapFocus = () => {
119
+ if (!isOpen.value) return
120
+
121
+ nextTick(() => {
122
+ const modal = document.querySelector('.fds-modal-inner')
123
+ if (!modal) return
124
+
125
+ const tabList = getTabList(modal)
126
+
127
+ if (tabList.length > 0) {
128
+ ;(tabList[0] as HTMLElement).focus()
129
+ }
130
+
131
+ const handleTabKey = (e: KeyboardEvent) => {
132
+ if (e.key !== 'Tab') return
133
+
134
+ const currentTabList = getTabList(modal)
135
+ if (currentTabList.length === 0) return
136
+
137
+ const firstFocusableEl = currentTabList[0] as HTMLElement
138
+ const lastFocusableEl = currentTabList[currentTabList.length - 1] as HTMLElement
139
+ const isTabOrderReversed = e.shiftKey
140
+
141
+ // Prevent leaving modal
142
+ if (isTabOrderReversed && document.activeElement === firstFocusableEl) {
143
+ lastFocusableEl.focus()
144
+ e.preventDefault()
145
+ } else if (!isTabOrderReversed && document.activeElement === lastFocusableEl) {
146
+ firstFocusableEl.focus()
147
+ e.preventDefault()
148
+ }
149
+ }
150
+
151
+ // Add event listener to the modal element
152
+ modal.addEventListener('keydown', handleTabKey as EventListener, true)
153
+
154
+ cleanupFocusTrap = () => {
155
+ modal.removeEventListener('keydown', handleTabKey as EventListener, true)
156
+ }
157
+ })
158
+ }
159
+
160
+ watch(
161
+ () => props.open,
162
+ (newValue) => {
163
+ isOpen.value = newValue
164
+ if (newValue) {
165
+ // Save the currently focused element before opening the modal
166
+ previouslyFocusedElement.value = document.activeElement as HTMLElement
167
+ if (props.lockScroll) {
168
+ setHtmlOverflow('hidden')
169
+ }
170
+ trapFocus()
171
+ } else {
172
+ setHtmlOverflow('auto')
173
+ if (cleanupFocusTrap) {
174
+ cleanupFocusTrap()
175
+ cleanupFocusTrap = null
176
+ }
177
+
178
+ // Restore focus to the element that opened the modal
179
+ nextTick(() => {
180
+ if (previouslyFocusedElement.value) {
181
+ previouslyFocusedElement.value.focus()
182
+ previouslyFocusedElement.value = null
183
+ }
184
+ })
185
+ }
186
+ },
187
+ )
188
+
189
+ onMounted(() => {
190
+ document.addEventListener('keydown', handleKeyDown, true)
191
+
192
+ if (props.open) {
193
+ // Save the currently focused element before opening the modal
194
+ previouslyFocusedElement.value = document.activeElement as HTMLElement
195
+
196
+ if (props.lockScroll) {
197
+ setHtmlOverflow('hidden')
198
+ }
199
+ trapFocus()
200
+ }
201
+ })
202
+
203
+ onBeforeUnmount(() => {
204
+ document.removeEventListener('keydown', handleKeyDown, true)
205
+ setHtmlOverflow('auto')
206
+ if (cleanupFocusTrap) {
207
+ cleanupFocusTrap()
208
+ }
209
+ })
210
+ </script>
211
+
212
+ <template>
213
+ <Teleport to="body">
214
+ <div
215
+ v-if="isOpen"
216
+ @click="handleBackdropClick"
217
+ :class="modalOuterClasses"
218
+ >
219
+ <!-- Backdrop -->
220
+ <div class="fixed top-0 left-0 w-full h-full bg-black/34 z-99"></div>
221
+
222
+ <!-- Modal Inner -->
223
+ <div
224
+ class="fds-modal-inner"
225
+ :class="modalInnerClasses"
226
+ role="alertdialog"
227
+ aria-modal="true"
228
+ @click.stop
229
+ >
230
+ <!-- Header -->
231
+ <div class="flex justify-between mb-4">
232
+ <h3
233
+ tabindex="-1"
234
+ :class="headerTitleClasses"
235
+ >
236
+ <FdsIcon
237
+ v-if="iconName"
238
+ :name="iconName"
239
+ :size="24"
240
+ :class="iconClasses"
241
+ />
242
+ {{ heading }}
243
+ </h3>
244
+ <FdsButtonIcon
245
+ v-if="!strict"
246
+ icon="cross"
247
+ :size="28"
248
+ @click="handleClose('icon')"
249
+ :ariaLabel="closeLabel"
250
+ class="ml-4"
251
+ />
252
+ </div>
253
+
254
+ <!-- Content -->
255
+ <div class="mb-0-last-child">
256
+ <slot />
257
+ </div>
258
+
259
+ <!-- Footer -->
260
+ <div
261
+ v-if="hasFooterSlot"
262
+ :class="footerClasses"
263
+ >
264
+ <slot name="modal-footer" />
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </Teleport>
269
+ </template>
@@ -0,0 +1,12 @@
1
+ export interface FdsModalProps {
2
+ open?: boolean
3
+ heading?: string
4
+ size?: 'sm' | 'md' | 'lg' | 'xl'
5
+ alignTop?: boolean
6
+ alignLeftFooter?: boolean
7
+ stickyFooter?: boolean
8
+ strict?: boolean
9
+ lockScroll?: boolean
10
+ info?: 'valid' | 'invalid' | 'warning'
11
+ locale?: 'sv' | 'en'
12
+ }
@@ -0,0 +1,109 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import FdsPagination from './FdsPagination.vue'
4
+
5
+ const meta: Meta<typeof FdsPagination> = {
6
+ title: 'FDS/FdsPagination',
7
+ component: FdsPagination,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ current: { control: { type: 'number' } },
11
+ max: { control: { type: 'number' } },
12
+ loading: { control: { type: 'boolean' } },
13
+ },
14
+ args: {
15
+ current: 1,
16
+ max: 10,
17
+ loading: false,
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof meta>
23
+
24
+ export const Default: Story = {
25
+ render: (args) => ({
26
+ components: { FdsPagination },
27
+ setup: () => {
28
+ const current = ref((args.current ?? 1) as number)
29
+ const handlePaginate = (page: number) => {
30
+ current.value = page
31
+ }
32
+ return { args, current, handlePaginate }
33
+ },
34
+ template: `<FdsPagination :current="current" :max="args.max" :loading="args.loading" @paginate="handlePaginate" />`,
35
+ }),
36
+ }
37
+
38
+ export const MiddlePage: Story = {
39
+ render: (args) => ({
40
+ components: { FdsPagination },
41
+ setup: () => {
42
+ const current = ref((args.current ?? 1) as number)
43
+ const handlePaginate = (page: number) => {
44
+ current.value = page
45
+ }
46
+ return { args, current, handlePaginate }
47
+ },
48
+ template: `<FdsPagination :current="current" :max="args.max" :loading="args.loading" @paginate="handlePaginate" />`,
49
+ }),
50
+ args: {
51
+ current: 5,
52
+ max: 10,
53
+ },
54
+ }
55
+
56
+ export const LastPage: Story = {
57
+ render: (args) => ({
58
+ components: { FdsPagination },
59
+ setup: () => {
60
+ const current = ref((args.current ?? 1) as number)
61
+ const handlePaginate = (page: number) => {
62
+ current.value = page
63
+ }
64
+ return { args, current, handlePaginate }
65
+ },
66
+ template: `<FdsPagination :current="current" :max="args.max" :loading="args.loading" @paginate="handlePaginate" />`,
67
+ }),
68
+ args: {
69
+ current: 10,
70
+ max: 10,
71
+ },
72
+ }
73
+
74
+ export const LargeMax: Story = {
75
+ render: (args) => ({
76
+ components: { FdsPagination },
77
+ setup: () => {
78
+ const current = ref((args.current ?? 1) as number)
79
+ const handlePaginate = (page: number) => {
80
+ current.value = page
81
+ }
82
+ return { args, current, handlePaginate }
83
+ },
84
+ template: `<FdsPagination :current="current" :max="args.max" :loading="args.loading" @paginate="handlePaginate" />`,
85
+ }),
86
+ args: {
87
+ current: 1,
88
+ max: 100,
89
+ },
90
+ }
91
+
92
+ export const WithLoading: Story = {
93
+ render: (args) => ({
94
+ components: { FdsPagination },
95
+ setup: () => {
96
+ const current = ref((args.current ?? 1) as number)
97
+ const handlePaginate = (page: number) => {
98
+ current.value = page
99
+ }
100
+ return { args, current, handlePaginate }
101
+ },
102
+ template: `<FdsPagination :current="current" :max="args.max" :loading="args.loading" @paginate="handlePaginate" />`,
103
+ }),
104
+ args: {
105
+ current: 1,
106
+ max: 10,
107
+ loading: true,
108
+ },
109
+ }