@energie360/ui-library 0.0.4 → 0.1.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/base/_resets.scss +2 -0
- package/base/abstracts/_mixins.scss +11 -5
- package/base/main-base.scss +1 -0
- package/components/icon-text-block/icon-text-block.scss +55 -0
- package/components/icon-text-block/u-icon-text-block.vue +32 -0
- package/components/icon-text-block-group/icon-text-block-group.scss +5 -0
- package/components/icon-text-block-group/u-icon-text-block-group.vue +9 -0
- package/components/index.js +14 -0
- package/components/panel/panel.scss +70 -0
- package/components/panel/u-panel.vue +37 -0
- package/components/progress-bar/progress-bar.scss +37 -0
- package/components/progress-bar/u-progress-bar.vue +21 -0
- package/components/table/cell-ctas.scss +12 -0
- package/components/table/cell-icon-group.scss +12 -0
- package/components/table/cell-icon-text.scss +22 -0
- package/components/table/cell-progress-bar.scss +23 -0
- package/components/table/table-cell.mixins.scss +60 -0
- package/components/table/table-cell.scss +24 -0
- package/components/table/table-header.scss +5 -0
- package/components/table/table-heading.scss +8 -0
- package/components/table/table-row.scss +20 -0
- package/components/table/table.scss +12 -0
- package/components/table/table.type.ts +31 -0
- package/components/table/u-cell-ctas.vue +33 -0
- package/components/table/u-cell-icon-group.vue +31 -0
- package/components/table/u-cell-icon-text.vue +23 -0
- package/components/table/u-cell-progress-bar.vue +22 -0
- package/components/table/u-table-cell.vue +47 -0
- package/components/table/u-table-header.vue +9 -0
- package/components/table/u-table-heading.vue +24 -0
- package/components/table/u-table-row.vue +17 -0
- package/components/table/u-table.vue +11 -0
- package/components/tooltip/dom.js +167 -0
- package/components/tooltip/popover.js +200 -0
- package/components/tooltip/tooltip.scss +75 -0
- package/components/tooltip/u-tooltip.vue +92 -0
- package/components/tooltip/viewport.js +21 -0
- package/custom-elements.js +1 -0
- package/dist/base-style.css +1 -1
- package/dist/custom-elements.css +1 -0
- package/dist/{index.js → custom-elements.js} +1591 -1600
- package/dist/custom-elements.js.map +1 -0
- package/elements/button/_button-base.scss +1 -1
- package/elements/button/_button-filled-inverted.scss +3 -3
- package/elements/button/_button-filled.scss +3 -3
- package/elements/button/_button-outlined-inverted.scss +3 -3
- package/elements/button/_button-outlined.scss +3 -3
- package/elements/button/_button-plain.scss +3 -3
- package/elements/button/_button-secondary-outlined.scss +3 -3
- package/elements/button/button.js +2 -2
- package/elements/button/button.scss +1 -1
- package/elements/button/u-button.vue +44 -0
- package/elements/elements.js +35 -0
- package/elements/form-field/form-field-base.scss +142 -0
- package/elements/form-field/form-field-error.scss +20 -0
- package/elements/form-field/form-field-prefix-suffix.scss +80 -0
- package/elements/form-field/form-field-states.scss +59 -0
- package/elements/form-field/index.scss +4 -0
- package/elements/icon/icon.js +2 -2
- package/elements/icon/{icon.vue → u-icon.vue} +11 -15
- package/elements/icon-button/icon-button.js +2 -2
- package/elements/icon-button/{icon-button.vue → u-icon-button.vue} +14 -15
- package/elements/image/image.scss +3 -0
- package/elements/image/u-image.vue +17 -0
- package/elements/index.js +6 -31
- package/elements/loader/loader.js +2 -2
- package/elements/loader/{loader.vue → u-loader.vue} +6 -7
- package/elements/spectro/spectro.scss +13 -0
- package/elements/spectro/u-spectro.vue +11 -0
- package/elements/text-field/text-field.scss +30 -0
- package/elements/text-field/text-field.types.ts +6 -0
- package/elements/text-field/u-text-field.vue +164 -0
- package/elements/types.ts +12 -0
- package/env.d.ts +1 -0
- package/layout/container/container.scss +30 -0
- package/layout/grid/grid-row.scss +10 -0
- package/layout/grid/grid.mixin.scss +90 -0
- package/layout/grid/grid.scss +121 -0
- package/package.json +20 -8
- package/tsconfig.app.json +12 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +19 -0
- package/{vite.config.js → vite.config.ts} +1 -1
- package/wizard/index.js +4 -0
- package/wizard/wizard-intro/u-wizard-intro.vue +40 -0
- package/wizard/wizard-intro/wizard-intro.scss +39 -0
- package/wizard/wizard-layout/u-wizard-layout-block.vue +21 -0
- package/wizard/wizard-layout/u-wizard-layout-element.vue +9 -0
- package/wizard/wizard-layout/u-wizard-layout.vue +11 -0
- package/wizard/wizard-layout/wizard-layout-block.scss +45 -0
- package/wizard/wizard-layout/wizard-layout-element.scss +3 -0
- package/wizard/wizard-layout/wizard-layout.scss +40 -0
- package/dist/index.css +0 -1
- package/dist/index.js.map +0 -1
- package/elements/button/button.vue +0 -42
- package/index.js +0 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import UProgressBar from '../progress-bar/u-progress-bar.vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label?: string
|
|
6
|
+
value: number
|
|
7
|
+
labelPosition?: 'top' | 'right'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { labelPosition = 'top' } = defineProps<Props>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div :class="['cell-progress-bar', `label-position-${labelPosition}`]">
|
|
15
|
+
<p class="progress-bar-label" v-if="label">{{ label }}</p>
|
|
16
|
+
<UProgressBar :value="value"></UProgressBar>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style scoped lang="scss">
|
|
21
|
+
@use './cell-progress-bar.scss';
|
|
22
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
TableCellBase,
|
|
4
|
+
TableCellHAlign,
|
|
5
|
+
TableCellTextStyle,
|
|
6
|
+
TableCellVAlign,
|
|
7
|
+
} from './table.type'
|
|
8
|
+
import UIcon from '../../elements/icon/u-icon.vue'
|
|
9
|
+
import UTooltip from '../tooltip/u-tooltip.vue'
|
|
10
|
+
|
|
11
|
+
interface Props extends TableCellBase {
|
|
12
|
+
infoText?: string
|
|
13
|
+
textStyle?: TableCellTextStyle
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
textStyle = TableCellTextStyle.normal,
|
|
18
|
+
hAlign = TableCellHAlign.left,
|
|
19
|
+
vAlign = TableCellVAlign.center,
|
|
20
|
+
} = defineProps<Props>()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div
|
|
25
|
+
role="cell"
|
|
26
|
+
:class="[
|
|
27
|
+
'table-cell',
|
|
28
|
+
`h-align-${hAlign}`,
|
|
29
|
+
`v-align-${vAlign}`,
|
|
30
|
+
{ 'has-tooltip': infoText },
|
|
31
|
+
]"
|
|
32
|
+
>
|
|
33
|
+
<div :class="['cell-content', `text-${textStyle}`]">
|
|
34
|
+
<slot>{{ text }}</slot>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="info-tooltip" v-if="infoText">
|
|
38
|
+
<UTooltip :title="infoText">
|
|
39
|
+
<UIcon name="info"></UIcon>
|
|
40
|
+
</UTooltip>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<style scoped lang="scss">
|
|
46
|
+
@use './table-cell.scss';
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
text?: string
|
|
4
|
+
hAlign?: 'left' | 'center' | 'right'
|
|
5
|
+
vAlign?: 'top' | 'center' | 'bottom'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { hAlign = 'left', vAlign = 'top' } = defineProps<Props>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
role="cell"
|
|
14
|
+
:class="['table-heading', `h-align-${hAlign}`, `v-align-${vAlign}`]"
|
|
15
|
+
>
|
|
16
|
+
<div class="cell-content">
|
|
17
|
+
<slot :text="text">{{ text }}</slot>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<style scoped lang="scss">
|
|
23
|
+
@use './table-heading.scss';
|
|
24
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
highlight?: boolean
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
defineProps<Props>()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div role="row" :class="['table-row', { highlight }]">
|
|
11
|
+
<slot></slot>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<style scoped lang="scss">
|
|
16
|
+
@use './table-row.scss';
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an element with another element
|
|
3
|
+
*
|
|
4
|
+
* @param {Node} el
|
|
5
|
+
* @param {string} wrapper - Tag name of wrapper element. Can also have a classname (like emmet notation)
|
|
6
|
+
*
|
|
7
|
+
* @returns {Node}
|
|
8
|
+
*/
|
|
9
|
+
export const wrap = (el, wrapper) => {
|
|
10
|
+
const parsedWrapper = wrapper.split('.')
|
|
11
|
+
const wrapperEl = document.createElement(parsedWrapper[0])
|
|
12
|
+
|
|
13
|
+
if (parsedWrapper.length > 1) {
|
|
14
|
+
wrapperEl.className = parsedWrapper.slice(1).join(' ')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
el.parentNode.insertBefore(wrapperEl, el)
|
|
18
|
+
wrapperEl.appendChild(el)
|
|
19
|
+
|
|
20
|
+
return wrapperEl
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get element position relative to document.
|
|
25
|
+
*
|
|
26
|
+
* @param {Node} el
|
|
27
|
+
* @returns {{top: number, left: number, width: number, height: number}}
|
|
28
|
+
*/
|
|
29
|
+
export const getElementPosition = (el) => {
|
|
30
|
+
const elRect = el.getBoundingClientRect()
|
|
31
|
+
const scrollTop = window.scrollY
|
|
32
|
+
const scrollLeft = window.scrollX
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
top: elRect.top + scrollTop,
|
|
36
|
+
left: elRect.left + scrollLeft,
|
|
37
|
+
width: elRect.width,
|
|
38
|
+
height: elRect.height,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decode HTML encoded string.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} str
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export const htmlDecode = (str) => {
|
|
49
|
+
const doc = new DOMParser().parseFromString(str, 'text/html')
|
|
50
|
+
return doc.documentElement.textContent
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const htmlToDocFragment = (str) => {
|
|
54
|
+
const tpl = document.createElement('template')
|
|
55
|
+
tpl.innerHTML = str
|
|
56
|
+
return tpl.content.cloneNode(true)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reverse order of direct child nodes of given parent.
|
|
61
|
+
*
|
|
62
|
+
* @param {Node} parent
|
|
63
|
+
*/
|
|
64
|
+
export const reverseChildren = (parent) => {
|
|
65
|
+
for (let i = 1; i < parent.childNodes.length; i++) {
|
|
66
|
+
parent.insertBefore(parent.childNodes[i], parent.firstChild)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if element is in a slotted element.
|
|
72
|
+
*
|
|
73
|
+
* @param {Element} el
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
const isInSlottedElement = (el) => {
|
|
77
|
+
const parentEl = el.parentElement
|
|
78
|
+
|
|
79
|
+
if (parentEl === document.body || !parentEl) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (parentEl.assignedSlot) {
|
|
84
|
+
return true
|
|
85
|
+
} else {
|
|
86
|
+
return isInSlottedElement(parentEl)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
*
|
|
92
|
+
* @param {Element} el
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
const isOffsetParent = (el) => {
|
|
96
|
+
const style = getComputedStyle(el)
|
|
97
|
+
return style.position === 'relative' || style.position === 'absolute'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the expected offsetParent within nested shadow roots.
|
|
102
|
+
*
|
|
103
|
+
* Because elements are nested in web components or slotted in web components we can't get desired offsetParent.
|
|
104
|
+
*
|
|
105
|
+
* @param {Element} el
|
|
106
|
+
* @returns {Element}
|
|
107
|
+
*/
|
|
108
|
+
export const getOffsetParent = (el) => {
|
|
109
|
+
if (el === document.body) {
|
|
110
|
+
// We arrived at the top.
|
|
111
|
+
// 'body' must be the offsetParent then.
|
|
112
|
+
return el
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const offsetParent = el.offsetParent
|
|
116
|
+
|
|
117
|
+
if (offsetParent === document.body || offsetParent === null) {
|
|
118
|
+
// It's possible that we're a slotted element, in which case we'll always have the potentially 'wrong' offsetParent 'body'.
|
|
119
|
+
// Dig deeper...
|
|
120
|
+
if (el.assignedSlot) {
|
|
121
|
+
// A <slot> doesn't have an offsetParent. Let's use the parentNode.
|
|
122
|
+
const parentElement = el.assignedSlot.parentElement
|
|
123
|
+
|
|
124
|
+
// Handle case where the parent node actually is an offsetParent
|
|
125
|
+
if (isOffsetParent(parentElement)) {
|
|
126
|
+
return parentElement
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return getOffsetParent(el.assignedSlot.parentElement)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isInSlottedElement(el)) {
|
|
133
|
+
return getOffsetParent(el.parentElement)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (el.getRootNode() instanceof ShadowRoot) {
|
|
137
|
+
// If we're nested in another shadow root we'll also always get 'body' as offsetParent.
|
|
138
|
+
// Check the offsetParent of the current host element.
|
|
139
|
+
return getOffsetParent(el.getRootNode().host)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return offsetParent
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get offset positions of parent which is not 'static'.
|
|
148
|
+
*
|
|
149
|
+
* @param {Element} el
|
|
150
|
+
* @returns {Object}
|
|
151
|
+
*/
|
|
152
|
+
export const getOffsetParentPosition = (el) => {
|
|
153
|
+
const offsetParent = getOffsetParent(el)
|
|
154
|
+
const elRect = el.getBoundingClientRect()
|
|
155
|
+
const offsetParentRect = offsetParent.getBoundingClientRect()
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
top: elRect.top - offsetParentRect.top,
|
|
159
|
+
left: elRect.left - offsetParentRect.left,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const moveChildren = (sourceParent, targetParent) => {
|
|
164
|
+
while (sourceParent.childNodes.length > 0) {
|
|
165
|
+
targetParent.appendChild(sourceParent.childNodes[0])
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { getViewportDimensions } from './viewport'
|
|
2
|
+
import { getOffsetParentPosition } from './dom'
|
|
3
|
+
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
offset: 0,
|
|
6
|
+
viewportPadding: 0,
|
|
7
|
+
placement: 'top-left',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This simply calculates the desired position of the popover and returns the coordinates.
|
|
12
|
+
* It won't change the DOM.
|
|
13
|
+
*
|
|
14
|
+
* @param {Element} refEl
|
|
15
|
+
* @param {Element} popoverEl
|
|
16
|
+
* @param {Object} options - Options object
|
|
17
|
+
* @param {number} options.offset - offset to refEl in px
|
|
18
|
+
* @param {number} options.viewportPadding - Padding to viewport in px
|
|
19
|
+
* @param {string} options.placement - One of the following keywords: top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
|
|
20
|
+
*/
|
|
21
|
+
export const getPopoverPosition = (refEl, popoverEl, options = {}) => {
|
|
22
|
+
options = Object.assign({}, defaultOptions, options)
|
|
23
|
+
|
|
24
|
+
const refRect = refEl.getBoundingClientRect()
|
|
25
|
+
let popoverRect = popoverEl.getBoundingClientRect()
|
|
26
|
+
|
|
27
|
+
const offsetParent = getOffsetParentPosition(refEl)
|
|
28
|
+
|
|
29
|
+
let top
|
|
30
|
+
let left
|
|
31
|
+
const offsetTop = offsetParent.top
|
|
32
|
+
const offsetLeft = offsetParent.left
|
|
33
|
+
const { vpWidth } = getViewportDimensions()
|
|
34
|
+
const docHeight = document.documentElement.scrollHeight
|
|
35
|
+
const scrollY = window.scrollY
|
|
36
|
+
|
|
37
|
+
if (popoverRect.width === 0 && popoverRect.height === 0) {
|
|
38
|
+
// Popover probably has display:none.
|
|
39
|
+
// Make it visible for a frame to get the correct dimensions.
|
|
40
|
+
popoverEl.style.display = 'block'
|
|
41
|
+
popoverRect = popoverEl.getBoundingClientRect()
|
|
42
|
+
popoverEl.style.display = ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
switch (options.placement) {
|
|
46
|
+
case 'top-right':
|
|
47
|
+
top = offsetTop - popoverRect.height - options.offset
|
|
48
|
+
left = offsetLeft + refRect.width - popoverRect.width
|
|
49
|
+
// console.log(left)
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
case 'top-left':
|
|
53
|
+
top = offsetTop - popoverRect.height - options.offset
|
|
54
|
+
left = offsetLeft
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
case 'bottom-right':
|
|
58
|
+
top = offsetTop + refRect.height + options.offset
|
|
59
|
+
left = offsetLeft + refRect.width - popoverRect.width
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
case 'bottom-left':
|
|
63
|
+
top = offsetTop + refRect.height + options.offset
|
|
64
|
+
left = offsetLeft
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
// TODO: add remaining placement options
|
|
68
|
+
case 'bottom-center':
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
case 'top-center':
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add corrections to position if necessary
|
|
75
|
+
|
|
76
|
+
// Check left or right overflow
|
|
77
|
+
const leftDiff = refRect.left - offsetLeft
|
|
78
|
+
if (left + leftDiff < options.viewportPadding) {
|
|
79
|
+
left = 0 - leftDiff + options.viewportPadding
|
|
80
|
+
} else if (
|
|
81
|
+
left + leftDiff + popoverRect.width >
|
|
82
|
+
vpWidth - options.viewportPadding
|
|
83
|
+
) {
|
|
84
|
+
left = vpWidth - leftDiff - popoverRect.width - options.viewportPadding
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check top or bottom overflow
|
|
88
|
+
const topDiff = refRect.top - offsetTop
|
|
89
|
+
if (top + topDiff + scrollY < options.viewportPadding) {
|
|
90
|
+
// popover is too high.
|
|
91
|
+
// console.log('too high')
|
|
92
|
+
// Switch position to bottom
|
|
93
|
+
top = offsetTop + refRect.height + options.offset
|
|
94
|
+
} else if (
|
|
95
|
+
top + topDiff + scrollY + popoverRect.height >
|
|
96
|
+
docHeight - options.viewportPadding
|
|
97
|
+
) {
|
|
98
|
+
// popover is too low.
|
|
99
|
+
// console.log('too low')
|
|
100
|
+
// Swith position to top.
|
|
101
|
+
top = offsetTop - popoverRect.height - options.offset
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
top,
|
|
106
|
+
left,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Calculates the desired position of the popover relative to <body>.
|
|
112
|
+
*
|
|
113
|
+
* @param {Element} refEl
|
|
114
|
+
* @param {Element} popoverEl
|
|
115
|
+
* @param {Object} options - Options object
|
|
116
|
+
* @param {number} options.offset - offset to refEl in px
|
|
117
|
+
* @param {number} options.viewportPadding - Padding to viewport in px
|
|
118
|
+
* @param {string} options.placement - One of the following keywords: top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
|
|
119
|
+
*/
|
|
120
|
+
export const getPopoverPositionBody = (refEl, popoverEl, options = {}) => {
|
|
121
|
+
options = Object.assign({}, defaultOptions, options)
|
|
122
|
+
|
|
123
|
+
const refRect = refEl.getBoundingClientRect()
|
|
124
|
+
let popoverRect = popoverEl.getBoundingClientRect()
|
|
125
|
+
|
|
126
|
+
let top
|
|
127
|
+
let left
|
|
128
|
+
const topCorrection = 0
|
|
129
|
+
let leftCorrection = 0
|
|
130
|
+
const scrollY = window.scrollY
|
|
131
|
+
const scrollX = window.scrollX
|
|
132
|
+
const { vpWidth } = getViewportDimensions()
|
|
133
|
+
// const docHeight = document.documentElement.scrollHeight
|
|
134
|
+
const refTop = refRect.top + scrollY
|
|
135
|
+
const refLeft = refRect.left + scrollX
|
|
136
|
+
|
|
137
|
+
if (popoverRect.width === 0 && popoverRect.height === 0) {
|
|
138
|
+
// Popover probably has display:none.
|
|
139
|
+
// Make it visible for a frame to get the correct dimensions.
|
|
140
|
+
popoverEl.style.display = 'block'
|
|
141
|
+
popoverRect = popoverEl.getBoundingClientRect()
|
|
142
|
+
popoverEl.style.display = ''
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
switch (options.placement) {
|
|
146
|
+
case 'top-right':
|
|
147
|
+
// top = offsetTop - popoverRect.height - options.offset
|
|
148
|
+
// left = offsetLeft + refRect.width - popoverRect.width
|
|
149
|
+
// console.log(left)
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
case 'top-left':
|
|
153
|
+
// top = offsetTop - popoverRect.height - options.offset
|
|
154
|
+
// left = offsetLeft
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
case 'bottom-right':
|
|
158
|
+
// top = offsetTop + refRect.height + options.offset
|
|
159
|
+
// left = offsetLeft + refRect.width - popoverRect.width
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
case 'bottom-left':
|
|
163
|
+
// top = offsetTop + refRect.height + options.offset
|
|
164
|
+
// left = offsetLeft
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
case 'bottom-center':
|
|
168
|
+
top = refTop + refRect.height + options.offset
|
|
169
|
+
left = refLeft + refRect.width / 2 - popoverRect.width / 2
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
case 'top-center':
|
|
173
|
+
top = refTop - options.offset - popoverRect.height
|
|
174
|
+
left = refLeft + refRect.width / 2 - popoverRect.width / 2
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add corrections to position if necessary
|
|
178
|
+
|
|
179
|
+
// Check left or right overflow
|
|
180
|
+
if (left < options.viewportPadding) {
|
|
181
|
+
// too left
|
|
182
|
+
leftCorrection = Math.abs(left) + options.viewportPadding
|
|
183
|
+
left = options.viewportPadding
|
|
184
|
+
} else if (left + popoverRect.width > vpWidth - options.viewportPadding) {
|
|
185
|
+
// too right
|
|
186
|
+
leftCorrection =
|
|
187
|
+
vpWidth - options.viewportPadding - popoverRect.width - left
|
|
188
|
+
left = vpWidth - options.viewportPadding - popoverRect.width
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check top or bottom overflow
|
|
192
|
+
// TODO: Add these corrections
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
top,
|
|
196
|
+
left,
|
|
197
|
+
leftCorrection,
|
|
198
|
+
topCorrection,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
@use '../../base/abstracts/' as a;
|
|
2
|
+
|
|
3
|
+
.tooltip {
|
|
4
|
+
pointer-events: none;
|
|
5
|
+
z-index: a.$layer-tooltip;
|
|
6
|
+
position: absolute;
|
|
7
|
+
top: 0;
|
|
8
|
+
left: 0;
|
|
9
|
+
max-width: a.rem(360);
|
|
10
|
+
min-width: a.rem(84);
|
|
11
|
+
opacity: 0;
|
|
12
|
+
transform: translateY(-5px);
|
|
13
|
+
transition:
|
|
14
|
+
opacity a.$trs-default,
|
|
15
|
+
transform a.$trs-default;
|
|
16
|
+
|
|
17
|
+
@include a.bp(lg) {
|
|
18
|
+
max-width: a.rem(280);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.tooltip__content {
|
|
22
|
+
@include a.type(100);
|
|
23
|
+
|
|
24
|
+
// This nesting here is only to make the new sass compiler happy.
|
|
25
|
+
& {
|
|
26
|
+
z-index: 0;
|
|
27
|
+
position: relative;
|
|
28
|
+
padding: var(--e-space-2);
|
|
29
|
+
background-color: var(--e-c-mono-00);
|
|
30
|
+
box-shadow: var(--e-elevation-sm);
|
|
31
|
+
border: 1px solid var(--e-c-mono-100);
|
|
32
|
+
border-radius: var(--e-brd-radius-1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@include a.bp(lg) {
|
|
36
|
+
@include a.type(50);
|
|
37
|
+
|
|
38
|
+
padding: var(--e-space-3);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.tooltip__pointer {
|
|
43
|
+
position: absolute;
|
|
44
|
+
bottom: -4px;
|
|
45
|
+
left: calc(50% - 6px);
|
|
46
|
+
|
|
47
|
+
&::before {
|
|
48
|
+
content: '';
|
|
49
|
+
display: block;
|
|
50
|
+
width: 10px;
|
|
51
|
+
height: 9.6px;
|
|
52
|
+
transform: rotate(53.13deg) skewX(15.36deg);
|
|
53
|
+
box-shadow: var(--e-elevation-sm);
|
|
54
|
+
background-color: var(--e-c-mono-00);
|
|
55
|
+
border-right: 1px solid var(--e-c-mono-100);
|
|
56
|
+
border-bottom: 1px solid var(--e-c-mono-100);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&::after {
|
|
60
|
+
content: '';
|
|
61
|
+
position: absolute;
|
|
62
|
+
bottom: 5px;
|
|
63
|
+
left: -2px;
|
|
64
|
+
width: 14px;
|
|
65
|
+
height: 7px;
|
|
66
|
+
background: var(--e-c-mono-00);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// States
|
|
71
|
+
&.tooltip--open {
|
|
72
|
+
opacity: 1;
|
|
73
|
+
transform: translateY(0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { getPopoverPositionBody } from './popover'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string
|
|
7
|
+
placement?:
|
|
8
|
+
| 'top-right'
|
|
9
|
+
| 'top-left'
|
|
10
|
+
| 'top-center'
|
|
11
|
+
| 'bottom-right'
|
|
12
|
+
| 'bottom-left'
|
|
13
|
+
| 'bottom-center'
|
|
14
|
+
offset?: number
|
|
15
|
+
viewportPadding?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
placement = 'top-center',
|
|
20
|
+
offset = 12,
|
|
21
|
+
viewportPadding = 20,
|
|
22
|
+
} = defineProps<Props>()
|
|
23
|
+
|
|
24
|
+
const tooltip = ref(null)
|
|
25
|
+
const root = ref(null)
|
|
26
|
+
const pointer = ref(null)
|
|
27
|
+
|
|
28
|
+
const showTooltip = () => {
|
|
29
|
+
const popoverPosition = getPopoverPositionBody(root.value, tooltip.value, {
|
|
30
|
+
offset,
|
|
31
|
+
placement,
|
|
32
|
+
viewportPadding,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Set position
|
|
36
|
+
tooltip.value.style.top = `${popoverPosition.top}px`
|
|
37
|
+
tooltip.value.style.left = `${popoverPosition.left}px`
|
|
38
|
+
|
|
39
|
+
// Fix pointer position if necessary
|
|
40
|
+
if (popoverPosition.leftCorrection) {
|
|
41
|
+
pointer.value.style.transform = `translateX(${popoverPosition.leftCorrection * -1}px)`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
tooltip.value.ariaHidden = 'false'
|
|
45
|
+
tooltip.value.classList.toggle('tooltip--open', true)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hideTooltip = () => {
|
|
49
|
+
const onTransitionend = () => {
|
|
50
|
+
tooltip.value.removeEventListener('transitionend', onTransitionend)
|
|
51
|
+
|
|
52
|
+
tooltip.value.style.top = ''
|
|
53
|
+
tooltip.value.style.left = ''
|
|
54
|
+
pointer.value.style.transform = ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tooltip.value.addEventListener('transitionend', onTransitionend)
|
|
58
|
+
|
|
59
|
+
tooltip.value.ariaHidden = 'true'
|
|
60
|
+
tooltip.value.classList.toggle('tooltip--open', false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// On desktop (or non-touch devices) mouse events will be fired before the pointer events.
|
|
64
|
+
// For mobile and touch devices it's th opposite, pointer events will be fired first.
|
|
65
|
+
// This way we can detect on what device we are and handle the behaviour accordingly.
|
|
66
|
+
const onMouseenter = () => {
|
|
67
|
+
showTooltip()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const onMouseleave = () => {
|
|
71
|
+
hideTooltip()
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<span @mouseenter="onMouseenter" @mouseleave="onMouseleave" ref="root">
|
|
77
|
+
<slot></slot>
|
|
78
|
+
</span>
|
|
79
|
+
|
|
80
|
+
<Teleport to="body">
|
|
81
|
+
<div ref="tooltip" class="tooltip">
|
|
82
|
+
<div class="tooltip__content">
|
|
83
|
+
{{ title }}
|
|
84
|
+
</div>
|
|
85
|
+
<span class="tooltip__pointer" ref="pointer"></span>
|
|
86
|
+
</div>
|
|
87
|
+
</Teleport>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<style scoped lang="scss">
|
|
91
|
+
@use './tooltip.scss';
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get element offset relative to document.
|
|
3
|
+
*
|
|
4
|
+
* @param {Element} el
|
|
5
|
+
* @returns {{top: *, left: *, width, height}}
|
|
6
|
+
*/
|
|
7
|
+
export const getElOffset = (el) => {
|
|
8
|
+
const rect = el.getBoundingClientRect()
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
top: window.scrollY + rect.top,
|
|
12
|
+
left: window.scrollX + rect.left,
|
|
13
|
+
width: rect.width,
|
|
14
|
+
height: rect.height,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getViewportDimensions = () => ({
|
|
19
|
+
vpWidth: window.innerWidth,
|
|
20
|
+
vpHeight: window.innerHeight,
|
|
21
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './elements/elements'
|