@fiscozen/composables 0.1.20-beta.2 → 0.1.21

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": "@fiscozen/composables",
3
- "version": "0.1.20-beta.2",
3
+ "version": "0.1.21",
4
4
  "description": "Design System utility composables",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, useSlots, watch, toRef, computed } from 'vue'
2
+ import { ref, useSlots, watch, toRef, computed, onMounted, onBeforeUnmount } from 'vue'
3
3
  import { useFloating } from './composables'
4
4
  import { FzFloatingProps, FzUseFloatingArgs } from './types'
5
5
 
@@ -9,18 +9,28 @@ const props = withDefaults(defineProps<FzFloatingProps>(), {
9
9
  teleport: false
10
10
  })
11
11
 
12
+ const emits = defineEmits(['fzfloating:setPosition'])
13
+
12
14
  const opener = ref(null)
13
15
  const content = ref<HTMLElement | null>(null)
14
16
 
15
17
  const slots = useSlots()
16
18
 
19
+ let scheduledAnimationFrame = false
20
+
17
21
  const useFloatingOpts: FzUseFloatingArgs = {
18
22
  position: computed(() => props.position),
19
23
  element: {
24
+ // @ts-ignore
20
25
  domRef: content
21
26
  },
22
27
  container: {
28
+ // @ts-ignore
23
29
  domRef: toRef(props.container || document.body)
30
+ },
31
+ useViewport: computed(() => props.useViewport),
32
+ callback(...args) {
33
+ emits('fzfloating:setPosition', ...args)
24
34
  }
25
35
  }
