@bethinkpl/design-system 43.0.0 → 45.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.
Files changed (97) hide show
  1. package/dist/design-system.css +1 -1
  2. package/dist/design-system.js +13826 -13252
  3. package/dist/design-system.js.map +1 -1
  4. package/dist/lib/js/components/Buttons/IconButton/IconButton.vue.d.ts +1 -1
  5. package/dist/lib/js/components/Callout/Callout.consts.d.ts +32 -0
  6. package/dist/lib/js/components/{ActionContent/ActionContent.vue.d.ts → Callout/Callout.vue.d.ts} +17 -6
  7. package/dist/lib/js/components/Callout/index.d.ts +4 -0
  8. package/dist/lib/js/components/DatePickers/DateBox/DateBox.vue.d.ts +1 -1
  9. package/dist/lib/js/components/DatePickers/DatePicker/DatePicker.vue.d.ts +5 -5
  10. package/dist/lib/js/components/DatePickers/DateRangePicker/DateRangePicker.vue.d.ts +2 -2
  11. package/dist/lib/js/components/Divider/Divider.vue.d.ts +1 -1
  12. package/dist/lib/js/components/Drawer/DrawerDivider/DrawerDivider.vue.d.ts +1 -1
  13. package/dist/lib/js/components/Drawer/DrawerSection/DrawerSection.vue.d.ts +4 -4
  14. package/dist/lib/js/components/Drawer/DrawerTile/DrawerTile.vue.d.ts +4 -4
  15. package/dist/lib/js/components/Dropdown/Dropdown.vue.d.ts +1 -1
  16. package/dist/lib/js/components/Form/FormField/FormField.utils.d.ts +1 -1
  17. package/dist/lib/js/components/Form/InputField/InputField.vue.d.ts +2 -2
  18. package/dist/lib/js/components/Form/PasswordField/PasswordField.vue.d.ts +2 -2
  19. package/dist/lib/js/components/Form/PinInputField/PinInputField.consts.d.ts +14 -0
  20. package/dist/lib/js/components/Form/PinInputField/PinInputField.types.d.ts +12 -0
  21. package/dist/lib/js/components/Form/PinInputField/PinInputField.utils.d.ts +2 -0
  22. package/dist/lib/js/components/Form/PinInputField/PinInputField.vue.d.ts +91 -0
  23. package/dist/lib/js/components/Form/PinInputField/index.d.ts +5 -0
  24. package/dist/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.d.ts +8 -0
  25. package/dist/lib/js/components/Form/SelectField/SelectField.vue.d.ts +2 -2
  26. package/dist/lib/js/components/Form/TextAreaField/TextAreaField.types.d.ts +9 -0
  27. package/dist/lib/js/components/Form/TextAreaField/TextAreaField.vue.d.ts +76 -0
  28. package/dist/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue.d.ts +6 -0
  29. package/dist/lib/js/components/Form/TextAreaField/index.d.ts +4 -0
  30. package/dist/lib/js/components/Headers/OverlayHeader/OverlayHeader.vue.d.ts +5 -5
  31. package/dist/lib/js/components/Headers/PageHeader/PageHeader.vue.d.ts +1 -1
  32. package/dist/lib/js/components/Headers/SectionHeader/SectionHeader.vue.d.ts +3 -3
  33. package/dist/lib/js/components/IconText/IconText.vue.d.ts +1 -1
  34. package/dist/lib/js/components/Image/Image.vue.d.ts +1 -1
  35. package/dist/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue.d.ts +12 -1
  36. package/dist/lib/js/components/Menu/MenuDivider/MenuDivider.vue.d.ts +1 -1
  37. package/dist/lib/js/components/Modals/Modal/Modal.vue.d.ts +2 -2
  38. package/dist/lib/js/components/Modals/ModalDialog/ModalDialog.vue.d.ts +2 -2
  39. package/dist/lib/js/components/Pagination/Pagination.vue.d.ts +2 -2
  40. package/dist/lib/js/components/RichList/BasicRichListItem/BasicRichListItem.vue.d.ts +7 -7
  41. package/dist/lib/js/components/RichList/RichListItem/RichListItem.vue.d.ts +3 -3
  42. package/dist/lib/js/components/SelectList/SelectListItemDivider/SelectListItemDivider.vue.d.ts +1 -1
  43. package/dist/lib/js/components/Skeleton/Skeleton.vue.d.ts +1 -1
  44. package/dist/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue.d.ts +3 -3
  45. package/dist/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue.d.ts +3 -3
  46. package/dist/lib/js/components/Tile/Tile.sb.shared.d.ts +2 -2
  47. package/dist/lib/js/components/Tile/Tile.vue.d.ts +2 -2
  48. package/dist/lib/js/components/Toast/Toast.consts.d.ts +11 -9
  49. package/dist/lib/js/components/Toast/Toast.vue.d.ts +51 -433
  50. package/dist/lib/js/components/Toggles/ToggleButton/ToggleButton.vue.d.ts +1 -1
  51. package/dist/lib/js/{components/Form/InputField/useInputFieldWithinForm.d.ts → composables/useTextFieldWithinForm.d.ts} +1 -1
  52. package/dist/lib/js/index.d.ts +6 -1
  53. package/lib/js/components/Callout/Callout.consts.ts +25 -0
  54. package/lib/js/components/Callout/Callout.spec.ts +257 -0
  55. package/lib/js/components/Callout/Callout.stories.ts +209 -0
  56. package/lib/js/components/Callout/Callout.vue +161 -0
  57. package/lib/js/components/Callout/index.ts +4 -0
  58. package/lib/js/components/Cards/Card/Card.vue +1 -1
  59. package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.spec.ts +15 -0
  60. package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.vue +5 -2
  61. package/lib/js/components/Form/Form.stories.ts +23 -2
  62. package/lib/js/components/Form/InputField/InputField.spec.ts +11 -1
  63. package/lib/js/components/Form/InputField/InputField.vue +7 -4
  64. package/lib/js/components/Form/PasswordField/PasswordField.vue +1 -1
  65. package/lib/js/components/Form/PinInputField/PinInputField.consts.ts +18 -0
  66. package/lib/js/components/Form/PinInputField/PinInputField.spec.ts +394 -0
  67. package/lib/js/components/Form/PinInputField/PinInputField.stories.ts +124 -0
  68. package/lib/js/components/Form/PinInputField/PinInputField.types.ts +42 -0
  69. package/lib/js/components/Form/PinInputField/PinInputField.utils.ts +5 -0
  70. package/lib/js/components/Form/PinInputField/PinInputField.vue +211 -0
  71. package/lib/js/components/Form/PinInputField/index.ts +6 -0
  72. package/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.ts +29 -0
  73. package/lib/js/components/Form/SelectField/SelectField.stories.ts +0 -5
  74. package/lib/js/components/Form/SelectField/useSelectFieldWithinForm.ts +1 -1
  75. package/lib/js/components/Form/TextAreaField/TextAreaField.spec.ts +341 -0
  76. package/lib/js/components/Form/TextAreaField/TextAreaField.stories.ts +99 -0
  77. package/lib/js/components/Form/TextAreaField/TextAreaField.types.ts +23 -0
  78. package/lib/js/components/Form/TextAreaField/TextAreaField.vue +190 -0
  79. package/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue +19 -0
  80. package/lib/js/components/Form/TextAreaField/index.ts +5 -0
  81. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.spec.ts +26 -0
  82. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.stories.ts +16 -3
  83. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue +5 -0
  84. package/lib/js/components/LoadingBar/LoadingBar.spec.ts +84 -0
  85. package/lib/js/components/LoadingBar/LoadingBar.vue +30 -19
  86. package/lib/js/components/Toast/Toast.consts.ts +4 -2
  87. package/lib/js/components/Toast/Toast.spec.ts +177 -0
  88. package/lib/js/components/Toast/Toast.stories.ts +65 -31
  89. package/lib/js/components/Toast/Toast.vue +176 -195
  90. package/lib/js/{components/Form/InputField/useInputFieldWithinForm.ts → composables/useTextFieldWithinForm.ts} +7 -2
  91. package/lib/js/index.ts +6 -1
  92. package/package.json +1 -1
  93. package/dist/lib/js/components/ActionContent/index.d.ts +0 -3
  94. package/lib/js/components/ActionContent/ActionContent.spec.ts +0 -99
  95. package/lib/js/components/ActionContent/ActionContent.stories.ts +0 -141
  96. package/lib/js/components/ActionContent/ActionContent.vue +0 -104
  97. package/lib/js/components/ActionContent/index.ts +0 -3
