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.
- package/components.d.ts +8 -0
- package/configs/tsconfig.base.json +2 -1
- package/dist/fds-vue-core.cjs.js +35 -15
- package/dist/fds-vue-core.cjs.js.map +1 -1
- package/dist/fds-vue-core.es.js +35 -15
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/global-components.d.ts +35 -33
- package/package.json +23 -21
- package/src/.DS_Store +0 -0
- package/src/App.vue +133 -0
- package/src/apply.css +60 -0
- package/src/assets/icons.ts +517 -0
- package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.stories.ts +94 -0
- package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.vue +112 -0
- package/src/components/Blocks/FdsBlockAlert/types.ts +12 -0
- package/src/components/Blocks/FdsBlockContent/FdsBlockContent.stories.ts +110 -0
- package/src/components/Blocks/FdsBlockContent/FdsBlockContent.vue +66 -0
- package/src/components/Blocks/FdsBlockContent/types.ts +6 -0
- package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.stories.ts +123 -0
- package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.vue +87 -0
- package/src/components/Blocks/FdsBlockExpander/types.ts +8 -0
- package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.stories.ts +110 -0
- package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.vue +75 -0
- package/src/components/Blocks/FdsBlockInfo/types.ts +9 -0
- package/src/components/Blocks/FdsBlockLink/FdsBlockLink.css +9 -0
- package/src/components/Blocks/FdsBlockLink/FdsBlockLink.stories.ts +179 -0
- package/src/components/Blocks/FdsBlockLink/FdsBlockLink.vue +149 -0
- package/src/components/Blocks/FdsBlockLink/types.ts +14 -0
- package/src/components/Buttons/ButtonBaseProps.ts +18 -0
- package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.stories.ts +53 -0
- package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.vue +87 -0
- package/src/components/Buttons/FdsButtonCopy/types.ts +8 -0
- package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.stories.ts +111 -0
- package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.vue +187 -0
- package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.stories.ts +55 -0
- package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue +57 -0
- package/src/components/Buttons/FdsButtonIcon/types.ts +12 -0
- package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.stories.ts +68 -0
- package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.vue +126 -0
- package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.stories.ts +86 -0
- package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.vue +107 -0
- package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.stories.ts +68 -0
- package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.vue +107 -0
- package/src/components/FdsIcon/FdsIcon.stories.ts +69 -0
- package/src/components/FdsIcon/FdsIcon.vue +34 -0
- package/src/components/FdsIcon/types.ts +9 -0
- package/src/components/FdsModal/FdsModal.stories.ts +241 -0
- package/src/components/FdsModal/FdsModal.vue +269 -0
- package/src/components/FdsModal/types.ts +12 -0
- package/src/components/FdsPagination/FdsPagination.stories.ts +109 -0
- package/src/components/FdsPagination/FdsPagination.vue +193 -0
- package/src/components/FdsPagination/types.ts +6 -0
- package/src/components/FdsSearchSelect/FdsSearchSelect.stories.ts +428 -0
- package/src/components/FdsSearchSelect/FdsSearchSelect.vue +621 -0
- package/src/components/FdsSearchSelect/types.ts +25 -0
- package/src/components/FdsSpinner/FdsSpinner.stories.ts +31 -0
- package/src/components/FdsSpinner/FdsSpinner.vue +90 -0
- package/src/components/FdsSpinner/types.ts +6 -0
- package/src/components/FdsSticker/FdsSticker.stories.ts +148 -0
- package/src/components/FdsSticker/FdsSticker.vue +44 -0
- package/src/components/FdsSticker/types.ts +4 -0
- package/src/components/FdsTreeView/FdsTreeView.stories.ts +136 -0
- package/src/components/FdsTreeView/FdsTreeView.vue +162 -0
- package/src/components/FdsTreeView/TreeNode.vue +383 -0
- package/src/components/FdsTreeView/types.ts +141 -0
- package/src/components/FdsTreeView/useTreeState.ts +607 -0
- package/src/components/FdsTreeView/utils.ts +69 -0
- package/src/components/FdsTruncatedText/FdsTruncatedText.stories.ts +78 -0
- package/src/components/FdsTruncatedText/FdsTruncatedText.vue +85 -0
- package/src/components/FdsTruncatedText/types.ts +6 -0
- package/src/components/Form/FdsCheckbox/FdsCheckbox.stories.ts +275 -0
- package/src/components/Form/FdsCheckbox/FdsCheckbox.vue +155 -0
- package/src/components/Form/FdsCheckbox/types.ts +10 -0
- package/src/components/Form/FdsInput/FdsInput.stories.ts +319 -0
- package/src/components/Form/FdsInput/FdsInput.vue +233 -0
- package/src/components/Form/FdsInput/types.ts +25 -0
- package/src/components/Form/FdsRadio/FdsRadio.stories.ts +63 -0
- package/src/components/Form/FdsRadio/FdsRadio.vue +88 -0
- package/src/components/Form/FdsRadio/types.ts +12 -0
- package/src/components/Form/FdsSelect/FdsSelect.stories.ts +78 -0
- package/src/components/Form/FdsSelect/FdsSelect.vue +136 -0
- package/src/components/Form/FdsSelect/types.ts +13 -0
- package/src/components/Form/FdsTextarea/FdsTextarea.stories.ts +52 -0
- package/src/components/Form/FdsTextarea/FdsTextarea.vue +110 -0
- package/src/components/Form/FdsTextarea/types.ts +12 -0
- package/src/components/Table/FdsTable/FdsTable.stories.ts +221 -0
- package/src/components/Table/FdsTable/FdsTable.vue +25 -0
- package/src/components/Table/FdsTable/types.ts +4 -0
- package/src/components/Table/FdsTableHead/FdsTableHead.stories.ts +151 -0
- package/src/components/Table/FdsTableHead/FdsTableHead.vue +54 -0
- package/src/components/Table/FdsTableHead/types.ts +5 -0
- package/src/components/Tabs/FdsTabs/FdsTabs.stories.ts +247 -0
- package/src/components/Tabs/FdsTabs/FdsTabs.vue +27 -0
- package/src/components/Tabs/FdsTabs/types.ts +4 -0
- package/src/components/Tabs/FdsTabsItem/FdsTabsItem.vue +125 -0
- package/src/components/Tabs/FdsTabsItem/types.ts +16 -0
- package/src/components/Typography/FdsHeading/FdsHeading.stories.ts +93 -0
- package/src/components/Typography/FdsHeading/FdsHeading.vue +51 -0
- package/src/components/Typography/FdsHeading/types.ts +5 -0
- package/src/components/Typography/FdsListHeading/FdsListHeading.stories.ts +58 -0
- package/src/components/Typography/FdsListHeading/FdsListHeading.vue +62 -0
- package/src/components/Typography/FdsListHeading/types.ts +8 -0
- package/src/components/Typography/FdsSeparator/FdsSeparator.stories.ts +31 -0
- package/src/components/Typography/FdsSeparator/FdsSeparator.vue +5 -0
- package/src/components/Typography/FdsText/FdsText.stories.ts +66 -0
- package/src/components/Typography/FdsText/FdsText.vue +28 -0
- package/src/components/Typography/FdsText/types.ts +3 -0
- package/src/composables/useBoldQuery.ts +29 -0
- package/src/composables/useElementFinalSize.ts +24 -0
- package/src/composables/useHasSlots.ts +17 -0
- package/src/composables/useIsPid.ts +48 -0
- package/src/docs/Start/Start.mdx +12 -0
- package/src/docs/Usage.md +117 -0
- package/src/fonts.css +28 -0
- package/src/global-components.ts +75 -0
- package/src/index.ts +180 -0
- package/src/main.ts +7 -0
- package/src/slot-styles.css +93 -0
- package/src/style.css +89 -0
- package/src/tokens.css +252 -0
- package/dist/index.d.ts +0 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useHasSlot } from '@/composables/useHasSlots'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import type { FdsRadioProps } from './types'
|
|
5
|
+
|
|
6
|
+
// Support v-model for selected value in a radio group (string|number)
|
|
7
|
+
const modelValue = defineModel<string | number>({ default: undefined, required: false })
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<FdsRadioProps>(), {
|
|
10
|
+
label: undefined,
|
|
11
|
+
checked: false,
|
|
12
|
+
disabled: false,
|
|
13
|
+
value: undefined,
|
|
14
|
+
name: undefined,
|
|
15
|
+
id: undefined,
|
|
16
|
+
required: false,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'update:modelValue', value: string | number): void
|
|
21
|
+
(e: 'change', value: string | number): void
|
|
22
|
+
(e: 'input', value: string | number): void
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const autoId = `fds-radio-${Math.random().toString(36).slice(2, 9)}`
|
|
26
|
+
const inputId = computed(() => props.id ?? autoId)
|
|
27
|
+
|
|
28
|
+
const hasLabelSlot = useHasSlot()
|
|
29
|
+
|
|
30
|
+
// Proxy for v-model on radio groups: binds the selected value (string|number)
|
|
31
|
+
const radioModel = computed({
|
|
32
|
+
get() {
|
|
33
|
+
return modelValue.value
|
|
34
|
+
},
|
|
35
|
+
set(newChecked: string | number) {
|
|
36
|
+
emit('update:modelValue', newChecked)
|
|
37
|
+
emit('change', newChecked)
|
|
38
|
+
emit('input', newChecked)
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const wrapperClasses = computed(() => ['block relative flex items-center mb-2 last:mb-0'])
|
|
43
|
+
const innerWrapperClasses = computed(() => [
|
|
44
|
+
'flex p-0.5 items-start rounded-l-xl rounded-r-md',
|
|
45
|
+
'hover:bg-blue_t-100 active:bg-blue_t-200',
|
|
46
|
+
'[&:has(:focus-visible)]:outline-2 [&:has(:focus-visible)]:outline-dashed [&:has(:focus-visible)]:-outline-offset-2 [&:has(:focus-visible)]:outline-blue-500',
|
|
47
|
+
props.disabled && 'hover:bg-transparent active:bg-transparent',
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
const inputClasses = computed(() => [
|
|
51
|
+
'peer z-2 bg-white min-w-[20px] min-h-[20px] focus-visible:outline-none rounded-full accent-blue-500',
|
|
52
|
+
props.disabled && 'cursor-not-allowed',
|
|
53
|
+
])
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div :class="wrapperClasses">
|
|
58
|
+
<label
|
|
59
|
+
:for="inputId"
|
|
60
|
+
:class="[innerWrapperClasses, { 'cursor-not-allowed': disabled }]"
|
|
61
|
+
v-bind="$attrs"
|
|
62
|
+
>
|
|
63
|
+
<input
|
|
64
|
+
:id="inputId"
|
|
65
|
+
:name="name"
|
|
66
|
+
:value="value"
|
|
67
|
+
v-model="radioModel"
|
|
68
|
+
:disabled="disabled"
|
|
69
|
+
:required="required"
|
|
70
|
+
type="radio"
|
|
71
|
+
:class="inputClasses"
|
|
72
|
+
class="m-[2px]"
|
|
73
|
+
/>
|
|
74
|
+
<span
|
|
75
|
+
v-if="hasLabelSlot || label"
|
|
76
|
+
class="relative inline-block leading-6 pl-1 select-none"
|
|
77
|
+
:class="{ 'cursor-not-allowed': disabled }"
|
|
78
|
+
>
|
|
79
|
+
<template v-if="hasLabelSlot">
|
|
80
|
+
<slot></slot>
|
|
81
|
+
</template>
|
|
82
|
+
<template v-else-if="label">
|
|
83
|
+
{{ label }}
|
|
84
|
+
</template>
|
|
85
|
+
</span>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface FdsRadioProps {
|
|
2
|
+
label?: string
|
|
3
|
+
checked?: boolean
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
value?: string | number
|
|
6
|
+
name?: string
|
|
7
|
+
id?: string
|
|
8
|
+
required?: boolean
|
|
9
|
+
onKeydown?: ((event: KeyboardEvent) => void) | Array<(event: KeyboardEvent) => void>
|
|
10
|
+
onBlur?: ((event: FocusEvent) => void) | Array<(event: FocusEvent) => void>
|
|
11
|
+
onChange?: ((event: Event) => void) | Array<(event: Event) => void>
|
|
12
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import FdsSelect from './FdsSelect.vue'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof FdsSelect> = {
|
|
5
|
+
title: 'FDS/Form/FdsSelect',
|
|
6
|
+
component: FdsSelect,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
modelValue: { control: { type: 'text' } },
|
|
10
|
+
label: { control: { type: 'text' } },
|
|
11
|
+
meta: { control: { type: 'text' } },
|
|
12
|
+
disabled: { control: { type: 'boolean' } },
|
|
13
|
+
optional: { control: { type: 'boolean' } },
|
|
14
|
+
valid: { control: { type: 'select' }, options: ['true', 'false', 'null'] },
|
|
15
|
+
invalidMessage: { control: { type: 'text' } },
|
|
16
|
+
placeholder: { control: { type: 'text' } },
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
modelValue: '',
|
|
20
|
+
label: 'Välj alternativ',
|
|
21
|
+
meta: undefined,
|
|
22
|
+
disabled: false,
|
|
23
|
+
optional: false,
|
|
24
|
+
valid: 'null',
|
|
25
|
+
invalidMessage: undefined,
|
|
26
|
+
placeholder: undefined,
|
|
27
|
+
options: [
|
|
28
|
+
{ value: 'option1', label: 'Alternativ 1' },
|
|
29
|
+
{ value: 'option2', label: 'Alternativ 2' },
|
|
30
|
+
{ value: 'option3', label: 'Alternativ 3', disabled: true },
|
|
31
|
+
{ value: 'option4', label: 'Alternativ 4' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default meta
|
|
37
|
+
type Story = StoryObj<typeof meta>
|
|
38
|
+
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
render: (args) => ({
|
|
41
|
+
components: { FdsSelect },
|
|
42
|
+
setup: () => ({ args }),
|
|
43
|
+
template: `<FdsSelect v-bind="args" />`,
|
|
44
|
+
}),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Disabled: Story = {
|
|
48
|
+
args: { disabled: true },
|
|
49
|
+
render: (args) => ({
|
|
50
|
+
components: { FdsSelect },
|
|
51
|
+
setup: () => ({ args }),
|
|
52
|
+
template: `<FdsSelect v-bind="args" />`,
|
|
53
|
+
}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Invalid: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
valid: 'false',
|
|
59
|
+
invalidMessage: 'Välj ett giltigt alternativ',
|
|
60
|
+
},
|
|
61
|
+
render: (args) => ({
|
|
62
|
+
components: { FdsSelect },
|
|
63
|
+
setup: () => ({ args }),
|
|
64
|
+
template: `<FdsSelect v-bind="args" />`,
|
|
65
|
+
}),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Valid: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
valid: 'true',
|
|
71
|
+
modelValue: 'option1',
|
|
72
|
+
},
|
|
73
|
+
render: (args) => ({
|
|
74
|
+
components: { FdsSelect },
|
|
75
|
+
setup: () => ({ args }),
|
|
76
|
+
template: `<FdsSelect v-bind="args" />`,
|
|
77
|
+
}),
|
|
78
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useSlots } from 'vue'
|
|
3
|
+
import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
|
|
4
|
+
import type { FdsSelectProps } from './types'
|
|
5
|
+
|
|
6
|
+
// Support both v-model (modelValue) and :value prop for backward compatibility
|
|
7
|
+
const modelValue = defineModel<string>({ default: undefined, required: false })
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<FdsSelectProps>(), {
|
|
10
|
+
value: undefined,
|
|
11
|
+
label: undefined,
|
|
12
|
+
meta: undefined,
|
|
13
|
+
disabled: false,
|
|
14
|
+
optional: false,
|
|
15
|
+
valid: undefined,
|
|
16
|
+
invalidMessage: undefined,
|
|
17
|
+
name: undefined,
|
|
18
|
+
id: undefined,
|
|
19
|
+
placeholder: undefined,
|
|
20
|
+
options: undefined,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
(e: 'input', ev: Event): void
|
|
25
|
+
(e: 'change', ev: Event): void
|
|
26
|
+
(e: 'update:value', value: string): void
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const autoId = `fds-select-${Math.random().toString(36).slice(2, 9)}`
|
|
30
|
+
const selectId = computed(() => props.id ?? autoId)
|
|
31
|
+
|
|
32
|
+
const slots = useSlots()
|
|
33
|
+
const hasDefaultSlot = computed(() => !!slots.default)
|
|
34
|
+
|
|
35
|
+
const showInvalidMessage = computed(
|
|
36
|
+
() => props.valid === 'false' && !props.optional && props.invalidMessage && !props.disabled,
|
|
37
|
+
)
|
|
38
|
+
const isInvalid = computed(() => props.valid === 'false' && !props.optional && !props.disabled)
|
|
39
|
+
|
|
40
|
+
const selectClasses = computed(() => [
|
|
41
|
+
'block w-full rounded-md border border-gray-500 px-3 py-2 pr-8 h-12 mb-4',
|
|
42
|
+
'focus:outline-2 focus:outline-blue-500 focus:border-transparent',
|
|
43
|
+
'appearance-none',
|
|
44
|
+
props.disabled
|
|
45
|
+
? 'outline-dashed text-gray-600 outline-2 -outline-offset-2 outline-gray-400 cursor-not-allowed border-transparent'
|
|
46
|
+
: 'bg-white cursor-pointer',
|
|
47
|
+
isInvalid.value && !props.disabled && 'outline-2 -outline-offset-2 outline-red-600 text-red-600',
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
// Use modelValue if bound via v-model, otherwise use value prop
|
|
51
|
+
const internalValue = computed({
|
|
52
|
+
get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
|
|
53
|
+
set: (newValue: string) => {
|
|
54
|
+
// Update modelValue if it's being used
|
|
55
|
+
if (modelValue.value !== undefined) {
|
|
56
|
+
modelValue.value = newValue
|
|
57
|
+
}
|
|
58
|
+
// Always emit update:value for backward compatibility
|
|
59
|
+
emit('update:value', newValue)
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function handleChange(ev: Event) {
|
|
64
|
+
emit('change', ev)
|
|
65
|
+
emit('input', ev)
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div class="w-full mb-4">
|
|
71
|
+
<label
|
|
72
|
+
v-if="label"
|
|
73
|
+
:for="selectId"
|
|
74
|
+
class="block font-bold text-gray-900 cursor-pointer"
|
|
75
|
+
:class="{ 'mb-0': meta, 'mb-1': !meta }"
|
|
76
|
+
>{{ label }}</label
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
v-if="meta"
|
|
80
|
+
class="font-thin mb-1"
|
|
81
|
+
>
|
|
82
|
+
{{ meta }}
|
|
83
|
+
</div>
|
|
84
|
+
<div class="relative">
|
|
85
|
+
<select
|
|
86
|
+
:id="selectId"
|
|
87
|
+
:name="name || undefined"
|
|
88
|
+
:disabled="disabled"
|
|
89
|
+
v-model="internalValue"
|
|
90
|
+
:aria-invalid="valid === 'false' ? 'true' : undefined"
|
|
91
|
+
:class="selectClasses"
|
|
92
|
+
v-bind="$attrs"
|
|
93
|
+
@change="handleChange"
|
|
94
|
+
@input="(e) => emit('input', e)"
|
|
95
|
+
>
|
|
96
|
+
<option
|
|
97
|
+
v-if="placeholder && !hasDefaultSlot"
|
|
98
|
+
value=""
|
|
99
|
+
disabled
|
|
100
|
+
>
|
|
101
|
+
{{ placeholder }}
|
|
102
|
+
</option>
|
|
103
|
+
<!-- Render options from prop if no slot is provided -->
|
|
104
|
+
<template v-if="!hasDefaultSlot && options">
|
|
105
|
+
<option
|
|
106
|
+
v-for="option in options"
|
|
107
|
+
:key="option.value"
|
|
108
|
+
:value="option.value"
|
|
109
|
+
:disabled="option.disabled"
|
|
110
|
+
>
|
|
111
|
+
{{ option.label }}
|
|
112
|
+
</option>
|
|
113
|
+
</template>
|
|
114
|
+
<!-- Render slot content (supports optgroups and custom options) -->
|
|
115
|
+
<slot v-else />
|
|
116
|
+
</select>
|
|
117
|
+
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
118
|
+
<FdsIcon
|
|
119
|
+
name="arrowDown"
|
|
120
|
+
:size="24"
|
|
121
|
+
:class="{
|
|
122
|
+
'fill-gray-500': disabled,
|
|
123
|
+
'fill-red-500': isInvalid && !disabled,
|
|
124
|
+
'fill-blue-500': !disabled && !isInvalid,
|
|
125
|
+
}"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div
|
|
130
|
+
v-if="showInvalidMessage"
|
|
131
|
+
class="text-red-600 font-bold mt-1"
|
|
132
|
+
>
|
|
133
|
+
{{ invalidMessage }}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FdsSelectProps {
|
|
2
|
+
value?: string
|
|
3
|
+
label?: string
|
|
4
|
+
meta?: string
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
optional?: boolean
|
|
7
|
+
valid?: 'true' | 'false' | 'null'
|
|
8
|
+
invalidMessage?: string
|
|
9
|
+
name?: string
|
|
10
|
+
id?: string
|
|
11
|
+
placeholder?: string
|
|
12
|
+
options?: { value: string; label: string; disabled?: boolean }[]
|
|
13
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import FdsTextarea from './FdsTextarea.vue'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof FdsTextarea> = {
|
|
5
|
+
title: 'FDS/Form/FdsTextarea',
|
|
6
|
+
component: FdsTextarea,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
modelValue: { control: { type: 'text' } },
|
|
10
|
+
label: { control: { type: 'text' } },
|
|
11
|
+
meta: { control: { type: 'text' } },
|
|
12
|
+
disabled: { control: { type: 'boolean' } },
|
|
13
|
+
optional: { control: { type: 'boolean' } },
|
|
14
|
+
valid: { control: { type: 'select' }, options: ['true', 'false', 'null'] },
|
|
15
|
+
invalidMessage: { control: { type: 'text' } },
|
|
16
|
+
rows: { control: { type: 'number', min: 1, step: 1 } },
|
|
17
|
+
id: { control: { type: 'text' } },
|
|
18
|
+
name: { control: { type: 'text' } },
|
|
19
|
+
},
|
|
20
|
+
args: {
|
|
21
|
+
modelValue: '',
|
|
22
|
+
label: 'Beskrivning',
|
|
23
|
+
meta: '',
|
|
24
|
+
disabled: false,
|
|
25
|
+
optional: false,
|
|
26
|
+
valid: 'null',
|
|
27
|
+
invalidMessage: undefined,
|
|
28
|
+
rows: 4,
|
|
29
|
+
id: '',
|
|
30
|
+
name: '',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default meta
|
|
35
|
+
type Story = StoryObj<typeof meta>
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: (args) => ({
|
|
39
|
+
components: { FdsTextarea },
|
|
40
|
+
setup: () => ({ args }),
|
|
41
|
+
template: `<FdsTextarea v-bind="args" />`,
|
|
42
|
+
}),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Disabled: Story = {
|
|
46
|
+
args: { disabled: true },
|
|
47
|
+
render: (args) => ({
|
|
48
|
+
components: { FdsTextarea },
|
|
49
|
+
setup: () => ({ args }),
|
|
50
|
+
template: `<FdsTextarea v-bind="args" />`,
|
|
51
|
+
}),
|
|
52
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
|
|
4
|
+
import type { FdsTextareaProps } from './types'
|
|
5
|
+
|
|
6
|
+
// Support both v-model (modelValue) and :value prop for backward compatibility
|
|
7
|
+
const modelValue = defineModel<string>({ default: undefined, required: false })
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<FdsTextareaProps>(), {
|
|
10
|
+
value: undefined,
|
|
11
|
+
label: undefined,
|
|
12
|
+
meta: undefined,
|
|
13
|
+
disabled: false,
|
|
14
|
+
optional: false,
|
|
15
|
+
valid: undefined,
|
|
16
|
+
rows: 4,
|
|
17
|
+
name: undefined,
|
|
18
|
+
id: undefined,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
(e: 'input', ev: Event): void
|
|
23
|
+
(e: 'update:value', value: string): void
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const autoId = `fds-textarea-${Math.random().toString(36).slice(2, 9)}`
|
|
27
|
+
const textareaId = computed(() => props.id ?? autoId)
|
|
28
|
+
|
|
29
|
+
const showInvalidMessage = computed(() => props.valid === 'false' && !props.optional && props.invalidMessage)
|
|
30
|
+
const isInvalid = computed(() => props.valid === 'false' && !props.optional && !props.disabled)
|
|
31
|
+
const isValid = computed(() => props.valid === 'true')
|
|
32
|
+
|
|
33
|
+
const inputClasses = computed(() => [
|
|
34
|
+
'block w-full rounded-md border border-gray-500 px-3 py-2',
|
|
35
|
+
'focus:outline-2 focus:outline-blue-500 focus:border-transparent',
|
|
36
|
+
props.disabled
|
|
37
|
+
? 'outline-dashed outline-2 -outline-offset-2 outline-gray-400 cursor-not-allowed border-transparent bg-gray-50'
|
|
38
|
+
: 'bg-white',
|
|
39
|
+
isInvalid.value && 'outline-2 -outline-offset-2 outline-red-600',
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
const validationIconClasses = computed(() => ['absolute right-3 top-3 flex items-center gap-2 pointer-events-none'])
|
|
43
|
+
|
|
44
|
+
// Use modelValue if bound via v-model, otherwise use value prop
|
|
45
|
+
const internalValue = computed({
|
|
46
|
+
get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
|
|
47
|
+
set: (newValue: string) => {
|
|
48
|
+
// Update modelValue if it's being used
|
|
49
|
+
if (modelValue.value !== undefined) {
|
|
50
|
+
modelValue.value = newValue
|
|
51
|
+
}
|
|
52
|
+
// Always emit update:value for backward compatibility
|
|
53
|
+
emit('update:value', newValue)
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div class="w-full mb-4">
|
|
60
|
+
<label
|
|
61
|
+
v-if="label"
|
|
62
|
+
:for="textareaId"
|
|
63
|
+
class="block font-bold text-gray-900 cursor-pointer"
|
|
64
|
+
:class="{ 'mb-0': meta, 'mb-1': !meta }"
|
|
65
|
+
>{{ label }}</label
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
v-if="meta"
|
|
69
|
+
class="font-thin mb-1"
|
|
70
|
+
>
|
|
71
|
+
{{ meta }}
|
|
72
|
+
</div>
|
|
73
|
+
<div class="relative">
|
|
74
|
+
<textarea
|
|
75
|
+
:id="textareaId"
|
|
76
|
+
:name="name || undefined"
|
|
77
|
+
:disabled="disabled"
|
|
78
|
+
:rows="rows"
|
|
79
|
+
v-model="internalValue"
|
|
80
|
+
:aria-invalid="valid === 'false' ? 'true' : undefined"
|
|
81
|
+
:class="inputClasses"
|
|
82
|
+
v-bind="$attrs"
|
|
83
|
+
@input="(e) => emit('input', e)"
|
|
84
|
+
/>
|
|
85
|
+
<div :class="validationIconClasses">
|
|
86
|
+
<FdsIcon
|
|
87
|
+
v-if="isInvalid"
|
|
88
|
+
name="alert"
|
|
89
|
+
class="fill-red-600"
|
|
90
|
+
/>
|
|
91
|
+
<FdsIcon
|
|
92
|
+
v-if="isValid"
|
|
93
|
+
name="bigSuccess"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div
|
|
98
|
+
v-if="showInvalidMessage"
|
|
99
|
+
class="text-red-600 font-bold mt-1"
|
|
100
|
+
>
|
|
101
|
+
{{ invalidMessage }}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div
|
|
105
|
+
v-if="isValid"
|
|
106
|
+
class="sr-only"
|
|
107
|
+
>
|
|
108
|
+
OK
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|