@farm-investimentos/front-mfe-components 15.0.6 → 15.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm-investimentos/front-mfe-components",
3
- "version": "15.0.6",
3
+ "version": "15.0.7",
4
4
  "author": "farm investimentos",
5
5
  "private": false,
6
6
  "main": "./dist/front-mfe-components.common.js",
@@ -74,7 +74,7 @@ export default Vue.extend({
74
74
  } as any);
75
75
 
76
76
  const inputValue = ref(props.value);
77
-
77
+
78
78
  let hasBeenBoostrapped = false;
79
79
 
80
80
  const outClick = event => {
@@ -23,4 +23,11 @@
23
23
  color: var(--farm-error-base);
24
24
  }
25
25
  }
26
+
27
+ &:has(.farm-tooltip):not(.farm-label--required) {
28
+ .farm-tooltip::before {
29
+ content: '';
30
+ margin-left: 2px;
31
+ }
32
+ }
26
33
  }
@@ -32,6 +32,20 @@ export const Required = () => ({
32
32
  });
33
33
 
34
34
  export const Tooltip = () => ({
35
+ template: `<div>
36
+ <farm-label>
37
+ Label with tooltip
38
+ <farm-tooltip>
39
+ this is the tooltip!
40
+ <template v-slot:activator="{ on, attrs }">
41
+ <farm-icon size="sm" color="gray">help-circle</farm-icon>
42
+ </template>
43
+ </farm-tooltip>
44
+ </farm-label>
45
+ </div>`,
46
+ });
47
+
48
+ export const TooltipWithRequired = () => ({
35
49
  template: `<div>
36
50
  <farm-label required>
37
51
  Label with tooltip
@@ -1,14 +1,73 @@
1
1
  <template>
2
- <ul>
2
+ <ul ref="contentRef" @focusin="onFocusin" @focusout="onFocusout" @focus="onFocus">
3
3
  <slot></slot>
4
4
  </ul>
5
5
  </template>
6
6
  <script lang="ts">
7
- import Vue from 'vue';
7
+ import Vue, { onMounted, onUnmounted, ref } from 'vue';
8
+ import { useFocus } from './composition';
8
9
 
9
10
  export default Vue.extend({
10
11
  name: 'farm-list',
11
- setup() {},
12
+ setup(_, { emit }) {
13
+ const contentRef = ref<HTMLElement>();
14
+ const isFocused = ref(false);
15
+ const { focus } = useFocus(contentRef);
16
+
17
+ onMounted(() => {
18
+ contentRef.value.querySelectorAll('[tabindex]:not([tabindex="-1"]), li').forEach(tag =>
19
+ tag.addEventListener('keydown', (e: KeyboardEvent) => {
20
+ e.preventDefault();
21
+ if (isFocused.value) {
22
+ emit('keydown', e);
23
+ }
24
+ })
25
+ );
26
+ });
27
+
28
+ onUnmounted(() => {
29
+ if (contentRef.value) {
30
+ contentRef.value
31
+ .querySelectorAll('[tabindex]:not([tabindex="-1"]), li')
32
+ .forEach(tag =>
33
+ tag.removeEventListener('keydown', (e: KeyboardEvent) => {
34
+ e.preventDefault();
35
+ if (isFocused.value) {
36
+ emit('keydown', e);
37
+ }
38
+ })
39
+ );
40
+ }
41
+ });
42
+
43
+ function onFocusin() {
44
+ isFocused.value = true;
45
+ }
46
+
47
+ function onFocusout() {
48
+ isFocused.value = false;
49
+ }
50
+
51
+ function onFocus(e: FocusEvent) {
52
+ if (
53
+ !(
54
+ isFocused.value ||
55
+ (e.relatedTarget && contentRef.value.contains(e.relatedTarget as Node))
56
+ )
57
+ ) {
58
+ focus();
59
+ }
60
+ }
61
+
62
+ return {
63
+ contentRef,
64
+ focus,
65
+ onFocus,
66
+ onFocusin,
67
+ onFocusout,
68
+ isFocused,
69
+ };
70
+ },
12
71
  });
13
72
  </script>
14
73
  <style lang="scss" scoped>
@@ -3,9 +3,11 @@ import List from '../List';
3
3
 
4
4
  describe('List component', () => {
5
5
  let wrapper;
6
+ let component;
6
7
 
7
8
  beforeEach(() => {
8
9
  wrapper = shallowMount(List);
10
+ component = wrapper.vm;
9
11
  });
10
12
 
11
13
  test('Created hook', () => {
@@ -17,4 +19,15 @@ describe('List component', () => {
17
19
  expect(wrapper.element).toMatchSnapshot();
18
20
  });
19
21
  });
22
+ describe('Methods', () => {
23
+ it('should isFocused be true when call onFocusin', () => {
24
+ component.onFocusin();
25
+ expect(component.isFocused).toBeTruthy();
26
+ });
27
+
28
+ it('should isFocused be false when call onFocusout', () => {
29
+ component.onFocusout();
30
+ expect(component.isFocused).toBeFalsy();
31
+ });
32
+ });
20
33
  });
@@ -0,0 +1,3 @@
1
+ import useFocus from './useFocus';
2
+
3
+ export { useFocus };
@@ -0,0 +1,49 @@
1
+ import { Ref } from 'vue';
2
+
3
+ type UseFocusReturn = {
4
+ focus: (location?: 'next' | 'prev' | 'first' | 'last') => void;
5
+ };
6
+
7
+ export default function useFocus(contentRef: Ref<HTMLElement>): UseFocusReturn {
8
+ function focus(location?: 'next' | 'prev' | 'first' | 'last') {
9
+ if (!contentRef.value) return;
10
+
11
+ const focusable = [
12
+ ...contentRef.value.querySelectorAll('[tabindex]:not([tabindex="-1"]), li'),
13
+ ].filter(el => !el.hasAttribute('disabled')) as HTMLElement[];
14
+
15
+ const idx = focusable.indexOf(document.activeElement as HTMLElement);
16
+
17
+ if (!location) {
18
+ if (!contentRef.value.contains(document.activeElement)) {
19
+ focusable[0].focus();
20
+ }
21
+ } else if (location === 'first') {
22
+ const savedTabIndex = focusable[0].getAttribute('tabindex');
23
+
24
+ focusable[0].setAttribute('tabindex', '-1');
25
+ focusable[0].focus();
26
+ focusable[0].setAttribute('tabindex', savedTabIndex);
27
+ } else if (location === 'last') {
28
+ focusable[focusable.length - 1].focus();
29
+ } else {
30
+ let el;
31
+ let idxx = idx;
32
+ const inc = location === 'next' ? 1 : -1;
33
+
34
+ do {
35
+ idxx += inc;
36
+ el = focusable[idxx];
37
+ } while ((!el || el.offsetParent == null) && idxx < focusable.length && idxx >= 0);
38
+ if (el) {
39
+ el.focus();
40
+ } else {
41
+ focus(location === 'next' ? 'first' : 'last');
42
+ }
43
+ }
44
+ }
45
+
46
+ return {
47
+ focus,
48
+ };
49
+ }
@@ -1,39 +1,37 @@
1
1
  @import '../../configurations/theme-colors';
2
2
 
3
3
  .farm-listitem {
4
- list-style-type: none;
5
- transition: background-color 300ms linear;
6
- padding: 8px;
7
- height: 36px;
8
- display: flex;
9
- align-items: center;
10
-
11
- &--clickable {
12
- cursor: pointer;
13
- }
14
-
15
- >a {
16
- text-decoration: none;
17
- }
18
-
19
- &:hover {
20
-
21
- border-radius: 5px;
22
-
23
- @each $color in $theme-colors-list {
24
-
25
- &#{'.farm-listitem--' + $color + '-base'} {
26
- background-color: rgba(themeColor($color), 0.27);
27
- }
28
-
29
- &#{'.farm-listitem--' + $color + '-lighten'} {
30
- background-color: rgba(themeColor($color, 'lighten'), 0.27);
31
- }
32
-
33
- &#{'.farm-listitem--' + $color + '-darken'} {
34
- background-color: rgba(themeColor($color, 'darken'), 0.27);
35
- }
36
- }
37
- }
38
-
39
- }
4
+ list-style-type: none;
5
+ transition: background-color 300ms linear;
6
+ padding: 8px;
7
+ height: 36px;
8
+ display: flex;
9
+ align-items: center;
10
+
11
+ &--clickable {
12
+ cursor: pointer;
13
+ }
14
+
15
+ > a {
16
+ text-decoration: none;
17
+ }
18
+
19
+ &:hover,
20
+ &:focus {
21
+ border-radius: 5px;
22
+
23
+ @each $color in $theme-colors-list {
24
+ &#{'.farm-listitem--' + $color + '-base'} {
25
+ background-color: rgba(themeColor($color), 0.27);
26
+ }
27
+
28
+ &#{'.farm-listitem--' + $color + '-lighten'} {
29
+ background-color: rgba(themeColor($color, 'lighten'), 0.27);
30
+ }
31
+
32
+ &#{'.farm-listitem--' + $color + '-darken'} {
33
+ background-color: rgba(themeColor($color, 'darken'), 0.27);
34
+ }
35
+ }
36
+ }
37
+ }
@@ -32,11 +32,18 @@
32
32
  color: var(--farm-primary-base);
33
33
  }
34
34
 
35
+ :deep(.farm-listitem:focus .farm-typography) {
36
+ color: var(--farm-primary-base);
37
+ }
38
+
35
39
  :deep(.farm-listitem:hover .farm-checkbox .farm-icon) {
36
40
  color: var(--farm-primary-lighten);
37
41
  }
38
42
 
43
+ :deep(.farm-listitem:focus .farm-checkbox .farm-icon) {
44
+ color: var(--farm-primary-lighten);
45
+ }
39
46
 
40
47
  :deep(.farm-listitem:hover .farm-checkbox.farm-checkbox--checked .farm-icon) {
41
48
  color: white;
42
- }
49
+ }
@@ -134,7 +134,6 @@ export const Validate = () => ({
134
134
  return {
135
135
  v: null,
136
136
  items: [
137
- { value: null, text: '' },
138
137
  { value: 1, text: ' value 1' },
139
138
  { value: 2, text: ' value 2' },
140
139
  { value: 3, text: ' value 3' },
@@ -14,8 +14,9 @@
14
14
  :id="customId"
15
15
  >
16
16
  <farm-contextmenu bottom v-model="isVisible" :stay-open="multiple" ref="contextmenu">
17
- <farm-list v-if="!readonly">
17
+ <farm-list v-if="!readonly" ref="listRef" @keydown="onKeyDown">
18
18
  <farm-listitem
19
+ tabindex="0"
19
20
  v-for="(item, index) in items"
20
21
  clickable
21
22
  hoverColorVariation="lighten"
@@ -45,7 +46,10 @@
45
46
  </farm-listitem>
46
47
  </farm-list>
47
48
  <template v-slot:activator="{}">
48
- <div class="farm-textfield--input farm-textfield--input--iconed">
49
+ <div
50
+ class="farm-textfield--input farm-textfield--input--iconed"
51
+ @keydown="onKeyDown"
52
+ >
49
53
  <input
50
54
  v-bind="$attrs"
51
55
  v-model="selectedText"
@@ -57,7 +61,11 @@
57
61
  @focusin="onFocus(true)"
58
62
  @focusout="onFocus(false)"
59
63
  />
60
- <farm-icon color="gray" :class="{ 'farm-icon--rotate': isVisible }">
64
+ <farm-icon
65
+ color="gray"
66
+ :class="{ 'farm-icon--rotate': isVisible }"
67
+ @click="addFocusToInput"
68
+ >
61
69
  menu-down
62
70
  </farm-icon>
63
71
  </div>
@@ -231,8 +239,13 @@ export default Vue.extend({
231
239
  checked,
232
240
  notChecked,
233
241
  inputField,
242
+ keys,
234
243
  } = buildData(props);
235
244
 
245
+ const listRef = ref();
246
+
247
+ const contextmenu = ref(null);
248
+
236
249
  const { errorBucket, valid, validatable } = validateFormStateBuilder();
237
250
 
238
251
  let fieldValidator = validateFormFieldBuilder(rules.value);
@@ -251,7 +264,10 @@ export default Vue.extend({
251
264
  newValue => {
252
265
  innerValue.value = newValue;
253
266
  errorBucket.value = [];
254
- if(multiple.value && newValue === null || (Array.isArray(newValue) && newValue.length === 0)) {
267
+ if (
268
+ (multiple.value && newValue === null) ||
269
+ (Array.isArray(newValue) && newValue.length === 0)
270
+ ) {
255
271
  multipleValues.value = [];
256
272
  }
257
273
  validate(newValue);
@@ -400,8 +416,27 @@ export default Vue.extend({
400
416
  );
401
417
  };
402
418
 
403
- const contextmenu = ref(null);
419
+ function onKeyDown(e) {
420
+ if (props.readonly) return;
421
+
422
+ if (['Space'].includes(e.code)) {
423
+ isVisible.value = true;
424
+ e.currentTarget.click();
425
+ }
426
+ if (['Escape'].includes(e.code)) {
427
+ isVisible.value = false;
428
+ }
429
+
430
+ if (keys[e.code]) {
431
+ listRef.value.focus(keys[e.code]);
432
+ }
433
+
434
+ e.preventDefault();
435
+ }
404
436
 
437
+ function addFocusToInput() {
438
+ inputField.value.focus();
439
+ }
405
440
 
406
441
  return {
407
442
  items,
@@ -432,6 +467,9 @@ export default Vue.extend({
432
467
  multipleValues,
433
468
  addLabelToMultiple,
434
469
  inputField,
470
+ onKeyDown,
471
+ addFocusToInput,
472
+ listRef,
435
473
  };
436
474
  },
437
475
  });
@@ -124,5 +124,51 @@ describe('Select component', () => {
124
124
  expect(component.selectedText).toBe('value 0 (+2 outros)');
125
125
  });
126
126
  });
127
+
128
+ describe('onKeyDown', () => {
129
+ it('should open the ContextMenu and click on current element', () => {
130
+ const event = {
131
+ code: 'Space',
132
+ preventDefault: jest.fn(),
133
+ currentTarget: {
134
+ click: jest.fn(),
135
+ },
136
+ };
137
+ component.onKeyDown(event);
138
+
139
+ expect(component.isVisible).toBeTruthy();
140
+ expect(event.currentTarget.click).toHaveBeenCalled();
141
+ expect(event.preventDefault).toHaveBeenCalled();
142
+ });
143
+ it('should open the ContextMenu and click on current element when prop readonly is true', async () => {
144
+ await wrapper.setProps({
145
+ readonly: true,
146
+ });
147
+ const event = {
148
+ code: 'Space',
149
+ preventDefault: jest.fn(),
150
+ currentTarget: {
151
+ click: jest.fn(),
152
+ },
153
+ };
154
+ component.onKeyDown(event);
155
+
156
+ expect(component.isVisible).toBeFalsy();
157
+ expect(event.currentTarget.click).not.toHaveBeenCalled();
158
+ expect(event.preventDefault).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('should close the ContextMenu', () => {
162
+ const event = {
163
+ code: 'Escape',
164
+ preventDefault: jest.fn(),
165
+ };
166
+ component.onKeyDown(event);
167
+
168
+ expect(component.isVisible).toBeFalsy();
169
+
170
+ expect(event.preventDefault).toHaveBeenCalled();
171
+ });
172
+ });
127
173
  });
128
174
  });
@@ -11,6 +11,12 @@ export default function (props) {
11
11
  const checked = ref('1');
12
12
  const notChecked = ref(false);
13
13
  const inputField = ref();
14
+ const keys = {
15
+ ArrowDown: 'next',
16
+ ArrowUp: 'prev',
17
+ Home: 'first',
18
+ End: 'last',
19
+ };
14
20
 
15
21
  return {
16
22
  multipleValues,
@@ -23,5 +29,6 @@ export default function (props) {
23
29
  checked,
24
30
  notChecked,
25
31
  inputField,
32
+ keys,
26
33
  };
27
34
  }
@@ -90,4 +90,23 @@ export const Colors = () => ({
90
90
  </farm-col>
91
91
 
92
92
  </farm-row>`,
93
- });
93
+ });
94
+
95
+ export const OutsideChangeVmodel = () => ({
96
+ data() {
97
+ return {
98
+ selectedValue: true,
99
+ };
100
+ },
101
+ methods: {
102
+ onClick() {
103
+ this.selectedValue = !this.selectedValue;
104
+ },
105
+ },
106
+ template: `<div>
107
+ <farm-switcher v-model="selectedValue" block />
108
+ <farm-btn @click="onClick">
109
+ toggle selection: {{ selectedValue }}
110
+ </farm-btn>
111
+ </div>`,
112
+ });
@@ -11,7 +11,7 @@
11
11
  </div>