@@ -0,0 +1,257 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { ComponentProps } from 'vue-component-type-helpers';
4
+ import { h } from 'vue';
5
+ import Callout from './Callout.vue';
6
+ import {
7
+ CALLOUT_ICON_COLORS,
8
+ CALLOUT_LAYOUTS,
9
+ CALLOUT_MAIN_TEXT_COLORS,
10
+ CALLOUT_SIZES,
11
+ } from './Callout.consts';
12
+ import DsFeatureIcon from '../Icons/FeatureIcon';
13
+ import { FEATURE_ICON_SIZES } from '../Icons/FeatureIcon/FeatureIcon.consts';
14
+ import DsTextGroup from '../TextGroup/TextGroup.vue';
15
+ import { TEXT_GROUP_ALIGNS, TEXT_GROUP_SIZES } from '../TextGroup/TextGroup.consts';
16
+ import DsButton from '../Buttons/Button';
17
+ import { BUTTON_COLORS, BUTTON_RADIUSES, BUTTON_SIZES, BUTTON_TYPES } from '../Buttons/Button';
18
+ import { ICONS } from '../Icons/Icon';
19
+
20
+ type MountOptions = Omit<NonNullable<Parameters<typeof mount<typeof Callout>>[1]>, 'props'> & {
21
+ props?: Partial<ComponentProps<typeof Callout>>;
22
+ };
23
+
24
+ const mountCallout = (options: MountOptions = {}) =>
25
+ mount(Callout, {
26
+ ...options,
27
+ props: { icon: ICONS.FA_COMMENT_DOTS, ...options.props },
28
+ });
29
+
30
+ describe('Callout', () => {
31
+ it('renders', () => {
32
+ const wrapper = mountCallout();
33
+
34
+ expect(wrapper.exists()).toBe(true);
35
+ });
36
+
37
+ it('renders the feature icon', () => {
38
+ const wrapper = mountCallout();
39
+
40
+ expect(wrapper.findComponent(DsFeatureIcon).exists()).toBe(true);
41
+ expect(wrapper.findComponent(DsFeatureIcon).props('icon')).toStrictEqual(
42
+ ICONS.FA_COMMENT_DOTS,
43
+ );
44
+ });
45
+
46
+ it('passes main and supporting text to the text group', () => {
47
+ const wrapper = mountCallout({
48
+ props: {
49
+ mainText: 'Main text',
50
+ supportingText: 'Supporting text',
51
+ },
52
+ });
53
+
54
+ expect(wrapper.find('.ds-callout__text').text()).toContain('Main text');
55
+ expect(wrapper.find('.ds-callout__text').text()).toContain('Supporting text');
56
+ });
57
+
58
+ describe('layout', () => {
59
+ it.each(Object.values(CALLOUT_LAYOUTS))('applies the layout class for "%s"', (layout) => {
60
+ const wrapper = mountCallout({ props: { layout } });
61
+
62
+ expect(wrapper.find('.ds-callout').classes()).toContain(`-ds-layout-${layout}`);
63
+ });
64
+
65
+ it('is vertical by default', () => {
66
+ const wrapper = mountCallout();
67
+
68
+ expect(wrapper.find('.ds-callout').classes()).toContain('-ds-layout-vertical');
69
+ });
70
+
71
+ it.each([
72
+ [CALLOUT_LAYOUTS.VERTICAL, TEXT_GROUP_ALIGNS.CENTER],
73
+ [CALLOUT_LAYOUTS.HORIZONTAL, TEXT_GROUP_ALIGNS.LEFT],
74
+ ])('aligns the text group for layout "%s"', (layout, expectedAlign) => {
75
+ const wrapper = mountCallout({ props: { layout } });
76
+
77
+ expect(wrapper.findComponent(DsTextGroup).props('align')).toBe(expectedAlign);
78
+ });
79
+ });
80
+
81
+ describe('size', () => {
82
+ it('is medium by default', () => {
83
+ const wrapper = mountCallout();
84
+
85
+ expect(wrapper.findComponent(DsFeatureIcon).props('size')).toBe(
86
+ FEATURE_ICON_SIZES.MEDIUM,
87
+ );
88
+ expect(wrapper.findComponent(DsTextGroup).props('size')).toBe(TEXT_GROUP_SIZES.LARGE);
89
+ });
90
+
91
+ it.each([
92
+ [CALLOUT_SIZES.SMALL, FEATURE_ICON_SIZES.MEDIUM, TEXT_GROUP_SIZES.MEDIUM],
93
+ [CALLOUT_SIZES.MEDIUM, FEATURE_ICON_SIZES.MEDIUM, TEXT_GROUP_SIZES.LARGE],
94
+ [CALLOUT_SIZES.LARGE, FEATURE_ICON_SIZES.LARGE, TEXT_GROUP_SIZES.LARGE],
95
+ ])(
96
+ 'maps size "%s" to feature icon "%s" and text group "%s"',
97
+ (size, featureIconSize, textGroupSize) => {
98
+ const wrapper = mountCallout({ props: { size } });
99
+
100
+ expect(wrapper.findComponent(DsFeatureIcon).props('size')).toBe(featureIconSize);
101
+ expect(wrapper.findComponent(DsTextGroup).props('size')).toBe(textGroupSize);
102
+ },
103
+ );
104
+ });
105
+
106
+ describe('icon color', () => {
107
+ it.each(Object.values(CALLOUT_ICON_COLORS))(
108
+ 'passes icon color "%s" to the feature icon',
109
+ (iconColor) => {
110
+ const wrapper = mountCallout({ props: { iconColor } });
111
+
112
+ expect(wrapper.findComponent(DsFeatureIcon).props('color')).toBe(iconColor);
113
+ },
114
+ );
115
+
116
+ it('is primary by default', () => {
117
+ const wrapper = mountCallout();
118
+
119
+ expect(wrapper.findComponent(DsFeatureIcon).props('color')).toBe(
120
+ CALLOUT_ICON_COLORS.PRIMARY,
121
+ );
122
+ });
123
+ });
124
+
125
+ describe('main text color', () => {
126
+ it.each(Object.values(CALLOUT_MAIN_TEXT_COLORS))(
127
+ 'passes main text color "%s" to the text group',
128
+ (mainTextColor) => {
129
+ const wrapper = mountCallout({ props: { mainTextColor } });
130
+
131
+ expect(wrapper.findComponent(DsTextGroup).props('mainTextColor')).toBe(
132
+ mainTextColor,
133
+ );
134
+ },
135
+ );
136
+
137
+ it('is primary by default', () => {
138
+ const wrapper = mountCallout();
139
+
140
+ expect(wrapper.findComponent(DsTextGroup).props('mainTextColor')).toBe(
141
+ CALLOUT_MAIN_TEXT_COLORS.PRIMARY,
142
+ );
143
+ });
144
+ });
145
+
146
+ describe('eyebrow text', () => {
147
+ it('does not render the eyebrow by default', () => {
148
+ const wrapper = mountCallout();
149
+
150
+ expect(wrapper.findComponent(DsTextGroup).props('eyebrowText')).toBe(null);
151
+ expect(wrapper.find('.ds-textGroup__eyebrow').exists()).toBe(false);
152
+ });
153
+
154
+ it('renders the eyebrow when eyebrowText is provided', () => {
155
+ const wrapper = mountCallout({ props: { eyebrowText: 'Eyebrow text' } });
156
+
157
+ expect(wrapper.find('.ds-textGroup__eyebrow').text()).toBe('Eyebrow text');
158
+ });
159
+
160
+ it('never uppercases the eyebrow', () => {
161
+ const wrapper = mountCallout({ props: { eyebrowText: 'Eyebrow text' } });
162
+
163
+ expect(wrapper.findComponent(DsTextGroup).props('isEyebrowTextUppercase')).toBe(false);
164
+ expect(wrapper.find('.ds-textGroup__eyebrow').classes()).not.toContain('-ds-uppercase');
165
+ });
166
+ });
167
+
168
+ describe('actions', () => {
169
+ it('renders a default primary button with the button label and icon', () => {
170
+ const wrapper = mountCallout({
171
+ props: {
172
+ buttonLabel: 'Confirm',
173
+ buttonIcon: ICONS.FA_COMMENT_DOTS,
174
+ },
175
+ });
176
+
177
+ const button = wrapper.findComponent(DsButton);
178
+ expect(button.exists()).toBe(true);
179
+ expect(button.text()).toBe('Confirm');
180
+ expect(button.props('iconLeft')).toStrictEqual(ICONS.FA_COMMENT_DOTS);
181
+ expect(button.props('color')).toBe(BUTTON_COLORS.PRIMARY);
182
+ expect(button.props('type')).toBe(BUTTON_TYPES.FILLED);
183
+ expect(button.props('size')).toBe(BUTTON_SIZES.SMALL);
184
+ expect(button.props('radius')).toBe(BUTTON_RADIUSES.ROUNDED);
185
+ });
186
+
187
+ it.each(Object.values(CALLOUT_ICON_COLORS))(
188
+ 'keeps the filled primary button for icon color "%s"',
189
+ (iconColor) => {
190
+ const wrapper = mountCallout({ props: { buttonLabel: 'Confirm', iconColor } });
191
+
192
+ const button = wrapper.findComponent(DsButton);
193
+ expect(button.props('color')).toBe(BUTTON_COLORS.PRIMARY);
194
+ expect(button.props('type')).toBe(BUTTON_TYPES.FILLED);
195
+ },
196
+ );
197
+
198
+ it('emits button-clicked when the default button is clicked', async () => {
199
+ const wrapper = mountCallout({
200
+ props: {
201
+ buttonLabel: 'Confirm',
202
+ },
203
+ });
204
+
205
+ await wrapper.findComponent(DsButton).trigger('click');
206
+
207
+ expect(wrapper.emitted('button-clicked')).toHaveLength(1);
208
+ });
209
+
210
+ it('renders the actions slot instead of the default button', () => {
211
+ const wrapper = mountCallout({
212
+ props: {
213
+ buttonLabel: 'Default button',
214
+ },
215
+ slots: {
216
+ actions: () => h('span', { class: 'custom-action' }, 'Custom action'),
217
+ },
218
+ });
219
+
220
+ expect(wrapper.find('.custom-action').exists()).toBe(true);
221
+ expect(wrapper.findComponent(DsButton).exists()).toBe(false);
222
+ });
223
+
224
+ it('does not render the actions block when there is no button label and no slot', () => {
225
+ const wrapper = mountCallout();
226
+
227
+ expect(wrapper.find('.ds-callout__actions').exists()).toBe(false);
228
+ expect(wrapper.findComponent(DsButton).exists()).toBe(false);
229
+ });
230
+
231
+ it('renders the actions block when only the slot is provided', () => {
232
+ const wrapper = mountCallout({
233
+ slots: {
234
+ actions: () => h('span', { class: 'custom-action' }, 'Custom action'),
235
+ },
236
+ });
237
+
238
+ expect(wrapper.find('.ds-callout__actions').exists()).toBe(true);
239
+ });
240
+
241
+ it('adds the stacked actions modifier on the root when isActionVertical is set', () => {
242
+ const wrapper = mountCallout({
243
+ props: {
244
+ isActionVertical: true,
245
+ },
246
+ });
247
+
248
+ expect(wrapper.find('.ds-callout').classes()).toContain('-ds-actions-vertical');
249
+ });
250
+
251
+ it('does not add the stacked actions modifier by default', () => {
252
+ const wrapper = mountCallout();
253
+
254
+ expect(wrapper.find('.ds-callout').classes()).not.toContain('-ds-actions-vertical');
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,209 @@
1
+ import { ComponentProps } from 'vue-component-type-helpers';
2
+ import { Meta, StoryObj } from '@storybook/vue3';
3
+ import { action } from '@storybook/addon-actions';
4
+
5
+ import Callout from './Callout.vue';
6
+ import {
7
+ CALLOUT_ICON_COLORS,
8
+ CALLOUT_LAYOUTS,
9
+ CALLOUT_MAIN_TEXT_COLORS,
10
+ CALLOUT_SIZES,
11
+ } from './Callout.consts';
12
+ import DsButton from '../Buttons/Button';
13
+ import { BUTTON_COLORS, BUTTON_SIZES, BUTTON_TYPES } from '../Buttons/Button/Button.consts';
14
+ import { ICONS } from '../Icons/Icon';
15
+
16
+ type CalloutProps = ComponentProps<typeof Callout>;
17
+
18
+ const VERTICAL_FIGMA_URL =
19
+ 'https://www.figma.com/design/izQdYyiBR1GQgFkaOIfIJI/LMS---DS-Components?node-id=17359-1009&m=dev';
20
+ const HORIZONTAL_FIGMA_URL =
21
+ 'https://www.figma.com/design/izQdYyiBR1GQgFkaOIfIJI/LMS---DS-Components?node-id=17760-416&m=dev';
22
+
23
+ /**
24
+ * Clearing a text control yields `''`, which TextGroup renders as a `&nbsp;` placeholder row.
25
+ * Mapping it to `null` removes the row instead, which is what an empty control should mean.
26
+ */
27
+ const emptyToNull = (value?: string | null) => value || null;
28
+
29
+ const mapArgs = (args: CalloutProps) => ({
30
+ ...args,
31
+ icon: ICONS[args.icon],
32
+ buttonIcon: ICONS[args.buttonIcon],
33
+ eyebrowText: emptyToNull(args.eyebrowText),
34
+ mainText: emptyToNull(args.mainText),
35
+ supportingText: emptyToNull(args.supportingText),
36
+ buttonLabel: emptyToNull(args.buttonLabel),
37
+ });
38
+
39
+ const meta: Meta<typeof Callout> = {
40
+ title: 'Components/Callout',
41
+ component: Callout,
42
+ render: (args: CalloutProps) => ({
43
+ components: { Callout },
44
+ setup() {
45
+ return { onButtonClicked: action('button-clicked') };
46
+ },
47
+ computed: {
48
+ props() {
49
+ return mapArgs(args);
50
+ },
51
+ },
52
+ template: `<div style="max-width: 500px;"><Callout v-bind="props" @button-clicked="onButtonClicked" /></div>`,
53
+ }),
54
+ argTypes: {
55
+ layout: {
56
+ control: 'select',
57
+ options: Object.values(CALLOUT_LAYOUTS),
58
+ },
59
+ size: {
60
+ control: 'select',
61
+ options: Object.values(CALLOUT_SIZES),
62
+ },
63
+ iconColor: {
64
+ control: 'select',
65
+ options: Object.values(CALLOUT_ICON_COLORS),
66
+ },
67
+ mainTextColor: {
68
+ control: 'select',
69
+ options: Object.values(CALLOUT_MAIN_TEXT_COLORS),
70
+ },
71
+ icon: {
72
+ control: 'select',
73
+ options: Object.keys(ICONS),
74
+ },
75
+ buttonIcon: {
76
+ control: 'select',
77
+ options: [null, ...Object.keys(ICONS)],
78
+ },
79
+ eyebrowText: { control: 'text' },
80
+ mainText: { control: 'text' },
81
+ supportingText: { control: 'text' },
82
+ buttonLabel: { control: 'text' },
83
+ isActionVertical: { control: 'boolean' },
84
+ },
85
+ };
86
+ export default meta;
87
+
88
+ type Story = StoryObj<typeof Callout>;
89
+
90
+ export const Interactive: Story = {
91
+ args: {
92
+ layout: CALLOUT_LAYOUTS.VERTICAL,
93
+ size: CALLOUT_SIZES.MEDIUM,
94
+ iconColor: CALLOUT_ICON_COLORS.PRIMARY,
95
+ mainTextColor: CALLOUT_MAIN_TEXT_COLORS.PRIMARY,
96
+ icon: 'FA_COMMENT_DOTS' as unknown as CalloutProps['icon'],
97
+ eyebrowText: '',
98
+ mainText: 'Main text tutaj sobie będzie',
99
+ supportingText: 'Supporting text tutaj będzie',
100
+ buttonLabel: 'Button label',
101
+ buttonIcon: null,
102
+ isActionVertical: false,
103
+ },
104
+ };
105
+
106
+ Interactive.parameters = {
107
+ design: {
108
+ type: 'figma',
109
+ url: VERTICAL_FIGMA_URL,
110
+ },
111
+ };
112
+
113
+ export const WithActionsSlot: Story = {
114
+ render: (args: CalloutProps) => ({
115
+ components: { Callout, DsButton },
116
+ setup() {
117
+ return { args, ICONS, BUTTON_COLORS, BUTTON_SIZES, BUTTON_TYPES };
118
+ },
119
+ computed: {
120
+ props() {
121
+ return mapArgs(args);
122
+ },
123
+ },
124
+ template: `
125
+ <div style="max-width: 500px;">
126
+ <Callout v-bind="props">
127
+ <template #actions>
128
+ <ds-button :color="BUTTON_COLORS.NEUTRAL" :type="BUTTON_TYPES.OUTLINED" :size="BUTTON_SIZES.SMALL">Cancel</ds-button>
129
+ <ds-button :color="BUTTON_COLORS.PRIMARY" :type="BUTTON_TYPES.FILLED" :size="BUTTON_SIZES.SMALL">Confirm</ds-button>
130
+ </template>
131
+ </Callout>
132
+ </div>`,
133
+ }),
134
+ args: {
135
+ layout: CALLOUT_LAYOUTS.VERTICAL,
136
+ size: CALLOUT_SIZES.MEDIUM,
137
+ iconColor: CALLOUT_ICON_COLORS.PRIMARY,
138
+ mainTextColor: CALLOUT_MAIN_TEXT_COLORS.PRIMARY,
139
+ icon: 'FA_COMMENT_DOTS' as unknown as CalloutProps['icon'],
140
+ mainText: 'Main text tutaj sobie będzie',
141
+ supportingText: 'Supporting text tutaj będzie',
142
+ isActionVertical: false,
143
+ },
144
+ };
145
+
146
+ WithActionsSlot.parameters = {
147
+ design: {
148
+ type: 'figma',
149
+ url: VERTICAL_FIGMA_URL,
150
+ },
151
+ };
152
+
153
+ /**
154
+ * The full variant matrix, laid out for side-by-side comparison with the Figma component set
155
+ * (node 17696:2260).
156
+ */
157
+ export const Variants: Story = {
158
+ render: () => ({
159
+ components: { Callout },
160
+ setup() {
161
+ return {
162
+ ICONS,
163
+ CALLOUT_LAYOUTS,
164
+ CALLOUT_SIZES,
165
+ sizes: Object.values(CALLOUT_SIZES),
166
+ };
167
+ },
168
+ template: `
169
+ <div style="display: flex; flex-direction: column; gap: 48px;">
170
+ <section v-for="layout in [CALLOUT_LAYOUTS.VERTICAL, CALLOUT_LAYOUTS.HORIZONTAL]" :key="layout">
171
+ <h3 style="margin-bottom: 16px;">{{ layout }}</h3>
172
+ <div style="display: flex; flex-direction: column; gap: 32px; max-width: 500px;">
173
+ <Callout
174
+ v-for="size in sizes"
175
+ :key="size"
176
+ :layout="layout"
177
+ :size="size"
178
+ :icon="ICONS.FA_COMMENT_DOTS"
179
+ :eyebrow-text="size === CALLOUT_SIZES.SMALL ? 'Eyebrow text' : null"
180
+ :main-text="'Main text tutaj sobie będzie (' + size + ')'"
181
+ supporting-text="Supporting text tutaj będzie"
182
+ button-label="Button small"
183
+ />
184
+ <Callout
185
+ :layout="layout"
186
+ :icon="ICONS.FA_COMMENT_DOTS"
187
+ main-text="Main text tutaj sobie będzie (stacked actions)"
188
+ supporting-text="Supporting text tutaj będzie"
189
+ button-label="Button small"
190
+ is-action-vertical
191
+ />
192
+ <Callout
193
+ :layout="layout"
194
+ :icon="ICONS.FA_COMMENT_DOTS"
195
+ main-text="Main text tutaj sobie będzie (no actions)"
196
+ supporting-text="Supporting text tutaj będzie"
197
+ />
198
+ </div>
199
+ </section>
200
+ </div>`,
201
+ }),
202
+ };
203
+
204
+ Variants.parameters = {
205
+ design: {
206
+ type: 'figma',
207
+ url: HORIZONTAL_FIGMA_URL,
208
+ },
209
+ };
@@ -0,0 +1,161 @@
1
+ <template>
2
+ <div
3
+ class="ds-callout"
4
+ :class="{
5
+ '-ds-layout-vertical': layout === CALLOUT_LAYOUTS.VERTICAL,
6
+ '-ds-layout-horizontal': layout === CALLOUT_LAYOUTS.HORIZONTAL,
7
+ '-ds-actions-vertical': isActionVertical,
8
+ }"
9
+ >
10
+ <div class="ds-callout__content">
11
+ <ds-feature-icon
12
+ class="ds-callout__icon"
13
+ :icon="icon"
14
+ :color="iconColor"
15
+ :size="featureIconSize"
16
+ double-background
17
+ />
18
+ <ds-text-group
19
+ class="ds-callout__text"
20
+ :eyebrow-text="eyebrowText"
21
+ :is-eyebrow-text-uppercase="false"
22
+ :main-text="mainText"
23
+ :supporting-text="supportingText"
24
+ :size="textGroupSize"
25
+ :align="textGroupAlign"
26
+ :main-text-color="mainTextColor"
27
+ :is-interactive="false"
28
+ />
29
+ </div>
30
+ <div v-if="$slots.actions || buttonLabel" class="ds-callout__actions">
31
+ <slot name="actions">
32
+ <ds-button
33
+ :color="BUTTON_COLORS.PRIMARY"
34
+ :type="BUTTON_TYPES.FILLED"
35
+ :size="BUTTON_SIZES.SMALL"
36
+ :radius="BUTTON_RADIUSES.ROUNDED"
37
+ :icon-left="buttonIcon"
38
+ @click="$emit('button-clicked', $event)"
39
+ >{{ buttonLabel }}</ds-button
40
+ >
41
+ </slot>
42
+ </div>
43
+ </div>
44
+ </template>
45
+
46
+ <style lang="scss" scoped>
47
+ @import '../../../styles/settings/spacings';
48
+
49
+ .ds-callout {
50
+ $self: &;
51
+
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: $space-8;
55
+ width: 100%;
56
+
57
+ &.-ds-layout-horizontal:not(.-ds-actions-vertical) {
58
+ align-items: center;
59
+ flex-direction: row;
60
+ }
61
+
62
+ &__content {
63
+ display: flex;
64
+
65
+ #{$self}.-ds-layout-vertical & {
66
+ align-items: center;
67
+ flex-direction: column;
68
+ gap: $space-4;
69
+ }
70
+
71
+ #{$self}.-ds-layout-horizontal & {
72
+ align-items: center;
73
+ flex: 1 1 auto;
74
+ gap: $space-6;
75
+ min-width: 0;
76
+ }
77
+ }
78
+
79
+ &__actions {
80
+ display: flex;
81
+ gap: $space-8;
82
+
83
+ #{$self}.-ds-layout-vertical & {
84
+ justify-content: center;
85
+ width: 100%;
86
+ }
87
+
88
+ #{$self}.-ds-actions-vertical & {
89
+ align-items: stretch;
90
+ flex-direction: column;
91
+ width: 100%;
92
+ }
93
+ }
94
+ }
95
+ </style>
96
+
97
+ <script setup lang="ts">
98
+ import { computed } from 'vue';
99
+ import DsFeatureIcon from '../Icons/FeatureIcon';
100
+ import { FEATURE_ICON_SIZES } from '../Icons/FeatureIcon/FeatureIcon.consts';
101
+ import DsTextGroup from '../TextGroup/TextGroup.vue';
102
+ import { TEXT_GROUP_ALIGNS, TEXT_GROUP_SIZES } from '../TextGroup/TextGroup.consts';
103
+ import DsButton, { BUTTON_RADIUSES } from '../Buttons/Button';
104
+ import { BUTTON_COLORS, BUTTON_SIZES, BUTTON_TYPES } from '../Buttons/Button/Button.consts';
105
+ import { IconItem } from '../Icons/Icon';
106
+ import {
107
+ CALLOUT_ICON_COLORS,
108
+ CALLOUT_LAYOUTS,
109
+ CALLOUT_MAIN_TEXT_COLORS,
110
+ CALLOUT_SIZES,
111
+ CalloutIconColor,
112
+ CalloutLayout,
113
+ CalloutMainTextColor,
114
+ CalloutSize,
115
+ } from './Callout.consts';
116
+
117
+ const {
118
+ layout = CALLOUT_LAYOUTS.VERTICAL,
119
+ size = CALLOUT_SIZES.MEDIUM,
120
+ iconColor = CALLOUT_ICON_COLORS.PRIMARY,
121
+ eyebrowText = null,
122
+ mainText = null,
123
+ supportingText = null,
124
+ mainTextColor = CALLOUT_MAIN_TEXT_COLORS.PRIMARY,
125
+ buttonLabel = null,
126
+ buttonIcon = null,
127
+ isActionVertical = false,
128
+ } = defineProps<{
129
+ icon: IconItem;
130
+ layout?: CalloutLayout;
131
+ size?: CalloutSize;
132
+ iconColor?: CalloutIconColor;
133
+ eyebrowText?: string | null;
134
+ mainText?: string | null;
135
+ supportingText?: string | null;
136
+ mainTextColor?: CalloutMainTextColor;
137
+ buttonLabel?: string | null;
138
+ buttonIcon?: IconItem | null;
139
+ isActionVertical?: boolean;
140
+ }>();
141
+
142
+ defineEmits<{
143
+ 'button-clicked': [e: Event];
144
+ }>();
145
+
146
+ defineSlots<{
147
+ actions?: () => any;
148
+ }>();
149
+
150
+ const featureIconSize = computed(() =>
151
+ size === CALLOUT_SIZES.LARGE ? FEATURE_ICON_SIZES.LARGE : FEATURE_ICON_SIZES.MEDIUM,
152
+ );
153
+
154
+ const textGroupSize = computed(() =>
155
+ size === CALLOUT_SIZES.SMALL ? TEXT_GROUP_SIZES.MEDIUM : TEXT_GROUP_SIZES.LARGE,
156
+ );
157
+
158
+ const textGroupAlign = computed(() =>
159
+ layout === CALLOUT_LAYOUTS.VERTICAL ? TEXT_GROUP_ALIGNS.CENTER : TEXT_GROUP_ALIGNS.LEFT,
160
+ );
161
+ </script>
@@ -0,0 +1,4 @@
1
+ import Callout from './Callout.vue';
2
+
3
+ export default Callout;
4
+ export * from './Callout.consts';
@@ -102,7 +102,7 @@
102
102
  }
