base-ui-vue 0.3.0 → 0.5.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.
Files changed (218) hide show
  1. package/dist/arrow/TooltipArrow.cjs +609 -0
  2. package/dist/arrow/TooltipArrow.cjs.map +1 -0
  3. package/dist/arrow/TooltipArrow.js +478 -0
  4. package/dist/arrow/TooltipArrow.js.map +1 -0
  5. package/dist/button/Button.cjs +5 -317
  6. package/dist/button/Button.cjs.map +1 -1
  7. package/dist/button/Button.js +3 -249
  8. package/dist/button/Button.js.map +1 -1
  9. package/dist/button/ToolbarButton.cjs +19 -6
  10. package/dist/button/ToolbarButton.cjs.map +1 -1
  11. package/dist/button/ToolbarButton.js +3 -2
  12. package/dist/button/ToolbarButton.js.map +1 -1
  13. package/dist/checkbox/index.cjs +20 -9
  14. package/dist/checkbox/index.cjs.map +1 -1
  15. package/dist/checkbox/index.js +7 -2
  16. package/dist/checkbox/index.js.map +1 -1
  17. package/dist/checkbox-group/CheckboxGroup.cjs +14 -13
  18. package/dist/checkbox-group/CheckboxGroup.cjs.map +1 -1
  19. package/dist/checkbox-group/CheckboxGroup.js +2 -1
  20. package/dist/checkbox-group/CheckboxGroup.js.map +1 -1
  21. package/dist/components/FloatingPortal.cjs +66 -0
  22. package/dist/components/FloatingPortal.cjs.map +1 -0
  23. package/dist/components/FloatingPortal.js +55 -0
  24. package/dist/components/FloatingPortal.js.map +1 -0
  25. package/dist/components/FloatingRootStore.cjs +101 -0
  26. package/dist/components/FloatingRootStore.cjs.map +1 -0
  27. package/dist/components/FloatingRootStore.js +84 -0
  28. package/dist/components/FloatingRootStore.js.map +1 -0
  29. package/dist/components/FloatingTree.cjs +33 -0
  30. package/dist/components/FloatingTree.cjs.map +1 -0
  31. package/dist/components/FloatingTree.js +22 -0
  32. package/dist/components/FloatingTree.js.map +1 -0
  33. package/dist/composite/composite.cjs +6 -0
  34. package/dist/composite/composite.js +1 -1
  35. package/dist/content/ScrollAreaContent.cjs +3 -3
  36. package/dist/content/ScrollAreaContent.cjs.map +1 -1
  37. package/dist/content/ScrollAreaContent.js +1 -1
  38. package/dist/control/FieldControl.cjs +3 -3
  39. package/dist/control/FieldControl.cjs.map +1 -1
  40. package/dist/control/FieldControl.js +1 -1
  41. package/dist/control/SliderControl.cjs +3 -3
  42. package/dist/control/SliderControl.cjs.map +1 -1
  43. package/dist/control/SliderControl.js +3 -3
  44. package/dist/corner/ScrollAreaCorner.cjs +3 -3
  45. package/dist/corner/ScrollAreaCorner.cjs.map +1 -1
  46. package/dist/corner/ScrollAreaCorner.js +1 -1
  47. package/dist/decrement/NumberFieldDecrement.cjs +868 -0
  48. package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
  49. package/dist/decrement/NumberFieldDecrement.js +701 -0
  50. package/dist/decrement/NumberFieldDecrement.js.map +1 -0
  51. package/dist/description/FieldDescription.cjs +2 -2
  52. package/dist/description/FieldDescription.cjs.map +1 -1
  53. package/dist/description/FieldDescription.js +1 -1
  54. package/dist/error/FieldError.cjs +2 -2
  55. package/dist/error/FieldError.cjs.map +1 -1
  56. package/dist/error/FieldError.js +1 -1
  57. package/dist/fallback/AvatarFallback.cjs +4 -48
  58. package/dist/fallback/AvatarFallback.cjs.map +1 -1
  59. package/dist/fallback/AvatarFallback.js +4 -42
  60. package/dist/fallback/AvatarFallback.js.map +1 -1
  61. package/dist/floating-ui-vue/index.js +638 -0
  62. package/dist/floating-ui-vue/index.js.map +1 -0
  63. package/dist/form/Form.cjs +3 -3
  64. package/dist/form/Form.cjs.map +1 -1
  65. package/dist/form/Form.js +1 -1
  66. package/dist/group/NumberFieldGroup.cjs +72 -0
  67. package/dist/group/NumberFieldGroup.cjs.map +1 -0
  68. package/dist/group/NumberFieldGroup.js +67 -0
  69. package/dist/group/NumberFieldGroup.js.map +1 -0
  70. package/dist/group/ToolbarGroup.cjs +2 -2
  71. package/dist/group/ToolbarGroup.cjs.map +1 -1
  72. package/dist/group/ToolbarGroup.js +1 -1
  73. package/dist/header/AccordionHeader.cjs +2 -2
  74. package/dist/header/AccordionHeader.cjs.map +1 -1
  75. package/dist/header/AccordionHeader.js +1 -1
  76. package/dist/hooks/useDismiss.cjs +502 -0
  77. package/dist/hooks/useDismiss.cjs.map +1 -0
  78. package/dist/hooks/useHoverFloatingInteraction.cjs +233 -0
  79. package/dist/hooks/useHoverFloatingInteraction.cjs.map +1 -0
  80. package/dist/image/AvatarImage.cjs +4 -4
  81. package/dist/image/AvatarImage.cjs.map +1 -1
  82. package/dist/image/AvatarImage.js +1 -1
  83. package/dist/increment/NumberFieldIncrement.cjs +113 -0
  84. package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
  85. package/dist/increment/NumberFieldIncrement.js +108 -0
  86. package/dist/increment/NumberFieldIncrement.js.map +1 -0
  87. package/dist/index.cjs +80 -8
  88. package/dist/index.d.cts +4328 -1600
  89. package/dist/index.d.cts.map +1 -1
  90. package/dist/index.d.ts +4328 -1600
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +8 -4
  93. package/dist/index2.cjs +9435 -3236
  94. package/dist/index2.cjs.map +1 -1
  95. package/dist/index2.js +8905 -3049
  96. package/dist/index2.js.map +1 -1
  97. package/dist/rolldown-runtime.js +36 -0
  98. package/package.json +1 -1
  99. package/src/floating-ui-vue/components/FloatingPortal.ts +171 -0
  100. package/src/floating-ui-vue/components/FloatingPortal.vue +259 -0
  101. package/src/floating-ui-vue/components/FloatingRootStore.ts +83 -0
  102. package/src/floating-ui-vue/components/FloatingTree.ts +64 -0
  103. package/src/floating-ui-vue/components/FloatingTreeStore.ts +21 -0
  104. package/src/floating-ui-vue/hooks/useDismiss.ts +689 -0
  105. package/src/floating-ui-vue/hooks/useHoverFloatingInteraction.ts +244 -0
  106. package/src/floating-ui-vue/hooks/useHoverInteractionSharedState.ts +125 -0
  107. package/src/floating-ui-vue/hooks/useHoverShared.ts +53 -0
  108. package/src/floating-ui-vue/index.ts +48 -0
  109. package/src/floating-ui-vue/middleware/arrow.ts +114 -0
  110. package/src/floating-ui-vue/types.ts +117 -0
  111. package/src/floating-ui-vue/utils/composite.ts +20 -0
  112. package/src/floating-ui-vue/utils/constants.ts +2 -0
  113. package/src/floating-ui-vue/utils/createAttribute.ts +3 -0
  114. package/src/floating-ui-vue/utils/createEventEmitter.ts +20 -0
  115. package/src/floating-ui-vue/utils/element.ts +59 -0
  116. package/src/floating-ui-vue/utils/event.ts +15 -0
  117. package/src/floating-ui-vue/utils/nodes.ts +14 -0
  118. package/src/floating-ui-vue/utils/tabbable.ts +309 -0
  119. package/src/index.ts +7 -1
  120. package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
  121. package/src/number-field/group/NumberFieldGroup.vue +47 -0
  122. package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
  123. package/src/number-field/index.ts +42 -0
  124. package/src/number-field/input/NumberFieldInput.vue +455 -0
  125. package/src/number-field/root/NumberFieldRoot.vue +626 -0
  126. package/src/number-field/root/NumberFieldRootContext.ts +94 -0
  127. package/src/number-field/root/useNumberFieldButton.ts +171 -0
  128. package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
  129. package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
  130. package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
  131. package/src/number-field/utils/constants.ts +4 -0
  132. package/src/number-field/utils/getViewportRect.ts +34 -0
  133. package/src/number-field/utils/parse.ts +248 -0
  134. package/src/number-field/utils/stateAttributesMapping.ts +9 -0
  135. package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
  136. package/src/number-field/utils/types.ts +24 -0
  137. package/src/number-field/utils/validate.ts +120 -0
  138. package/src/otp-field/index.ts +22 -0
  139. package/src/otp-field/input/OtpFieldInput.vue +336 -0
  140. package/src/otp-field/root/OtpFieldRoot.vue +583 -0
  141. package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
  142. package/src/otp-field/utils/otp.ts +135 -0
  143. package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
  144. package/src/radio/index.ts +28 -0
  145. package/src/radio/indicator/RadioIndicator.vue +90 -0
  146. package/src/radio/indicator/RadioIndicatorDataAttributes.ts +54 -0
  147. package/src/radio/root/RadioRoot.vue +544 -0
  148. package/src/radio/root/RadioRootContext.ts +18 -0
  149. package/src/radio/root/RadioRootDataAttributes.ts +46 -0
  150. package/src/radio/utils/stateAttributesMapping.ts +29 -0
  151. package/src/radio-group/RadioGroup.vue +381 -0
  152. package/src/radio-group/RadioGroupContext.ts +42 -0
  153. package/src/radio-group/RadioGroupDataAttributes.ts +6 -0
  154. package/src/radio-group/index.ts +10 -0
  155. package/src/tabs/index.ts +38 -0
  156. package/src/tabs/indicator/TabsIndicator.vue +201 -0
  157. package/src/tabs/indicator/TabsIndicatorCssVars.ts +26 -0
  158. package/src/tabs/indicator/TabsIndicatorDataAttributes.ts +12 -0
  159. package/src/tabs/indicator/prehydrationScript.min.ts +5 -0
  160. package/src/tabs/indicator/prehydrationScript.template.js +87 -0
  161. package/src/tabs/list/TabsList.vue +201 -0
  162. package/src/tabs/list/TabsListContext.ts +29 -0
  163. package/src/tabs/list/TabsListDataAttributes.ts +12 -0
  164. package/src/tabs/panel/TabsPanel.vue +167 -0
  165. package/src/tabs/panel/TabsPanelDataAttributes.ts +28 -0
  166. package/src/tabs/root/TabsRoot.vue +468 -0
  167. package/src/tabs/root/TabsRootContext.ts +56 -0
  168. package/src/tabs/root/TabsRootDataAttributes.ts +12 -0
  169. package/src/tabs/root/stateAttributesMapping.ts +9 -0
  170. package/src/tabs/tab/TabsTab.vue +274 -0
  171. package/src/tabs/tab/TabsTabDataAttributes.ts +20 -0
  172. package/src/tabs/utils/areTabValuesEqual.ts +6 -0
  173. package/src/tooltip/arrow/TooltipArrow.vue +116 -0
  174. package/src/tooltip/arrow/TooltipArrowDataAttributes.ts +31 -0
  175. package/src/tooltip/index.ts +54 -0
  176. package/src/tooltip/popup/TooltipPopup.vue +142 -0
  177. package/src/tooltip/popup/TooltipPopupDataAttributes.ts +35 -0
  178. package/src/tooltip/portal/TooltipPortal.vue +83 -0
  179. package/src/tooltip/portal/TooltipPortalContext.ts +19 -0
  180. package/src/tooltip/positioner/TooltipPositioner.vue +221 -0
  181. package/src/tooltip/positioner/TooltipPositionerContext.ts +23 -0
  182. package/src/tooltip/positioner/TooltipPositionerCssVars.ts +27 -0
  183. package/src/tooltip/positioner/TooltipPositionerDataAttributes.ts +26 -0
  184. package/src/tooltip/provider/TooltipProvider.vue +78 -0
  185. package/src/tooltip/provider/TooltipProviderContext.ts +19 -0
  186. package/src/tooltip/root/TooltipRoot.vue +401 -0
  187. package/src/tooltip/root/TooltipRootContext.ts +71 -0
  188. package/src/tooltip/store/TooltipHandle.ts +215 -0
  189. package/src/tooltip/trigger/TooltipTrigger.vue +343 -0
  190. package/src/tooltip/trigger/TooltipTriggerDataAttributes.ts +12 -0
  191. package/src/tooltip/utils/constants.ts +2 -0
  192. package/src/tooltip/viewport/TooltipViewport.vue +149 -0
  193. package/src/tooltip/viewport/TooltipViewportCssVars.ts +14 -0
  194. package/src/tooltip/viewport/TooltipViewportDataAttributes.ts +26 -0
  195. package/src/utils/FloatingPortalLite.vue +66 -0
  196. package/src/utils/FocusGuard.vue +36 -0
  197. package/src/utils/adaptiveOriginMiddleware.ts +79 -0
  198. package/src/utils/addEventListener.ts +16 -0
  199. package/src/utils/constants.ts +11 -0
  200. package/src/utils/createBaseUIEventDetails.ts +2 -0
  201. package/src/utils/detectBrowser.ts +15 -0
  202. package/src/utils/formatNumber.ts +35 -2
  203. package/src/utils/getCssDimensions.ts +19 -0
  204. package/src/utils/getDisabledMountTransitionStyles.ts +9 -0
  205. package/src/utils/hideMiddleware.ts +19 -0
  206. package/src/utils/mergeCleanups.ts +7 -0
  207. package/src/utils/platform.ts +18 -0
  208. package/src/utils/popupStateMapping.ts +103 -0
  209. package/src/utils/reasons.ts +4 -0
  210. package/src/utils/serializeValue.ts +17 -0
  211. package/src/utils/useAnchorPositioning.ts +644 -0
  212. package/src/utils/useInterval.ts +45 -0
  213. package/src/utils/usePopupAutoResize.ts +254 -0
  214. package/src/utils/usePopupViewport.ts +398 -0
  215. package/src/utils/usePositioner.ts +61 -0
  216. package/src/utils/usePressAndHold.ts +260 -0
  217. package/src/utils/useRenderElement.ts +3 -0
  218. package/src/utils/useValueChanged.ts +21 -0