26
36
  if (slots.opener) {
@@ -31,22 +41,39 @@ if (slots.opener) {
31
41
 
32
42
  const floating = useFloating(useFloatingOpts)
33
43
 
44
+ const setPositionWhenOpen = () => {
45
+ if (scheduledAnimationFrame) {
46
+ return
47
+ }
48
+
49
+ scheduledAnimationFrame = true
50
+ requestAnimationFrame(() => {
51
+ props.isOpen && floating.setPosition()
52
+ scheduledAnimationFrame = false
53
+ })
54
+ }
55
+
34
56
  watch(
35
57
  () => props.position,
36
- () => props.isOpen && floating.setPosition()
58
+ () => setPositionWhenOpen()
37
59
  )
38
60
  watch(
39
61
  () => props.isOpen,
40
62
  (newVal) => {
41
63
  if (!newVal || !content.value) {
64
+ window.removeEventListener('scroll', setPositionWhenOpen)
42
65
  return
43
66
  }
67
+ window.addEventListener('scroll', setPositionWhenOpen)
44
68
  content.value.style.top = '0px'
45
69
  content.value.style.left = '0px'
46
70
  content.value.style.transform = 'none'
47
71
  floating.setPosition()
48
72
  }
49
73
  )
74
+ onBeforeUnmount(() => {
75
+ window.removeEventListener('scroll', setPositionWhenOpen)
76
+ })
50
77
 
51
78
  const contentClass = computed(() => {
52
79
  if (props.overrideContentClass) {
@@ -68,7 +95,7 @@ const contentClass = computed(() => {
68
95
  v-if="!teleport"
69
96
  ref="content"
70
97
  v-show="$slots.default && (!$slots.opener || ($slots.opener && isOpen))"
71
- class="fz__floating__content bg-core-white fixed p-4 z-10"
98
+ class="fz__floating__content bg-core-white fixed z-10 p-4"
72
99
  :class="contentClass"
73
100
  >
74
101
  <slot :isOpen :floating></slot>
@@ -77,7 +104,7 @@ const contentClass = computed(() => {
77
104
  <div
78
105
  ref="content"
79
106
  v-show="$slots.default && (!$slots.opener || ($slots.opener && isOpen))"
80
- class="fz__floating__content bg-core-white absolute p-4 z-10"
107
+ class="fz__floating__content bg-core-white fixed z-10 p-4"
81
108
  :class="contentClass"
82
109
  >
83
110
  <slot :isOpen :floating></slot>
@@ -3,107 +3,119 @@
3
3
  exports[`FzFloating > should match snapshot 1`] = `
4
4
  "<div>
5
5
  <div class="inline-flex"><button>opener</button></div>
6
- <div class="bg-core-white absolute p-4 z-10">
6
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
7
7
  <div>content</div>
8
8
  </div>
9
+ <!--v-if-->
9
10
  </div>"
10
11
  `;
11
12
 
12
13
  exports[`FzFloating > should match snapshot 2`] = `
13
14
  "<div>
14
15
  <div class="inline-flex"><button>opener</button></div>
15
- <div class="bg-core-white absolute p-4 z-10">
16
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
16
17
  <div>content</div>
17
18
  </div>
19
+ <!--v-if-->
18
20
  </div>"
19
21
  `;
20
22
 
21
23
  exports[`FzFloating > should match snapshot 3`] = `
22
24
  "<div>
23
25
  <div class="inline-flex"><button>opener</button></div>
24
- <div class="bg-core-white absolute p-4 z-10">
26
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
25
27
  <div>content</div>
26
28
  </div>
29
+ <!--v-if-->
27
30
  </div>"
28
31
  `;
29
32
 
30
33
  exports[`FzFloating > should match snapshot 4`] = `
31
34
  "<div>
32
35
  <div class="inline-flex"><button>opener</button></div>
33
- <div class="bg-core-white absolute p-4 z-10">
36
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
34
37
  <div>content</div>
35
38
  </div>
39
+ <!--v-if-->
36
40
  </div>"
37
41
  `;
38
42
 
39
43
  exports[`FzFloating > should match snapshot 5`] = `
40
44
  "<div>
41
45
  <div class="inline-flex"><button>opener</button></div>
42
- <div class="bg-core-white absolute p-4 z-10">
46
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
43
47
  <div>content</div>
44
48
  </div>
49
+ <!--v-if-->
45
50
  </div>"
46
51
  `;
47
52
 
48
53
  exports[`FzFloating > should match snapshot 6`] = `
49
54
  "<div>
50
55
  <div class="inline-flex"><button>opener</button></div>
51
- <div class="bg-core-white absolute p-4 z-10">
56
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
52
57
  <div>content</div>
53
58
  </div>
59
+ <!--v-if-->
54
60
  </div>"
55
61
  `;
56
62
 
57
63
  exports[`FzFloating > should match snapshot 7`] = `
58
64
  "<div>
59
65
  <div class="inline-flex"><button>opener</button></div>
60
- <div class="bg-core-white absolute p-4 z-10">
66
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
61
67
  <div>content</div>
62
68
  </div>
69
+ <!--v-if-->
63
70
  </div>"
64
71
  `;
65
72
 
66
73
  exports[`FzFloating > should match snapshot 8`] = `
67
74
  "<div>
68
75
  <div class="inline-flex"><button>opener</button></div>
69
- <div class="bg-core-white absolute p-4 z-10">
76
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
70
77
  <div>content</div>
71
78
  </div>
79
+ <!--v-if-->
72
80
  </div>"
73
81
  `;
74
82
 
75
83
  exports[`FzFloating > should match snapshot 9`] = `
76
84
  "<div>
77
85
  <div class="inline-flex"><button>opener</button></div>
78
- <div class="bg-core-white absolute p-4 z-10">
86
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
79
87
  <div>content</div>
80
88
  </div>
89
+ <!--v-if-->
81
90
  </div>"
82
91
  `;
83
92
 
84
93
  exports[`FzFloating > should match snapshot 10`] = `
85
94
  "<div>
86
95
  <div class="inline-flex"><button>opener</button></div>
87
- <div class="bg-core-white absolute p-4 z-10">
96
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
88
97
  <div>content</div>
89
98
  </div>
99
+ <!--v-if-->
90
100
  </div>"
91
101
  `;
92
102
 
93
103
  exports[`FzFloating > should match snapshot 11`] = `
94
104
  "<div>
95
105
  <div class="inline-flex"><button>opener</button></div>
96
- <div class="bg-core-white absolute p-4 z-10">
106
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
97
107
  <div>content</div>
98
108
  </div>
109
+ <!--v-if-->
99
110
  </div>"
100
111
  `;
101
112
 
102
113
  exports[`FzFloating > should match snapshot 12`] = `
103
114
  "<div>
104
115
  <div class="inline-flex"><button>opener</button></div>
105
- <div class="bg-core-white absolute p-4 z-10">
116
+ <div class="fz__floating__content bg-core-white fixed z-10 p-4 bg-core-white fixed p-4 z-10">
106
117
  <div>content</div>
107
118
  </div>
119
+ <!--v-if-->
108
120
  </div>"
109
121
  `;
@@ -4,4 +4,4 @@ export * from './useBreakpoints'
4
4
  export * from './useClickOutside'
5
5
  export * from './useKeyDown'
6
6
  export * from './useKeyUp'
7
- export * from './useCurrency'
7
+ export * from './useCurrency'
@@ -1,9 +1,9 @@
1
1
  import { onBeforeUnmount, onMounted, Ref, watch } from 'vue'
2
2
 
3
3
  function useClickOutside(
4
- component: Ref<HTMLElement | undefined>,
4
+ component: Ref<HTMLElement | undefined>,
5
5
  callback: () => void,
6
- elementToListenClicksOn?: Ref<HTMLElement | undefined>,
6
+ elementToListenClicksOn?: Ref<HTMLElement | undefined>
7
7
  ) {
8
8
  // fail early if any of the required params is missing
9
9
  if (!component) {
@@ -14,7 +14,7 @@ function useClickOutside(
14
14
  throw new Error('A callback has to be provided.')
15
15
  }
16
16
 
17
- const listener = (event: MouseEvent) => {
17
+ const listener = (event: Event) => {
18
18
  if (
19
19
  !component.value ||
20
20
  event.target === component.value ||
@@ -28,12 +28,12 @@ function useClickOutside(
28
28
  }
29
29
 
30
30
  if (elementToListenClicksOn) {
31
- watch(elementToListenClicksOn, (newVal: HTMLElement | undefined, oldVal: HTMLElement | undefined) => {
31
+ watch(elementToListenClicksOn, (newVal: Element | undefined, oldVal: Element | undefined) => {
32
32
  if (oldVal) {
33
33
  oldVal.removeEventListener('click', listener)
34
34
  }
35
35
  newVal?.addEventListener('click', listener)
36
- });
36
+ })
37
37
  }
38
38
 
39
39
  onMounted(() => {
@@ -45,7 +45,7 @@ function useClickOutside(
45
45
  onBeforeUnmount(() => {
46
46
  if (elementToListenClicksOn) {
47
47
  elementToListenClicksOn.value!.removeEventListener('click', listener)
48
- return;
48
+ return
49
49
  }
50
50
  document.removeEventListener('click', listener)
51
51
  })
@@ -1,103 +1,90 @@
1
- import {Ref, watch, getCurrentInstance, computed, ref, nextTick} from 'vue'
1
+ import { Ref, watch, getCurrentInstance, computed, ref, onMounted } from 'vue'
2
2
 
3
3
  export const useCurrency = () => {
4
- const inputRef: Ref<HTMLInputElement|null> = ref(null)
5
- const vm = getCurrentInstance()
4
+ const inputRef: Ref<HTMLInputElement | null> = ref(null)
5
+ const vm = getCurrentInstance()
6
6
 
7
- const computedModel = computed<number|string|undefined>(() => vm?.props.amount as unknown as number|undefined)
8
- const internalVal = ref<number|string>()
7
+ const modelValue = computed<number | undefined>(
8
+ () => vm?.props.modelValue as unknown as number | undefined
9
+ )
9
10
 
10
- const format = (input: number|string) => {
11
- return input.toLocaleString('it-IT', {
12
- minimumFractionDigits: 2,
13
- maximumFractionDigits: 2,
14
- useGrouping: false
15
- })
11
+ const format = (input: number | null) => {
12
+ if (input === null) {
13
+ return '';
16
14
  }
15
+ return input.toLocaleString('it-IT', {
16
+ minimumFractionDigits: 2,
17
+ maximumFractionDigits: 2
18
+ })
19
+ }
17
20
 
18
- const parse = (text: string) => {
19
- // strip currency, handle edge cases...
20
- return parseFloat(text.replace(/,/g,"."))
21
- }
21
+ const parse = (text: string) => {
22
+ // strip currency, handle edge cases...
23
+ return parseFloat(text)
24
+ }
22
25
 
23
- const emitAmount = (val: number) => {
24
- if (Number.isNaN(val)) {
25
- return;
26
- }
27
- if (vm) {
28
- internalVal.value = val
29
- vm.emit('update:amount', typeof computedModel.value === 'number' ? val : val.toString())
30
- }
26
+ const setValue = (val: number | null) => {
27
+ if (Number.isNaN(val)) {
28
+ return
31
29
  }
32
30
 
33
- const setValue = (val: string) => {
34
- // nextTick doesn't seem to work
35
- setTimeout(() => {
36
- if (!inputRef.value) {
37
- return
38
- }
39
- inputRef.value.value = val;
40
- }, 0)
41
- }
31
+ vm && vm.emit('update:modelValue', val)
32
+ }
42
33
 
43
- const onInput = (el: HTMLInputElement) => (e: Event) => {
44
- if (!inputRef.value || !e.target) {
45
- return
46
- }
47
- let {value} = el;
48
- value = value.replace(/[^0-9,.]/g, '')
49
- setValue(value)
50
- const number = parse(value)
51
- emitAmount(Number.isNaN(number) ? 0 : number)
34
+ const onInput = (el: HTMLInputElement) => (e: Event) => {
35
+ if (!inputRef.value || !e.target) {
36
+ return
52
37
  }
38
+ let { value } = el
39
+ value = value.replace(/[^0-9,.]/g, '')
40
+ inputRef.value.value = value
41
+ const numberValue = (vm?.props.nullOnEmpty && value === '') ? null : parse(value)
42
+ setValue(Number.isNaN(numberValue) ? 0 : numberValue)
43
+ }
53
44
 
54
- const onBlur = (e: FocusEvent) => {
55
- if (!inputRef.value || !e.target) {
56
- return
57
- }
58
- let number = parse((e.target as HTMLInputElement).value)
59
- if (Number.isNaN(number)) {
60
- number = 0;
61
- }
62
- const text = format(number)
63
- setValue(text)
64
- emitAmount(parse(text))
45
+ const onBlur = (e: FocusEvent) => {
46
+ if (!inputRef.value || !e.target) {
47
+ return
65
48
  }
49
+ let rawValue = (e.target as HTMLInputElement).value.replace(/,/g, '.');
50
+ let number: number | null;
66
51
 
67
- watch(inputRef, (newVal, oldVal) => {
68
- if(!newVal) {
69
- return
70
- }
52
+ if (rawValue === '' && vm?.props.nullOnEmpty) {
53
+ number = null
54
+ }
55
+ else {
56
+ number = parse(rawValue)
57
+ if (Number.isNaN(number)) {
58
+ number = 0
59
+ }
60
+ }
61
+ const text = format(number)
71
62
 
72
- if (oldVal) {
73
- oldVal?.removeEventListener('input', onInput(newVal))
74
- oldVal?.removeEventListener('blur', onBlur)
75
- }
76
- newVal.addEventListener('input', onInput(newVal))
77
- newVal.addEventListener('blur', onBlur)
78
- })
63
+ inputRef.value.value = text
64
+ setValue(number)
65
+ }
79
66
 
80
- watch(computedModel, (newVal) => {
81
- nextTick(() => {
82
- if (!inputRef.value || !newVal) {
83
- return
84
- }
85
- // we need to format here only if someone externally set the
86
- // value of the amount model
87
- if (internalVal.value !== newVal) {
88
- const formatted = format(newVal)
89
- inputRef.value.value = formatted
90
- internalVal.value = newVal
91
- }
92
- })
93
- })
67
+ watch(inputRef, (newVal, oldVal) => {
68
+ if (!newVal) {
69
+ return
70
+ }
94
71
 
72
+ if (oldVal) {
73
+ oldVal?.removeEventListener('input', onInput(newVal))
74
+ oldVal?.removeEventListener('blur', onBlur)
75
+ }
76
+ newVal.addEventListener('input', onInput(newVal))
77
+ newVal.addEventListener('blur', onBlur)
95
78
 
96
- return {
97
- inputRef,
98
- parse,
99
- format,
100
- emitAmount,
101
- setValue
79
+ if (modelValue.value) {
80
+ newVal.value = format(modelValue.value)
102
81
  }
103
- }
82
+ })
83
+
84
+ return {
85
+ inputRef,
86
+ parse,
87
+ format,
88
+ setValue
89
+ }
90
+ }
@@ -1,19 +1,25 @@
1
1
  import { FzFloatingPosition, FzRect, FzUseFloatingArgs } from '../types'
2
- import { ref, reactive, onUnmounted, Ref, nextTick, computed } from 'vue'
2
+ import { ref, reactive, onUnmounted, Ref, nextTick } from 'vue'
3
3
  import { calcRealPos, getHighestAvailableSpacePos } from '../utils'
4
4
 
5
5
  export const useFloating = (
6
6
  args: FzUseFloatingArgs
7
7
  ): {
8
8
  float: FzRect
9
- rect: Ref<DOMRect | null>
10
- floatObserver: Ref<IntersectionObserver>
9
+ rect: Ref<DOMRect | undefined>
11
10
  setPosition: () => Promise<void>
11
+ position: Ref<FzFloatingPosition>
12
+ actualPosition?: Ref<FzFloatingPosition | undefined>
13
+ openerRect: Ref<DOMRect | undefined>
14
+ containerRect: Ref<DOMRect | undefined>
12
15
  } => {
13
16
  const safeElementDomRef = ref<HTMLElement | null>(null)
14
17
  const safeContainerDomRef = ref<HTMLElement | null>(null)
15
18
  const safeOpenerDomRef = ref<HTMLElement | null>(null)
16
- const rect = ref<DOMRect | null>(null)
19
+ const openerRect = ref<DOMRect | undefined>()
20
+ const containerRect = ref<DOMRect | undefined>()
21
+ const position = ref<FzFloatingPosition>('auto')
22
+ const rect = ref<DOMRect | undefined>()
17
23
  const float = reactive<FzRect>({
18
24
  position: { x: 0, y: 0 }
19
25
  })
@@ -30,20 +36,24 @@ export const useFloating = (
30
36
  ) => {}
31
37
 
32
38
  const floatObserver = ref(new IntersectionObserver(handleIntersect, options))
39
+ const actualPosition = ref<FzFloatingPosition>()
33
40
 
34
41
  const setPosition = () =>
35
42
  nextTick(() => {
36
- safeElementDomRef.value =
43
+ actualPosition.value = args.position ? args.position.value : 'auto'
44
+ safeElementDomRef.value = (
37
45
  typeof args.element.domRef.value === 'string'
38
46
  ? document.querySelector(args.element.domRef.value)
39
47
  : args.element.domRef.value
48
+ ) as HTMLElement
40
49
  if (!args.container) {
41
50
  safeContainerDomRef.value = document.body
42
51
  } else {
43
- safeContainerDomRef.value =
52
+ safeContainerDomRef.value = (
44
53
  typeof args.container.domRef.value === 'string'
45
54
  ? document.querySelector(args.container.domRef.value)
46
55
  : args.container.domRef.value
56
+ ) as HTMLElement
47
57
  safeContainerDomRef.value ??= document.body
48
58
  }
49
59
 
@@ -52,78 +62,104 @@ export const useFloating = (
52
62
  }
53
63
 
54
64
  if (args.opener) {
55
- safeOpenerDomRef.value =
65
+ safeOpenerDomRef.value = (
56
66
  typeof args.opener.domRef.value === 'string'
57
67
  ? document.querySelector(args.opener.domRef.value)
58
68
  : args.opener.domRef.value
69
+ ) as HTMLElement
59
70
  }
60
71
 
61
72
  rect.value = safeElementDomRef.value.getBoundingClientRect()
62
- const openerRect = safeOpenerDomRef.value?.getBoundingClientRect()
63
- const containerRect = safeContainerDomRef.value.getBoundingClientRect()
73
+ openerRect.value = safeOpenerDomRef.value?.getBoundingClientRect()
74
+ containerRect.value = safeContainerDomRef.value.getBoundingClientRect()
64
75
 
65
- const elStyle = window.getComputedStyle(safeElementDomRef.value)
76
+ const elStyle = window.getComputedStyle(safeElementDomRef.value as HTMLElement)
66
77
 
67
78
  // multiple observer calls on same target do not cause multiple registrations
68
- floatObserver.value.observe(safeElementDomRef.value)
69
- floatObserver.value.observe(safeContainerDomRef.value)
70
-
71
- let position: FzFloatingPosition = args.position ? args.position.value : 'auto'
79
+ floatObserver.value.observe(safeElementDomRef.value as HTMLElement)
80
+ floatObserver.value.observe(safeContainerDomRef.value as HTMLElement)
72
81
 
73
82
  let translateY = 0
74
83
  let translateX = 0
75
84
 
76
- if (args.opener && safeOpenerDomRef.value && openerRect) {
85
+ if (args.opener && safeOpenerDomRef.value && openerRect && openerRect.value) {
77
86
  const leftWithoutXMargin =
78
- openerRect.left - parseFloat(elStyle.marginLeft) - parseFloat(elStyle.marginRight)
79
- const leftWithoutLeftMargin = openerRect.left - parseFloat(elStyle.marginLeft)
87
+ openerRect.value.left - parseFloat(elStyle.marginLeft) - parseFloat(elStyle.marginRight)
88
+ const leftWithoutLeftMargin = openerRect.value.left - parseFloat(elStyle.marginLeft)
80
89
  const topWithoutYMargin =
81
- openerRect.top - parseFloat(elStyle.marginTop) - parseFloat(elStyle.marginBottom)
82
- const topWithoutTopMargin = openerRect.top - parseFloat(elStyle.marginTop)
90
+ openerRect.value.top - parseFloat(elStyle.marginTop) - parseFloat(elStyle.marginBottom)
91
+ const topWithoutTopMargin = openerRect.value.top - parseFloat(elStyle.marginTop)
83
92
 
84
- switch (position) {
93
+ switch (actualPosition.value) {
85
94
  case 'auto':
86
- position = getHighestAvailableSpacePos(
87
- safeContainerDomRef.value,
88
- safeElementDomRef.value,
89
- safeOpenerDomRef.value
95
+ actualPosition.value = getHighestAvailableSpacePos(
96
+ args.useViewport ? null : (safeContainerDomRef.value as HTMLElement),
97
+ safeElementDomRef.value as HTMLElement,
98
+ safeOpenerDomRef.value as HTMLElement
99
+ )
100
+ break
101
+ case 'auto-vertical':
102
+ actualPosition.value = getHighestAvailableSpacePos(
103
+ args.useViewport ? null : (safeContainerDomRef.value as HTMLElement),
104
+ safeElementDomRef.value as HTMLElement,
105
+ safeOpenerDomRef.value as HTMLElement,
106
+ undefined,
107
+ true
90
108
  )
91
109
  break
92
110
  case 'auto-start':
93
- position = getHighestAvailableSpacePos(
94
- safeContainerDomRef.value,
95
- safeElementDomRef.value,
96
- safeOpenerDomRef.value,
111
+ actualPosition.value = getHighestAvailableSpacePos(
112
+ args.useViewport ? null : (safeContainerDomRef.value as HTMLElement),
113
+ safeElementDomRef.value as HTMLElement,
114
+ safeOpenerDomRef.value as HTMLElement,
97
115
  'start'
98
116
  )
99
117
  break
118
+ case 'auto-vertical-start':
119
+ actualPosition.value = getHighestAvailableSpacePos(
120
+ args.useViewport ? null : (safeContainerDomRef.value as HTMLElement),
121
+ safeElementDomRef.value as HTMLElement,
122
+ safeOpenerDomRef.value as HTMLElement,
123
+ 'start',
124
+ true
125
+ )
126
+ break
100
127
  case 'auto-end':
101
- position = getHighestAvailableSpacePos(
102
- safeContainerDomRef.value,
103
- safeElementDomRef.value,
104
- safeOpenerDomRef.value,
128
+ actualPosition.value = getHighestAvailableSpacePos(
129
+ args.useViewport ? null : (safeContainerDomRef.value as HTMLElement),
130
+ safeElementDomRef.value as HTMLElement,
131
+ safeOpenerDomRef.value as HTMLElement,
105
132
  'end'
106
133
  )
107
134
  break
135
+ case 'auto-vertical-end':
136
+ actualPosition.value = getHighestAvailableSpacePos(
137
+ args.useViewport ? null : (safeContainerDomRef.value as HTMLElement),
138
+ safeElementDomRef.value as HTMLElement,
139
+ safeOpenerDomRef.value as HTMLElement,
140
+ 'end',
141
+ true
142
+ )
143
+ break
108
144
  default:
109
145
  break
110
146
  }
111
- switch (position) {
147
+ switch (actualPosition.value) {
112
148
  case 'bottom':
113
- float.position.y = openerRect.bottom
114
- float.position.x = leftWithoutLeftMargin + openerRect.width / 2
149
+ float.position.y = openerRect.value.bottom
150
+ float.position.x = leftWithoutLeftMargin + openerRect.value.width / 2
115
151
  translateX = -50
116
152
  translateY = 0
117
153
  break
118
154
  case 'bottom-start':
119
- float.position.y = openerRect.bottom
155
+ float.position.y = openerRect.value.bottom
120
156
  float.position.x = leftWithoutXMargin
121
157
  translateX = 0
122
158
  translateY = 0
123
159
  break
124
160
  case 'bottom-end':
125
- float.position.y = openerRect.bottom
126
- float.position.x = openerRect.right
161
+ float.position.y = openerRect.value.bottom
162
+ float.position.x = openerRect.value.right
127
163
  translateX = -100
128
164
  translateY = 0
129
165
  break
@@ -134,13 +170,13 @@ export const useFloating = (
134
170
  translateY = 0
135
171
  break
136
172
  case 'left':
137
- float.position.y = topWithoutTopMargin + openerRect.height / 2
173
+ float.position.y = topWithoutTopMargin + openerRect.value.height / 2
138
174
  float.position.x = leftWithoutXMargin
139
175
  translateY = -50
140
176
  translateX = -100
141
177
  break
142
178
  case 'left-end':
143
- float.position.y = openerRect.bottom
179
+ float.position.y = openerRect.value.bottom
144
180
  float.position.x = leftWithoutXMargin
145
181
  translateY = -100
146
182
  translateX = -100
@@ -153,31 +189,31 @@ export const useFloating = (
153
189
  break
154
190
  case 'top':
155
191
  float.position.y = topWithoutYMargin
156
- float.position.x = leftWithoutLeftMargin + openerRect.width / 2
192
+ float.position.x = leftWithoutLeftMargin + openerRect.value.width / 2
157
193
  translateX = -50
158
194
  translateY = -100
159
195
  break
160
196
  case 'top-end':
161
197
  float.position.y = topWithoutYMargin
162
- float.position.x = openerRect.right
198
+ float.position.x = openerRect.value.right
163
199
  translateX = -100
164
200
  translateY = -100
165
201
  break
166
202
  case 'right-start':
167
203
  float.position.y = topWithoutYMargin
168
- float.position.x = openerRect.right
204
+ float.position.x = openerRect.value.right
169
205
  translateX = 0
170
206
  translateY = 0
171
207
  break
172
208
  case 'right':
173
- float.position.y = topWithoutTopMargin + openerRect.height / 2
174
- float.position.x = openerRect.right
209
+ float.position.y = topWithoutTopMargin + openerRect.value.height / 2
210
+ float.position.x = openerRect.value.right
175
211
  translateY = -50
176
212
  translateX = 0
177
213
  break
178
214
  case 'right-end':
179
- float.position.y = openerRect.bottom
180
- float.position.x = openerRect.right
215
+ float.position.y = openerRect.value.bottom
216
+ float.position.x = openerRect.value.right
181
217
  translateY = -100
182
218
  translateX = 0
183
219
  break
@@ -185,42 +221,45 @@ export const useFloating = (
185
221
  break
186
222
  }
187
223
  } else {
188
- switch (position) {
224
+ switch (actualPosition.value) {
189
225
  case 'bottom':
190
- float.position.y = containerRect.bottom - rect.value.height
191
- float.position.x = containerRect.left + containerRect.width / 2
226
+ float.position.y = containerRect.value.bottom - rect.value.height
227
+ float.position.x = containerRect.value.left + containerRect.value.width / 2
192
228
  translateX = -50
193
229
  break
194
230
  case 'left-end':
195
231
  case 'bottom-start':
196
- float.position.y = containerRect.bottom - rect.value.height
197
- float.position.x = containerRect.left
232
+ float.position.y = containerRect.value.bottom - rect.value.height
233
+ float.position.x = containerRect.value.left
198
234
  break
199
235
  case 'right-end':
200
236
  case 'bottom-end':
201
- float.position.y = containerRect.bottom - rect.value.height
202
- float.position.x = containerRect.right - rect.value.width
237
+ float.position.y = containerRect.value.bottom - rect.value.height
238
+ float.position.x = containerRect.value.right - rect.value.width
203
239
  break
204
240
  case 'left':
205
- float.position.y = containerRect.top + (containerRect.height - rect.value.height) / 2
206
- float.position.x = containerRect.left
241
+ float.position.y =
242
+ containerRect.value.top + (containerRect.value.height - rect.value.height) / 2
243
+ float.position.x = containerRect.value.left
207
244
  break
208
245
  case 'top-start':
209
246
  case 'left-start':
210
- float.position.y = containerRect.top
211
- float.position.x = containerRect.left
247
+ float.position.y = containerRect.value.top
248
+ float.position.x = containerRect.value.left
212
249
  break
213
250
  case 'top':
214
- float.position.y = containerRect.top
215
- float.position.x = containerRect.left + (containerRect.width - rect.value.width) / 2
251
+ float.position.y = containerRect.value.top
252
+ float.position.x =
253
+ containerRect.value.left + (containerRect.value.width - rect.value.width) / 2
216
254
  break
217
255
  case 'top-end':
218
256
  case 'right-start':
219
- float.position.y = containerRect.top
220
- float.position.x = containerRect.right - rect.value.width
257
+ float.position.y = containerRect.value.top
258
+ float.position.x = containerRect.value.right - rect.value.width
221
259
  case 'right':
222
- float.position.y = containerRect.top + (containerRect.height - rect.value.height) / 2
223
- float.position.x = containerRect.right - rect.value.width
260
+ float.position.y =
261
+ containerRect.value.top + (containerRect.value.height - rect.value.height) / 2
262
+ float.position.x = containerRect.value.right - rect.value.width
224
263
  break
225
264
  default:
226
265
  break
@@ -232,32 +271,40 @@ export const useFloating = (
232
271
  float.position.x = realPos.x
233
272
  float.position.y = realPos.y
234
273
 
235
- if (realPos.x < containerRect.left) {
236
- float.position.x = containerRect.left
274
+ if (realPos.x < containerRect.value.left) {
275
+ float.position.x = containerRect.value.left
237
276
  translateX = 0
238
277
  }
239
278
 
240
- if (realPos.x + rect.value.width > containerRect.right) {
241
- float.position.x = containerRect.right - rect.value.width
279
+ if (realPos.x + rect.value.width > containerRect.value.right) {
280
+ const fixVal = containerRect.value.right - rect.value.width
281
+ if (fixVal > 0) {
282
+ float.position.x = fixVal
283
+ }
242
284
  translateX = 0
243
285
  }
244
286
 
245
- if (realPos.y < containerRect.top) {
246
- float.position.y = containerRect.top
287
+ if (realPos.y < containerRect.value.top) {
288
+ float.position.y = containerRect.value.top
247
289
  translateY = 0
248
290
  }
249
291
 
250
- if (realPos.y + rect.value.height > containerRect.bottom) {
251
- float.position.y = containerRect.bottom - rect.value.height
292
+ if (realPos.y + rect.value.height > containerRect.value.bottom) {
293
+ const fixVal = containerRect.value.bottom - rect.value.height
294
+ if (fixVal > 0) {
295
+ float.position.y = fixVal
296
+ }
252
297
  translateY = 0
253
298
  }
254
299
 
255
300
  safeElementDomRef.value.style.top = `${float.position.y}px`
256
301
  safeElementDomRef.value.style.left = `${float.position.x}px`
257
- // safeElementDomRef.value.style.position = 'fixed'
302
+ safeElementDomRef.value.style.position = 'fixed'
258
303
  safeElementDomRef.value.style.display = 'flex'
259
304
 
260
305
  rect.value = safeElementDomRef.value.getBoundingClientRect()
306
+
307
+ args.callback && args.callback(rect, openerRect, containerRect, position, actualPosition)
261
308
  })
262
309
 
263
310
  onUnmounted(() => {
@@ -267,7 +314,10 @@ export const useFloating = (
267
314
  return {
268
315
  float,
269
316
  rect,
270
- floatObserver,
271
- setPosition
317
+ setPosition,
318
+ position,
319
+ actualPosition,
320
+ openerRect,
321
+ containerRect
272
322
  }
273
323
  }
@@ -1,6 +1,9 @@
1
1
  import { onBeforeUnmount, onMounted, Ref } from 'vue'
2
2
 
3
- function useKeyDown(component: Ref<HTMLElement | undefined>, callback: (event: KeyboardEvent) => void) {
3
+ function useKeyDown(
4
+ component: Ref<HTMLElement | undefined>,
5
+ callback: (event: KeyboardEvent) => void
6
+ ) {
4
7
  // fail early if any of the required params is missing
5
8
  if (!component) {
6
9
  throw new Error('A target component has to be provided.')
@@ -1,7 +1,9 @@
1
1
  import { onBeforeUnmount, onMounted, Ref } from 'vue'
2
2
 
3
- function useKeyUp(callback: (event: KeyboardEvent) => void, component?: Ref<HTMLElement | undefined>) {
4
-
3
+ function useKeyUp(
4
+ callback: (event: KeyboardEvent) => void,
5
+ component?: Ref<HTMLElement | undefined>
6
+ ) {
5
7
  if (!callback || typeof callback !== 'function') {
6
8
  throw new Error('A callback has to be provided.')
7
9
  }
@@ -13,7 +15,7 @@ function useKeyUp(callback: (event: KeyboardEvent) => void, component?: Ref<HTML
13
15
  onMounted(() => {
14
16
  if (!component) {
15
17
  document.addEventListener('keyup', listener)
16
- return;
18
+ return
17
19
  }
18
20
  component.value!.addEventListener('keyup', listener)
19
21
  })
@@ -21,7 +23,7 @@ function useKeyUp(callback: (event: KeyboardEvent) => void, component?: Ref<HTML
21
23
  onBeforeUnmount(() => {
22
24
  if (!component) {
23
25
  document.removeEventListener('keyup', listener)
24
- return;
26
+ return
25
27
  }
26
28
  component.value!.removeEventListener('keyup', listener)
27
29
  })
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Ref } from 'vue'
2
2
 
3
- type PositionPrimary = 'bottom' | 'left' | 'top' | 'right' | 'auto'
3
+ type PositionPrimary = 'bottom' | 'left' | 'top' | 'right' | 'auto' | 'auto-vertical'
4
4
  type PositionSecondary = 'start' | 'end'
5
5
  export type FzFloatingPosition = PositionPrimary | `${PositionPrimary}-${PositionSecondary}`
6
6
 
@@ -20,6 +20,7 @@ export interface FzFloatingProps {
20
20
  | Array<string | Record<string, boolean | undefined>>
21
21
  overrideContentClass?: boolean
22
22
  teleport?: boolean
23
+ useViewport?: boolean
23
24
  }
24
25
 
25
26
  export interface FzAbsolutePosition {
@@ -40,4 +41,12 @@ export interface FzUseFloatingArgs {
40
41
  container?: FzFloatElement
41
42
  opener?: FzFloatElement
42
43
  position?: Ref<FzFloatingPosition>
44
+ useViewport?: Ref<boolean>
45
+ callback?: (
46
+ rect: Ref<DOMRect | undefined>,
47
+ openerRect: Ref<DOMRect | undefined>,
48
+ containerRect: Ref<DOMRect | undefined>,
49
+ position: Ref<FzFloatingPosition>,
50
+ actualPosition: Ref<FzFloatingPosition | undefined>
51
+ ) => void
43
52
  }
@@ -5,18 +5,23 @@ interface PositionListItem {
5
5
  key: MainPosition
6
6
  space: number
7
7
  }
8
-
8
+ const windowDOMrect = new DOMRect(0, 0, window.innerWidth, window.innerHeight)
9
9
  export const getHighestAvailableSpacePos = (
10
- container: HTMLElement,
10
+ container: HTMLElement | null,
11
11
  el: HTMLElement,
12
12
  opener: HTMLElement,
13
- justify?: 'start' | 'end'
13
+ justify?: 'start' | 'end',
14
+ verticalOnly?: boolean
14
15
  ): FzFloatingPosition => {
15
- let mainPosition: MainPosition = 'right'
16
+ let mainPosition: MainPosition = verticalOnly ? 'bottom' : 'right'
16
17
 
17
- let positionRes: FzFloatingPosition = 'right-start'
18
+ let positionRes: FzFloatingPosition = verticalOnly ? 'bottom-start' : 'right-start'
18
19
 
19
- const containerRect = container.getBoundingClientRect()
20
+ windowDOMrect.width = window.innerWidth
21
+ windowDOMrect.height = window.innerHeight
22
+ windowDOMrect.x = 0
23
+ windowDOMrect.y = 0
24
+ const containerRect = container ? container.getBoundingClientRect() : windowDOMrect
20
25
  const elRect = el.getBoundingClientRect()
21
26
  const openerRect = opener.getBoundingClientRect()
22
27
 
@@ -29,7 +34,7 @@ export const getHighestAvailableSpacePos = (
29
34
  const spaceBottomNormalized =
30
35
  (containerRect.bottom - openerRect.bottom - elRect.height) / containerRect.height
31
36
 
32
- const positionList = [
37
+ let positionList = [
33
38
  {
34
39
  key: 'right',
35
40
  space: spaceRightNormalized
@@ -48,6 +53,10 @@ export const getHighestAvailableSpacePos = (
48
53
  } as PositionListItem
49
54
  ].sort((a, b) => b.space - a.space)
50
55
 
56
+ if (verticalOnly) {
57
+ positionList = positionList.filter((pos) => pos.key === 'top' || pos.key === 'bottom')
58
+ }
59
+
51
60
  mainPosition = positionList[0].key
52
61
 
53
62
  if (!justify) {