tide-design-system 2.1.7 → 2.1.8

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 (37) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/dist/css/realm/aero.css +3 -3
  3. package/dist/css/realm/boatmart.css +1 -1
  4. package/dist/css/realm/cycle.css +1 -1
  5. package/dist/css/realm/equipment.css +1 -1
  6. package/dist/css/realm/pwc.css +1 -1
  7. package/dist/css/reset.css +7 -0
  8. package/dist/style.css +1 -1
  9. package/dist/tide-design-system.cjs +2 -2
  10. package/dist/tide-design-system.esm.d.ts +40 -22
  11. package/dist/tide-design-system.esm.js +787 -785
  12. package/dist/utilities/event.ts +4 -0
  13. package/dist/utilities/storybook.ts +4 -0
  14. package/dist/utilities/viewport.ts +44 -0
  15. package/index.ts +2 -4
  16. package/package.json +4 -1
  17. package/src/assets/css/realm/aero.css +3 -3
  18. package/src/assets/css/realm/boatmart.css +1 -1
  19. package/src/assets/css/realm/cycle.css +1 -1
  20. package/src/assets/css/realm/equipment.css +1 -1
  21. package/src/assets/css/realm/pwc.css +1 -1
  22. package/src/assets/css/reset.css +7 -0
  23. package/src/components/TideCard.vue +3 -7
  24. package/src/components/TideModal.vue +164 -132
  25. package/src/components/TidePopover.vue +167 -0
  26. package/src/stories/TideAccordionItem.stories.ts +1 -0
  27. package/src/stories/TideButtonSegmented.stories.ts +1 -0
  28. package/src/stories/TideCard.stories.ts +1 -11
  29. package/src/stories/TideCarousel.stories.ts +1 -0
  30. package/src/stories/TideModal.stories.ts +68 -6
  31. package/src/stories/TidePagination.stories.ts +1 -0
  32. package/src/stories/TidePopover.stories.ts +98 -0
  33. package/src/stories/TideSwitch.stories.ts +1 -0
  34. package/src/types/Card.ts +0 -7
  35. package/src/utilities/event.ts +4 -0
  36. package/src/utilities/storybook.ts +4 -0
  37. package/src/utilities/viewport.ts +44 -0