12
12
  </template>
13
13
  <script lang="ts">
14
- import Vue, { PropType } from 'vue';
14
+ import Vue, { PropType, computed, ref, watch } from 'vue';
15
15
 
16
16
  export default Vue.extend({
17
17
  name: 'farm-switcher',
@@ -47,37 +47,45 @@ export default Vue.extend({
47
47
  * Is disabled?
48
48
  */
49
49
  disabled: { type: Boolean, default: false },
50
- },
51
- data() {
52
- return {
53
- isDisabled: this.disabled,
54
- };
55
- },
56
- computed: {
57
- backgroundStyles() {
58
- return {
59
- 'farm-switch--selected': this.value && !this.isDisabled,
60
- 'farm-switch--idle': !this.value && !this.isDisabled,
61
- 'farm-switch--disabled-on': this.value && this.isDisabled,
62
- 'farm-switch--disabled-off': !this.value && this.isDisabled,
63
- };
64
- },
65
- indicatorStyles() {
66
- return { transform: this.value ? 'translateX(16px)' : 'translateX(0)' };
67
- },
68
- },
69
- watch: {
70
- disabled(newValue: boolean) {
71
- this.isDisabled = newValue;
50
+ /**
51
+ * The updated bound model<br />
52
+ * _event_
53
+ */
54
+ input: {
55
+ type: Function,
56
+ // eslint-disable-next-line
57
+ default: (value: [String, Number]) => {},
72
58
  },
73
59
  },
74
- methods: {
75
- toggle() {
76
- if (this.isDisabled) {
60
+ setup(props, { emit }) {
61
+ const isDisabled = ref(props.disabled);
62
+
63
+ const backgroundStyles = computed(() => ({
64
+ 'farm-switch--selected': props.value && !isDisabled.value,
65
+ 'farm-switch--idle': !props.value && !isDisabled.value,
66
+ 'farm-switch--disabled-on': props.value && isDisabled.value,
67
+ 'farm-switch--disabled-off': !props.value && isDisabled.value,
68
+ }));
69
+
70
+ const indicatorStyles = computed(() => ({
71
+ transform: props.value ? 'translateX(16px)' : 'translateX(0)',
72
+ }));
73
+
74
+ watch(
75
+ () => props.disabled,
76
+ newValue => {
77
+ isDisabled.value = newValue;
78
+ }
79
+ );
80
+
81
+ const toggle = () => {
82
+ if (isDisabled.value) {
77
83
  return false;
78
84
  }
79
- this.$emit('input', !this.value);
80
- },
85
+ emit('input', !props.value);
86
+ };
87
+
88
+ return { isDisabled, backgroundStyles, indicatorStyles, toggle };
81
89
  },
82
90
  });
83
91
  </script>