103
103
 
104
104
  &.-ds-elevationTop {
105
- box-shadow: $shadow-top-s;
105
+ box-shadow: $shadow-top-l;
106
106
  }
107
107
 
108
108
  &.-ds-elevationNone {
@@ -104,6 +104,21 @@ describe('CheckboxGroupField', () => {
104
104
  );
105
105
  });
106
106
 
107
+ it('sets aria-describedby only when a message is rendered', () => {
108
+ const wrapper = mount(CheckboxGroupField, {
109
+ props: {
110
+ label: 'Test Label',
111
+ },
112
+ slots: {
113
+ field: '<div>Content</div>',
114
+ },
115
+ });
116
+
117
+ expect(
118
+ wrapper.find('.ds-checkboxGroupField').attributes('aria-describedby'),
119
+ ).toBeUndefined();
120
+ });
121
+
107
122
  describe('vee-validate integration', () => {
108
123
  it('integrates with vee-validate form context and reflects initial values', async () => {
109
124
  // eslint-disable-next-line vue/one-component-per-file
@@ -5,7 +5,7 @@
5
5
  v-model="value"
6
6
  class="ds-checkboxGroupField"
7
7
  role="group"
8
- :aria-describedby="messageId"
8
+ :aria-describedby="hasMessage ? messageId : undefined"
9
9
  :aria-labelledby="labelId"
10
10
  :disabled="formFieldProps.state === FORM_FIELD_STATES.DISABLED"
11
11
  loop
@@ -64,7 +64,7 @@ const {
64
64
  ...rest
65
65
  } = defineProps<CheckboxGroupFieldProps>();
66
66
 
67
- defineSlots<CheckboxGroupFieldSlots>();
67
+ const slots = defineSlots<CheckboxGroupFieldSlots>();
68
68
 
69
69
  const modelValue = defineModel<Array<string>>({
70
70
  default: [],
@@ -77,6 +77,9 @@ const formFieldProps = computed<FormFieldProps>(() => {
77
77
  return extractFormFieldProps(rest, errors.value);
78
78
  });
79
79
 
80
+ // Avoids pointing `aria-describedby` at a message element that is never rendered.
81
+ const hasMessage = computed(() => !!(formFieldProps.value.messageText || slots.message));
82
+
80
83
  const mappedState = computed(() => {
81
84
  const stateMap: Record<FormFieldState, CheckboxState> = {
82
85
  [FORM_FIELD_STATES.DEFAULT]: CHECKBOX_STATES.DEFAULT,