@@ -0,0 +1,167 @@
1
+ <script lang="ts" setup>
2
+ import { autoPlacement, autoUpdate, offset as offsetMiddleware, shift, useFloating } from '@floating-ui/vue';
3
+ import { computed, onBeforeMount, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue';
4
+
5
+ import { CSS } from '@/types/Styles';
6
+ import { isClickOutside } from '@/utilities/event';
7
+ import { TOP_LAYER_ID, initFauxTopLayer } from '@/utilities/viewport';
8
+
9
+ import type { Ref } from 'vue';
10
+
11
+ type Props = {
12
+ anchorId: string;
13
+ offset?: number;
14
+ };
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ offset: 16,
18
+ });
19
+
20
+ const anchor: Ref<HTMLElement | null> = ref(null);
21
+ const floating: Ref<HTMLElement | null> = ref(null);
22
+ const root: Ref<HTMLElement | null> = ref(null);
23
+ const isHovered = ref(false);
24
+ const isToggledOpen = ref(false);
25
+ const middleware = ref([autoPlacement(), offsetMiddleware({ mainAxis: props.offset }), shift({ padding: 16 })]);
26
+
27
+ const isShowPopover = computed(() => isHovered.value || isToggledOpen.value);
28
+
29
+ const { floatingStyles } = useFloating(anchor, floating, {
30
+ middleware,
31
+ strategy: 'fixed',
32
+ whileElementsMounted: autoUpdate,
33
+ });
34
+
35
+ const handlePermanentOpenBodyClick = (e: MouseEvent) => {
36
+ if (!anchor.value || !floating.value) return;
37
+ if (isClickOutside(e, [anchor.value, floating.value])) {
38
+ e.stopImmediatePropagation();
39
+ isToggledOpen.value = false;
40
+ }
41
+ };
42
+
43
+ const handleAnchorElementMouseOver = () => {
44
+ isHovered.value = true;
45
+ };
46
+
47
+ const handleAnchorElementMouseLeave = () => {
48
+ isHovered.value = false;
49
+ };
50
+
51
+ const handleAnchorElementClick = () => {
52
+ isToggledOpen.value = true;
53
+ };
54
+
55
+ const addListenersToAnchorElement = () => {
56
+ if (!anchor.value) return null;
57
+ anchor.value.addEventListener('mouseover', handleAnchorElementMouseOver);
58
+ anchor.value.addEventListener('mouseleave', handleAnchorElementMouseLeave);
59
+ anchor.value.addEventListener('click', handleAnchorElementClick);
60
+ };
61
+
62
+ const removeListenersFromAnchorElement = () => {
63
+ if (!anchor.value) return null;
64
+ anchor.value.removeEventListener('mouseover', handleAnchorElementMouseOver);
65
+ anchor.value.removeEventListener('mouseleave', handleAnchorElementMouseLeave);
66
+ anchor.value.removeEventListener('click', handleAnchorElementClick);
67
+ };
68
+
69
+ const handlePermanentOpenBodyKeydown = (e: KeyboardEvent) => {
70
+ if (e.key === 'Escape') {
71
+ isToggledOpen.value = false;
72
+ }
73
+ };
74
+
75
+ const addOpenListenersToRoot = () => {
76
+ if (!root.value) return;
77
+ root.value.addEventListener('click', handlePermanentOpenBodyClick, true);
78
+ root.value.addEventListener('keydown', handlePermanentOpenBodyKeydown);
79
+ };
80
+
81
+ const removeOpenListenersFromRoot = () => {
82
+ if (!root.value) return;
83
+ root.value.removeEventListener('click', handlePermanentOpenBodyClick, true);
84
+ root.value.removeEventListener('keydown', handlePermanentOpenBodyKeydown);
85
+ };
86
+
87
+ const updateAnchorElement = () => {
88
+ anchor.value = document.getElementById(props.anchorId);
89
+ };
90
+
91
+ watch(
92
+ () => isToggledOpen.value,
93
+ (newValue) => {
94
+ if (newValue) {
95
+ addOpenListenersToRoot();
96
+ } else {
97
+ removeOpenListenersFromRoot();
98
+ }
99
+ }
100
+ );
101
+
102
+ watch(
103
+ () => props.anchorId,
104
+ () => {
105
+ removeListenersFromAnchorElement();
106
+ updateAnchorElement();
107
+ addListenersToAnchorElement();
108
+ }
109
+ );
110
+
111
+ watchEffect(() => {
112
+ middleware.value = [autoPlacement(), offsetMiddleware({ mainAxis: props.offset }), shift({ padding: 16 })];
113
+ });
114
+
115
+ onBeforeMount(() => {
116
+ initFauxTopLayer();
117
+ });
118
+
119
+ onMounted(() => {
120
+ updateAnchorElement();
121
+ root.value = document.documentElement;
122
+ addListenersToAnchorElement();
123
+ });
124
+
125
+ onUnmounted(() => {
126
+ removeListenersFromAnchorElement();
127
+ });
128
+ </script>
129
+
130
+ <template>
131
+ <Teleport :to="`#${TOP_LAYER_ID}`">
132
+ <Transition>
133
+ <div
134
+ :class="[
135
+ 'tide-popover',
136
+ CSS.BG.SURFACE.DEFAULT,
137
+ CSS.BORDER.COLOR.LOW,
138
+ CSS.BORDER.FULL.ONE,
139
+ CSS.BORDER.RADIUS.HALF,
140
+ CSS.FONT.ROLE.BODY_2,
141
+ CSS.PADDING.FULL.ONE,
142
+ CSS.SHADOW.BOTTOM,
143
+ ]"
144
+ ref="floating"
145
+ :style="{ ...floatingStyles, maxWidth: `calc(100% - ${props.offset * 2}px)` }"
146
+ v-show="isShowPopover"
147
+ >
148
+ <slot />
149
+ </div>
150
+ </Transition>
151
+ </Teleport>
152
+ </template>
153
+
154
+ <style scoped>
155
+ .v-enter-from,
156
+ .v-leave-to {
157
+ opacity: 0;
158
+ }
159
+ .v-enter-active,
160
+ .v-leave-active {
161
+ transition: opacity var(--tide-animate);
162
+ }
163
+ .v-enter-to,
164
+ .v-leave-from {
165
+ opacity: 1;
166
+ }
167
+ </style>
@@ -43,6 +43,7 @@ export default {
43
43
  isEmit: true,
44
44
  name: 'toggle',
45
45
  table: {
46
+ category: 'Events',
46
47
  defaultValue: { summary: 'None' },
47
48
  type: { summary: '(isExpanded: boolean) => void' },
48
49
  },
@@ -68,6 +68,7 @@ export default {
68
68
  handleChange: {
69
69
  ...change,
70
70
  table: {
71
+ category: 'Events',
71
72
  defaultValue: { summary: 'None' },
72
73
  type: { summary: '(tabIndex: number) => void' },
73
74
  },
@@ -1,7 +1,7 @@
1
1
  import { action } from '@storybook/addon-actions';
2
2
 
3
3
  import TideCard from '@/components/TideCard.vue';
4
- import { POSITION_CARD_ICON as STANDARD_POSITION_CARD_ICON, TYPE_CARD as STANDARD_TYPE_CARD } from '@/types/Card';
4
+ import { TYPE_CARD as STANDARD_TYPE_CARD } from '@/types/Card';
5
5
  import { ICON } from '@/types/Icon';
6
6
  import {
7
7
  argTypeBooleanUnrequired,
@@ -13,7 +13,6 @@ import {
13
13
  } from '@/utilities/storybook';
14
14
 
15
15
  const TYPE_CARD = prependNoneAsUndefined(STANDARD_TYPE_CARD);
16
- const POSITION_CARD_ICON = prependNoneAsUndefined(STANDARD_POSITION_CARD_ICON);
17
16
  const CARD_ICON = prependNoneAsUndefined(ICON);
18
17
 
19
18
  const render = (args: any) => ({
@@ -67,14 +66,6 @@ export default {
67
66
  type: { summary: 'Icon' },
68
67
  },
69
68
  },
70
- iconPosition: {
71
- ...formatArgType({ POSITION_CARD_ICON }),
72
- description: 'Position of the icon relative to the content.',
73
- table: {
74
- defaultValue: { summary: 'LEFT' },
75
- type: { summary: 'CardIconPosition' },
76
- },
77
- },
78
69
  selected: {
79
70
  ...argTypeBooleanUnrequired,
80
71
  description: 'Determines whether the Card is selected (for selectable cards).',
@@ -93,7 +84,6 @@ export default {
93
84
  description: '',
94
85
  heading: 'Demo',
95
86
  icon: CARD_ICON.None,
96
- iconPosition: POSITION_CARD_ICON.None,
97
87
  selected: undefined,
98
88
  type: TYPE_CARD.None,
99
89
  },
@@ -75,6 +75,7 @@ export default {
75
75
  handleChange: {
76
76
  ...change,
77
77
  table: {
78
+ category: 'Events',
78
79
  defaultValue: { summary: 'None' },
79
80
  type: { summary: '(currentSlide: number) => void' },
80
81
  },
@@ -2,17 +2,33 @@ import { action } from '@storybook/addon-actions';
2
2
 
3
3
  import TideButton from '@/components/TideButton.vue';
4
4
  import TideModal from '@/components/TideModal.vue';
5
- import { disabledArgType, doSomething, lineBreak, tab } from '@/utilities/storybook';
5
+ import {
6
+ argTypeBooleanUnrequired,
7
+ disabledArgType,
8
+ doSomething,
9
+ doSomethingElse,
10
+ lineBreak,
11
+ tab,
12
+ } from '@/utilities/storybook';
6
13
 
7
14
  import type { StoryContext } from '@storybook/vue3';
8
15
 
9
16
  const formatSnippet = (code: string, context: StoryContext) => {
10
17
  const { args } = context;
11
18
 
19
+ const argsWithValues: string[] = [`:is-open="${args.isOpen}"`];
12
20
  const slotContentIndentationFixed = (args.default as string).replace(/(<\/[^>]+>)$/, `${tab}$1`);
13
21
 
22
+ if (args.isBackButton !== undefined) argsWithValues.push(`:is-back-button="${args.isBackButton}"`);
23
+ if (args.isDismissible !== undefined) argsWithValues.push(`:is-dismissible="${args.isDismissible}"`);
24
+ if (args.title !== undefined) argsWithValues.push(`title="${args.title}"`);
25
+ if (args.width !== '') argsWithValues.push(`width="${args.width}"`);
26
+
27
+ if (args.handleBack !== undefined) argsWithValues.push(`@back="${args.handleBack}"`);
28
+ if (args.handleClose !== undefined) argsWithValues.push(`@close="${args.handleClose}"`);
29
+
14
30
  return (
15
- `<TideModal @close="handleClose">${lineBreak}` +
31
+ `<TideModal ${argsWithValues.join(' ')}>${lineBreak}` +
16
32
  `${tab}${slotContentIndentationFixed}${lineBreak}${lineBreak}` +
17
33
  `${tab}<template #footer>${lineBreak}` +
18
34
  `${tab}${tab}${args.footer}${lineBreak}` +
@@ -35,6 +51,20 @@ const render = (args: any, { updateArgs }: any) => ({
35
51
  components: { TideButton, TideModal },
36
52
  methods: {
37
53
  doSomething,
54
+ doSomethingElse,
55
+ handleBack: () => {
56
+ action('Back button pressed')({});
57
+
58
+ try {
59
+ const callback = eval(args.handleBack);
60
+
61
+ if (callback) {
62
+ callback();
63
+ }
64
+ } catch {
65
+ alert('Please specify a valid handler in the "back" control.');
66
+ }
67
+ },
38
68
  handleClose: () => {
39
69
  action('Modal closed')({});
40
70
  updateArgs({ ...args, isOpen: false });
@@ -49,11 +79,14 @@ const render = (args: any, { updateArgs }: any) => ({
49
79
  alert('Please specify a valid handler in the "close" control.');
50
80
  }
51
81
  },
82
+ handleOpenModalClick: () => {
83
+ updateArgs({ ...args, isOpen: true });
84
+ },
52
85
  },
53
86
  setup: () => ({ args }),
54
87
  template: `
55
- <p>Toggle "isOpen" prop below to preview.</p>
56
- <TideModal v-bind="args" @close="handleClose">
88
+ <TideButton label="Open Modal" @click="handleOpenModalClick" />
89
+ <TideModal v-bind="args" @close="handleClose" @back="handleBack">
57
90
  ${args.default}
58
91
  <template #footer>${args.footer}</template>
59
92
  </TideModal>`,
@@ -61,6 +94,7 @@ const render = (args: any, { updateArgs }: any) => ({
61
94
 
62
95
  export default {
63
96
  argTypes: {
97
+ back: disabledArgType,
64
98
  close: disabledArgType,
65
99
  default: {
66
100
  control: 'text',
@@ -78,15 +112,40 @@ export default {
78
112
  type: { summary: 'HTML' },
79
113
  },
80
114
  },
115
+ handleBack: {
116
+ control: 'text',
117
+ description: "JS function to execute when modal's back button is pressed",
118
+ name: 'back',
119
+ table: {
120
+ category: 'Events',
121
+ defaultValue: { summary: 'None' },
122
+ type: { summary: '() => void' },
123
+ },
124
+ },
81
125
  handleClose: {
82
126
  control: 'text',
83
127
  description: 'JS function to execute when modal is closed',
84
128
  name: 'close',
85
129
  table: {
130
+ category: 'Events',
86
131
  defaultValue: { summary: 'None' },
87
132
  type: { summary: '() => void' },
88
133
  },
89
134
  },
135
+ isBackButton: {
136
+ ...argTypeBooleanUnrequired,
137
+ description: 'Determines whether the back button is displayed',
138
+ table: {
139
+ defaultValue: { summary: 'False' },
140
+ },
141
+ },
142
+ isDismissible: {
143
+ ...argTypeBooleanUnrequired,
144
+ description: 'Determines whether the close button is displayed',
145
+ table: {
146
+ defaultValue: { summary: 'False' },
147
+ },
148
+ },
90
149
  isOpen: {
91
150
  description: 'Determines whether the Modal is displayed',
92
151
  table: {
@@ -109,10 +168,13 @@ export default {
109
168
  args: {
110
169
  default: `<div>${lineBreak}${tab}Default Slot Demo${lineBreak}</div>`,
111
170
  footer: '<TideButton label="Footer Slot Demo" />',
112
- handleClose: 'doSomething',
171
+ handleBack: 'doSomething',
172
+ handleClose: 'doSomethingElse',
173
+ isBackButton: undefined,
174
+ isDismissible: undefined,
113
175
  isOpen: false,
114
176
  title: 'Modal Demo',
115
- width: '400px',
177
+ width: '',
116
178
  },
117
179
  component: TideModal,
118
180
  parameters,
@@ -50,6 +50,7 @@ export default {
50
50
  handleChange: {
51
51
  ...change,
52
52
  table: {
53
+ category: 'Events',
53
54
  defaultValue: { summary: 'None' },
54
55
  type: { summary: '(pageIndex: number) => void' },
55
56
  },
@@ -0,0 +1,98 @@
1
+ import TideIcon from '@/components/TideIcon.vue';
2
+ import TidePopover from '@/components/TidePopover.vue';
3
+ import { ICON } from '@/types/Icon';
4
+ import { lineBreak, tab } from '@/utilities/storybook';
5
+
6
+ import type { StoryContext } from '@storybook/vue3';
7
+
8
+ const formatSnippet = (code: string, context: StoryContext) => {
9
+ const { args } = context;
10
+
11
+ const argsWithValues: string[] = [];
12
+
13
+ if (args.anchorId !== '') argsWithValues.push(`anchor-id="${args.anchorId}"`);
14
+ if (args.offset !== '') argsWithValues.push(`:offset="${args.offset}"`);
15
+
16
+ return (
17
+ `<TideIcon :icon="ICON.INFO" id="anchorDemo" />${lineBreak}${lineBreak}` +
18
+ `<TidePopover ${argsWithValues.join(' ')}>${lineBreak}` +
19
+ `${tab}${args.default}${lineBreak}` +
20
+ `</TidePopover>`
21
+ );
22
+ };
23
+
24
+ const parameters = {
25
+ docs: {
26
+ source: {
27
+ format: 'vue',
28
+ language: 'html',
29
+ transform: formatSnippet,
30
+ },
31
+ },
32
+ };
33
+
34
+ const render = (args: any) => ({
35
+ components: { TideIcon, TidePopover },
36
+ setup: () => {
37
+ if (args.offset === '') {
38
+ delete args.offset;
39
+ }
40
+
41
+ return { ICON, args };
42
+ },
43
+ template: `
44
+ <TideIcon :icon="ICON.INFO" id="anchorDemo" />
45
+
46
+ <TidePopover v-bind="args">
47
+ ${args.default}
48
+ </TidePopover>
49
+ `,
50
+ updated: () => {
51
+ if (args.offset === '') {
52
+ delete args.offset;
53
+ }
54
+
55
+ return { ICON, args };
56
+ },
57
+ });
58
+
59
+ export default {
60
+ argTypes: {
61
+ anchorId: {
62
+ control: 'text',
63
+ description: 'Determines the HTML node over which the Popover will appear',
64
+ table: {
65
+ defaultValue: { summary: 'None' },
66
+ type: { summary: 'string' },
67
+ },
68
+ },
69
+ default: {
70
+ control: 'text',
71
+ description: 'Popover content',
72
+ table: {
73
+ defaultValue: { summary: 'None' },
74
+ type: { summary: 'HTML' },
75
+ },
76
+ },
77
+ offset: {
78
+ control: 'text',
79
+ description: 'Determines spacing between Popover and anchor node',
80
+ table: {
81
+ defaultValue: { summary: '16' },
82
+ type: { summary: 'number' },
83
+ },
84
+ },
85
+ },
86
+ args: {
87
+ anchorId: 'anchorDemo',
88
+ default: '<span>Demo</span>',
89
+ offset: '',
90
+ },
91
+ component: TidePopover,
92
+ parameters,
93
+ render,
94
+ tags: ['autodocs'],
95
+ title: 'Basic Components/TidePopover',
96
+ };
97
+
98
+ export const Demo = {};
@@ -44,6 +44,7 @@ export default {
44
44
  handleChange: {
45
45
  ...change,
46
46
  table: {
47
+ category: 'Events',
47
48
  defaultValue: { summary: 'None' },
48
49
  type: { summary: '() => void' },
49
50
  },
package/src/types/Card.ts CHANGED
@@ -5,10 +5,3 @@ export const TYPE_CARD = {
5
5
  } as const;
6
6
 
7
7
  export type CardType = (typeof TYPE_CARD)[keyof typeof TYPE_CARD];
8
-
9
- export const POSITION_CARD_ICON = {
10
- LEFT: 'left',
11
- TOP: 'top',
12
- } as const;
13
-
14
- export type CardIconPosition = (typeof POSITION_CARD_ICON)[keyof typeof POSITION_CARD_ICON];
@@ -0,0 +1,4 @@
1
+ export const isClickOutside = (event: MouseEvent, elements: HTMLElement | HTMLElement[]): boolean => {
2
+ const targets = Array.isArray(elements) ? elements : [elements];
3
+ return !targets.some((el) => el.contains(event.target as Node));
4
+ };
@@ -95,6 +95,10 @@ export const doSomething = () => {
95
95
  alert('Did something.');
96
96
  };
97
97
 
98
+ export const doSomethingElse = () => {
99
+ alert('Did something else.');
100
+ };
101
+
98
102
  // Flatten a nested constant into a simple constant.
99
103
  export const flatten = (input: Nested): KeyString => {
100
104
  const output: KeyString = {};
@@ -0,0 +1,44 @@
1
+ import { nextTick } from 'vue';
2
+
3
+ /**
4
+ * Directly modifies the `<body>` element to apply or remove scroll lock.
5
+ * When `false` is provided, it only unlocks scroll if there are no open
6
+ * HTML dialog elements.
7
+ */
8
+ export const setScrollLock = async (isLocked: boolean) => {
9
+ const BODY_LOCK_CLASS = 'body-scroll-lock';
10
+ const body = document.body;
11
+
12
+ if (isLocked) {
13
+ if (!body.dataset.scrollLockY) {
14
+ const scrollY = window.scrollY;
15
+ body.dataset.scrollLockY = scrollY.toString();
16
+ body.style.setProperty('--saved-scroll-y', `${scrollY}px`);
17
+ body.classList.add(BODY_LOCK_CLASS);
18
+ }
19
+ } else {
20
+ await nextTick();
21
+ if (!document.querySelector('dialog[open]')) {
22
+ const savedScrollY = parseInt(body.dataset.scrollLockY || '0');
23
+ body.classList.remove(BODY_LOCK_CLASS);
24
+ body.style.removeProperty('--saved-scroll-y');
25
+ window.scrollTo({
26
+ behavior: 'auto',
27
+ top: savedScrollY,
28
+ });
29
+ delete body.dataset.scrollLockY;
30
+ }
31
+ }
32
+ };
33
+
34
+ export const TOP_LAYER_ID = 'tideTopLayer';
35
+
36
+ export const initFauxTopLayer = () => {
37
+ let topLayer = document.getElementById(TOP_LAYER_ID);
38
+ if (!topLayer) {
39
+ topLayer = document.createElement('div');
40
+ topLayer.id = TOP_LAYER_ID;
41
+ document.body.appendChild(topLayer);
42
+ }
43
+ topLayer.style.isolation = 'isolate';
44
+ };