@@ -3,6 +3,7 @@ import type { Ref } from 'vue'
3
3
  import type { Dimensions } from '../types'
4
4
 
5
5
  import { floor } from '@floating-ui/utils'
6
+ import { getComputedStyle } from '@floating-ui/utils/dom'
6
7
  import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from './constants'
7
8
  import { stopEvent } from './event'
8
9
 
@@ -473,3 +474,22 @@ export function isListIndexDisabled(
473
474
  || element.getAttribute('aria-disabled') === 'true'
474
475
  )
475
476
  }
477
+
478
+ export function isHiddenByStyles(styles: CSSStyleDeclaration) {
479
+ return styles.visibility === 'hidden' || styles.visibility === 'collapse'
480
+ }
481
+
482
+ export function isElementVisible(
483
+ element: Element | null,
484
+ styles: CSSStyleDeclaration | null = element ? getComputedStyle(element) : null,
485
+ ) {
486
+ if (!element || !element.isConnected || !styles || isHiddenByStyles(styles)) {
487
+ return false
488
+ }
489
+
490
+ if (typeof element.checkVisibility === 'function') {
491
+ return element.checkVisibility()
492
+ }
493
+
494
+ return styles.display !== 'none' && styles.display !== 'contents'
495
+ }
@@ -2,3 +2,5 @@ export const ARROW_LEFT = 'ArrowLeft'
2
2
  export const ARROW_RIGHT = 'ArrowRight'
