@veritree/ui 0.1.9 → 0.3.1

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/index.js CHANGED
@@ -12,10 +12,10 @@ import VTInputUpload from './src/Input/VTInputUpload.vue';
12
12
 
13
13
  import VTTextarea from './src/Textarea/VTTextarea.vue';
14
14
 
15
- // import VTListbox from './src/Listbox/VTListbox.vue';
16
- // import VTListboxButton from './src/Listbox/VTListboxButton.vue';
17
- // import VTListboxOption from './src/Listbox/VTListboxOption.vue';
18
- // import VTListboxOptions from './src/Listbox/VTListboxOptions.vue';
15
+ import VTListbox from './src/Listbox/VTListbox.vue';
16
+ import VTListboxButton from './src/Listbox/VTListboxButton.vue';
17
+ import VTListboxOption from './src/Listbox/VTListboxOption.vue';
18
+ import VTListboxOptions from './src/Listbox/VTListboxOptions.vue';
19
19
 
20
20
  import VTModal from './src/Modal/VTModal.vue';
21
21
 
@@ -30,6 +30,22 @@ import VTTabList from './src/Tabs/VTTabList.vue';
30
30
  import VTTabPanel from './src/Tabs/VTTabPanel.vue';
31
31
  import VTTabPanels from './src/Tabs/VTTabPanels.vue';
32
32
 
