frappe-ui 0.1.184 → 0.1.185

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": "frappe-ui",
3
- "version": "0.1.184",
3
+ "version": "0.1.185",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -6,6 +6,7 @@ import { Dropdown } from '../Dropdown'
6
6
  import LucideSettings from '~icons/lucide/settings'
7
7
  import LucideStar from '~icons/lucide/star'
8
8
  import LucideChevronDown from '~icons/lucide/chevron-down'
9
+ import { Autocomplete } from '../Autocomplete'
9
10
 
10
11
  const dialog1 = ref(false)
11
12
  const dialog2 = ref(false)
@@ -17,6 +18,8 @@ const dialog6 = ref(false)
17
18
  // Dropdown state
18
19
  const selectedOption = ref('Option 1')
19
20
 
21
+ const autocompleteValue = ref({ label: '', value: '' })
22
+
20
23
  const dropdownOptions = [
21
24
  {
22
25
  label: 'Option 1',
@@ -181,6 +184,16 @@ const createPromise = (): Promise<void> => {
181
184
  This dialog contains interactive elements to test proper layering.
182
185
  </p>
183
186
 
187
+ <Autocomplete
188
+ :options="[
189
+ { label: 'Option A', value: 'A' },
190
+ { label: 'Option B', value: 'B' },
191
+ { label: 'Option C', value: 'C' },
192
+ ]"
193
+ placeholder="Type to search..."
194
+ v-model="autocompleteValue"
195
+ />
196
+
184
197
  <div class="space-y-3">
185
198
  <label class="block text-sm font-medium text-gray-700">
186
199
  Select an option:
@@ -1,284 +1,276 @@
1
1
  <template>
2
- <div ref="reference">
3
- <div
4
- ref="target"
5
- :class="['flex', $attrs.class]"
6
- @click="updatePosition"
7
- @focusin="updatePosition"
8
- @keydown="updatePosition"
9
- @mouseover="onMouseover"
10
- @mouseleave="onMouseleave"
11
- >
12
- <slot
13
- name="target"
14
- v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
15
- />
16
- </div>
17
- <teleport to="#frappeui-popper-root">
2
+ <PopoverRoot v-model:open="isOpen" @update:open="onUpdateOpen">
3
+ <PopoverAnchor asChild>
18
4
  <div
19
- ref="popover"
20
- class="relative z-[100]"
21
- :class="[popoverContainerClass, popoverClass]"
22
- :style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
23
- @mouseover="pointerOverTargetOrPopup = true"
5
+ ref="anchorRef"
6
+ :class="['flex', $attrs.class]"
7
+ @mouseover="onMouseover"
24
8
  @mouseleave="onMouseleave"
25
9
  >
26
- <transition v-bind="popupTransition">
27
- <div v-show="isOpen">
28
- <slot
29
- name="body"
30
- v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
31
- >
32
- <div class="rounded-lg border bg-surface-modal shadow-xl">
33
- <slot
34
- name="body-main"
35
- v-bind="{
36
- togglePopover,
37
- updatePosition,
38
- open,
39
- close,
40
- isOpen,
41
- }"
42
- />
43
- </div>
44
- </slot>
45
- </div>
46
- </transition>
10
+ <slot
11
+ name="target"
12
+ v-bind="{
13
+ togglePopover,
14
+ updatePosition,
15
+ open,
16
+ close,
17
+ isOpen,
18
+ }"
19
+ />
47
20
  </div>
48
- </teleport>
49
- </div>
21
+ </PopoverAnchor>
22
+ <PopoverPortal>
23
+ <PopoverContent
24
+ :side="placementSide"
25
+ :align="placementAlign"
26
+ :style="{
27
+ minWidth: 'var(--reka-popover-trigger-width)',
28
+ }"
29
+ class="PopoverContent"
30
+ :class="{ 'has-transition': hasTransition }"
31
+ @mouseover="
32
+ () => {
33
+ pointerOverTargetOrPopup = true
34
+ }
35
+ "
36
+ @mouseleave="onMouseleave"
37
+ @interact-outside="onInteractOutside"
38
+ >
39
+ <div class="relative" :class="['body-container', popoverClass]">
40
+ <slot
41
+ name="body"
42
+ v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
43
+ >
44
+ <div class="rounded-lg border bg-surface-modal shadow-xl">
45
+ <slot
46
+ name="body-main"
47
+ v-bind="{
48
+ togglePopover,
49
+ updatePosition,
50
+ open,
51
+ close,
52
+ isOpen,
53
+ }"
54
+ />
55
+ </div>
56
+ </slot>
57
+ </div>
58
+ </PopoverContent>
59
+ </PopoverPortal>
60
+ </PopoverRoot>
50
61
  </template>
