@veritree/ui 0.6.1 → 0.9.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.
@@ -0,0 +1,83 @@
1
+ <template>
2
+ <transition
3
+ enter-active-class="duration-200 ease-out"
4
+ enter-class="translate-y-[15px] opacity-0"
5
+ enter-to-class="translate-y-0 opacity-100"
6
+ leave-active-class="duration-200 ease-in"
7
+ leave-class="translate-y-0 opacity-100"
8
+ leave-to-class="translate-y-[15px] opacity-0"
9
+ @after-leave="hide"
10
+ >
11
+ <div
12
+ v-show="visible"
13
+ :id="id"
14
+ :class="{
15
+ PopoverPanel: headless,
16
+ 'absolute top-full mt-3 rounded-md shadow-300 ':
17
+ !headless,
18
+ 'bg-white': !dark,
19
+ 'border-gray-700 bg-forest-default shadow-gray-700': dark,
20
+ 'left-0': !right,
21
+ 'right-0': right,
22
+ }"
23
+ >
24
+ <slot></slot>
25
+ </div>
26
+ </transition>
27
+ </template>
28
+
29
+ <script>
30
+ import { genId } from '~/utils/ids';
31
+
32
+ export default {
33
+ name: 'VTPopoverContent',
34
+
35
+ inject: ['api'],
36
+
37
+ data() {
38
+ return {
39
+ id: `popover-panel-${genId()}`,
40
+ visible: false,
41
+ };
42
+ },
43
+
44
+ computed: {
45
+ dark() {
46
+ return this.api().isDark;
47
+ },
48
+
49
+ headless() {
50
+ return this.api().isHeadless;
51
+ },
52
+
53
+ right() {
54
+ return this.api().isRight;
55
+ },
56
+ },
57
+
58
+ mounted() {
59
+ this.api().registerContent(this);
60
+
61
+ // TODO: Create a directive or mixin for this
62
+ document.addEventListener('click', (e) => {
63
+ e.stopPropagation();
64
+ if (this.visible && !this.$el.contains(e.target)) this.hide();
65
+ });
66
+ },
67
+
68
+ destroyed() {
69
+ // TODO: Create a directive or mixin for this
70
+ document.removeEventListener('click', this.hide());
71
+ },
72
+
73
+ methods: {
74
+ show() {
75
+ this.visible = true;
76
+ },
77
+
78
+ hide() {
79
+ this.visible = false;
80
+ },
81
+ },
82
+ };
83
+ </script>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <div
3
+ :class="{
4
+ PopoverDivider: headless,
5
+ 'h-[1px]': !headless,
6
+ 'bg-white': !dark,
7
+ 'bg-fd-500': dark,
8
+ }"
9
+ ></div>
10
+ </template>
11
+
12
+ <script>
13
+ export default {
14
+ name: 'VTPopoverDivider',
15
+
16
+ inject: ['api'],
17
+
18
+ computed: {
19
+ dark() {
20
+ return this.api().isDark;
21
+ },
22
+
23
+ headless() {
24
+ return this.api().isHeadless;
25
+ },
26
+ },
27
+ };
28
+ </script>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="grid">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :class="{
5
+ PopoverItem: headless,
6
+ '-mx-3 flex min-w-max items-center gap-2 px-3 py-2 no-underline':
7
+ !headless,
8
+ 'text-fd-500': !dark,
9
+ 'text-white hover:bg-fd-450': dark,
10
+ }"
11
+ @click="onClick"
12
+ >
13
+ <slot></slot>
14
+ </component>
15
+ </template>
16
+
17
+ <script>
18
+ export default {
19
+ name: 'VTPopoverItem',
20
+
21
+ inject: ['api'],
22
+
23
+ props: {
24
+ to: {
25
+ type: [String, Object],
26
+ default: null,
27
+ },
28
+ href: {
29
+ type: String,
30
+ default: null,
31
+ },
32
+ },
33
+
34
+ computed: {
35
+ dark() {
36
+ return this.api().isDark;
37
+ },
38
+
39
+ headless() {
40
+ return this.api().isHeadless;
41
+ },
42
+
43
+ as() {
44
+ return this.href ? 'a' : this.to ? 'NuxtLink' : 'button';
45
+ },
46
+ },
47
+
48
+ methods: {
49
+ onClick() {
50
+ if (this.href || this.to) return;
51
+ this.$emit('click');
52
+ },
53
+ },
54
+ };
55
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div @keydown.esc.prevent="onEsc">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'VTPopoverTrigger',
10
+
11
+ inject: ['api'],
12
+
13
+ computed: {
14
+ content() {
15
+ return this.api().content;
16
+ },
17
+ },
18
+
19
+ mounted() {
20
+ this.api().registerButton(this);
21
+ this.handleSlotTrigger();
22
+ },
23
+
24
+ methods: {
25
+ handleSlotTrigger() {
26
+ const slot = this.$slots.default;
27
+ const trigger = slot[0].elm;
28
+
29
+ if (slot.length > 1) {
30
+ console.error('VTPopoverButton only accepts one item in its slot');
31
+ return;
32
+ }
33
+
34
+ trigger.addEventListener('click', (e) => {
35
+ this.onClick();
36
+ e.stopPropagation();
37
+ });
38
+ },
39
+
40
+ onClick() {
41
+ if (this.content.visible) this.content.hide();
42
+ else this.content.show();
43
+ },
44
+
45
+ onEsc() {
46
+ if (!this.content) return;
47
+ this.content.hide();
48
+ },
49
+ },
50
+ };
51
+ </script>
@@ -1,81 +0,0 @@
1
- <template>
2
- <button
3
- :aria-expanded="expanded"
4
- aria-haspopup="listbox"
5
- class="Listbox-button"
6
- :data-theme="theme"
7
- type="button"
8
- @click.prevent="onClick"
9
- @keydown="onKeyDown"
10
- >
11
- <span class="Listbox-button__text"><slot></slot></span>
12
- <span class="Listbox-button__icon">
13
- <IconChevronUp v-if="expanded" />
14
- <IconChevronDown v-else />
15
- </span>
16
- </button>
17
- </template>
18
-
19
- <script>
20
- import { IconChevronDown, IconChevronUp } from '@veritree/icons';
21
- import { keys } from '../utils/keyboard';
22
-
23
- export default {
24
- name: 'VTListboxButton',
25
-
26
- components: { IconChevronDown, IconChevronUp },
27
-
28
- inject: ['api'],
29
-
30
- props: {
31
- theme: {
32
- type: String,
33
- default: null,
34
- validator(value) {
35
- return ['dark'].includes(value);
36
- },
37
- },
38
- },
39
-
40
- data() {
41
- return {
42
- expanded: false,
43
- };
44
- },
45
-
46
- mounted() {
47
- this.api().registerListboxButton(this);
48
- },
49
-
50
- methods: {
51
- focus() {
52
- this.$el.focus();
53
- this.toggleExpanded();
54
- },
55
-
56
- toggleExpanded() {
57
- this.expanded = !this.expanded;
58
- },
59
-
60
- onClick() {
61
- const listbox = this.api().getListbox();
62
- this.expanded ? listbox.hide() : listbox.show();
63
-
64
- this.toggleExpanded();
65
- },
66
-
67
- onKeyDown(event) {
68
- const key = event.key;
69
- const listbox = this.api().getListbox();
70
-
71
- switch (key) {
72
- case keys.down:
73
- event.preventDefault();
74
- listbox.show();
75
- this.toggleExpanded();
76
- break;
77
- }
78
- },
79
- },
80
- };
81
- </script>
@@ -1,126 +0,0 @@
1
- <template>
2
- <li
3
- :id="id"
4
- class="Listbox-option"
5
- role="option"
6
- @mousedown.prevent="onMouseDown"
7
- @mousemove="onMousemove"
8
- @mouseout="onMouseleave"
9
- >
10
- <slot></slot>
11
- </li>
12
- </template>
13
-
14
- <script>
15
- import { scrollElementIntoView } from '../utils/components';
16
- import { genId } from '../utils/ids';
17
- import { areObjsEqual, isObj } from '../utils/objects';
18
-
19
- export default {
20
- name: 'VTListboxOption',
21
-
22
- inject: ['api'],
23
-
24
- props: {
25
- value: {
26
- type: [String, Number, Object],
27
- required: true,
28
- },
29
- selected: {
30
- type: Boolean,
31
- default: false,
32
- },
33
- },
34
-
35
- data() {
36
- return {
37
- id: `listbox-option-${genId()}`,
38
- focused: false,
39
- isMousemove: false,
40
- parent: null,
41
- };
42
- },
43
-
44
- watch: {
45
- focused(newValue) {
46
- if (!newValue) return;
47
-
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
- },
52
- },
53
-
54
- mounted() {
55
- this.api().registerOption(this);
56
-
57
- if (this.selected) {
58
- this.select();
59
- this.focus();
60
- }
61
-
62
- // todo: make sure it works with other values than objects
63
- const listboxOptionSelected = this.api().getListboxValue();
64
-
65
- if (!listboxOptionSelected) {
66
- return;
67
- }
68
-
69
- if (isObj(this.value)) {
70
- if (areObjsEqual(this.value, listboxOptionSelected)) {
71
- this.select();
72
- this.focus();
73
- }
74
-
75
- return;
76
- }
77
-
78
- if (this.value === listboxOptionSelected) {
79
- this.select();
80
- this.focus();
81
- }
82
- },
83
-
84
- beforeDestroy() {
85
- this.api().unregisterOption(this.id);
86
- },
87
-
88
- methods: {
89
- focus() {
90
- this.focused = true;
91
- this.$el.classList.add('is-focused');
92
- },
93
-
94
- unfocus() {
95
- this.focused = false;
96
- this.$el.classList.remove('is-focused');
97
- },
98
-
99
- select() {
100
- this.$el.setAttribute('aria-selected', true);
101
- },
102
-
103
- unselect() {
104
- this.$el.setAttribute('aria-selected', false);
105
- },
106
-
107
- onMouseDown(event) {
108
- if (event.buttons === 1) this.api().selectOption(this);
109
- },
110
-
111
- // Mousemove instead of mouseover to support keyboard navigation.
112
- // The problem with mouseover is that when scrolling (scrollIntoView),
113
- // mouseover event gets triggered.
114
- onMousemove() {
115
- this.isMousemove = true;
116
- this.api().unfocusOptions();
117
- this.focus();
118
- },
119
-
120
- onMouseleave() {
121
- this.isMousemove = false;
122
- this.unfocus();
123
- },
124
- },
125
- };
126
- </script>
@@ -1,119 +0,0 @@
1
- <template>
2
- <ul
3
- v-if="visible"
4
- aria-orientation="vertical"
5
- role="listbox"
6
- class="Listbox-options"
7
- tabindex="-1"
8
- @keydown="onKeyDown"
9
- @blur="onBlur"
10
- >
11
- <slot></slot>
12
- </ul>
13
- </template>
14
-
15
- <script>
16
- import { keys } from '../utils/keyboard';
17
- let timer = null;
18
-
19
- export default {
20
- name: 'VTListboxOptions',
21
-
22
- inject: ['api'],
23
-
24
- data() {
25
- return {
26
- visible: false,
27
- filter: '',
28
- };
29
- },
30
-
31
- watch: {
32
- visible(newValue) {
33
- if (newValue) {
34
- this.$nextTick(() => {
35
- this.$el.focus();
36
- this.handleOptionFocus();
37
- });
38
- } else {
39
- const listboxButton = this.api().getListboxButton();
40
-
41
- this.$nextTick(() => {
42
- listboxButton.focus();
43
- });
44
- }
45
- },
46
- },
47
-
48
- mounted() {
49
- this.api().registerListbox(this);
50
- },
51
-
52
- methods: {
53
- show() {
54
- this.visible = true;
55
- },
56
-
57
- hide() {
58
- this.visible = false;
59
- },
60
-
61
- handleOptionFocus() {
62
- const selectedIndex = this.api().getFocusedIndex();
63
- if (!selectedIndex) this.api().focusFirstOption();
64
- },
65
-
66
- updateActiveDescendant(id) {
67
- this.$el.setAttribute('aria-activedescendant', id);
68
- },
69
-
70
- onKeyDown(event) {
71
- const key = event.key;
72
- const code = event.code;
73
-
74
- if (code.includes('Key')) {
75
- this.filter += code.replace('Key', '');
76
-
77
- clearTimeout(timer);
78
-
79
- timer = setTimeout(() => {
80
- this.api().focusOptionByFilter(this.filter);
81
- this.filter = '';
82
- }, 100);
83
- }
84
-
85
- switch (key) {
86
- case keys.up:
87
- event.preventDefault();
88
- this.api().focusPreviousOption();
89
- break;
90
- case keys.down:
91
- event.preventDefault();
92
- this.api().focusNextOption();
93
- break;
94
- case keys.home:
95
- event.preventDefault();
96
- this.api().focusFirstOption();
97
- break;
98
- case keys.end:
99
- event.preventDefault();
100
- this.api().focusLastOption();
101
- break;
102
- case keys.enter:
103
- event.preventDefault();
104
- this.api().selectOption();
105
- break;
106
- case keys.escape:
107
- this.hide();
108
- break;
109
- default:
110
- break;
111
- }
112
- },
113
-
114
- onBlur() {
115
- this.hide();
116
- },
117
- },
118
- };
119
- </script>