@budibase/bbui 2.21.8 → 2.22.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/bbui",
3
3
  "description": "A UI solution used in the different Budibase projects.",
4
- "version": "2.21.8",
4
+ "version": "2.22.0",
5
5
  "license": "MPL-2.0",
6
6
  "svelte": "src/index.js",
7
7
  "module": "dist/bbui.es.js",
@@ -35,8 +35,8 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@adobe/spectrum-css-workflow-icons": "1.2.1",
38
- "@budibase/shared-core": "2.21.8",
39
- "@budibase/string-templates": "2.21.8",
38
+ "@budibase/shared-core": "2.22.0",
39
+ "@budibase/string-templates": "2.22.0",
40
40
  "@spectrum-css/accordion": "3.0.24",
41
41
  "@spectrum-css/actionbutton": "1.0.1",
42
42
  "@spectrum-css/actiongroup": "1.0.1",
@@ -103,5 +103,5 @@
103
103
  }
104
104
  }
105
105
  },
106
- "gitHead": "f2541bd150fcb84cd403e29a759843b2952563a4"
106
+ "gitHead": "45f438318a63b0f60cbee8cef5115ca7947f356b"
107
107
  }
@@ -38,7 +38,7 @@
38
38
  <div use:getAnchor on:click={openMenu}>
39
39
  <slot name="control" />
40
40
  </div>
41
- <Popover bind:this={dropdown} {anchor} {align} {portalTarget}>
41
+ <Popover bind:this={dropdown} {anchor} {align} {portalTarget} on:open on:close>
42
42
  <Menu>
43
43
  <slot />
44
44
  </Menu>
@@ -32,6 +32,13 @@ const handleClick = event => {
32
32
  return
33
33
  }
34
34
 
35
+ // Ignore clicks for drawers, unless the handler is registered from a drawer
36
+ const sourceInDrawer = handler.anchor.closest(".drawer-wrapper") != null
37
+ const clickInDrawer = event.target.closest(".drawer-wrapper") != null
38
+ if (clickInDrawer && !sourceInDrawer) {
39
+ return
40
+ }
41
+
35
42
  handler.callback?.(event)
36
43
  })
37
44
  }
