tide-design-system 2.5.0 → 2.5.3
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/.storybook/main.ts +2 -0
- package/README.md +3 -1
- package/dist/css/reset.css +5 -1
- package/dist/css/utilities-base.css +6 -6
- package/dist/css/utilities-responsive.css +24 -24
- package/dist/css/variables.css +3 -0
- package/dist/style.css +1 -1
- package/dist/tide-design-system.cjs +2 -2
- package/dist/tide-design-system.esm.d.ts +51 -9
- package/dist/tide-design-system.esm.js +1621 -1481
- package/dist/utilities/validation.ts +1 -1
- package/docs/assets/full-bleed.gif +0 -0
- package/docs/assets/layout-grid-default.webp +0 -0
- package/docs/assets/layout-grid-fluid.webp +0 -0
- package/docs/assets/layout-grid.webp +0 -0
- package/docs/configuation.md +47 -0
- package/docs/grid-layout.md +83 -0
- package/index.ts +4 -0
- package/package.json +1 -1
- package/src/assets/css/reset.css +5 -1
- package/src/assets/css/utilities-base.css +6 -6
- package/src/assets/css/utilities-responsive.css +24 -24
- package/src/assets/css/variables.css +3 -0
- package/src/components/TideAlert.vue +1 -1
- package/src/components/TideCarousel.vue +104 -40
- package/src/components/TideInputSelect.vue +1 -1
- package/src/components/TideInputSelectDeprecated.vue +1 -1
- package/src/components/TideInputText.vue +2 -2
- package/src/components/TideInputTextDeprecated.vue +2 -2
- package/src/components/TideInputTextarea.vue +2 -2
- package/src/components/TideInputTextareaDeprecated.vue +2 -2
- package/src/components/TideLink.vue +6 -0
- package/src/components/TideMenuItem.vue +1 -1
- package/src/components/TideModal.vue +1 -1
- package/src/components/TideRating.vue +93 -0
- package/src/components/TideSheet.vue +1 -1
- package/src/components/TideTabs.vue +58 -0
- package/src/stories/TideCarousel.stories.ts +47 -25
- package/src/stories/TideRating.stories.ts +120 -0
- package/src/stories/TideTabs.stories.ts +115 -0
- package/src/types/Formatted.ts +1 -1
- package/src/utilities/validation.ts +1 -1
- package/tests/utilities-format.spec.ts +40 -0
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
import InternalBaseLink from '@/components/InternalBaseLink.vue';
|
|
5
5
|
import TideIcon from '@/components/TideIcon.vue';
|
|
6
6
|
import { ELEMENT } from '@/types/Element';
|
|
7
|
+
import { SIZE } from '@/types/Size';
|
|
7
8
|
import { CSS } from '@/types/Styles';
|
|
8
9
|
import { TARGET } from '@/types/Target';
|
|
9
10
|
|
|
10
11
|
import type { Element } from '@/types/Element';
|
|
11
12
|
import type { Icon } from '@/types/Icon';
|
|
13
|
+
import type { Size } from '@/types/Size';
|
|
12
14
|
|
|
13
15
|
type Props = {
|
|
14
16
|
element?: Element;
|
|
15
17
|
href?: string;
|
|
16
18
|
iconLeading?: Icon;
|
|
19
|
+
iconSize?: Size;
|
|
17
20
|
iconTrailing?: Icon;
|
|
18
21
|
isNewTab?: boolean;
|
|
19
22
|
label: string;
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
element: ELEMENT.LINK,
|
|
25
28
|
href: undefined,
|
|
26
29
|
iconLeading: undefined,
|
|
30
|
+
iconSize: SIZE.SMALL,
|
|
27
31
|
iconTrailing: undefined,
|
|
28
32
|
isNewTab: false,
|
|
29
33
|
label: undefined,
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
<TideIcon
|
|
50
54
|
:class="[CSS.DISPLAY.INLINE_BLOCK, CSS.ALIGN.Y.MIDDLE, CSS.MARGIN.RIGHT.QUARTER]"
|
|
51
55
|
:icon="props.iconLeading"
|
|
56
|
+
:size="iconSize"
|
|
52
57
|
v-if="props.iconLeading"
|
|
53
58
|
/>
|
|
54
59
|
|
|
@@ -59,6 +64,7 @@
|
|
|
59
64
|
<TideIcon
|
|
60
65
|
:class="[CSS.DISPLAY.INLINE_BLOCK, CSS.ALIGN.Y.MIDDLE, CSS.MARGIN.LEFT.QUARTER]"
|
|
61
66
|
:icon="props.iconTrailing"
|
|
67
|
+
:size="iconSize"
|
|
62
68
|
v-if="props.iconTrailing"
|
|
63
69
|
/>
|
|
64
70
|
</component>
|
|
@@ -236,7 +236,7 @@
|
|
|
236
236
|
padding-inline: var(--modal-padding-x);
|
|
237
237
|
}
|
|
238
238
|
.tide-modal-content {
|
|
239
|
-
grid-template-columns: var(--modal-padding-x)
|
|
239
|
+
grid-template-columns: var(--modal-padding-x) var(--tide-safe-fr) var(--modal-padding-x);
|
|
240
240
|
}
|
|
241
241
|
:where(.tide-modal-content):deep(> :where(*)) {
|
|
242
242
|
grid-column: 2;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { CSS } from '@/types/Styles';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
description?: string;
|
|
8
|
+
maxRating?: number;
|
|
9
|
+
showRating?: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
description: undefined,
|
|
15
|
+
maxRating: 10,
|
|
16
|
+
showRating: false,
|
|
17
|
+
title: undefined,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ratingValue = defineModel<number>({ required: true });
|
|
21
|
+
|
|
22
|
+
const segments = computed(() => Array.from({ length: props.maxRating }, (_, index) => index + 1));
|
|
23
|
+
|
|
24
|
+
const handleClick = (segment: number) => {
|
|
25
|
+
ratingValue.value = segment;
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div :class="['tide-rating', CSS.WIDTH.FULL]">
|
|
31
|
+
<section :class="[CSS.DISPLAY.FLEX, CSS.AXIS2.CENTER, CSS.AXIS1.BETWEEN, CSS.PADDING.BOTTOM.HALF]">
|
|
32
|
+
<div :class="[CSS.FONT.ROLE.LABEL_2]">
|
|
33
|
+
{{ props.title }}
|
|
34
|
+
</div>
|
|
35
|
+
<div
|
|
36
|
+
:class="[CSS.FONT.ROLE.LABEL_3, CSS.FONT.COLOR.SURFACE.VARIANT]"
|
|
37
|
+
v-if="props.showRating"
|
|
38
|
+
>
|
|
39
|
+
{{ ratingValue }}/{{ props.maxRating }}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
<section
|
|
43
|
+
:class="[CSS.FONT.ROLE.BODY_2, CSS.PADDING.BOTTOM.ONE, CSS.OVERFLOW.XY.HIDDEN, CSS.ELLIPSIS]"
|
|
44
|
+
v-if="props.description"
|
|
45
|
+
>
|
|
46
|
+
{{ props.description }}
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section :class="['tide-rating-bar', CSS.DISPLAY.FLEX, CSS.GAP.QUARTER, CSS.FLEX.GROW.ON, CSS.FLEX.SHRINK.ON]">
|
|
50
|
+
<button
|
|
51
|
+
:class="[
|
|
52
|
+
'tide-rating-segment',
|
|
53
|
+
CSS.BG.SURFACE.VARIANT,
|
|
54
|
+
CSS.POSITION.RELATIVE,
|
|
55
|
+
CSS.OVERFLOW.XY.HIDDEN,
|
|
56
|
+
CSS.FLEX.GROW.ON,
|
|
57
|
+
CSS.FLEX.SHRINK.ON,
|
|
58
|
+
CSS.FLEX.BASIS.ZERO,
|
|
59
|
+
CSS.BORDER.FULL.ZERO,
|
|
60
|
+
segment === 1 && ['tide-rating-segment-first'],
|
|
61
|
+
segment === segments.length && ['tide-rating-segment-last'],
|
|
62
|
+
segment <= ratingValue && [CSS.BG.SURFACE.GRADIENT],
|
|
63
|
+
]"
|
|
64
|
+
:key="segment"
|
|
65
|
+
@click="handleClick(segment)"
|
|
66
|
+
type="button"
|
|
67
|
+
v-for="segment in segments"
|
|
68
|
+
/>
|
|
69
|
+
</section>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<style scoped>
|
|
74
|
+
.tide-rating-bar {
|
|
75
|
+
height: 28px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.tide-rating-segment {
|
|
79
|
+
clip-path: polygon(12% 0, 100% 0, 88% 100%, 0 100%);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.tide-rating-segment-first {
|
|
83
|
+
border-top-left-radius: 9999px;
|
|
84
|
+
border-bottom-left-radius: 9999px;
|
|
85
|
+
clip-path: polygon(0 0, 100% 0, 88% 100%, 0 100%);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.tide-rating-segment-last {
|
|
89
|
+
border-top-right-radius: 9999px;
|
|
90
|
+
border-bottom-right-radius: 9999px;
|
|
91
|
+
clip-path: polygon(12% 0, 100% 0, 100% 100%, 0 100%);
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
.tide-sheet-content {
|
|
173
|
-
grid-template-columns: var(--sheet-padding-x)
|
|
173
|
+
grid-template-columns: var(--sheet-padding-x) var(--tide-safe-fr) var(--sheet-padding-x);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
:where(.tide-sheet-content):deep(> :where(*)) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import TideCarousel from '@/components/TideCarousel.vue';
|
|
3
|
+
import TideLink from '@/components/TideLink.vue';
|
|
4
|
+
import { ELEMENT } from '@/types/Element';
|
|
5
|
+
import { CSS } from '@/types/Styles';
|
|
6
|
+
|
|
7
|
+
import type { Tab } from '@/types/Tab';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
tabs: Tab[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
defineProps<Props>();
|
|
14
|
+
|
|
15
|
+
const currentTab = defineModel<number>({ required: true });
|
|
16
|
+
|
|
17
|
+
const handleClick = (index: number) => {
|
|
18
|
+
currentTab.value = index;
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div :class="['tide-tabs', CSS.DISPLAY.FLEX, CSS.BORDER.BOTTOM.TWO, CSS.BORDER.COLOR.LOW]">
|
|
24
|
+
<TideCarousel :is-floating="true">
|
|
25
|
+
<li
|
|
26
|
+
:key="tab.label"
|
|
27
|
+
v-for="(tab, index) in tabs"
|
|
28
|
+
>
|
|
29
|
+
<TideLink
|
|
30
|
+
:class="[
|
|
31
|
+
index === currentTab ? CSS.FONT.COLOR.SURFACE.BRAND : CSS.FONT.COLOR.SURFACE.VARIANT,
|
|
32
|
+
CSS.PADDING.BOTTOM.ONE,
|
|
33
|
+
CSS.FONT.ROLE.LABEL_1,
|
|
34
|
+
CSS.WHITESPACE_WRAP.OFF,
|
|
35
|
+
]"
|
|
36
|
+
:element="ELEMENT.BUTTON"
|
|
37
|
+
:label="tab.label"
|
|
38
|
+
:subtle="true"
|
|
39
|
+
@click="handleClick(index)"
|
|
40
|
+
/>
|
|
41
|
+
<div :class="['rounded-border', index === currentTab && 'active']" />
|
|
42
|
+
</li>
|
|
43
|
+
</TideCarousel>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<style scoped>
|
|
48
|
+
.rounded-border {
|
|
49
|
+
border-radius: var(--tide-radius-full) var(--tide-radius-full) 0 0;
|
|
50
|
+
border-top-width: 4px;
|
|
51
|
+
border-top-style: solid;
|
|
52
|
+
border-color: transparent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.active {
|
|
56
|
+
border-color: var(--tide-on-surface-brand);
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { action } from '@storybook/addon-actions';
|
|
2
|
+
import { computed } from 'vue';
|
|
2
3
|
|
|
3
4
|
import TideCard from '@/components/TideCard.vue';
|
|
4
5
|
import TideCarousel from '@/components/TideCarousel.vue';
|
|
@@ -8,6 +9,8 @@ import type { StoryContext } from '@storybook/vue3';
|
|
|
8
9
|
|
|
9
10
|
type Args = InstanceType<typeof TideCarousel>['$props'] & {
|
|
10
11
|
handleSlidesAddedToView: string;
|
|
12
|
+
isFullWidthCards: boolean | undefined;
|
|
13
|
+
maxDots: string | undefined;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
const formatSnippet = (code: string, context: StoryContext) => {
|
|
@@ -19,7 +22,6 @@ const formatSnippet = (code: string, context: StoryContext) => {
|
|
|
19
22
|
if (args.isFloating !== undefined) argsWithValues.push(`:is-floating="${args.isFloating}"`);
|
|
20
23
|
if (args.isHideawayButtons !== undefined) argsWithValues.push(`:is-hideaway-buttons="${args.isHideawayButtons}"`);
|
|
21
24
|
if (args.isScrollByPage !== undefined) argsWithValues.push(`:is-scroll-by-page="false"`);
|
|
22
|
-
if (args.isTouchscreen !== undefined) argsWithValues.push(`:is-touchscreen="${args.isTouchscreen}"`);
|
|
23
25
|
if (args.maxDots !== '') argsWithValues.push(`max-dots="${args.maxDots}"`);
|
|
24
26
|
if (args.subtitle !== '') argsWithValues.push(`subtitle="${args.subtitle}"`);
|
|
25
27
|
if (args.title !== '') argsWithValues.push(`title="${args.title}"`);
|
|
@@ -31,7 +33,7 @@ const formatSnippet = (code: string, context: StoryContext) => {
|
|
|
31
33
|
// prettier-ignore
|
|
32
34
|
`<TideCarousel ${argsWithValues.join(' ')}>` + lineBreak +
|
|
33
35
|
slotContentMisc + tab +
|
|
34
|
-
`<li class="tide-shrink-none" v-for="(_child, index) in new Array(12)">` + lineBreak + tab + tab +
|
|
36
|
+
`<li class="tide-shrink-none${args.isFullWidthCards ? ' tide-width-full' : ''}" v-for="(_child, index) in new Array(12)">` + lineBreak + tab + tab +
|
|
35
37
|
args.default + lineBreak + tab +
|
|
36
38
|
`</li>` + lineBreak +
|
|
37
39
|
`</TideCarousel>`
|
|
@@ -66,19 +68,31 @@ const render = (args: Args) => ({
|
|
|
66
68
|
}
|
|
67
69
|
},
|
|
68
70
|
},
|
|
69
|
-
setup: () =>
|
|
71
|
+
setup: () => {
|
|
72
|
+
const conditionalFullWidth = computed(() => (args.isFullWidthCards ? ' tide-width-full' : ''));
|
|
73
|
+
const argsFormatted = computed(() => ({
|
|
74
|
+
...args,
|
|
75
|
+
maxDots: args.maxDots === '' ? undefined : args.maxDots,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
argsFormatted,
|
|
80
|
+
conditionalFullWidth,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
70
83
|
// prettier-ignore
|
|
71
84
|
template:
|
|
72
85
|
`<TideCarousel
|
|
73
86
|
@slides-added-to-view="handleSlidesAddedToView"
|
|
74
|
-
v-bind="
|
|
87
|
+
v-bind="argsFormatted"
|
|
88
|
+
:key="argsFormatted.isFullWidthCards"
|
|
75
89
|
>
|
|
76
|
-
<template #misc>{{
|
|
90
|
+
<template #misc>{{ argsFormatted.misc }}</template>
|
|
77
91
|
<li
|
|
78
|
-
class="tide-shrink-none tide-border-1 sb-border-blue tide-padding-1 sb-bg-blue-light"
|
|
92
|
+
:class="['tide-shrink-none tide-border-1 sb-border-blue tide-padding-1 sb-bg-blue-light', conditionalFullWidth]"
|
|
79
93
|
v-for="(_child, index) in new Array(12)"
|
|
80
94
|
>
|
|
81
|
-
{{
|
|
95
|
+
{{ argsFormatted.default.replace('#', index) }}
|
|
82
96
|
</li>
|
|
83
97
|
</TideCarousel>`,
|
|
84
98
|
});
|
|
@@ -106,7 +120,7 @@ export default {
|
|
|
106
120
|
},
|
|
107
121
|
hasDots: {
|
|
108
122
|
...argTypeBooleanUnrequired,
|
|
109
|
-
description: 'Determines whether to display the indicator dots overlay',
|
|
123
|
+
description: 'Determines whether to display the indicator dots overlay<br />(Only valid with full width cards)',
|
|
110
124
|
table: {
|
|
111
125
|
defaultValue: { summary: 'False' },
|
|
112
126
|
},
|
|
@@ -118,6 +132,16 @@ export default {
|
|
|
118
132
|
defaultValue: { summary: 'False' },
|
|
119
133
|
},
|
|
120
134
|
},
|
|
135
|
+
isFullWidthCards: {
|
|
136
|
+
...argTypeBooleanUnrequired,
|
|
137
|
+
description: 'Preview 1 card per page implementation',
|
|
138
|
+
table: {
|
|
139
|
+
category: 'Demo',
|
|
140
|
+
defaultValue: { summary: 'None' },
|
|
141
|
+
disable: true,
|
|
142
|
+
type: { summary: 'boolean' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
121
145
|
isHeadline1: {
|
|
122
146
|
...argTypeBooleanUnrequired,
|
|
123
147
|
description: 'Determines font role used for title display',
|
|
@@ -133,23 +157,11 @@ export default {
|
|
|
133
157
|
defaultValue: { summary: 'True' },
|
|
134
158
|
},
|
|
135
159
|
},
|
|
136
|
-
isScrollByPage:
|
|
137
|
-
...argTypeBooleanUnrequired,
|
|
138
|
-
description: 'Determines pagination method',
|
|
139
|
-
table: {
|
|
140
|
-
defaultValue: { summary: 'None' },
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
isTouchscreen: {
|
|
144
|
-
...argTypeBooleanUnrequired,
|
|
145
|
-
description: 'Determines button and/or swipe control scheme',
|
|
146
|
-
table: {
|
|
147
|
-
defaultValue: { summary: 'None' },
|
|
148
|
-
},
|
|
149
|
-
},
|
|
160
|
+
isScrollByPage: disabledArgType,
|
|
150
161
|
maxDots: {
|
|
151
162
|
control: 'text',
|
|
152
163
|
description: 'Determines the max number of indicator dots to display at a given time',
|
|
164
|
+
if: { arg: 'hasDots', eq: true },
|
|
153
165
|
table: {
|
|
154
166
|
defaultValue: { summary: 'None' },
|
|
155
167
|
type: { summary: 'number' },
|
|
@@ -186,9 +198,8 @@ export default {
|
|
|
186
198
|
handleSlidesAddedToView: 'doSomething',
|
|
187
199
|
hasDots: undefined,
|
|
188
200
|
isFloating: undefined,
|
|
201
|
+
isFullWidthCards: undefined,
|
|
189
202
|
isHideawayButtons: undefined,
|
|
190
|
-
isScrollByPage: undefined,
|
|
191
|
-
isTouchscreen: undefined,
|
|
192
203
|
maxDots: '',
|
|
193
204
|
misc: '',
|
|
194
205
|
subtitle: '',
|
|
@@ -201,4 +212,15 @@ export default {
|
|
|
201
212
|
title: 'Components/TideCarousel',
|
|
202
213
|
};
|
|
203
214
|
|
|
204
|
-
export const Demo = {
|
|
215
|
+
export const Demo = {
|
|
216
|
+
name: 'Default',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const FullWidth = {
|
|
220
|
+
args: {
|
|
221
|
+
default: 'Full-width card #',
|
|
222
|
+
isFullWidthCards: true,
|
|
223
|
+
title: 'Demo',
|
|
224
|
+
},
|
|
225
|
+
name: 'Full-width cards ',
|
|
226
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions';
|
|
2
|
+
import { ref, watchEffect } from 'vue';
|
|
3
|
+
|
|
4
|
+
import TideRating from '@/components/TideRating.vue';
|
|
5
|
+
import { argTypeBooleanUnrequired, change, doSomething, parameters } from '@/utilities/storybook';
|
|
6
|
+
|
|
7
|
+
import type { StoryContext } from '@storybook/vue3';
|
|
8
|
+
|
|
9
|
+
type Args = InstanceType<typeof TideRating>['$props'] & {
|
|
10
|
+
handleChange: string;
|
|
11
|
+
vModel: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const maxRating = 10;
|
|
15
|
+
|
|
16
|
+
const render = (args: Args, context: StoryContext) => ({
|
|
17
|
+
components: { TideRating },
|
|
18
|
+
methods: {
|
|
19
|
+
doSomething,
|
|
20
|
+
handleChange: (index: number) => {
|
|
21
|
+
action('Current rating')(index);
|
|
22
|
+
context.updateArgs({ ...args, vModel: index });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const callback = eval(args.handleChange);
|
|
26
|
+
|
|
27
|
+
if (callback) {
|
|
28
|
+
callback();
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
alert('Please specify a valid handler in the "change" control.');
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
setup: () => {
|
|
36
|
+
const ratingValue = ref<number>(args.vModel);
|
|
37
|
+
|
|
38
|
+
watchEffect(() => {
|
|
39
|
+
ratingValue.value = args.vModel;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
args,
|
|
44
|
+
ratingValue,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
template: '<TideRating v-model="ratingValue" @update:modelValue="handleChange" v-bind="args" />',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
argTypes: {
|
|
52
|
+
description: {
|
|
53
|
+
control: 'text',
|
|
54
|
+
description: 'Determines the text displayed below the title',
|
|
55
|
+
table: {
|
|
56
|
+
defaultValue: { summary: 'None' },
|
|
57
|
+
type: { summary: 'string' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
handleChange: {
|
|
61
|
+
...change,
|
|
62
|
+
control: 'text',
|
|
63
|
+
description: 'JS code or function to execute on change event',
|
|
64
|
+
name: 'update:modelValue',
|
|
65
|
+
table: {
|
|
66
|
+
category: 'Events',
|
|
67
|
+
defaultValue: { summary: 'None' },
|
|
68
|
+
type: { summary: '() => void' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
maxRating: {
|
|
72
|
+
control: 'text',
|
|
73
|
+
description: 'Maximum rating value / Number of segments',
|
|
74
|
+
table: {
|
|
75
|
+
defaultValue: { summary: 10 },
|
|
76
|
+
type: { summary: 'number' },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
showRating: {
|
|
80
|
+
...argTypeBooleanUnrequired,
|
|
81
|
+
description: 'Determines whether to display the current rating value',
|
|
82
|
+
},
|
|
83
|
+
title: {
|
|
84
|
+
control: 'text',
|
|
85
|
+
description: 'Determines the title text displayed above the rating component',
|
|
86
|
+
table: {
|
|
87
|
+
defaultValue: { summary: 'None' },
|
|
88
|
+
type: { summary: 'string' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
vModel: {
|
|
92
|
+
control: {
|
|
93
|
+
type: 'select',
|
|
94
|
+
},
|
|
95
|
+
description: 'Data binding to Vue ref (selected rating index)',
|
|
96
|
+
isDynamic: true,
|
|
97
|
+
options: Array.from({ length: maxRating + 1 }, (_, i) => i),
|
|
98
|
+
table: {
|
|
99
|
+
category: 'Native',
|
|
100
|
+
defaultValue: { summary: 'None' },
|
|
101
|
+
type: { summary: 'Ref<number>' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
args: {
|
|
106
|
+
description: 'Description',
|
|
107
|
+
handleChange: 'doSomething',
|
|
108
|
+
maxRating: '10',
|
|
109
|
+
showRating: undefined,
|
|
110
|
+
title: 'Title',
|
|
111
|
+
vModel: 0,
|
|
112
|
+
},
|
|
113
|
+
component: TideRating,
|
|
114
|
+
parameters,
|
|
115
|
+
render,
|
|
116
|
+
tags: ['autodocs'],
|
|
117
|
+
title: 'Components/TideRating',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const Demo = {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions';
|
|
2
|
+
import { ref, watchEffect } from 'vue';
|
|
3
|
+
|
|
4
|
+
import TideTabs from '@/components/TideTabs.vue';
|
|
5
|
+
import { change, disabledArgType, doSomething, parameters } from '@/utilities/storybook';
|
|
6
|
+
|
|
7
|
+
import type { Tab } from '@/types/Tab';
|
|
8
|
+
import type { StoryContext } from '@storybook/vue3';
|
|
9
|
+
|
|
10
|
+
type Args = InstanceType<typeof TideTabs>['$props'] & {
|
|
11
|
+
handleChange: string;
|
|
12
|
+
vModel: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const tabs: Tab[] = [
|
|
16
|
+
{
|
|
17
|
+
dataTrack: 'Tab 0 Click',
|
|
18
|
+
label: 'First Tab',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
dataTrack: 'Tab 1 Click',
|
|
22
|
+
label: 'Second Tab',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
dataTrack: 'Tab 2 Click',
|
|
26
|
+
label: 'Third Tab',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
dataTrack: 'Tab 3 Click',
|
|
30
|
+
label: 'Fourth Tab',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const render = (args: Args, context: StoryContext) => ({
|
|
35
|
+
components: { TideTabs },
|
|
36
|
+
methods: {
|
|
37
|
+
doSomething,
|
|
38
|
+
handleChange: (index: number) => {
|
|
39
|
+
action('Current tab')(index);
|
|
40
|
+
context.updateArgs({ ...args, vModel: index });
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const callback = eval(args.handleChange);
|
|
44
|
+
|
|
45
|
+
if (callback) {
|
|
46
|
+
callback();
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
alert('Please specify a valid handler in the "change" control.');
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
setup: () => {
|
|
54
|
+
const currentTab = ref<number>(args.vModel);
|
|
55
|
+
|
|
56
|
+
watchEffect(() => {
|
|
57
|
+
currentTab.value = args.vModel;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
args,
|
|
62
|
+
currentTab,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
template: `<TideTabs @update:modelValue="handleChange" v-bind="args" v-model="currentTab" />`,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export default {
|
|
69
|
+
argTypes: {
|
|
70
|
+
change: disabledArgType,
|
|
71
|
+
handleChange: {
|
|
72
|
+
...change,
|
|
73
|
+
name: 'update:modelValue',
|
|
74
|
+
table: {
|
|
75
|
+
category: 'Events',
|
|
76
|
+
defaultValue: { summary: 'None' },
|
|
77
|
+
type: { summary: '(tabIndex: number) => void' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
tabs: {
|
|
81
|
+
control: 'object',
|
|
82
|
+
description: 'Sets labels and callback for each tab',
|
|
83
|
+
isCustom: true,
|
|
84
|
+
table: {
|
|
85
|
+
defaultValue: { summary: 'None' },
|
|
86
|
+
type: { summary: 'Tab[]' },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
vModel: {
|
|
90
|
+
control: {
|
|
91
|
+
type: 'select',
|
|
92
|
+
},
|
|
93
|
+
description: 'Data binding to Vue ref (active tab index)',
|
|
94
|
+
isDynamic: true,
|
|
95
|
+
options: Object.keys(tabs).map((index) => parseInt(index)),
|
|
96
|
+
table: {
|
|
97
|
+
category: 'Native',
|
|
98
|
+
defaultValue: { summary: 'None' },
|
|
99
|
+
type: { summary: 'Ref<number>' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
args: {
|
|
104
|
+
handleChange: 'doSomething',
|
|
105
|
+
tabs,
|
|
106
|
+
vModel: 0,
|
|
107
|
+
},
|
|
108
|
+
component: TideTabs,
|
|
109
|
+
parameters,
|
|
110
|
+
render,
|
|
111
|
+
tags: ['autodocs'],
|
|
112
|
+
title: 'Components/TideTabs',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const Demo = {};
|
package/src/types/Formatted.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const FORMAT_REGEX = {
|
|
|
15
15
|
alphaNumberOrEmpty: /^$|^[a-z0-9]+$/i,
|
|
16
16
|
alphaSpace: /^[a-zA-Z ]+$/g,
|
|
17
17
|
email:
|
|
18
|
-
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-
|
|
18
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?!(?:[a-zA-Z0-9-]+\.)*(?<tld>[a-zA-Z]{2,})\.\k<tld>$)((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}))$/,
|
|
19
19
|
numberFormatted: /^$|(?=.)^\$?(([1-9][0-9]{0,2}(,[0-9]{3})*)|0)?(\.[0-9]{1,2})?$/,
|
|
20
20
|
phone: /^(?:\d{3}-\d{3}-\d{4}|\d{1}-\d{3}-\d{3}-\d{4})?$/,
|
|
21
21
|
price: /(?=.)^\$?(([1-9][0-9]{0,2}(,[0-9]{3})*)|0)?(\.[0-9]{1,2})?$/,
|
|
@@ -62,7 +62,7 @@ export const getFieldValidationResult = ({
|
|
|
62
62
|
|
|
63
63
|
// custom validator prop errors from have second highest precedence
|
|
64
64
|
if (validators) {
|
|
65
|
-
const validation = validateProperty(value.value, validators);
|
|
65
|
+
const validation = validateProperty(value.value ?? '', validators);
|
|
66
66
|
|
|
67
67
|
if (!validation.valid) {
|
|
68
68
|
return validation;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { FORMAT_REGEX } from '../src/types/Formatted';
|
|
3
4
|
import {
|
|
4
5
|
formatCamelCase,
|
|
5
6
|
formatKebabCase,
|
|
@@ -428,3 +429,42 @@ describe('@/src/utilities/format.ts', () => {
|
|
|
428
429
|
});
|
|
429
430
|
});
|
|
430
431
|
});
|
|
432
|
+
|
|
433
|
+
describe('@/src/types/Formatted.ts', () => {
|
|
434
|
+
describe('FORMAT_REGEX.email', () => {
|
|
435
|
+
it('accepts a standard email address.', () => {
|
|
436
|
+
const input = 'john.doe@gmail.com';
|
|
437
|
+
const output = true;
|
|
438
|
+
|
|
439
|
+
expect(FORMAT_REGEX.email.test(input)).toEqual(output);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('accepts an email address with subdomains.', () => {
|
|
443
|
+
const input = 'john.doe@mail.sub.example.co.uk';
|
|
444
|
+
const output = true;
|
|
445
|
+
|
|
446
|
+
expect(FORMAT_REGEX.email.test(input)).toEqual(output);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('rejects an email address that ends with a repeated TLD label (e.g. .com.com).', () => {
|
|
450
|
+
const input = 'john@gmail.com.com';
|
|
451
|
+
const output = false;
|
|
452
|
+
|
|
453
|
+
expect(FORMAT_REGEX.email.test(input)).toEqual(output);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('rejects an email address that ends with a repeated TLD label (e.g. .net.net).', () => {
|
|
457
|
+
const input = 'john@company.net.net';
|
|
458
|
+
const output = false;
|
|
459
|
+
|
|
460
|
+
expect(FORMAT_REGEX.email.test(input)).toEqual(output);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('rejects an email address missing a valid top-level domain.', () => {
|
|
464
|
+
const input = 'john@gmail';
|
|
465
|
+
const output = false;
|
|
466
|
+
|
|
467
|
+
expect(FORMAT_REGEX.email.test(input)).toEqual(output);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
});
|