33
+ import VTDialog from './src/Dialog/VTDialog.vue';
34
+ import VTDialogClose from './src/Dialog/VTDialogClose.vue';
35
+ import VTDialogContent from './src/Dialog/VTDialogContent.vue';
36
+ import VTDialogFooter from './src/Dialog/VTDialogFooter.vue';
37
+ import VTDialogHeader from './src/Dialog/VTDialogHeader.vue';
38
+ import VTDialogMain from './src/Dialog/VTDialogMain.vue';
39
+ import VTDialogOverlay from './src/Dialog/VTDialogOverlay.vue';
40
+
41
+ import VTDrawer from './src/Drawer/VTDrawer.vue';
42
+ import VTDrawerClose from './src/Drawer/VTDrawerClose.vue';
43
+ import VTDrawerContent from './src/Drawer/VTDrawerContent.vue';
44
+ import VTDrawerFooter from './src/Drawer/VTDrawerFooter.vue';
45
+ import VTDrawerHeader from './src/Drawer/VTDrawerHeader.vue';
46
+ import VTDrawerMain from './src/Drawer/VTDrawerMain.vue';
47
+ import VTDrawerOverlay from './src/Drawer/VTDrawerOverlay.vue';
48
+
33
49
  export {
34
50
  VTAlert,
35
51
  // VTSpinner,
@@ -40,10 +56,10 @@ export {
40
56
  VTInputFile,
41
57
  VTInputUpload,
42
58
  VTTextarea,
43
- // VTListbox,
44
- // VTListboxButton,
45
- // VTListboxOption,
46
- // VTListboxOptions,
59
+ VTListbox,
60
+ VTListboxButton,
61
+ VTListboxOption,
62
+ VTListboxOptions,
47
63
  VTModal,
48
64
  VTAccordion,
49
65
  VTAccordionButton,
@@ -54,5 +70,19 @@ export {
54
70
  VTTabList,
55
71
  VTTabPanel,
56
72
  VTTabPanels,
73
+ VTDrawer,
74
+ VTDrawerClose,
75
+ VTDrawerContent,
76
+ VTDrawerFooter,
77
+ VTDrawerHeader,
78
+ VTDrawerMain,
79
+ VTDrawerOverlay,
80
+ VTDialog,
81
+ VTDialogClose,
82
+ VTDialogContent,
83
+ VTDialogFooter,
84
+ VTDialogHeader,
85
+ VTDialogMain,
86
+ VTDialogOverlay,
57
87
  }
58
88
 
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@veritree/ui",
3
+ "version": "0.1.9",
4
+ "lockfileVersion": 1,
5
+ "requires": true,
6
+ "dependencies": {
7
+ "@veritree/icons": {
8
+ "version": "0.12.0",
9
+ "resolved": "https://registry.npmjs.org/@veritree/icons/-/icons-0.12.0.tgz",
10
+ "integrity": "sha512-vunUKzvS9neslSf3R3y6RYQrcfRpxmp8PnhWWe2peYiyElLIJcb7zAsfCZ+I0Fg5PQ6GZG6StqWy0WF7MJ7VOg=="
11
+ }
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.1.9",
3
+ "version": "0.3.1",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -11,6 +11,6 @@
11
11
  "access": "public"
12
12
  },
13
13
  "dependencies": {
14
- "@veritree/icons": "^0.12.0"
14
+ "@veritree/icons": "^0.19.0"
15
15
  }
16
16
  }
@@ -0,0 +1,118 @@
1
+ <template>
2
+ <div
3
+ v-if="visible"
4
+ :id="id"
5
+ :class="{
6
+ Dialog: headless,
7
+ 'fixed inset-0 z-50 grid grid-cols-1 grid-rows-1 p-4 md:p-8': !headless,
8
+ }"
9
+ aria-modal="true"
10
+ @click="hide"
11
+ >
12
+ <slot></slot>
13
+ </div>
14
+ </template>
15
+
16
+ <script>
17
+ import { genId } from '~/utils/ids';
18
+
19
+ export default {
20
+ name: 'VTDialog',
21
+
22
+ provide() {
23
+ return {
24
+ api: () => {
25
+ const id = this.id;
26
+ const isDark = this.dark;
27
+ const isHeadless = this.headless;
28
+
29
+ const registerContent = (content) => {
30
+ if (!content) return;
31
+ this.content = content;
32
+ };
33
+
34
+ const registerOverlay = (overlay) => {
35
+ if (!overlay) return;
36
+ this.overlay = overlay;
37
+ };
38
+
39
+ const hide = () => this.hide();
40
+
41
+ const emit = () => this.emit();
42
+
43
+ return {
44
+ id,
45
+ isDark,
46
+ isHeadless,
47
+ hide,
48
+ emit,
49
+ registerContent,
50
+ registerOverlay,
51
+ };
52
+ },
53
+ };
54
+ },
55
+
56
+ model: {
57
+ prop: 'visible',
58
+ },
59
+
60
+ props: {
61
+ visible: {
62
+ type: Boolean,
63
+ default: false,
64
+ },
65
+ headless: {
66
+ type: Boolean,
67
+ default: false,
68
+ },
69
+ dark: {
70
+ type: Boolean,
71
+ default: false,
72
+ },
73
+ },
74
+
75
+ data() {
76
+ return {
77
+ id: `dialog-${genId()}`,
78
+ content: null,
79
+ overlay: null,
80
+ };
81
+ },
82
+
83
+ computed: {
84
+ hasContent() {
85
+ return this.content !== null;
86
+ },
87
+
88
+ hasOverlay() {
89
+ return this.overlay !== null;
90
+ },
91
+ },
92
+
93
+ watch: {
94
+ visible(isVisible) {
95
+ if (!isVisible) this.hide();
96
+ },
97
+ },
98
+
99
+ mounted() {
100
+ if (this.hasContent) this.content.show();
101
+ if (this.hasOverlay) this.overlay.show();
102
+ },
103
+
104
+ methods: {
105
+ hide() {
106
+ if (this.hasOverlay) this.overlay.hide();
107
+ if (this.hasContent) this.content.hide();
108
+ },
109
+
110
+ emit() {
111
+ this.$nextTick(() => {
112
+ this.$emit('input', false);
113
+ this.$emit('hidden');
114
+ });
115
+ },
116
+ },
117
+ };
118
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <VTButton
3
+ variant="icon"
4
+ :class="{
5
+ 'Dialog-close': headless,
6
+ 'absolute right-4 top-4': !headless,
7
+ }"
8
+ :theme="theme"
9
+ @click.prevent="hide"
10
+ ><slot><IconClose class="h-5 w-5" /></slot
11
+ ></VTButton>
12
+ </template>
13
+
14
+ <script>
15
+ import { IconClose } from '@veritree/icons';
16
+ import VTButton from './VTButton.vue';
17
+
18
+ export default {
19
+ name: 'VTDialogClose',
20
+
21
+ components: { IconClose, VTButton },
22
+
23
+ inject: ['api'],
24
+
25
+ computed: {
26
+ dark() {
27
+ return this.api().isDark;
28
+ },
29
+
30
+ headless() {
31
+ return this.api().isHeadless;
32
+ },
33
+
34
+ // temporary till button theme is implemented
35
+ theme() {
36
+ return this.dark ? 'dark' : null;
37
+ },
38
+ },
39
+
40
+ methods: {
41
+ hide() {
42
+ this.api().hide();
43
+ },
44
+ },
45
+ };
46
+ </script>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <transition
3
+ enter-active-class="duration-300 ease-out"
4
+ enter-class="translate-y-[50px] opacity-0"
5
+ enter-to-class="translate-y-0 opacity-100"
6
+ leave-active-class="duration-300 ease-out"
7
+ leave-class="translate-y-0 opacity-100"
8
+ leave-to-class="translate-y-[50px] opacity-0"
9
+ @after-leave="hideDialog"
10
+ >
11
+ <div
12
+ v-show="visible"
13
+ :class="{
14
+ 'Dialog-content': headless,
15
+ 'relative m-auto max-h-full max-w-full overflow-auto rounded p-6 focus:outline-none sm:p-10':
16
+ !headless,
17
+ 'bg-white': !dark,
18
+ 'bg-fd-600': dark,
19
+ }"
20
+ tabindex="-1"
21
+ @keyup.esc="hide"
22
+ @click.stop
23
+ >
24
+ <slot></slot>
25
+ </div>
26
+ </transition>
27
+ </template>
28
+
29
+ <script>
30
+ export default {
31
+ name: 'VTDialogContent',
32
+
33
+ inject: ['api'],
34
+
35
+ data() {
36
+ return {
37
+ visible: false,
38
+ };
39
+ },
40
+
41
+ computed: {
42
+ dark() {
43
+ return this.api().isDark;
44
+ },
45
+
46
+ headless() {
47
+ return this.api().isHeadless;
48
+ },
49
+ },
50
+
51
+ mounted() {
52
+ this.api().registerContent(this);
53
+ this.show();
54
+
55
+ this.$nextTick(() => this.$el.focus());
56
+ },
57
+
58
+ methods: {
59
+ show() {
60
+ this.visible = true;
61
+ },
62
+
63
+ hide() {
64
+ this.visible = false;
65
+ },
66
+
67
+ hideDialog() {
68
+ this.api().emit();
69
+ },
70
+ },
71
+ };
72
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <component :is="as" :class="{ 'Dialog-footer': headless }">
3
+ <slot></slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'VTDialogFooter',
10
+
11
+ inject: ['api'],
12
+
13
+ props: {
14
+ as: {
15
+ type: String,
16
+ default: 'footer',
17
+ },
18
+ },
19
+
20
+ computed: {
21
+ dark() {
22
+ return this.api().isDark;
23
+ },
24
+
25
+ headless() {
26
+ return this.api().isHeadless;
27
+ },
28
+ },
29
+ };
30
+ </script>
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :id="id"
5
+ :class="{
6
+ 'Dialog-header': headless,
7
+ 'mb-8 text-2xl font-semibold': !headless,
8
+ }"
9
+ >
10
+ <slot></slot>
11
+ </component>
12
+ </template>
13
+
14
+ <script>
15
+ export default {
16
+ name: 'VTDialogHeader',
17
+
18
+ inject: ['api'],
19
+
20
+ props: {
21
+ as: {
22
+ type: String,
23
+ default: 'header',
24
+ },
25
+ },
26
+
27
+ computed: {
28
+ dark() {
29
+ return this.api().isDark;
30
+ },
31
+
32
+ headless() {
33
+ return this.api().isHeadless;
34
+ },
35
+
36
+ id() {
37
+ return `${this.api().id}-header`;
38
+ },
39
+ },
40
+
41
+ mounted() {
42
+ this.setDialogLabelledby();
43
+ },
44
+
45
+ methods: {
46
+ // In here because if there is no header, the dialog will not be labelled by
47
+ setDialogLabelledby() {
48
+ const dialog = document.getElementById(this.api().id);
49
+
50
+ if (dialog) {
51
+ dialog.setAttribute('aria-labelledby', this.id);
52
+ }
53
+ },
54
+ },
55
+ };
56
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <component :is="as" :id="id" :class="{ 'Dialog-body': headless }">
3
+ <slot></slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'VTDialogMain',
10
+
11
+ inject: ['api'],
12
+
13
+ props: {
14
+ as: {
15
+ type: String,
16
+ default: 'main',
17
+ },
18
+ },
19
+
20
+ computed: {
21
+ dark() {
22
+ return this.api().isDark;
23
+ },
24
+
25
+ headless() {
26
+ return this.api().isHeadless;
27
+ },
28
+
29
+ id() {
30
+ return `${this.api().id}-desc`;
31
+ },
32
+ },
33
+
34
+ mounted() {
35
+ this.setDialogDescribedby();
36
+ },
37
+
38
+ methods: {
39
+ // In here because if there is no body, the dialog will not be described by
40
+ setDialogDescribedby() {
41
+ const dialog = document.getElementById(this.api().id);
42
+
43
+ if (dialog) {
44
+ dialog.setAttribute('aria-describedby', this.id);
45
+ }
46
+ },
47
+ },
48
+ };
49
+ </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <FadeInOut>
3
+ <div
4
+ v-if="visible"
5
+ :class="{
6
+ 'Dialog-overlay': headless,
7
+ 'fixed inset-0 bg-fd-450/80': !headless,
8
+ }"
9
+ ></div>
10
+ </FadeInOut>
11
+ </template>
12
+
13
+ <script>
14
+ import FadeInOut from '~/components/Transitions/FadeInOut.vue';
15
+
16
+ export default {
17
+ name: 'VTDialogOverlay',
18
+
19
+ components: {
20
+ FadeInOut,
21
+ },
22
+
23
+ inject: ['api'],
24
+
25
+ data() {
26
+ return {
27
+ visible: false,
28
+ };
29
+ },
30
+
31
+ computed: {
32
+ dark() {
33
+ return this.api().isDark;
34
+ },
35
+
36
+ headless() {
37
+ return this.api().isHeadless;
38
+ },
39
+ },
40
+
41
+ mounted() {
42
+ this.visible = true;
43
+ this.api().registerOverlay(this);
44
+ },
45
+
46
+ methods: {
47
+ hide() {
48
+ this.visible = false;
49
+ },
50
+ },
51
+ };
52
+ </script>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div
3
+ v-if="visible"
4
+ :id="id"
5
+ :class="{ Drawer: headless, 'fixed inset-0 z-50 h-screen': !headless }"
6
+ aria-modal="true"
7
+ @click="hide"
8
+ >
9
+ <slot></slot>
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import { genId } from '~/utils/ids';
15
+
16
+ export default {
17
+ name: 'VTDrawer',
18
+
19
+ provide() {
20
+ return {
21
+ api: () => {
22
+ const id = this.id;
23
+ const isDark = this.dark;
24
+ const isHeadless = this.headless;
25
+ const isRight = this.right;
26
+
27
+ const registerContent = (content) => {
28
+ if (!content) return;
29
+ this.content = content;
30
+ };
31
+
32
+ const registerOverlay = (overlay) => {
33
+ if (!overlay) return;
34
+ this.overlay = overlay;
35
+ };
36
+
37
+ const hide = () => this.hide();
38
+
39
+ const emit = () => this.emit();
40
+
41
+ return {
42
+ id,
43
+ isDark,
44
+ isHeadless,
45
+ isRight,
46
+ hide,
47
+ emit,
48
+ registerContent,
49
+ registerOverlay,
50
+ };
51
+ },
52
+ };
53
+ },
54
+
55
+ model: {
56
+ prop: 'visible',
57
+ },
58
+
59
+ props: {
60
+ visible: {
61
+ type: Boolean,
62
+ default: false,
63
+ },
64
+ headless: {
65
+ type: Boolean,
66
+ default: false,
67
+ },
68
+ dark: {
69
+ type: Boolean,
70
+ default: false,
71
+ },
72
+ right: {
73
+ type: Boolean,
74
+ default: false,
75
+ },
76
+ },
77
+
78
+ data() {
79
+ return {
80
+ id: `drawer-${genId()}`,
81
+ content: null,
82
+ overlay: null,
83
+ };
84
+ },
85
+
86
+ watch: {
87
+ visible(isVisible) {
88
+ if (!isVisible) this.hide();
89
+ },
90
+ },
91
+
92
+ mounted() {
93
+ if (!this.content) return;
94
+ this.content.show();
95
+ },
96
+
97
+ methods: {
98
+ hide() {
99
+ if (!this.content) return;
100
+ this.content.hide();
101
+ if (!this.overlay) return;
102
+ this.overlay.hide();
103
+ },
104
+
105
+ emit() {
106
+ this.$nextTick(() => {
107
+ this.$emit('input', false);
108
+ this.$emit('hidden');
109
+ });
110
+ },
111
+ },
112
+ };
113
+ </script>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <VTButton
3
+ variant="icon"
4
+ :class="{
5
+ 'Drawer-close': headless,
6
+ 'ml-auto mb-4 text-inherit sm:mb-8': !headless,
7
+ }"
8
+ :theme="theme"
9
+ @click.prevent="hide"
10
+ ><slot><IconLeft :class="{ 'rotate-180': right }" /></slot
11
+ ></VTButton>
12
+ </template>
13
+
14
+ <script>
15
+ import { IconLeft } from '@veritree/icons';
16
+ import VTButton from './VTButton.vue';
17
+
18
+ export default {
19
+ name: 'VTDrawerClose',
20
+
21
+ components: { IconLeft, VTButton },
22
+
23
+ inject: ['api'],
24
+
25
+ computed: {
26
+ dark() {
27
+ return this.api().isDark;
28
+ },
29
+
30
+ headless() {
31
+ return this.api().isHeadless;
32
+ },
33
+
34
+ right() {
35
+ return this.api().isRight;
36
+ },
37
+
38
+ // temporary till button theme is implemented
39
+ theme() {
40
+ return this.dark ? 'dark' : null;
41
+ },
42
+ },
43
+
44
+ methods: {
45
+ hide() {
46
+ this.api().hide();
47
+ },
48
+ },
49
+ };
50
+ </script>
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <transition
3
+ :enter-active-class="activeClass"
4
+ :enter-class="enterClass"
5
+ :enter-to-class="enterToClass"
6
+ :leave-active-class="activeClass"
7
+ :leave-class="leaveClass"
8
+ :leave-to-class="leaveToClass"
9
+ @after-leave="hideDrawer"
10
+ >
11
+ <div
12
+ v-show="visible"
13
+ :class="{
14
+ 'Drawer-content': headless,
15
+ 'absolute z-20 flex h-screen max-h-full max-w-full flex-col overflow-auto p-5 outline-0 sm:px-10 sm:py-6':
16
+ !headless,
17
+ 'bg-white': !dark,
18
+ 'bg-fd-600': dark,
19
+ 'right-0': right,
20
+ }"
21
+ tabindex="-1"
22
+ @keyup.esc="hide"
23
+ @click.stop
24
+ >
25
+ <slot></slot>
26
+ </div>
27
+ </transition>
28
+ </template>
29
+
30
+ <script>
31
+ export default {
32
+ name: 'VTDrawerContent',
33
+
34
+ inject: ['api'],
35
+
36
+ data() {
37
+ return {
38
+ visible: false,
39
+ };
40
+ },
41
+
42
+ computed: {
43
+ dark() {
44
+ return this.api().isDark;
45
+ },
46
+
47
+ headless() {
48
+ return this.api().isHeadless;
49
+ },
50
+
51
+ right() {
52
+ return this.api().isRight;
53
+ },
54
+
55
+ activeClass() {
56
+ return 'transform transition duration-300 ease-in-out';
57
+ },
58
+
59
+ enterClass() {
60
+ return `opacity-0 ${this.right ? '' : '-'}translate-x-full`;
61
+ },
62
+
63
+ enterToClass() {
64
+ return `opacity-100 ${this.right ? '-' : ''}translate-x-0`;
65
+ },
66
+
67
+ leaveClass() {
68
+ return `opacity-100 ${this.right ? '-' : ''}translate-x-0`;
69
+ },
70
+
71
+ leaveToClass() {
72
+ return `opacity-0 ${this.right ? '' : '-'}translate-x-full`;
73
+ },
74
+ },
75
+
76
+ mounted() {
77
+ this.api().registerContent(this);
78
+ this.show();
79
+
80
+ this.$nextTick(() => this.$el.focus());
81
+ },
82
+
83
+ methods: {
84
+ show() {
85
+ this.visible = true;
86
+ },
87
+
88
+ hide() {
89
+ this.visible = false;
90
+ },
91
+
92
+ hideDrawer() {
93
+ this.api().emit();
94
+ },
95
+ },
96
+ };
97
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <component :is="as" :class="{ 'Dialog-footer': headless }">
3
+ <slot></slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'VTDrawerFooter',
10
+
11
+ inject: ['api'],
12
+
13
+ props: {
14
+ as: {
15
+ type: String,
16
+ default: 'footer',
17
+ },
18
+ },
19
+
20
+ computed: {
21
+ dark() {
22
+ return this.api().isDark;
23
+ },
24
+
25
+ headless() {
26
+ return this.api().isHeadless;
27
+ },
28
+ },
29
+ };
30
+ </script>
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :id="id"
5
+ :class="{
6
+ 'Drawer-header': headless,
7
+ 'mb-8 text-2xl font-semibold': !headless,
8
+ }"
9
+ >
10
+ <slot></slot>
11
+ </component>
12
+ </template>
13
+
14
+ <script>
15
+ export default {
16
+ name: 'VTDrawerHeader',
17
+
18
+ inject: ['api'],
19
+
20
+ props: {
21
+ as: {
22
+ type: String,
23
+ default: 'header',
24
+ },
25
+ },
26
+
27
+ computed: {
28
+ dark() {
29
+ return this.api().isDark;
30
+ },
31
+
32
+ headless() {
33
+ return this.api().isHeadless;
34
+ },
35
+
36
+ id() {
37
+ return `${this.api().id}-header`;
38
+ },
39
+ },
40
+
41
+ mounted() {
42
+ this.setDialogLabelledby();
43
+ },
44
+
45
+ methods: {
46
+ // In here because if there is no header, the dialog will not be labelled by
47
+ setDialogLabelledby() {
48
+ const dialog = document.getElementById(this.api().id);
49
+
50
+ if (dialog) {
51
+ dialog.setAttribute('aria-labelledby', this.id);
52
+ }
53
+ },
54
+ },
55
+ };
56
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :id="id"
5
+ :class="{ 'Drawer-body': headless, 'flex-1': !headless }"
6
+ >
7
+ <slot></slot>
8
+ </component>
9
+ </template>
10
+
11
+ <script>
12
+ export default {
13
+ name: 'VTDrawerMain',
14
+
15
+ inject: ['api'],
16
+
17
+ props: {
18
+ as: {
19
+ type: String,
20
+ default: 'main',
21
+ },
22
+ },
23
+
24
+ computed: {
25
+ dark() {
26
+ return this.api().isDark;
27
+ },
28
+
29
+ headless() {
30
+ return this.api().isHeadless;
31
+ },
32
+
33
+ id() {
34
+ return `${this.api().id}-desc`;
35
+ },
36
+ },
37
+
38
+ mounted() {
39
+ this.setDialogDescribedby();
40
+ },
41
+
42
+ methods: {
43
+ // In here because if there is no body, the dialog will not be described by
44
+ setDialogDescribedby() {
45
+ const dialog = document.getElementById(this.api().id);
46
+
47
+ if (dialog) {
48
+ dialog.setAttribute('aria-describedby', this.id);
49
+ }
50
+ },
51
+ },
52
+ };
53
+ </script>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <FadeInOut>
3
+ <div
4
+ v-if="visible"
5
+ :class="{
6
+ 'Drawer-overlay': headless,
7
+ 'fixed inset-0 z-10 bg-fd-450/80': !headless,
8
+ }"
9
+ ></div>
10
+ </FadeInOut>
11
+ </template>
12
+
13
+ <script>
14
+ import FadeInOut from '../Transitions/FadeInOut.vue';
15
+
16
+ export default {
17
+ components: {
18
+ FadeInOut,
19
+ },
20
+
21
+ inject: ['api'],
22
+
23
+ data() {
24
+ return {
25
+ visible: false,
26
+ };
27
+ },
28
+
29
+ computed: {
30
+ dark() {
31
+ return this.api().isDark;
32
+ },
33
+
34
+ headless() {
35
+ return this.api().isHeadless;
36
+ },
37
+ },
38
+
39
+ mounted() {
40
+ this.visible = true;
41
+ this.api().registerOverlay(this);
42
+ },
43
+
44
+ methods: {
45
+ hide() {
46
+ this.visible = false;
47
+ },
48
+ },
49
+ };
50
+ </script>
@@ -88,7 +88,7 @@ export default {
88
88
 
89
89
  const unfocusOptions = () => {
90
90
  this.listboxOptions.forEach((option) => {
91
- option.unfocus();
91
+ if (option.focused) option.unfocus();
92
92
  });
93
93
  };
94
94
 
@@ -2,7 +2,7 @@
2
2
  <button
3
3
  :aria-expanded="expanded"
4
4
  aria-haspopup="listbox"
5
- class="Listbox-button debugger"
5
+ class="Listbox-button"
6
6
  :data-theme="theme"
7
7
  type="button"
8
8
  @click.prevent="onClick"
@@ -39,13 +39,12 @@ export default {
39
39
 
40
40
  data() {
41
41
  return {
42
- api: this.api(),
43
42
  expanded: false,
44
43
  };
45
44
  },
46
45
 
47
46
  mounted() {
48
- this.api.registerListboxButton(this);
47
+ this.api().registerListboxButton(this);
49
48
  },
50
49
 
51
50
  methods: {
@@ -59,7 +58,7 @@ export default {
59
58
  },
60
59
 
61
60
  onClick() {
62
- const listbox = this.api.getListbox();
61
+ const listbox = this.api().getListbox();
63
62
  this.expanded ? listbox.hide() : listbox.show();
64
63
 
65
64
  this.toggleExpanded();
@@ -67,7 +66,7 @@ export default {
67
66
 
68
67
  onKeyDown(event) {
69
68
  const key = event.key;
70
- const listbox = this.api.getListbox();
69
+ const listbox = this.api().getListbox();
71
70
 
72
71
  switch (key) {
73
72
  case keys.down:
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <li
3
3
  :id="id"
4
- :aria-selected="selected"
5
4
  class="Listbox-option"
6
5
  role="option"
7
6
  @mousedown.prevent="onMouseDown"
@@ -13,6 +12,7 @@
13
12
  </template>
14
13
 
15
14
  <script>
15
+ import { scrollElementIntoView } from '../utils/components';
16
16
  import { genId } from '../utils/ids';
17
17
  import { areObjsEqual, isObj } from '../utils/objects';
18
18
 
@@ -26,36 +26,41 @@ export default {
26
26
  type: [String, Number, Object],
27
27
  required: true,
28
28
  },
29
+ selected: {
30
+ type: Boolean,
31
+ default: false,
32
+ },
29
33
  },
30
34
 
31
35
  data() {
32
36
  return {
33
- api: this.api(),
34
37
  id: `listbox-option-${genId()}`,
35
- selected: false,
36
38
  focused: false,
37
39
  isMousemove: false,
40
+ parent: null,
38
41
  };
39
42
  },
40
43
 
41
44
  watch: {
42
45
  focused(newValue) {
43
- if (newValue) {
44
- if (!this.isMousemove) {
45
- this.$el.scrollIntoView({ block: 'nearest' });
46
- }
46
+ if (!newValue) return;
47
47
 
48
- const listbox = this.api.getListbox();
49
- listbox.updateActiveDescendant(this.id);
50
- }
48
+ if (!this.parent) this.parent = this.api().getListbox();
49
+ if (!this.isMousemove) scrollElementIntoView(this.$el, this.parent.$el);
50
+ this.parent.updateActiveDescendant(this.id);
51
51
  },
52
52
  },
53
53
 
54
54
  mounted() {
55
- this.api.registerOption(this);
55
+ this.api().registerOption(this);
56
+
57
+ if (this.selected) {
58
+ this.select();
59
+ this.focus();
60
+ }
56
61
 
57
62
  // todo: make sure it works with other values than objects
58
- const listboxOptionSelected = this.api.getListboxValue();
63
+ const listboxOptionSelected = this.api().getListboxValue();
59
64
 
60
65
  if (!listboxOptionSelected) {
61
66
  return;
@@ -77,7 +82,7 @@ export default {
77
82
  },
78
83
 
79
84
  beforeDestroy() {
80
- this.api.unregisterOption(this.id);
85
+ this.api().unregisterOption(this.id);
81
86
  },
82
87
 
83
88
  methods: {
@@ -92,15 +97,15 @@ export default {
92
97
  },
93
98
 
94
99
  select() {
95
- this.selected = true;
100
+ this.$el.setAttribute('aria-selected', true);
96
101
  },
97
102
 
98
103
  unselect() {
99
- this.selected = false;
104
+ this.$el.setAttribute('aria-selected', false);
100
105
  },
101
106
 
102
107
  onMouseDown(event) {
103
- if (event.buttons === 1) this.api.selectOption(this);
108
+ if (event.buttons === 1) this.api().selectOption(this);
104
109
  },
105
110
 
106
111
  // Mousemove instead of mouseover to support keyboard navigation.
@@ -108,7 +113,7 @@ export default {
108
113
  // mouseover event gets triggered.
109
114
  onMousemove() {
110
115
  this.isMousemove = true;
111
- this.api.unfocusOptions();
116
+ this.api().unfocusOptions();
112
117
  this.focus();
113
118
  },
114
119
 
@@ -23,7 +23,6 @@ export default {
23
23
 
24
24
  data() {
25
25
  return {
26
- api: this.api(),
27
26
  visible: false,
28
27
  filter: '',
29
28
  };
@@ -37,7 +36,7 @@ export default {
37
36
  this.handleOptionFocus();
38
37
  });
39
38
  } else {
40
- const listboxButton = this.api.getListboxButton();
39
+ const listboxButton = this.api().getListboxButton();
41
40
 
42
41
  this.$nextTick(() => {
43
42
  listboxButton.focus();
@@ -47,7 +46,7 @@ export default {
47
46
  },
48
47
 
49
48
  mounted() {
50
- this.api.registerListbox(this);
49
+ this.api().registerListbox(this);
51
50
  },
52
51
 
53
52
  methods: {
@@ -60,8 +59,8 @@ export default {
60
59
  },
61
60
 
62
61
  handleOptionFocus() {
63
- const selectedIndex = this.api.getFocusedIndex();
64
- if (!selectedIndex) this.api.focusFirstOption();
62
+ const selectedIndex = this.api().getFocusedIndex();
63
+ if (!selectedIndex) this.api().focusFirstOption();
65
64
  },
66
65
 
67
66
  updateActiveDescendant(id) {
@@ -78,31 +77,31 @@ export default {
78
77
  clearTimeout(timer);
79
78
 
80
79
  timer = setTimeout(() => {
81
- this.api.focusOptionByFilter(this.filter);
80
+ this.api().focusOptionByFilter(this.filter);
82
81
  this.filter = '';
83
- }, 300);
82
+ }, 100);
84
83
  }
85
84
 
86
85
  switch (key) {
87
86
  case keys.up:
88
87
  event.preventDefault();
89
- this.api.focusPreviousOption();
88
+ this.api().focusPreviousOption();
90
89
  break;
91
90
  case keys.down:
92
91
  event.preventDefault();
93
- this.api.focusNextOption();
92
+ this.api().focusNextOption();
94
93
  break;
95
94
  case keys.home:
96
95
  event.preventDefault();
97
- this.api.focusFirstOption();
96
+ this.api().focusFirstOption();
98
97
  break;
99
98
  case keys.end:
100
99
  event.preventDefault();
101
- this.api.focusLastOption();
100
+ this.api().focusLastOption();
102
101
  break;
103
102
  case keys.enter:
104
103
  event.preventDefault();
105
- this.api.selectOption();
104
+ this.api().selectOption();
106
105
  break;
107
106
  case keys.escape:
108
107
  this.hide();
@@ -33,7 +33,7 @@
33
33
 
34
34
  <script>
35
35
  import { IconClose } from '@veritree/icons';
36
- import VTButton from '../Button/VTButton.vue';
36
+ import VTButton from './VTButton.vue';
37
37
 
38
38
  export default {
39
39
  name: 'VTModal',
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <transition
3
+ enter-active-class="duration-300 ease-out"
4
+ enter-class="transform opacity-0"
5
+ enter-to-class="opacity-100"
6
+ leave-active-class="duration-300 ease-in"
7
+ leave-class="opacity-100"
8
+ leave-to-class="transform opacity-0"
9
+ @after-enter="afterEnter"
10
+ @after-leave="afterLeave"
11
+ >
12
+ <slot></slot>
13
+ </transition>
14
+ </template>
15
+
16
+ <script>
17
+ export default {
18
+ methods: {
19
+ afterEnter() {
20
+ this.$emit('shown');
21
+ },
22
+
23
+ afterLeave() {
24
+ this.$emit('hidden');
25
+ },
26
+ },
27
+ };
28
+ </script>
@@ -0,0 +1,18 @@
1
+ /**
2
+ *
3
+ * @param {HTMLElement} el
4
+ * @param {HTMLElement} parent
5
+ */
6
+ export const scrollElementIntoView = (el, parent) => {
7
+ // this works better than scrollIntoView
8
+ if (parent.scrollHeight <= parent.clientHeight) return;
9
+
10
+ const scrollBottom = parent.clientHeight + parent.scrollTop;
11
+ const elBottom = el.offsetTop + el.offsetHeight;
12
+
13
+ if (elBottom > scrollBottom) {
14
+ parent.scrollTop = elBottom - parent.clientHeight;
15
+ } else if (el.offsetTop < parent.scrollTop) {
16
+ parent.scrollTop = el.offsetTop;
17
+ }
18
+ };
package/src/utils/ids.js CHANGED
@@ -11,24 +11,3 @@ export const genId = () => {
11
11
  if (!gen) gen = idMaker();
12
12
  return gen.next().value;
13
13
  };
14
-
15
- // Add leading zeros
16
- // export const pad = (num, size) => {
17
- // let s = num + '';
18
-
19
- // while (s.length < size) {
20
- // s = '0' + s;
21
- // }
22
-
23
- // return s;
24
- // };
25
-
26
- export const addZerosToId = (id) => {
27
- let formattedId = null;
28
-
29
- if (id < 10) formattedId = '000' + id;
30
- else if (id < 100) formattedId = '00' + id;
31
- else if (id < 1000) formattedId = '0' + id;
32
-
33
- return '#' + formattedId;
34
- };