51
62
 
52
- <script>
53
- import { createPopper } from '@popperjs/core'
63
+ <script setup lang="ts">
64
+ import { computed, ref, onUnmounted } from 'vue'
65
+ import {
66
+ PopoverAnchor,
67
+ PopoverContent,
68
+ PopoverPortal,
69
+ PopoverRoot,
70
+ } from 'reka-ui'
71
+ import { PopoverProps } from './types'
72
+
73
+ const props = withDefaults(defineProps<PopoverProps>(), {
74
+ show: undefined,
75
+ trigger: 'click',
76
+ hoverDelay: 0,
77
+ leaveDelay: 0.5,
78
+ placement: 'bottom-start',
79
+ popoverClass: '',
80
+ transition: null,
81
+ hideOnBlur: true,
82
+ })
54
83
 
55
- export default {
56
- name: 'Popover',
84
+ const emit = defineEmits<{
85
+ (event: 'open'): void
86
+ (event: 'close'): void
87
+ (event: 'update:show', value: boolean): void
88
+ }>()
89
+
90
+ defineExpose({ open, close })
91
+
92
+ defineOptions({
57
93
  inheritAttrs: false,
58
- props: {
59
- show: {
60
- default: undefined,
61
- },
62
- trigger: {
63
- type: String,
64
- default: 'click', // click, hover
65
- },
66
- hoverDelay: {
67
- type: Number,
68
- default: 0,
69
- },
70
- leaveDelay: {
71
- type: Number,
72
- default: 0,
73
- },
74
- placement: {
75
- type: String,
76
- default: 'bottom-start',
77
- },
78
- popoverClass: [String, Object, Array],
79
- transition: {
80
- default: null,
81
- },
82
- hideOnBlur: {
83
- default: true,
84
- },
85
- },
86
- emits: ['open', 'close', 'update:show'],
87
- expose: ['open', 'close'],
88
- data() {
89
- return {
90
- popoverContainerClass: 'body-container',
91
- showPopup: false,
92
- targetWidth: null,
93
- pointerOverTargetOrPopup: false,
94
- }
95
- },
96
- watch: {
97
- show(val) {
98
- if (val) {
99
- this.open()
100
- } else {
101
- this.close()
102
- }
103
- },
104
- },
105
- created() {
106
- if (typeof window === 'undefined') return
107
- if (!document.getElementById('frappeui-popper-root')) {
108
- const root = document.createElement('div')
109
- root.id = 'frappeui-popper-root'
110
- document.body.appendChild(root)
94
+ })
95
+
96
+ const _isOpen = ref(false)
97
+ const pointerOverTargetOrPopup = ref(false)
98
+ const hoverTimer = ref<number | null>(null)
99
+ const leaveTimer = ref<number | null>(null)
100
+ const anchorRef = ref<HTMLElement | null>(null)
101
+
102
+ const isOpen = computed({
103
+ get: () => (isShowPropPassed.value ? props.show : _isOpen.value),
104
+ set: (value: boolean) => {
105
+ if (!isShowPropPassed.value) {
106
+ _isOpen.value = value
111
107
  }
108
+ emit('update:show', value)
112
109
  },
113
- mounted() {
114
- this.listener = (e) => {
115
- const clickedElement = e.target
116
- const reference = this.$refs.reference
117
- const popoverBody = this.$refs.popover
118
- const insideClick =
119
- clickedElement === reference ||
120
- clickedElement === popoverBody ||
121
- reference?.contains(clickedElement) ||
122
- popoverBody?.contains(clickedElement)
123
- if (insideClick) {
124
- return
125
- }
110
+ })
126
111
 
127
- const root = document.getElementById('frappeui-popper-root')
128
- const insidePopoverRoot = root.contains(clickedElement)
129
- if (!insidePopoverRoot) {
130
- return this.close()
131
- }
112
+ const isShowPropPassed = computed(() => {
113
+ return props.show !== undefined
114
+ })
132
115
 
133
- const bodyClass = `.${this.popoverContainerClass}`
134
- const clickedElementBody = clickedElement?.closest(bodyClass)
135
- const currentPopoverBody = reference?.closest(bodyClass)
136
- const isSiblingClicked =
137
- clickedElementBody &&
138
- currentPopoverBody &&
139
- clickedElementBody === currentPopoverBody
116
+ const placementSide = computed(() => {
117
+ const [side] = props.placement.split('-')
118
+ return side as 'top' | 'right' | 'bottom' | 'left'
119
+ })
140
120
 
141
- if (isSiblingClicked) {
142
- this.close()
143
- }
121
+ const placementAlign = computed(() => {
122
+ const [, align] = props.placement.split('-')
123
+ if (!align) return 'center'
124
+ return align as 'start' | 'center' | 'end'
125
+ })
126
+
127
+ function togglePopover(flag?: boolean | Event) {
128
+ if (flag instanceof Event) {
129
+ flag = undefined
130
+ }
131
+ if (flag == null) {
132
+ flag = !isOpen.value
133
+ }
134
+ flag = Boolean(flag)
135
+ if (flag) {
136
+ open()
137
+ } else {
138
+ close()
139
+ }
140
+ }
141
+
142
+ function updatePosition() {
143
+ // not needed
144
+ }
145
+
146
+ function open() {
147
+ isOpen.value = true
148
+ }
149
+
150
+ function close() {
151
+ isOpen.value = false
152
+ }
153
+
154
+ function onUpdateOpen(value: boolean) {
155
+ emit('update:show', value)
156
+ if (value) {
157
+ emit('open')
158
+ } else {
159
+ emit('close')
160
+ }
161
+ }
162
+
163
+ function onMouseover() {
164
+ pointerOverTargetOrPopup.value = true
165
+ if (leaveTimer.value) {
166
+ clearTimeout(leaveTimer.value)
167
+ leaveTimer.value = null
168
+ }
169
+ if (props.trigger === 'hover') {
170
+ if (props.hoverDelay) {
171
+ hoverTimer.value = setTimeout(
172
+ () => {
173
+ if (pointerOverTargetOrPopup.value) {
174
+ open()
175
+ }
176
+ },
177
+ Number(props.hoverDelay) * 1000,
178
+ ) as unknown as number
179
+ } else {
180
+ open()
144
181
  }
145
- if (this.hideOnBlur) {
146
- document.addEventListener('click', this.listener)
147
- // https://github.com/tailwindlabs/headlessui/issues/834#issuecomment-1030907894
148
- document.addEventListener('mousedown', this.listener)
182
+ }
183
+ }
184
+
185
+ function onMouseleave() {
186
+ pointerOverTargetOrPopup.value = false
187
+ if (hoverTimer.value) {
188
+ clearTimeout(hoverTimer.value)
189
+ hoverTimer.value = null
190
+ }
191
+ if (props.trigger === 'hover') {
192
+ if (leaveTimer.value) {
193
+ clearTimeout(leaveTimer.value)
149
194
  }
150
- this.$nextTick(() => {
151
- this.targetWidth = this.$refs['target'].clientWidth
152
- })
153
- },
154
- beforeDestroy() {
155
- this.popper && this.popper.destroy()
156
- document.removeEventListener('click', this.listener)
157
- document.removeEventListener('mousedown', this.listener)
158
- },
159
- computed: {
160
- showPropPassed() {
161
- return this.show != null
162
- },
163
- isOpen: {
164
- get() {
165
- if (this.showPropPassed) {
166
- return this.show
167
- }
168
- return this.showPopup
169
- },
170
- set(val) {
171
- val = Boolean(val)
172
- if (this.showPropPassed) {
173
- this.$emit('update:show', val)
174
- } else {
175
- this.showPopup = val
176
- }
177
- if (val === false) {
178
- this.$emit('close')
179
- } else if (val === true) {
180
- this.$emit('open')
181
- }
182
- },
183
- },
184
- popupTransition() {
185
- let templates = {
186
- default: {
187
- enterActiveClass: 'transition duration-150 ease-out',
188
- enterFromClass: 'translate-y-1 opacity-0',
189
- enterToClass: 'translate-y-0 opacity-100',
190
- leaveActiveClass: 'transition duration-150 ease-in',
191
- leaveFromClass: 'translate-y-0 opacity-100',
192
- leaveToClass: 'translate-y-1 opacity-0',
193
- },
194
- }
195
- if (typeof this.transition === 'string') {
196
- return templates[this.transition]
197
- }
198
- return this.transition
199
- },
200
- },
201
- methods: {
202
- setupPopper() {
203
- if (!this.popper) {
204
- this.popper = createPopper(this.$refs.reference, this.$refs.popover, {
205
- placement: this.placement,
206
- })
207
- } else {
208
- this.updatePosition()
209
- }
210
- },
211
- updatePosition() {
212
- this.popper && this.popper.update()
213
- },
214
- togglePopover(flag) {
215
- if (flag instanceof Event) {
216
- flag = null
217
- }
218
- if (flag == null) {
219
- flag = !this.isOpen
220
- }
221
- flag = Boolean(flag)
222
- if (flag) {
223
- this.open()
224
- } else {
225
- this.close()
226
- }
227
- },
228
- open() {
229
- this.isOpen = true
230
- this.$nextTick(() => this.setupPopper())
231
- },
232
- close() {
233
- this.isOpen = false
234
- },
235
- onMouseover() {
236
- this.pointerOverTargetOrPopup = true
237
- if (this.leaveTimer) {
238
- clearTimeout(this.leaveTimer)
239
- this.leaveTimer = null
240
- }
241
- if (this.trigger === 'hover') {
242
- if (this.hoverDelay) {
243
- this.hoverTimer = setTimeout(
244
- () => {
245
- if (this.pointerOverTargetOrPopup) {
246
- this.open()
247
- }
248
- },
249
- Number(this.hoverDelay) * 1000,
250
- )
251
- } else {
252
- this.open()
253
- }
254
- }
255
- },
256
- onMouseleave(e) {
257
- this.pointerOverTargetOrPopup = false
258
- if (this.hoverTimer) {
259
- clearTimeout(this.hoverTimer)
260
- this.hoverTimer = null
261
- }
262
- if (this.trigger === 'hover') {
263
- if (this.leaveTimer) {
264
- clearTimeout(this.leaveTimer)
265
- }
266
- if (this.leaveDelay) {
267
- this.leaveTimer = setTimeout(
268
- () => {
269
- if (!this.pointerOverTargetOrPopup) {
270
- this.close()
271
- }
272
- },
273
- Number(this.leaveDelay) * 1000,
274
- )
275
- } else {
276
- if (!this.pointerOverTargetOrPopup) {
277
- this.close()
195
+ if (props.leaveDelay) {
196
+ leaveTimer.value = setTimeout(
197
+ () => {
198
+ if (!pointerOverTargetOrPopup.value) {
199
+ close()
278
200
  }
279
- }
201
+ },
202
+ Number(props.leaveDelay) * 1000,
203
+ ) as unknown as number
204
+ } else {
205
+ if (!pointerOverTargetOrPopup.value) {
206
+ close()
280
207
  }
281
- },
282
- },
208
+ }
209
+ }
210
+ }
211
+
212
+ function onInteractOutside(event: Event) {
213
+ if (!props.hideOnBlur) {
214
+ event.preventDefault()
215
+ return
216
+ }
217
+
218
+ // Check if the click is on the trigger/anchor element
219
+ const target = event.target as Element
220
+ if (
221
+ anchorRef.value &&
222
+ (anchorRef.value.contains(target) || anchorRef.value === target)
223
+ ) {
224
+ event.preventDefault()
225
+ return
226
+ }
283
227
  }
228
+
229
+ const hasTransition = computed(() => {
230
+ return props.transition === 'default'
231
+ })
232
+
233
+ // Cleanup timers on unmount
234
+ onUnmounted(() => {
235
+ if (hoverTimer.value) {
236
+ clearTimeout(hoverTimer.value)
237
+ }
238
+ if (leaveTimer.value) {
239
+ clearTimeout(leaveTimer.value)
240
+ }
241
+ })
284
242
  </script>
243
+
244
+ <style>
245
+ /* Default transition animations */
246
+ @keyframes popover-enter {
247
+ from {
248
+ opacity: 0;
249
+ transform: translateY(4px);
250
+ }
251
+ to {
252
+ opacity: 1;
253
+ transform: translateY(0);
254
+ }
255
+ }
256
+
257
+ @keyframes popover-exit {
258
+ from {
259
+ opacity: 1;
260
+ transform: translateY(0);
261
+ }
262
+ to {
263
+ opacity: 0;
264
+ transform: translateY(4px);
265
+ }
266
+ }
267
+
268
+ /* Default transition */
269
+ .PopoverContent.has-transition[data-state='open'] {
270
+ animation: popover-enter 150ms ease-out;
271
+ }
272
+
273
+ .PopoverContent.has-transition[data-state='closed'] {
274
+ animation: popover-exit 150ms ease-in;
275
+ }
276
+ </style>
@@ -0,0 +1,24 @@
1
+ export interface PopoverProps {
2
+ show?: boolean
3
+ trigger?: 'click' | 'hover'
4
+ hoverDelay?: number
5
+ leaveDelay?: number
6
+ placement?:
7
+ | 'top-start'
8
+ | 'top-end'
9
+ | 'bottom-start'
10
+ | 'bottom-end'
11
+ | 'right-start'
12
+ | 'right-end'
13
+ | 'left-start'
14
+ | 'left-end'
15
+ popoverClass?: string | object | Array<string | object>
16
+ transition?: 'default' | null
17
+ hideOnBlur?: boolean
18
+ }
19
+
20
+ export interface PopoverEmits {
21
+ (event: 'open'): void
22
+ (event: 'close'): void
23
+ (event: 'update:show', value: boolean): void
24
+ }