3
3
  export const ARROW_UP = 'ArrowUp'
4
4
  export const ARROW_DOWN = 'ArrowDown'
5
+
6
+ export const TYPEABLE_SELECTOR = 'input:not([type="hidden"]):not([disabled]),[contenteditable]:not([contenteditable="false"]),textarea:not([disabled])'
@@ -0,0 +1,3 @@
1
+ export function createAttribute(name: string): string {
2
+ return `data-base-ui-vue-${name}`
3
+ }
@@ -0,0 +1,20 @@
1
+ import type { FloatingEvents } from '../types'
2
+
3
+ export function createEventEmitter(): FloatingEvents {
4
+ const map = new Map<string, Set<(data: any) => void>>()
5
+
6
+ return {
7
+ emit(event: string, data?: any) {
8
+ map.get(event)?.forEach(listener => listener(data))
9
+ },
10
+ on(event: string, listener: (data: any) => void) {
11
+ if (!map.has(event)) {
12
+ map.set(event, new Set())
13
+ }
14
+ map.get(event)!.add(listener)
15
+ },
16
+ off(event: string, listener: (data: any) => void) {
17
+ map.get(event)?.delete(listener)
18
+ },
19
+ }
20
+ }
@@ -1,3 +1,62 @@
1
+ import type { FloatingTriggerMap } from '../types'
2
+ import { isElement, isHTMLElement } from '@floating-ui/utils/dom'
3
+ import { TYPEABLE_SELECTOR } from './constants'
4
+ import { contains, getTarget } from './shadowDom'
5
+
6
+ export { contains, getTarget }
7
+
8
+ export function isTargetInsideEnabledTrigger(
9
+ target: EventTarget | null,
10
+ triggerElements: FloatingTriggerMap,
11
+ ) {
12
+ if (!isElement(target)) {
13
+ return false
14
+ }
15
+
16
+ const targetElement = target
17
+
18
+ if (triggerElements.hasElement(targetElement)) {
19
+ return !targetElement.hasAttribute('data-trigger-disabled')
20
+ }
21
+
22
+ for (const [, trigger] of triggerElements.entries()) {
23
+ if (contains(trigger, targetElement)) {
24
+ return !trigger.hasAttribute('data-trigger-disabled')
25
+ }
26
+ }
27
+
28
+ return false
29
+ }
30
+
31
+ export function isEventTargetWithin(event: Event, node: Node | null | undefined) {
32
+ if (node == null) {
33
+ return false
34
+ }
35
+
36
+ if ('composedPath' in event) {
37
+ return event.composedPath().includes(node)
38
+ }
39
+
40
+ const eventAgain = event as Event
41
+ return eventAgain.target != null && node.contains(eventAgain.target as Node)
42
+ }
43
+
44
+ export function isRootElement(element: Element): boolean {
45
+ return element.matches('html,body')
46
+ }
47
+
48
+ export function isTypeableElement(element: unknown): boolean {
49
+ return isHTMLElement(element) && element.matches(TYPEABLE_SELECTOR)
50
+ }
51
+
52
+ export function isInteractiveElement(element: Element | null) {
53
+ return (
54
+ element?.closest(
55
+ `button,a[href],[role="button"],select,[tabindex]:not([tabindex="-1"]),${TYPEABLE_SELECTOR}`,
56
+ ) != null
57
+ )
58
+ }
59
+
1
60
  export function matchesFocusVisible(element: Element | null) {
2
61
  if (!element) {
3
62
  return true
@@ -2,3 +2,18 @@ export function stopEvent(event: Event) {
2
2
  event.preventDefault()
3
3
  event.stopPropagation()
4
4
  }
5
+
6
+ export function isMouseLikePointerType(pointerType: string | undefined, strict?: boolean) {
7
+ const values: Array<string | undefined> = ['mouse', 'pen']
8
+
9
+ if (!strict) {
10
+ values.push('', undefined)
11
+ }
12
+
13
+ return values.includes(pointerType)
14
+ }
15
+
16
+ export function isClickLikeEvent(event: Event) {
17
+ const type = event.type
18
+ return type === 'click' || type === 'mousedown' || type === 'keydown' || type === 'keyup'
19
+ }
@@ -0,0 +1,14 @@
1
+ import type { FloatingNodeType } from '../types'
2
+
3
+ export function getNodeChildren(
4
+ nodes: FloatingNodeType[],
5
+ id: string | undefined,
6
+ onlyOpenChildren = true,
7
+ ): FloatingNodeType[] {
8
+ const directChildren = nodes.filter(node => node.parentId === id)
9
+
10
+ return directChildren.flatMap(child => [
11
+ ...(!onlyOpenChildren || child.context?.open.value ? [child] : []),
12
+ ...getNodeChildren(nodes, child.id, onlyOpenChildren),
13
+ ])
14
+ }
@@ -0,0 +1,309 @@
1
+ import { getComputedStyle, getNodeName, isHTMLElement, isShadowRoot } from '@floating-ui/utils/dom'
2
+ import { ownerDocument } from '../../utils/owner'
3
+ import { isElementVisible } from './composite'
4
+ import { activeElement, contains } from './shadowDom'
5
+
6
+ export type FocusableElement = HTMLElement | SVGElement
7
+
8
+ const CANDIDATE_SELECTOR
9
+ = 'a[href],button,input,select,textarea,summary,details,iframe,object,embed,[tabindex],[contenteditable]:not([contenteditable="false"]),audio[controls],video[controls]'
10
+
11
+ function getParentElement(element: Element) {
12
+ const assignedSlot = (
13
+ element as Element & {
14
+ assignedSlot?: HTMLSlotElement | null | undefined
15
+ }
16
+ ).assignedSlot
17
+
18
+ if (assignedSlot) {
19
+ return assignedSlot
20
+ }
21
+
22
+ if (element.parentElement) {
23
+ return element.parentElement
24
+ }
25
+
26
+ const rootNode = element.getRootNode()
27
+ return isShadowRoot(rootNode) ? rootNode.host : null
28
+ }
29
+
30
+ function getDetailsSummary(details: Element) {
31
+ for (const child of Array.from(details.children)) {
32
+ if (getNodeName(child) === 'summary') {
33
+ return child
34
+ }
35
+ }
36
+
37
+ return null
38
+ }
39
+
40
+ function isWithinOpenDetailsSummary(element: Element, details: Element) {
41
+ const summary = getDetailsSummary(details)
42
+ return !!summary && (element === summary || contains(summary, element))
43
+ }
44
+
45
+ function isFocusableCandidate(element: Element | null): element is FocusableElement {
46
+ const nodeName = element ? getNodeName(element) : ''
47
+
48
+ return (
49
+ element != null
50
+ && element.matches(CANDIDATE_SELECTOR)
51
+ && (
52
+ nodeName !== 'summary'
53
+ || (
54
+ element.parentElement != null
55
+ && getNodeName(element.parentElement) === 'details'
56
+ && getDetailsSummary(element.parentElement) === element
57
+ )
58
+ )
59
+ && (nodeName !== 'details' || getDetailsSummary(element) == null)
60
+ && (nodeName !== 'input' || (element as HTMLInputElement).type !== 'hidden')
61
+ )
62
+ }
63
+
64
+ function isFocusableElement(element: Element | null): element is FocusableElement {
65
+ if (!isFocusableCandidate(element) || !element.isConnected || element.matches(':disabled')) {
66
+ return false
67
+ }
68
+
69
+ for (let current: Element | null = element; current; current = getParentElement(current)) {
70
+ const isAncestor = current !== element
71
+ const isSlot = getNodeName(current) === 'slot'
72
+
73
+ if (current.hasAttribute('inert')) {
74
+ return false
75
+ }
76
+
77
+ if (
78
+ (
79
+ isAncestor
80
+ && getNodeName(current) === 'details'
81
+ && !(current as HTMLDetailsElement).open
82
+ && !isWithinOpenDetailsSummary(element, current)
83
+ )
84
+ || current.hasAttribute('hidden')
85
+ || (!isSlot && !isVisibleInTabbableTree(current, isAncestor))
86
+ ) {
87
+ return false
88
+ }
89
+ }
90
+
91
+ return true
92
+ }
93
+
94
+ function isHTMLElementOrSVGElement(element: Element): element is FocusableElement {
95
+ return (
96
+ isHTMLElement(element)
97
+ || (typeof SVGElement !== 'undefined' && element instanceof SVGElement)
98
+ )
99
+ }
100
+
101
+ function isVisibleInTabbableTree(element: Element, isAncestor: boolean) {
102
+ const styles = getComputedStyle(element)
103
+
104
+ if (!isAncestor) {
105
+ return isElementVisible(element, styles)
106
+ }
107
+
108
+ return styles.display !== 'none'
109
+ }
110
+
111
+ function getTabIndex(element: FocusableElement) {
112
+ const tabIndex = element.tabIndex
113
+
114
+ if (tabIndex < 0) {
115
+ const nodeName = getNodeName(element)
116
+ if (
117
+ nodeName === 'details'
118
+ || nodeName === 'audio'
119
+ || nodeName === 'video'
120
+ || (isHTMLElement(element) && element.isContentEditable)
121
+ ) {
122
+ return 0
123
+ }
124
+ }
125
+
126
+ return tabIndex
127
+ }
128
+
129
+ function getNamedRadioInput(element: FocusableElement) {
130
+ if (getNodeName(element) !== 'input') {
131
+ return null
132
+ }
133
+
134
+ const input = element as HTMLInputElement
135
+ return input.type === 'radio' && input.name !== '' ? input : null
136
+ }
137
+
138
+ function isTabbableRadio(element: FocusableElement, candidates: FocusableElement[]) {
139
+ const input = getNamedRadioInput(element)
140
+
141
+ if (!input) {
142
+ return true
143
+ }
144
+
145
+ const checkedRadio = candidates.find((candidate) => {
146
+ const radio = getNamedRadioInput(candidate)
147
+ return radio?.name === input.name && radio.form === input.form && radio.checked
148
+ })
149
+
150
+ if (checkedRadio) {
151
+ return checkedRadio === input
152
+ }
153
+
154
+ return (
155
+ candidates.find((candidate) => {
156
+ const radio = getNamedRadioInput(candidate)
157
+ return radio?.name === input.name && radio.form === input.form
158
+ }) === input
159
+ )
160
+ }
161
+
162
+ function getComposedChildren(container: ParentNode): Element[] {
163
+ if (isHTMLElement(container) && getNodeName(container) === 'slot') {
164
+ const assignedElements = (container as HTMLSlotElement).assignedElements({ flatten: true })
165
+ if (assignedElements.length > 0) {
166
+ return assignedElements
167
+ }
168
+ }
169
+
170
+ if (isHTMLElement(container) && container.shadowRoot) {
171
+ return Array.from(container.shadowRoot.children)
172
+ }
173
+
174
+ return Array.from(container.children)
175
+ }
176
+
177
+ function appendCandidates(container: ParentNode, list: FocusableElement[]) {
178
+ getComposedChildren(container).forEach((child) => {
179
+ if (isFocusableCandidate(child)) {
180
+ list.push(child)
181
+ }
182
+
183
+ appendCandidates(child, list)
184
+ })
185
+ }
186
+
187
+ function appendMatchingElements(container: ParentNode, selector: string, list: FocusableElement[]) {
188
+ getComposedChildren(container).forEach((child) => {
189
+ if (isHTMLElementOrSVGElement(child) && child.matches(selector)) {
190
+ list.push(child)
191
+ }
192
+
193
+ appendMatchingElements(child, selector, list)
194
+ })
195
+ }
196
+
197
+ function getOwnerDocument(element: Element | null) {
198
+ return ownerDocument(element) ?? (typeof document !== 'undefined' ? document : null)
199
+ }
200
+
201
+ function getTabbableIn(container: HTMLElement, dir: 1 | -1): FocusableElement | undefined {
202
+ const list = tabbable(container)
203
+ const len = list.length
204
+
205
+ if (len === 0) {
206
+ return undefined
207
+ }
208
+
209
+ const doc = getOwnerDocument(container)
210
+ const active = doc ? activeElement(doc) as FocusableElement : null
211
+ const index = active ? list.indexOf(active) : -1
212
+ const nextIndex = index === -1 ? (dir === 1 ? 0 : len - 1) : index + dir
213
+
214
+ return list[nextIndex]
215
+ }
216
+
217
+ function getTabbableNearElement(referenceElement: Element | null, dir: 1 | -1) {
218
+ if (!referenceElement) {
219
+ return null
220
+ }
221
+
222
+ const body = getOwnerDocument(referenceElement)?.body
223
+ if (!body) {
224
+ return null
225
+ }
226
+
227
+ const list = tabbable(body)
228
+ const elementCount = list.length
229
+
230
+ if (elementCount === 0) {
231
+ return null
232
+ }
233
+
234
+ const index = list.indexOf(referenceElement as FocusableElement)
235
+
236
+ if (index === -1) {
237
+ return null
238
+ }
239
+
240
+ const nextIndex = (index + dir + elementCount) % elementCount
241
+ return list[nextIndex]
242
+ }
243
+
244
+ export function isTabbable(element: Element | null) {
245
+ return isFocusableElement(element) && getTabIndex(element) >= 0
246
+ }
247
+
248
+ export function focusable(container: Element) {
249
+ const candidates: FocusableElement[] = []
250
+ appendCandidates(container, candidates)
251
+ return candidates.filter(isFocusableElement)
252
+ }
253
+
254
+ export function tabbable(container: Element) {
255
+ const candidates = focusable(container)
256
+ return candidates.filter(
257
+ element => getTabIndex(element) >= 0 && isTabbableRadio(element, candidates),
258
+ )
259
+ }
260
+
261
+ export function getNextTabbable(referenceElement: Element | null): FocusableElement | null {
262
+ const body = getOwnerDocument(referenceElement)?.body
263
+ return (body && getTabbableIn(body, 1)) || (referenceElement as FocusableElement | null)
264
+ }
265
+
266
+ export function getPreviousTabbable(referenceElement: Element | null): FocusableElement | null {
267
+ const body = getOwnerDocument(referenceElement)?.body
268
+ return (body && getTabbableIn(body, -1)) || (referenceElement as FocusableElement | null)
269
+ }
270
+
271
+ export function getTabbableAfterElement(referenceElement: Element | null): FocusableElement | null {
272
+ return getTabbableNearElement(referenceElement, 1)
273
+ }
274
+
275
+ export function getTabbableBeforeElement(
276
+ referenceElement: Element | null,
277
+ ): FocusableElement | null {
278
+ return getTabbableNearElement(referenceElement, -1)
279
+ }
280
+
281
+ export function isOutsideEvent(event: FocusEvent, container?: Element) {
282
+ const containerElement = container || (event.currentTarget as Element)
283
+ const relatedTarget = event.relatedTarget as HTMLElement | null
284
+ return !relatedTarget || !contains(containerElement, relatedTarget)
285
+ }
286
+
287
+ export function disableFocusInside(container: HTMLElement) {
288
+ const tabbableElements = tabbable(container)
289
+ tabbableElements.forEach((element) => {
290
+ element.dataset.tabindex = element.getAttribute('tabindex') || ''
291
+ element.setAttribute('tabindex', '-1')
292
+ })
293
+ }
294
+
295
+ export function enableFocusInside(container: HTMLElement) {
296
+ const elements: FocusableElement[] = []
297
+ appendMatchingElements(container, '[data-tabindex]', elements)
298
+ elements.forEach((element) => {
299
+ const tabindex = element.getAttribute('data-tabindex')
300
+ element.removeAttribute('data-tabindex')
301
+
302
+ if (tabindex) {
303
+ element.setAttribute('tabindex', tabindex)
304
+ }
305
+ else {
306
+ element.removeAttribute('tabindex')
307
+ }
308
+ })
309
+ }
package/src/index.ts CHANGED
@@ -15,13 +15,19 @@ export * from './form'
15
15
  export * from './input'
16
16
  export * from './merge-props'
17
17
  export * from './meter'
18
+ export * from './number-field'
19
+ export * from './otp-field'
18
20
  export * from './progress'
21
+ export * from './radio'
22
+ export * from './radio-group'
23
+ export * from './scroll-area'
19
24
  export * from './separator'
20
25
  export * from './slider'
21
26
  export * from './switch'
22
- export * from './scroll-area'
27
+ export * from './tabs'
23
28
  export * from './toggle'
24
29
  export * from './toggle-group'
25
30
  export * from './toolbar'
31
+ export * from './tooltip'
26
32
  export * from './use-render'
27
33
  export { Slot } from './utils/slot'
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps, NativeButtonProps } from '../../utils/types'
3
+ import type { NumberFieldRootState } from '../root/NumberFieldRoot.vue'
4
+ import { computed, useAttrs } from 'vue'
5
+ import { mergeProps } from '../../merge-props/mergeProps'
6
+ import { useButton } from '../../use-button/useButton'
7
+ import { useRenderElement } from '../../utils/useRenderElement'
8
+ import { useNumberFieldRootContext } from '../root/NumberFieldRootContext'
9
+ import { useNumberFieldButton } from '../root/useNumberFieldButton'
10
+ import { stateAttributesMapping } from '../utils/stateAttributesMapping'
11
+
12
+ export type NumberFieldDecrementState = NumberFieldRootState
13
+
14
+ export interface NumberFieldDecrementProps
15
+ extends NativeButtonProps, BaseUIComponentProps<NumberFieldDecrementState> {
16
+ disabled?: boolean
17
+ }
18
+
19
+ defineOptions({
20
+ name: 'NumberFieldDecrement',
21
+ inheritAttrs: false,
22
+ })
23
+
24
+ const props = withDefaults(defineProps<NumberFieldDecrementProps>(), {
25
+ as: 'button',
26
+ disabled: false,
27
+ nativeButton: true,
28
+ })
29
+
30
+ const attrs = useAttrs()
31
+ const attrsObject = attrs as Record<string, any>
32
+
33
+ const {
34
+ allowInputSyncRef,
35
+ disabled: contextDisabled,
36
+ formatOptionsRef,
37
+ getStepAmount,
38
+ id,
39
+ incrementValue,
40
+ inputRef,
41
+ inputValue,
42
+ minWithDefault,
43
+ readOnly,
44
+ setValue,
45
+ state,
46
+ value,
47
+ valueRef,
48
+ locale,
49
+ lastChangedValueRef,
50
+ onValueCommitted,
51
+ } = useNumberFieldRootContext()
52
+
53
+ const isMin = computed(() => value.value != null && value.value <= minWithDefault.value)
54
+ const disabled = computed(() => props.disabled || contextDisabled.value || isMin.value)
55
+
56
+ const buttonProps = useNumberFieldButton({
57
+ isIncrement: false,
58
+ inputRef,
59
+ inputValue: () => inputValue.value,
60
+ disabled: () => disabled.value,
61
+ readOnly: () => readOnly.value,
62
+ id: () => id.value,
63
+ setValue,
64
+ getStepAmount,
65
+ incrementValue,
66
+ allowInputSyncRef,
67
+ formatOptionsRef,
68
+ valueRef,
69
+ locale: () => locale.value,
70
+ lastChangedValueRef,
71
+ onValueCommitted,
72
+ })
73
+
74
+ const { getButtonProps, buttonRef } = useButton({
75
+ disabled: () => disabled.value,
76
+ native: () => props.nativeButton,
77
+ focusableWhenDisabled: true,
78
+ })
79
+
80
+ const buttonState = computed<NumberFieldDecrementState>(() => ({
81
+ ...state.value,
82
+ disabled: disabled.value,
83
+ }))
84
+
85
+ const combinedProps = computed(() =>
86
+ mergeProps(buttonProps.value, attrsObject, getButtonProps()),
87
+ )
88
+
89
+ const {
90
+ tag,
91
+ mergedProps,
92
+ renderless,
93
+ ref: renderRef,
94
+ } = useRenderElement({
95
+ componentProps: props,
96
+ state: buttonState,
97
+ props: combinedProps,
98
+ stateAttributesMapping,
99
+ defaultTagName: 'button',
100
+ ref: buttonRef,
101
+ })
102
+ </script>
103
+
104
+ <template>
105
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="buttonState" />
106
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
107
+ <slot :state="buttonState" />
108
+ </component>
109
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps } from '../../utils/types'
3
+ import type { NumberFieldRootState } from '../root/NumberFieldRoot.vue'
4
+ import { computed, useAttrs } from 'vue'
5
+ import { mergeProps } from '../../merge-props/mergeProps'
6
+ import { useRenderElement } from '../../utils/useRenderElement'
7
+ import { useNumberFieldRootContext } from '../root/NumberFieldRootContext'
8
+ import { stateAttributesMapping } from '../utils/stateAttributesMapping'
9
+
10
+ export type NumberFieldGroupState = NumberFieldRootState
11
+
12
+ export interface NumberFieldGroupProps extends BaseUIComponentProps<NumberFieldGroupState> {}
13
+
14
+ defineOptions({
15
+ name: 'NumberFieldGroup',
16
+ inheritAttrs: false,
17
+ })
18
+
19
+ const props = defineProps<NumberFieldGroupProps>()
20
+
21
+ const attrs = useAttrs()
22
+ const attrsObject = attrs as Record<string, any>
23
+
24
+ const { state } = useNumberFieldRootContext()
25
+
26
+ const groupProps = computed(() => mergeProps(attrsObject, { role: 'group' }))
27
+
28
+ const {
29
+ tag,
30
+ mergedProps,
31
+ renderless,
32
+ ref: renderRef,
33
+ } = useRenderElement({
34
+ componentProps: props,
35
+ state,
36
+ props: groupProps,
37
+ stateAttributesMapping,
38
+ defaultTagName: 'div',
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
44
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
45
+ <slot :state="state" />
46
+ </component>
47
+ </template>