@globalbrain/sefirot 3.41.0 → 3.42.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.
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { onKeyStroke } from '@vueuse/core'
3
- import { computed, ref, shallowRef } from 'vue'
2
+ import { onClickOutside, onKeyStroke, useElementHover, useFocusWithin } from '@vueuse/core'
3
+ import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
4
4
  import { type Position, useTooltip } from '../composables/Tooltip'
5
5
 
6
6
  const props = withDefaults(defineProps<{
@@ -17,13 +17,13 @@ const props = withDefaults(defineProps<{
17
17
  trigger: 'hover'
18
18
  })
19
19
 
20
- const el = shallowRef<HTMLElement | null>(null)
21
- const tip = shallowRef<HTMLElement | null>(null)
20
+ const root = shallowRef<HTMLElement | null>(null)
22
21
  const content = shallowRef<HTMLElement | null>(null)
22
+ const trigger = shallowRef<HTMLElement | null>(null)
23
23
 
24
24
  const rootClasses = computed(() => [
25
25
  props.display,
26
- props.tabindex && (props.tabindex > -1) && 'focusable'
26
+ props.tabindex && props.tabindex > -1 && 'focusable'
27
27
  ])
28
28
 
29
29
  const containerClasses = computed(() => [props.position])
@@ -38,77 +38,76 @@ const tabindex = computed(() => {
38
38
  })
39
39
 
40
40
  const { on, show, hide } = useTooltip(
41
- el,
41
+ root,
42
+ trigger,
42
43
  content,
43
- tip,
44
44
  computed(() => props.position),
45
45
  timeoutId
46
46
  )
47
47
 
48
- onKeyStroke('Escape', (e) => {
49
- if (on.value && el.value?.matches(':focus-within')) {
50
- e.preventDefault()
51
- e.stopPropagation()
52
- hide()
53
- }
54
- })
55
-
56
- function onMouseEnter() {
57
- if (props.trigger === 'hover' || props.trigger === 'both') {
58
- show()
59
- }
60
- }
61
-
62
- function onMouseLeave() {
63
- if (
64
- props.trigger === 'hover'
65
- || (props.trigger === 'both' && !el.value?.matches(':focus-within'))
66
- ) {
67
- hide()
48
+ const isRootHovered = useElementHover(root, { delayLeave: 100 })
49
+ const isContentHovered = useElementHover(content, { delayLeave: 100 })
50
+ const { focused: isRootFocused } = useFocusWithin(root)
51
+ const { focused: isContentFocused } = useFocusWithin(content)
52
+ const ignore = ref(false)
53
+
54
+ watch(
55
+ [isRootHovered, isContentHovered, isRootFocused, isContentFocused],
56
+ ([rootHover, contentHover, rootFocus, contentFocus]) => {
57
+ if (ignore.value) { return }
58
+ if (
59
+ (props.trigger === 'hover' && (rootHover || contentHover))
60
+ || (props.trigger === 'focus' && (rootFocus || contentHover || contentFocus))
61
+ || (props.trigger === 'both' && (rootHover || contentHover || rootFocus || contentFocus))
62
+ ) {
63
+ show()
64
+
65
+ if (rootFocus && props.timeout) {
66
+ timeoutId.value = window.setTimeout(hide, props.timeout)
67
+ }
68
+ } else {
69
+ hide()
70
+ }
68
71
  }
69
- }
72
+ )
70
73
 
71
- function onFocus() {
72
- if (props.trigger === 'focus' || props.trigger === 'both') {
73
- show()
74
- if (props.timeout) {
75
- timeoutId.value = setTimeout(hide, props.timeout) as any
74
+ const cleanups = [
75
+ onKeyStroke('Escape', (e) => {
76
+ if (
77
+ on.value
78
+ && props.trigger !== 'hover'
79
+ && (isRootFocused.value || isContentHovered.value || isContentFocused.value)
80
+ ) {
81
+ e.preventDefault()
82
+ e.stopPropagation()
83
+ ignore.value = true
84
+ hide()
85
+ setTimeout(() => { ignore.value = false })
76
86
  }
77
- }
78
- }
87
+ }),
88
+ onClickOutside(root, hide, { ignore: [content] }),
89
+ () => timeoutId.value != null && window.clearTimeout(timeoutId.value)
90
+ ]
79
91
 
80
- function onBlur() {
81
- if (
82
- props.trigger === 'focus'
83
- || (props.trigger === 'both' && !el.value?.matches(':hover'))
84
- ) {
85
- hide()
86
- }
87
- }
92
+ onBeforeUnmount(() => {
93
+ cleanups.forEach((cleanup) => cleanup())
94
+ })
88
95
  </script>
89
96
 
90
97
  <template>
91
- <component
92
- ref="el"
93
- :is="tag"
94
- class="STooltip"
95
- :class="rootClasses"
96
- :tabindex="tabindex"
97
- @mouseenter="onMouseEnter"
98
- @mouseleave="onMouseLeave"
99
- @focusin="onFocus"
100
- @focusout="onBlur"
101
- >
102
- <span class="content" ref="content">
98
+ <component ref="root" :is="tag" class="STooltip" :class="rootClasses" :tabindex="tabindex">
99
+ <span class="trigger" ref="trigger">
103
100
  <slot />
104
101
  </span>
105
102
 
106
- <transition name="fade">
107
- <span v-show="on" class="container" :class="containerClasses" ref="tip">
108
- <span v-if="$slots.text" class="tip"><slot name="text" /></span>
109
- <span v-else-if="text" class="tip" v-html="text" />
110
- </span>
111
- </transition>
103
+ <Teleport to="#sefirot-modals">
104
+ <Transition name="fade">
105
+ <span v-show="on" class="container" :class="containerClasses" ref="content">
106
+ <span v-if="$slots.text" class="tip"><slot name="text" /></span>
107
+ <span v-else-if="text" class="tip" v-html="text" />
108
+ </span>
109
+ </Transition>
110
+ </Teleport>
112
111
  </component>
113
112
  </template>
114
113
 
@@ -129,7 +128,7 @@ function onBlur() {
129
128
  &.block { display: block; }
130
129
  }
131
130
 
132
- .content {
131
+ .trigger {
133
132
  white-space: nowrap;
134
133
  }
135
134
 
@@ -138,6 +137,7 @@ function onBlur() {
138
137
  z-index: var(--z-index-tooltip);
139
138
  display: block;
140
139
  transition: opacity 0.25s;
140
+ padding: 8px;
141
141
  }
142
142
 
143
143
  .container.fade-enter-from,
@@ -149,37 +149,11 @@ function onBlur() {
149
149
  &.left .tip { transform: translateX(8px); }
150
150
  }
151
151
 
152
- .container.top {
153
- top: 0;
154
- left: 50%;
155
- padding-bottom: 8px;
156
- transform: translate(-50%, -100%);
157
- }
158
-
159
- .container.right {
160
- top: 50%;
161
- left: 100%;
162
- transform: translate(8px, -50%);
163
- }
164
-
165
- .container.bottom {
166
- bottom: 0;
167
- left: 50%;
168
- padding-top: 8px;
169
- transform: translate(-50%, 100%);
170
- }
171
-
172
- .container.left {
173
- top: 50%;
174
- right: 100%;
175
- transform: translate(-8px, -50%);
176
- }
177
-
178
152
  .tip {
179
153
  display: block;
180
154
  border: 1px solid var(--tooltip-border-color);
181
155
  border-radius: 6px;
182
- padding: 10px 12px;
156
+ padding: 8px 12px;
183
157
  width: max-content;
184
158
  max-width: var(--tooltip-max-width);
185
159
  line-height: 20px;
@@ -1,113 +1,148 @@
1
1
  import { type Ref, ref } from 'vue'
2
2
 
3
- export interface Tooltip {
4
- on: Ref<boolean>
5
- show: () => void
6
- hide: () => void
7
- }
8
-
9
3
  export type Position = 'top' | 'right' | 'bottom' | 'left'
10
4
 
11
5
  const globalHide = ref<() => void>()
12
6
 
13
7
  /**
14
8
  * Prevent tooltip going off-screen by adjusting the position depending on
15
- * the current window size. This only applies to position `top` and
16
- * `bottom` since we only care about left and right of the screen.
9
+ * the current window size.
17
10
  */
18
11
  export function useTooltip(
19
- el: Ref<HTMLElement | null>,
12
+ root: Ref<HTMLElement | null>,
13
+ trigger: Ref<HTMLElement | null>,
20
14
  content: Ref<HTMLElement | null>,
21
- tip: Ref<HTMLElement | null>,
22
15
  position: Ref<Position>,
23
16
  timeoutId: Ref<number | null>
24
- ): Tooltip {
17
+ ) {
25
18
  const on = ref(false)
19
+ const showTimeout = ref<number | null>()
26
20
 
27
21
  function show(): void {
28
22
  if (on.value) { return }
29
23
  globalHide.value?.()
30
- setPosition()
31
- setTimeout(() => { on.value = true })
24
+ setPosition(trigger.value, content.value, position.value)
25
+ showTimeout.value = window.setTimeout(() => {
26
+ showTimeout.value = null
27
+ on.value = true
28
+ }, 200)
32
29
  globalHide.value = hide
33
30
  }
34
31
 
35
32
  function hide(): void {
33
+ if (showTimeout.value != null) {
34
+ window.clearTimeout(showTimeout.value)
35
+ showTimeout.value = null
36
+ }
37
+ if (timeoutId.value != null) {
38
+ window.clearTimeout(timeoutId.value)
39
+ timeoutId.value = null
40
+ }
36
41
  if (!on.value) { return }
37
- setTimeout(() => {
38
- if (timeoutId.value) {
39
- clearTimeout(timeoutId.value)
40
- timeoutId.value = null
41
- }
42
+ globalHide.value = undefined
43
+ window.setTimeout(() => {
42
44
  on.value = false
43
- if (el.value?.matches(':focus-within')) {
44
- (document.activeElement as HTMLElement)?.blur?.()
45
+ if (root.value?.matches(':focus-within')) {
46
+ ;(document.activeElement as HTMLElement)?.blur?.()
45
47
  }
46
48
  })
47
49
  }
48
50
 
49
- function setPosition(): void {
50
- if (shouldPosition()) {
51
- doSetPosition()
52
- }
51
+ return {
52
+ on,
53
+ show,
54
+ hide
53
55
  }
56
+ }
54
57
 
55
- function doSetPosition(): void {
56
- // Reset position first so that we can get the original position.
57
- resetPosition()
58
-
59
- // Temporally show tip to get its size.
60
- tip.value!.style.display = 'block'
58
+ function setPosition(
59
+ trigger: HTMLElement | null,
60
+ content: HTMLElement | null,
61
+ placement: Position
62
+ ): void {
63
+ if (typeof document === 'undefined' || !trigger || !content) { return }
61
64
 
62
- const screenPadding = document.body.clientWidth >= 512 ? 24 : 8
63
- const contentRect = content.value!.getBoundingClientRect()
64
- const tipRect = tip.value!.getBoundingClientRect()
65
+ const pos = getCurrentPositions(trigger, content)
65
66
 
66
- const contentRightX = contentRect.x + contentRect.width
67
- const tipRightX = tipRect.x + tipRect.width
67
+ let top = pos.minY
68
+ let left = pos.minX
68
69
 
69
- if (tipRect.x < screenPadding) {
70
- adjustLeftPosition(screenPadding, contentRect.x)
71
- } else if (tipRightX > (document.body.clientWidth - screenPadding)) {
72
- adjustRightPosition(screenPadding, contentRightX)
70
+ if (placement === 'top') {
71
+ top = pos.triggerTop - pos.contentHeight
72
+ if (top < pos.minY) {
73
+ top = pos.triggerTop + pos.triggerHeight
74
+ }
75
+ left = pos.triggerLeft + pos.triggerWidth / 2 - pos.contentWidth / 2
76
+ if (left + pos.contentWidth > pos.maxX) {
77
+ left = pos.maxX - pos.contentWidth
78
+ }
79
+ if (left < pos.minX) {
80
+ left = pos.minX
81
+ }
82
+ } else if (placement === 'right') {
83
+ top = pos.triggerTop + pos.triggerHeight / 2 - pos.contentHeight / 2
84
+ if (top + pos.contentHeight > pos.maxY) {
85
+ top = pos.maxY - pos.contentHeight
86
+ }
87
+ if (top < pos.minY) {
88
+ top = pos.minY
89
+ }
90
+ left = pos.triggerLeft + pos.triggerWidth
91
+ if (left + pos.contentWidth > pos.maxX) {
92
+ left = pos.triggerLeft - pos.contentWidth
93
+ }
94
+ } else if (placement === 'bottom') {
95
+ top = pos.triggerTop + pos.triggerHeight
96
+ if (top + pos.contentHeight > pos.maxY) {
97
+ top = pos.triggerTop - pos.contentHeight
98
+ }
99
+ left = pos.triggerLeft + pos.triggerWidth / 2 - pos.contentWidth / 2
100
+ if (left + pos.contentWidth > pos.maxX) {
101
+ left = pos.maxX - pos.contentWidth
102
+ }
103
+ if (left < pos.minX) {
104
+ left = pos.minX
105
+ }
106
+ } else if (placement === 'left') {
107
+ top = pos.triggerTop + pos.triggerHeight / 2 - pos.contentHeight / 2
108
+ if (top + pos.contentHeight > pos.maxY) {
109
+ top = pos.maxY - pos.contentHeight
110
+ }
111
+ if (top < pos.minY) {
112
+ top = pos.minY
113
+ }
114
+ left = pos.triggerLeft - pos.contentWidth
115
+ if (left < pos.minX) {
116
+ left = pos.triggerLeft + pos.triggerWidth
73
117
  }
74
-
75
- tip.value!.style.display = 'none'
76
- }
77
-
78
- function adjustLeftPosition(screenPadding: number, contentRectX: number): void {
79
- tip.value!.style.left = '0'
80
- tip.value!.style.right = 'auto'
81
- setTransform(-contentRectX + screenPadding)
82
- }
83
-
84
- function adjustRightPosition(screenPadding: number, contentRightX: number): void {
85
- tip.value!.style.left = 'auto'
86
- tip.value!.style.right = '0'
87
- setTransform((document.body.clientWidth - contentRightX) - screenPadding)
88
- }
89
-
90
- function resetPosition(): void {
91
- tip.value!.style.left = ''
92
- tip.value!.style.right = ''
93
- tip.value!.style.transform = ''
94
- }
95
-
96
- function setTransform(x: number): void {
97
- tip.value!.style.transform = `translate(${x}px, ${position.value === 'top' ? -100 : 100}%)`
98
118
  }
99
119
 
100
- function shouldPosition(): boolean {
101
- if (!tip.value || !content.value) {
102
- return false
103
- }
120
+ content.style.top = `${top}px`
121
+ content.style.left = `${left}px`
122
+ }
104
123
 
105
- return position.value === 'top' || position.value === 'bottom'
106
- }
124
+ function getCurrentPositions(trigger: HTMLElement, content: HTMLElement) {
125
+ const bodyRect = document.body.getBoundingClientRect()
126
+ const triggerRect = trigger.getBoundingClientRect()
127
+ const contentDisplay = content.style.display
128
+ content.style.display = 'block'
129
+ const contentRect = content.getBoundingClientRect()
130
+ content.style.display = contentDisplay
107
131
 
108
132
  return {
109
- on,
110
- show,
111
- hide
133
+ minX: -bodyRect.left,
134
+ minY: -bodyRect.top,
135
+ maxX: bodyRect.width - bodyRect.left,
136
+ maxY: bodyRect.height - bodyRect.top,
137
+
138
+ triggerTop: triggerRect.top - bodyRect.top,
139
+ triggerLeft: triggerRect.left - bodyRect.left,
140
+ triggerWidth: triggerRect.width,
141
+ triggerHeight: triggerRect.height,
142
+
143
+ contentTop: contentRect.top - bodyRect.top,
144
+ contentLeft: contentRect.left - bodyRect.left,
145
+ contentWidth: contentRect.width,
146
+ contentHeight: contentRect.height
112
147
  }
113
148
  }
@@ -6,6 +6,7 @@ import { _required } from './validators'
6
6
  export interface RuleOptions {
7
7
  optional?: boolean
8
8
  async?: boolean
9
+ params?: Record<string, any>
9
10
  message(params: MessageProps): string
10
11
  validation(value: unknown): boolean | Promise<boolean>
11
12
  }
@@ -19,14 +20,27 @@ export function createRule(
19
20
  ): ValidationRuleWithParams {
20
21
  const lang = useLang()
21
22
 
22
- function validation(value: unknown) {
23
- return options.optional && !_required(value)
24
- ? true
25
- : options.validation(value)
26
- }
23
+ const params = options.params ?? {}
24
+
25
+ const validator = helpers.withParams(
26
+ params,
27
+ (value: unknown) => {
28
+ return options.optional && !_required(value)
29
+ ? true
30
+ : options.validation(value)
31
+ }
32
+ )
27
33
 
28
34
  return helpers.withMessage(
29
35
  (params) => options.message({ ...params, lang }),
30
- options.async ? helpers.withAsync(validation) : validation
36
+ options.async
37
+ ? helpers.withAsync(validator, createParamsForAsyncValidator(params))
38
+ : validator
31
39
  )
32
40
  }
41
+
42
+ function createParamsForAsyncValidator(params: Record<string, any>) {
43
+ return Object.keys(params).map((key) => {
44
+ return params[key]
45
+ })
46
+ }
@@ -10,6 +10,7 @@ export const message = {
10
10
  export function requiredHmsIf(condition: RequiredIfCondition, required?: HmsType[], msg?: string) {
11
11
  return createRule({
12
12
  async: true,
13
+ params: { condition },
13
14
  message: ({ lang }) => msg ?? message[lang],
14
15
  validation: (value) => baseRequiredHmsIf(value, condition, required)
15
16
  })
@@ -12,6 +12,7 @@ export function requiredIf(
12
12
  ) {
13
13
  return createRule({
14
14
  async: true,
15
+ params: { condition },
15
16
  message: ({ lang }) => msg ?? message[lang],
16
17
  validation: (value) => baseRequiredIf(value, condition)
17
18
  })
@@ -10,6 +10,7 @@ export const message = {
10
10
  export function requiredYmdIf(condition: RequiredIfCondition, required?: YmdType[], msg?: string) {
11
11
  return createRule({
12
12
  async: true,
13
+ params: { condition },
13
14
  message: ({ lang }) => msg ?? message[lang],
14
15
  validation: (value) => baseRequiredYmdIf(value, condition, required)
15
16
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "3.41.0",
4
- "packageManager": "pnpm@8.15.5",
3
+ "version": "3.42.0",
4
+ "packageManager": "pnpm@8.15.7",
5
5
  "description": "Vue Components for Global Brain Design System.",
6
6
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
7
7
  "license": "MIT",
@@ -45,8 +45,9 @@
45
45
  "@iconify/vue": "^4.1.1",
46
46
  "@types/body-scroll-lock": "^3.1.2",
47
47
  "@types/lodash-es": "^4.17.12",
48
- "@types/markdown-it": "^13.0.7",
49
- "@vue/reactivity": "^3.4.21",
48
+ "@types/markdown-it": "^14.0.1",
49
+ "@vue/reactivity": "^3.4.22",
50
+ "@vue/runtime-core": "^3.4.22",
50
51
  "@vuelidate/core": "^2.0.3",
51
52
  "@vuelidate/validators": "^2.0.4",
52
53
  "@vueuse/core": "^10.9.0",
@@ -59,20 +60,20 @@
59
60
  "postcss": "^8.4.38",
60
61
  "postcss-nested": "^6.0.1",
61
62
  "v-calendar": "^3.1.2",
62
- "vue": "^3.4.21",
63
+ "vue": "^3.4.22",
63
64
  "vue-router": "^4.3.0"
64
65
  },
65
66
  "dependencies": {
66
- "@sentry/browser": "^8.0.0-alpha.7",
67
+ "@sentry/browser": "^8.0.0-beta.1",
67
68
  "@tanstack/vue-virtual": "3.0.0-beta.62",
68
69
  "@tinyhttp/content-disposition": "^2.2.0",
69
70
  "@tinyhttp/cookie": "^2.1.0",
70
71
  "@types/file-saver": "^2.0.7",
71
- "@types/qs": "^6.9.14",
72
+ "@types/qs": "^6.9.15",
72
73
  "dayjs": "^1.11.10",
73
74
  "file-saver": "^2.0.5",
74
75
  "ofetch": "^1.3.4",
75
- "qs": "^6.12.0"
76
+ "qs": "^6.12.1"
76
77
  },
77
78
  "devDependencies": {
78
79
  "@globalbrain/eslint-config": "^1.6.0",
@@ -83,11 +84,12 @@
83
84
  "@release-it/conventional-changelog": "^8.0.1",
84
85
  "@types/body-scroll-lock": "^3.1.2",
85
86
  "@types/lodash-es": "^4.17.12",
86
- "@types/markdown-it": "^13.0.7",
87
- "@types/node": "^20.11.30",
87
+ "@types/markdown-it": "^14.0.1",
88
+ "@types/node": "^20.12.7",
88
89
  "@vitejs/plugin-vue": "^5.0.4",
89
- "@vitest/coverage-v8": "^1.4.0",
90
- "@vue/reactivity": "^3.4.21",
90
+ "@vitest/coverage-v8": "^1.5.0",
91
+ "@vue/reactivity": "^3.4.22",
92
+ "@vue/runtime-core": "^3.4.22",
91
93
  "@vue/test-utils": "^2.4.5",
92
94
  "@vuelidate/core": "^2.0.3",
93
95
  "@vuelidate/validators": "^2.0.4",
@@ -95,7 +97,7 @@
95
97
  "body-scroll-lock": "4.0.0-beta.0",
96
98
  "eslint": "^8.57.0",
97
99
  "fuse.js": "^7.0.0",
98
- "happy-dom": "^14.3.9",
100
+ "happy-dom": "^14.7.1",
99
101
  "histoire": "0.16.5",
100
102
  "lodash-es": "^4.17.21",
101
103
  "markdown-it": "^14.1.0",
@@ -104,13 +106,13 @@
104
106
  "postcss": "^8.4.38",
105
107
  "postcss-nested": "^6.0.1",
106
108
  "punycode": "^2.3.1",
107
- "release-it": "^17.1.1",
108
- "typescript": "~5.4.3",
109
+ "release-it": "^17.2.0",
110
+ "typescript": "~5.4.5",
109
111
  "v-calendar": "^3.1.2",
110
- "vite": "^5.2.6",
111
- "vitepress": "1.0.1",
112
- "vitest": "^1.4.0",
113
- "vue": "^3.4.21",
112
+ "vite": "^5.2.9",
113
+ "vitepress": "^1.1.0",
114
+ "vitest": "^1.5.0",
115
+ "vue": "^3.4.22",
114
116
  "vue-router": "^4.3.0",
115
117
  "vue-tsc": "^1.8.27"
116
118
  }
@@ -1,7 +0,0 @@
1
- /// <reference types="vite/client" />
2
-
3
- declare module '*.vue' {
4
- import { DefineComponent } from 'vue'
5
- const component: DefineComponent<{}, {}, any>
6
- export default component
7
- }