@@ -15,6 +15,7 @@ export default function positionDropdown(element, opts) {
15
15
  align,
16
16
  maxHeight,
17
17
  maxWidth,
18
+ minWidth,
18
19
  useAnchorWidth,
19
20
  offset = 5,
20
21
  customUpdate,
@@ -28,7 +29,7 @@ export default function positionDropdown(element, opts) {
28
29
  const elementBounds = element.getBoundingClientRect()
29
30
  let styles = {
30
31
  maxHeight: null,
31
- minWidth: null,
32
+ minWidth,
32
33
  maxWidth,
33
34
  left: null,
34
35
  top: null,
@@ -41,8 +42,13 @@ export default function positionDropdown(element, opts) {
41
42
  })
42
43
  } else {
43
44
  // Determine vertical styles
44
- if (align === "right-outside") {
45
- styles.top = anchorBounds.top
45
+ if (align === "right-outside" || align === "left-outside") {
46
+ styles.top =
47
+ anchorBounds.top + anchorBounds.height / 2 - elementBounds.height / 2
48
+ styles.maxHeight = maxHeight
49
+ if (styles.top + elementBounds.height > window.innerHeight) {
50
+ styles.top = window.innerHeight - elementBounds.height
51
+ }
46
52
  } else if (
47
53
  window.innerHeight - anchorBounds.bottom <
48
54
  (maxHeight || 100)
@@ -1,28 +1,111 @@
1
+ <script context="module">
2
+ import { writable, get } from "svelte/store"
3
+
4
+ // Observe this class name if possible in order to know how to size the
5
+ // drawer. If this doesn't exist we'll use a fixed size.
6
+ const drawerContainer = "drawer-container"
7
+
8
+ // Context level stores to keep drawers in sync
9
+ const openDrawers = writable([])
10
+ const modal = writable(false)
11
+ const resizable = writable(true)
12
+ const drawerLeft = writable(null)
13
+ const drawerWidth = writable(null)
14
+
15
+ // Resize observer to keep track of size changes
16
+ let observer
17
+
18
+ // Starts observing the target node to watching to size changes.
19
+ // Invoked when the first drawer of a chain is rendered.
20
+ const observe = () => {
21
+ const target = document.getElementsByClassName(drawerContainer)[0]
22
+ if (observer || !target) {
23
+ return
24
+ }
25
+ observer = new ResizeObserver(entries => {
26
+ if (!entries?.[0]) {
27
+ return
28
+ }
29
+ const bounds = entries[0].target.getBoundingClientRect()
30
+ drawerLeft.set(bounds.left)
31
+ drawerWidth.set(bounds.width)
32
+ })
33
+ observer.observe(target)
34
+
35
+ // Manually measure once to ensure that we have dimensions for the initial
36
+ // paint
37
+ const bounds = target.getBoundingClientRect()
38
+ drawerLeft.set(bounds.left)
39
+ drawerWidth.set(bounds.width)
40
+ }
41
+
42
+ // Stops observing the target node.
43
+ // Invoked when the last drawer of a chain is removed.
44
+ const unobserve = () => {
45
+ if (get(openDrawers).length) {
46
+ return
47
+ }
48
+ observer?.disconnect()
49
+
50
+ // Reset state
51
+ observer = null
52
+ modal.set(false)
53
+ resizable.set(true)
54
+ drawerLeft.set(null)
55
+ drawerWidth.set(null)
56
+ }
57
+ </script>
58
+
1
59
  <script>
2
- import Portal from "svelte-portal"
3
60
  import Button from "../Button/Button.svelte"
4
- import Body from "../Typography/Body.svelte"
5
- import Heading from "../Typography/Heading.svelte"
6
- import { setContext, createEventDispatcher } from "svelte"
61
+ import Icon from "../Icon/Icon.svelte"
62
+ import ActionButton from "../ActionButton/ActionButton.svelte"
63
+ import Portal from "svelte-portal"
64
+ import { setContext, createEventDispatcher, onDestroy } from "svelte"
7
65
  import { generate } from "shortid"
8
66
 
9
67
  export let title
10
- export let fillWidth
11
- export let left = "314px"
12
- export let width = "calc(100% - 626px)"
13
- export let headless = false
68
+ export let forceModal = false
14
69
 
15
70
  const dispatch = createEventDispatcher()
71
+ const spacing = 11
16
72
 
17
73
  let visible = false
18
74
  let drawerId = generate()
19
75
 
76
+ $: depth = $openDrawers.length - $openDrawers.indexOf(drawerId) - 1
77
+ $: style = getStyle(depth, $drawerLeft, $drawerWidth, $modal)
78
+
79
+ const getStyle = (depth, left, width, modal) => {
80
+ let style = `
81
+ --scale-factor: ${getScaleFactor(depth)};
82
+ --spacing: ${spacing}px;
83
+ `
84
+ // Most modal styles are handled by class names
85
+ if (modal || left == null || width == null) {
86
+ return style
87
+ }
88
+
89
+ // Drawers observing another dom node need custom position styles
90
+ return `
91
+ ${style}
92
+ left: ${left + spacing}px;
93
+ width: ${width - 2 * spacing}px;
94
+ `
95
+ }
96
+
20
97
  export function show() {
21
98
  if (visible) {
22
99
  return
23
100
  }
101
+ if (forceModal) {
102
+ modal.set(true)
103
+ resizable.set(false)
104
+ }
105
+ observe()
24
106
  visible = true
25
107
  dispatch("drawerShow", drawerId)
108
+ openDrawers.update(state => [...state, drawerId])
26
109
  }
27
110
 
28
111
  export function hide() {
@@ -31,12 +114,15 @@
31
114
  }
32
115
  visible = false
33
116
  dispatch("drawerHide", drawerId)
117
+ openDrawers.update(state => state.filter(id => id !== drawerId))
118
+ unobserve()
34
119
  }
35
120
 
36
- setContext("drawer-actions", {
121
+ setContext("drawer", {
37
122
  hide,
38
123
  show,
39
- headless,
124
+ modal,
125
+ resizable,
40
126
  })
41
127
 
42
128
  const easeInOutQuad = x => {
@@ -45,66 +131,142 @@
45
131
 
46
132
  // Use a custom svelte transition here because the built-in slide
47
133
  // transition has a horrible overshoot
48
- const slide = () => {
134
+ const drawerSlide = () => {
135
+ return {
136
+ duration: 260,
137
+ css: t => {
138
+ const f = easeInOutQuad(t)
139
+ const yOffset = (1 - f) * 200
140
+ return `
141
+ transform: translateY(calc(${yOffset}px - 800px * (1 - var(--scale-factor))));
142
+ opacity: ${f};
143
+ `
144
+ },
145
+ }
146
+ }
147
+
148
+ // Custom fade transition because the default svelte one doesn't work any more
149
+ // with svelte 4
150
+ const drawerFade = () => {
49
151
  return {
50
- duration: 360,
152
+ duration: 260,
51
153
  css: t => {
52
- const translation = 100 - Math.round(easeInOutQuad(t) * 100)
53
- return `transform: translateY(${translation}%);`
154
+ return `opacity: ${easeInOutQuad(t)};`
54
155
  },
55
156
  }
56
157
  }
158
+
159
+ const getScaleFactor = depth => {
160
+ // Quadratic function approaching a limit of 1 as depth tends to infinity
161
+ const lim = 1 - 1 / (depth * depth + 1)
162
+ // Scale drawers between 1 and 0.9 as depth approaches infinity
163
+ return 1 - lim * 0.1
164
+ }
165
+
166
+ onDestroy(() => {
167
+ if (visible) {
168
+ hide()
169
+ }
170
+ })
57
171
  </script>
58
172
 
59
173
  {#if visible}
60
- <Portal>
61
- <section
62
- class:fillWidth
63
- class="drawer"
64
- class:headless
65
- transition:slide|local
66
- style={`width: ${width}; left: ${left};`}
67
- >
68
- {#if !headless}
174
+ <Portal target=".modal-container">
175
+ <!-- This class is unstyled, but needed by click_outside -->
176
+ <div class="drawer-wrapper">
177
+ <div
178
+ class="underlay"
179
+ class:hidden={!$modal}
180
+ transition:drawerFade|local
181
+ />
182
+ <div
183
+ class="drawer"
184
+ class:stacked={depth > 0}
185
+ class:modal={$modal}
186
+ transition:drawerSlide|local
187
+ {style}
188
+ >
69
189
  <header>
70
- <div class="text">
71
- <Heading size="XS">{title}</Heading>
72
- <Body size="S">
73
- <slot name="description" />
74
- </Body>
75
- </div>
190
+ {#if $$slots.title}
191
+ <slot name="title" />
192
+ {:else}
193
+ <div class="text">{title || "Bindings"}</div>
194
+ {/if}
76
195
  <div class="buttons">
77
196
  <Button secondary quiet on:click={hide}>Cancel</Button>
78
197
  <slot name="buttons" />
198
+ {#if $resizable}
199
+ <ActionButton
200
+ size="M"
201
+ quiet
202
+ selected={$modal}
203
+ on:click={() => modal.set(!$modal)}
204
+ >
205
+ <Icon name={$modal ? "Minimize" : "Maximize"} size="S" />
206
+ </ActionButton>
207
+ {/if}
79
208
  </div>
80
209
  </header>
81
- {/if}
82
- <slot name="body" />
83
- </section>
210
+ <slot name="body" />
211
+ <div class="overlay" class:hidden={$modal || depth === 0} />
212
+ </div>
213
+ </div>
84
214
  </Portal>
85
215
  {/if}
86
216
 
87
217
  <style>
88
- .drawer.headless :global(.drawer-contents) {
89
- height: calc(40vh + 75px);
90
- }
91
-
92
- .buttons {
218
+ .drawer {
219
+ position: absolute;
220
+ left: 25vw;
221
+ width: 50vw;
222
+ bottom: var(--spacing);
223
+ height: 420px;
224
+ background: var(--background);
225
+ border: var(--border-light);
226
+ z-index: 100;
227
+ border-radius: 8px;
228
+ overflow: hidden;
229
+ box-sizing: border-box;
230
+ transition: transform 260ms ease-out, bottom 260ms ease-out,
231
+ left 260ms ease-out, width 260ms ease-out, height 260ms ease-out;
93
232
  display: flex;
94
- gap: var(--spacing-m);
233
+ flex-direction: column;
234
+ align-items: stretch;
235
+ }
236
+ .drawer.modal {
237
+ left: 15vw;
238
+ width: 70vw;
239
+ bottom: 15vh;
240
+ height: 70vh;
241
+ }
242
+ .drawer.stacked {
243
+ transform: translateY(calc(-1 * 1024px * (1 - var(--scale-factor))))
244
+ scale(var(--scale-factor));
95
245
  }
96
246
 
97
- .drawer {
247
+ .overlay,
248
+ .underlay {
249
+ top: 0;
250
+ left: 0;
251
+ width: 100%;
252
+ height: 100%;
253
+ z-index: 100;
254
+ display: block;
255
+ transition: opacity 260ms ease-out;
256
+ }
257
+ .overlay {
98
258
  position: absolute;
99
- bottom: 0;
100
259
  background: var(--background);
101
- border-top: var(--border-light);
102
- z-index: 3;
260
+ opacity: 0.5;
103
261
  }
104
-
105
- .fillWidth {
106
- left: 260px !important;
107
- width: calc(100% - 260px) !important;
262
+ .underlay {
263
+ position: fixed;
264
+ background: rgba(0, 0, 0, 0.5);
265
+ }
266
+ .underlay.hidden,
267
+ .overlay.hidden {
268
+ opacity: 0 !important;
269
+ pointer-events: none;
108
270
  }
109
271
 
110
272
  header {
@@ -112,10 +274,9 @@
112
274
  justify-content: space-between;
113
275
  align-items: center;
114
276
  border-bottom: var(--border-light);
115
- padding: var(--spacing-l) var(--spacing-xl);
277
+ padding: var(--spacing-m) var(--spacing-xl);
116
278
  gap: var(--spacing-xl);
117
279
  }
118
-
119
280
  .text {
120
281
  display: flex;
121
282
  flex-direction: column;
@@ -123,7 +284,6 @@
123
284
  align-items: flex-start;
124
285
  gap: var(--spacing-xs);
125
286
  }
126
-
127
287
  .buttons {
128
288
  display: flex;
129
289
  flex-direction: row;
@@ -131,4 +291,8 @@
131
291
  align-items: center;
132
292
  gap: var(--spacing-m);
133
293
  }
294
+ .buttons :global(.icon) {
295
+ width: 16px;
296
+ display: flex;
297
+ }
134
298
  </style>
@@ -1,4 +1,8 @@
1
- <div class="drawer-contents">
1
+ <script>
2
+ export let padding = true
3
+ </script>
4
+
5
+ <div class="drawer-contents" class:padding>
2
6
  <div class:no-sidebar={!$$slots.sidebar} class="container">
3
7
  {#if $$slots.sidebar}
4
8
  <div class="sidebar">
@@ -13,8 +17,8 @@
13
17
 
14
18
  <style>
15
19
  .drawer-contents {
16
- height: 40vh;
17
20
  overflow-y: auto;
21
+ flex: 1 1 auto;
18
22
  }
19
23
  .container {
20
24
  height: 100%;
@@ -27,14 +31,22 @@
27
31
  .sidebar {
28
32
  border-right: var(--border-light);
29
33
  overflow: auto;
30
- padding: var(--spacing-xl);
31
34
  scrollbar-width: none;
32
35
  }
36
+ .padding .sidebar {
37
+ padding: var(--spacing-xl);
38
+ }
33
39
  .sidebar::-webkit-scrollbar {
34
40
  display: none;
35
41
  }
36
42
  .main {
43
+ height: 100%;
44
+ overflow: auto;
45
+ overflow-x: hidden;
46
+ }
47
+ .padding .main {
37
48
  padding: var(--spacing-xl);
49
+ height: calc(100% - var(--spacing-xl) * 2);
38
50
  }
39
51
  .main :global(textarea) {
40
52
  min-height: 200px;
@@ -1,58 +1,54 @@
1
- <script context="module">
2
- export const directions = ["n", "ne", "e", "se", "s", "sw", "w", "nw"]
3
- </script>
4
-
5
1
  <script>
6
- import Tooltip from "../Tooltip/Tooltip.svelte"
7
- import { fade } from "svelte/transition"
2
+ import {
3
+ default as AbsTooltip,
4
+ TooltipPosition,
5
+ TooltipType,
6
+ } from "../Tooltip/AbsTooltip.svelte"
8
7
 
9
- export let direction = "n"
10
8
  export let name = "Add"
11
9
  export let hidden = false
12
10
  export let size = "M"
13
11
  export let hoverable = false
14
12
  export let disabled = false
15
13
  export let color
14
+ export let hoverColor
16
15
  export let tooltip
17
-
18
- $: rotation = getRotation(direction)
19
-
20
- let showTooltip = false
21
-
22
- const getRotation = direction => {
23
- return directions.indexOf(direction) * 45
24
- }
16
+ export let tooltipPosition = TooltipPosition.Bottom
17
+ export let tooltipType = TooltipType.Default
18
+ export let tooltipColor
19
+ export let tooltipWrap = true
20
+ export let newStyles = false
25
21
  </script>
26
22
 
27
- <!-- svelte-ignore a11y-no-static-element-interactions -->
28
- <!-- svelte-ignore a11y-click-events-have-key-events -->
29
- <div
30
- class="icon"
31
- on:mouseover={() => (showTooltip = true)}
32
- on:focus={() => (showTooltip = true)}
33
- on:mouseleave={() => (showTooltip = false)}
34
- on:click={() => (showTooltip = false)}
23
+ <AbsTooltip
24
+ text={tooltip}
25
+ type={tooltipType}
26
+ position={tooltipPosition}
27
+ color={tooltipColor}
28
+ noWrap={tooltipWrap}
35
29
  >
36
- <svg
37
- on:click
38
- class:hoverable
39
- class:disabled
40
- class="spectrum-Icon spectrum-Icon--size{size}"
41
- focusable="false"
42
- aria-hidden={hidden}
43
- aria-label={name}
44
- style={`transform: rotate(${rotation}deg); ${
45
- color ? `color: ${color};` : ""
46
- }`}
47
- >
48
- <use style="pointer-events: none;" xlink:href="#spectrum-icon-18-{name}" />
49
- </svg>
50
- {#if tooltip && showTooltip}
51
- <div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
52
- <Tooltip textWrapping direction="top" text={tooltip} />
53
- </div>
54
- {/if}
55
- </div>
30
+ <div class="icon" class:newStyles>
31
+ <svg
32
+ on:click
33
+ class:hoverable
34
+ class:disabled
35
+ class="spectrum-Icon spectrum-Icon--size{size}"
36
+ focusable="false"
37
+ aria-hidden={hidden}
38
+ aria-label={name}
39
+ style={`${color ? `color: ${color};` : ""} ${
40
+ hoverColor
41
+ ? `--hover-color: ${hoverColor}`
42
+ : "--hover-color: var(--spectrum-alias-icon-color-selected-hover)"
43
+ }`}
44
+ >
45
+ <use
46
+ style="pointer-events: none;"
47
+ xlink:href="#spectrum-icon-18-{name}"
48
+ />
49
+ </svg>
50
+ </div>
51
+ </AbsTooltip>
56
52
 
57
53
  <style>
58
54
  .icon {
@@ -60,19 +56,25 @@
60
56
  display: grid;
61
57
  place-items: center;
62
58
  }
59
+ .newStyles {
60
+ color: var(--spectrum-global-color-gray-700);
61
+ }
63
62
 
64
63
  svg.hoverable {
65
64
  pointer-events: all;
66
65
  transition: color var(--spectrum-global-animation-duration-100, 130ms);
67
66
  }
68
67
  svg.hoverable:hover {
69
- color: var(--spectrum-alias-icon-color-selected-hover) !important;
68
+ color: var(--hover-color) !important;
70
69
  cursor: pointer;
71
70
  }
72
71
  svg.hoverable:active {
73
72
  color: var(--spectrum-global-color-blue-400) !important;
74
73
  }
75
-
74
+ .newStyles svg.hoverable:hover,
75
+ .newStyles svg.hoverable:active {
76
+ color: var(--spectrum-global-color-gray-900) !important;
77
+ }
76
78
  svg.disabled {
77
79
  color: var(--spectrum-global-color-gray-500) !important;
78
80
  pointer-events: none !important;
@@ -10,6 +10,7 @@
10
10
  export let inline = false
11
11
  export let disableCancel = false
12
12
  export let autoFocus = true
13
+ export let zIndex = 999
13
14
 
14
15
  const dispatch = createEventDispatcher()
15
16
  let visible = fixed || inline
@@ -101,7 +102,11 @@
101
102
  <Portal target=".modal-container">
102
103
  {#if visible}
103
104
  <!-- svelte-ignore a11y-no-static-element-interactions -->
104
- <div class="spectrum-Underlay is-open" on:mousedown|self={cancel}>
105
+ <div
106
+ class="spectrum-Underlay is-open"
107
+ on:mousedown|self={cancel}
108
+ style="z-index:{zIndex || 999}"
109
+ >
105
110
  <div
106
111
  class="background"
107
112
  in:fade={{ duration: 200 }}
@@ -132,7 +137,6 @@
132
137
  flex-direction: row;
133
138
  justify-content: center;
134
139
  align-items: center;
135
- z-index: 999;
136
140
  overflow: auto;
137
141
  overflow-x: hidden;
138
142
  background: transparent;
@@ -12,6 +12,7 @@
12
12
  export let anchor
13
13
  export let align = "right"
14
14
  export let portalTarget
15
+ export let minWidth
15
16
  export let maxWidth
16
17
  export let maxHeight
17
18
  export let open = false
@@ -21,7 +22,6 @@
21
22
  export let customHeight
22
23
  export let animate = true
23
24
  export let customZindex
24
-
25
25
  export let handlePostionUpdate
26
26
  export let showPopover = true
27
27
  export let clickOutsideOverride = false
@@ -87,6 +87,7 @@
87
87
  align,
88
88
  maxHeight,
89
89
  maxWidth,
90
+ minWidth,
90
91
  useAnchorWidth,
91
92
  offset,
92
93
  customUpdate: handlePostionUpdate,
@@ -102,6 +103,8 @@
102
103
  role="presentation"
103
104
  style="height: {customHeight}; --customZindex: {customZindex};"
104
105
  transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
106
+ on:mouseenter
107
+ on:mouseleave
105
108
  >
106
109
  <slot />
107
110
  </div>
@@ -24,6 +24,7 @@
24
24
  export let text = ""
25
25
  export let fixed = false
26
26
  export let color = null
27
+ export let noWrap = false
27
28
 
28
29
  let wrapper
29
30
  let hovered = false
@@ -105,6 +106,7 @@
105
106
  <Portal target=".spectrum">
106
107
  <span
107
108
  class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
109
+ class:noWrap
108
110
  style={`left:${left}px;top:${top}px;${tooltipStyle}`}
109
111
  transition:fade|local={{ duration: 130 }}
110
112
  >
@@ -118,6 +120,9 @@
118
120
  .abs-tooltip {
119
121
  display: contents;
120
122
  }
123
+ .spectrum-Tooltip.noWrap .spectrum-Tooltip-label {
124
+ width: max-content;
125
+ }
121
126
  .spectrum-Tooltip {
122
127
  position: absolute;
123
128
  z-index: 9999;