@datability/8ui 1.1.0 → 1.1.2

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 (128) hide show
  1. package/dist/index.css +1 -0
  2. package/dist/index.es.js +1853 -0
  3. package/dist/index.es.js.map +1 -0
  4. package/dist/index.umd.js +8 -0
  5. package/dist/index.umd.js.map +1 -0
  6. package/dist/types/App.d.ts +2 -0
  7. package/dist/types/components/blackdrop/index.d.ts +4 -0
  8. package/dist/types/components/blackdrop/index.type.d.ts +6 -0
  9. package/dist/types/components/button/index.d.ts +4 -0
  10. package/dist/types/components/button/index.type.d.ts +12 -0
  11. package/dist/types/components/chip/index.d.ts +4 -0
  12. package/dist/types/components/chip/index.type.d.ts +9 -0
  13. package/dist/types/components/context.d.ts +8 -0
  14. package/dist/types/components/divider/index.d.ts +3 -0
  15. package/dist/types/components/index.d.ts +41 -0
  16. package/dist/types/components/input/input-auto-complete/index.d.ts +4 -0
  17. package/dist/types/components/input/input-auto-complete/index.type.d.ts +14 -0
  18. package/dist/types/components/input/input-base/index.d.ts +4 -0
  19. package/dist/types/components/input/input-base/index.type.d.ts +11 -0
  20. package/dist/types/components/input/input-basic/index.d.ts +4 -0
  21. package/dist/types/components/input/input-basic/index.type.d.ts +10 -0
  22. package/dist/types/components/input/input-checkbox/index.d.ts +4 -0
  23. package/dist/types/components/input/input-checkbox/index.type.d.ts +13 -0
  24. package/dist/types/components/input/input-date/index.d.ts +22 -0
  25. package/dist/types/components/input/input-date/index.type.d.ts +13 -0
  26. package/dist/types/components/input/input-date-range/index.d.ts +4 -0
  27. package/dist/types/components/input/input-date-range/index.type.d.ts +13 -0
  28. package/dist/types/components/input/input-date-time/index.d.ts +4 -0
  29. package/dist/types/components/input/input-date-time/index.type.d.ts +13 -0
  30. package/dist/types/components/input/input-number/index.d.ts +4 -0
  31. package/dist/types/components/input/input-number/index.type.d.ts +12 -0
  32. package/dist/types/components/input/input-password/index.d.ts +4 -0
  33. package/dist/types/components/input/input-password/index.type.d.ts +10 -0
  34. package/dist/types/components/input/input-radio/index.d.ts +4 -0
  35. package/dist/types/components/input/input-radio/index.type.d.ts +14 -0
  36. package/dist/types/components/input/input-select/index.d.ts +4 -0
  37. package/dist/types/components/input/input-select/index.type.d.ts +16 -0
  38. package/dist/types/components/input/input-switch/index.d.ts +4 -0
  39. package/dist/types/components/input/input-switch/index.type.d.ts +6 -0
  40. package/dist/types/components/input/input-textarea/index.d.ts +4 -0
  41. package/dist/types/components/input/input-textarea/index.type.d.ts +12 -0
  42. package/dist/types/components/menu/index.d.ts +4 -0
  43. package/dist/types/components/menu/index.type.d.ts +11 -0
  44. package/dist/types/components/modal/index.d.ts +4 -0
  45. package/dist/types/components/modal/index.type.d.ts +7 -0
  46. package/dist/types/index.d.ts +41 -0
  47. package/dist/types/main.d.ts +1 -0
  48. package/package.json +5 -1
  49. package/.prettierrc +0 -8
  50. package/.vscode/extensions.json +0 -6
  51. package/declaration.d.ts +0 -10
  52. package/docker-compose.yml +0 -20
  53. package/eslint.config.js +0 -23
  54. package/index.html +0 -13
  55. package/src/App.tsx +0 -370
  56. package/src/components/blackdrop/index.tsx +0 -18
  57. package/src/components/blackdrop/index.type.ts +0 -7
  58. package/src/components/button/index.tsx +0 -44
  59. package/src/components/button/index.type.ts +0 -13
  60. package/src/components/chip/index.tsx +0 -39
  61. package/src/components/chip/index.type.ts +0 -12
  62. package/src/components/context.tsx +0 -26
  63. package/src/components/divider/index.tsx +0 -13
  64. package/src/components/index.ts +0 -62
  65. package/src/components/input/input-auto-complete/index.tsx +0 -140
  66. package/src/components/input/input-auto-complete/index.type.tsx +0 -13
  67. package/src/components/input/input-base/index.tsx +0 -39
  68. package/src/components/input/input-base/index.type.tsx +0 -13
  69. package/src/components/input/input-basic/index.tsx +0 -47
  70. package/src/components/input/input-basic/index.type.tsx +0 -8
  71. package/src/components/input/input-checkbox/index.tsx +0 -69
  72. package/src/components/input/input-checkbox/index.type.tsx +0 -11
  73. package/src/components/input/input-date/index.tsx +0 -354
  74. package/src/components/input/input-date/index.type.tsx +0 -11
  75. package/src/components/input/input-date-range/index.tsx +0 -284
  76. package/src/components/input/input-date-range/index.type.tsx +0 -11
  77. package/src/components/input/input-date-time/index.tsx +0 -367
  78. package/src/components/input/input-date-time/index.type.tsx +0 -11
  79. package/src/components/input/input-number/index.tsx +0 -118
  80. package/src/components/input/input-number/index.type.tsx +0 -11
  81. package/src/components/input/input-password/index.tsx +0 -60
  82. package/src/components/input/input-password/index.type.tsx +0 -8
  83. package/src/components/input/input-radio/index.tsx +0 -72
  84. package/src/components/input/input-radio/index.type.tsx +0 -12
  85. package/src/components/input/input-select/index.tsx +0 -113
  86. package/src/components/input/input-select/index.type.tsx +0 -15
  87. package/src/components/input/input-switch/index.tsx +0 -44
  88. package/src/components/input/input-switch/index.type.tsx +0 -4
  89. package/src/components/input/input-textarea/index.tsx +0 -48
  90. package/src/components/input/input-textarea/index.type.tsx +0 -10
  91. package/src/components/menu/index.tsx +0 -136
  92. package/src/components/menu/index.type.ts +0 -8
  93. package/src/components/modal/index.tsx +0 -99
  94. package/src/components/modal/index.type.tsx +0 -8
  95. package/src/index.scss +0 -44
  96. package/src/index.ts +0 -62
  97. package/src/logoDownload.svg +0 -3
  98. package/src/main.tsx +0 -9
  99. package/tsconfig.app.json +0 -28
  100. package/tsconfig.json +0 -42
  101. package/tsconfig.node.json +0 -29
  102. package/vite.config.d.ts +0 -2
  103. package/vite.config.ts +0 -35
  104. /package/{src → dist}/components/assets/closed.svg +0 -0
  105. /package/{src → dist}/components/assets/expand-arrow.svg +0 -0
  106. /package/{src → dist}/components/assets/visibility-off.svg +0 -0
  107. /package/{src → dist}/components/assets/visibility.svg +0 -0
  108. /package/{src → dist}/components/blackdrop/index.scss +0 -0
  109. /package/{src → dist}/components/button/index.scss +0 -0
  110. /package/{src → dist}/components/chip/index.scss +0 -0
  111. /package/{src → dist}/components/divider/index.scss +0 -0
  112. /package/{src → dist}/components/input/extend.scss +0 -0
  113. /package/{src → dist}/components/input/input-auto-complete/index.scss +0 -0
  114. /package/{src → dist}/components/input/input-base/index.scss +0 -0
  115. /package/{src → dist}/components/input/input-basic/index.scss +0 -0
  116. /package/{src → dist}/components/input/input-checkbox/index.scss +0 -0
  117. /package/{src → dist}/components/input/input-date/index.scss +0 -0
  118. /package/{src → dist}/components/input/input-date-range/index.scss +0 -0
  119. /package/{src → dist}/components/input/input-date-time/index.scss +0 -0
  120. /package/{src → dist}/components/input/input-number/index.scss +0 -0
  121. /package/{src → dist}/components/input/input-password/index.scss +0 -0
  122. /package/{src → dist}/components/input/input-radio/index.scss +0 -0
  123. /package/{src → dist}/components/input/input-select/index.scss +0 -0
  124. /package/{src → dist}/components/input/input-switch/index.scss +0 -0
  125. /package/{src → dist}/components/input/input-textarea/index.scss +0 -0
  126. /package/{src → dist}/components/menu/index.scss +0 -0
  127. /package/{src → dist}/components/modal/index.scss +0 -0
  128. /package/{public → dist}/vite.svg +0 -0
@@ -1,118 +0,0 @@
1
- // Lib
2
- import React, { useEffect, useState } from "react"
3
- import { useFormContext, Controller } from "react-hook-form"
4
-
5
- // Include in project
6
- import "./index.scss"
7
- import InputBase from "../input-base"
8
- import type { PropsInputNumber } from "./index.type"
9
-
10
- const InputNumber: React.FC<PropsInputNumber> = ({
11
- name,
12
- label,
13
- placeholder,
14
- disabled = false,
15
- require = false,
16
- fullWidth = false,
17
- isPhoneNumber = false,
18
- isAvailableMinus = false,
19
- }) => {
20
- const { control } = useFormContext()
21
- const [showValue, setShowValue] = useState<string>("")
22
-
23
- return (
24
- <Controller
25
- name={name}
26
- control={control}
27
- render={({ field, fieldState }) => {
28
- const { value, onChange } = field
29
- const { error, invalid } = fieldState
30
-
31
- // sync RHF value → showValue
32
- // eslint-disable-next-line react-hooks/rules-of-hooks
33
- useEffect(() => {
34
- if (!isPhoneNumber && typeof value === "number" && !isNaN(value)) {
35
- setShowValue(value.toLocaleString("en-US"))
36
- } else if (typeof value === "string") {
37
- setShowValue(value)
38
- } else {
39
- setShowValue(value ?? "")
40
- }
41
- }, [value])
42
-
43
- const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
44
- const input = event.target.value
45
-
46
- // ล้างช่อง
47
- if (input === "") {
48
- if (isPhoneNumber) {
49
- onChange("")
50
- setShowValue("")
51
- } else {
52
- onChange(0)
53
- setShowValue("0")
54
- }
55
- return
56
- }
57
-
58
- // phone number
59
- if (isPhoneNumber) {
60
- onChange(input)
61
- setShowValue(input)
62
- return
63
- }
64
-
65
- const rawValue = input.replace(/,/g, "")
66
- const regex = isAvailableMinus ? /^-?[0-9]*\.?[0-9]*$/ : /^[0-9]*\.?[0-9]*$/
67
-
68
- if (!regex.test(rawValue)) return
69
-
70
- if (rawValue === "-" || rawValue.endsWith(".")) {
71
- setShowValue(formatWithComma(rawValue))
72
- onChange(rawValue)
73
- return
74
- }
75
-
76
- const numericValue = Number(rawValue)
77
- if (isNaN(numericValue)) return
78
-
79
- onChange(numericValue)
80
- setShowValue(formatWithComma(rawValue))
81
- }
82
-
83
- return (
84
- <InputBase
85
- name={name}
86
- label={label}
87
- require={require}
88
- fullWidth={fullWidth}
89
- isInvalid={invalid}
90
- errorMessage={error?.message}
91
- >
92
- <input
93
- className="DBui-inputNumber"
94
- type="text"
95
- placeholder={placeholder}
96
- disabled={disabled}
97
- value={showValue}
98
- onChange={handleChange}
99
- inputMode="decimal"
100
- pattern="-?[0-9,]*\.?[0-9]*"
101
- data-invalid={invalid}
102
- />
103
- </InputBase>
104
- )
105
- }}
106
- />
107
- )
108
- }
109
-
110
- export default InputNumber
111
-
112
- const formatWithComma = (val: string): string => {
113
- if (val === "" || val === "-" || val === "." || val === "-.") return val
114
-
115
- const [intPart, decPart] = val.split(".")
116
- const formattedInt = intPart ? Number(intPart).toLocaleString("en-US") : ""
117
- return decPart !== undefined ? `${formattedInt}.${decPart}` : formattedInt
118
- }
@@ -1,11 +0,0 @@
1
- export type PropsInputNumber = {
2
- name: string
3
- label?: string
4
- placeholder?: string
5
- disabled?: boolean
6
- require?: boolean
7
- fullWidth?: boolean
8
- isPhoneNumber?: boolean // If isPhoneNumber === true type will be string, else type number
9
- isAvailableMinus?: boolean
10
- }
11
-
@@ -1,60 +0,0 @@
1
- // Lib
2
- import React, { useState } from "react"
3
- import { useFormContext, useFormState } from "react-hook-form"
4
-
5
- // Images
6
- import visibilitySVG from "../../assets/visibility.svg"
7
- import visibilityOffSVG from "../../assets/visibility-off.svg"
8
-
9
- // Include in project
10
- import "./index.scss"
11
- import InputBase from "../input-base"
12
- import type { PropsInputPassword } from "./index.type"
13
-
14
- const InputPassword: React.FC<PropsInputPassword> = ({
15
- name,
16
- label,
17
- placeholder,
18
- disabled = false,
19
- require = false,
20
- fullWidth = false,
21
- }) => {
22
- const { register, control } = useFormContext()
23
- const { errors } = useFormState({ control, name })
24
-
25
- const error = errors?.[name]
26
- const isInvalid = Boolean(error)
27
-
28
- const [isShow, setIsShow] = useState(false)
29
-
30
- return (
31
- <InputBase
32
- name={name}
33
- label={label}
34
- require={require}
35
- fullWidth={fullWidth}
36
- isInvalid={isInvalid}
37
- errorMessage={error?.message}
38
- >
39
- <div className="DBui-wrapInputPassword">
40
- <input
41
- {...register(name)}
42
- className="DBui-inputPassword"
43
- type={isShow ? "text" : "password"}
44
- placeholder={placeholder}
45
- disabled={disabled}
46
- data-invalid={isInvalid}
47
- />
48
-
49
- <img
50
- className="DBui-inputPasswordIcon"
51
- src={isShow ? visibilitySVG : visibilityOffSVG}
52
- alt="toggle visibility"
53
- onClick={() => setIsShow((s) => !s)}
54
- />
55
- </div>
56
- </InputBase>
57
- )
58
- }
59
-
60
- export default InputPassword
@@ -1,8 +0,0 @@
1
- export type PropsInputPassword = {
2
- name: string
3
- label?: string
4
- placeholder?: string
5
- disabled?: boolean
6
- require?: boolean
7
- fullWidth?: boolean
8
- }
@@ -1,72 +0,0 @@
1
- // Lib
2
- import React from "react"
3
- import { Controller, useFormContext } from "react-hook-form"
4
-
5
- // Include in project
6
- import "./index.scss"
7
- import InputBase from "../input-base"
8
- import type { PropsInputRadio } from "./index.type"
9
-
10
- const InputRadio: React.FC<PropsInputRadio> = ({
11
- name,
12
- label,
13
- disabled = false,
14
- require = false,
15
- fullWidth = false,
16
- isVertical = false,
17
- options,
18
- }) => {
19
- const { control } = useFormContext()
20
-
21
- return (
22
- <Controller
23
- name={name}
24
- control={control}
25
- render={({ field, fieldState }) => {
26
- const { value, onChange } = field
27
- const { invalid, error } = fieldState
28
-
29
- return (
30
- <InputBase
31
- name={name}
32
- label={label}
33
- require={require}
34
- fullWidth={fullWidth}
35
- isInvalid={invalid}
36
- errorMessage={error?.message}
37
- >
38
- <div className="DBui-wrapInputRadioList" data-vertical={isVertical}>
39
- {options.map((data, index) => {
40
- const isChecked = value === data.value
41
-
42
- return (
43
- <div key={index} className="DBui-wrapInputRadio">
44
- <input
45
- className="DBui-inputRadio"
46
- type="radio"
47
- disabled={disabled}
48
- checked={isChecked}
49
- onClick={() => {
50
- // คลิกซ้ำ = uncheck
51
- if (isChecked) {
52
- onChange("")
53
- } else {
54
- onChange(data.value)
55
- }
56
- }}
57
- />
58
- <p className="DBui-labelRadio" data-invalid={invalid}>
59
- <small>{data.label}</small>
60
- </p>
61
- </div>
62
- )
63
- })}
64
- </div>
65
- </InputBase>
66
- )
67
- }}
68
- />
69
- )
70
- }
71
-
72
- export default InputRadio
@@ -1,12 +0,0 @@
1
- export type PropsInputRadio = {
2
- name: string
3
- label?: string
4
- disabled?: boolean
5
- require?: boolean
6
- fullWidth?: boolean
7
- isVertical?: boolean
8
- options: {
9
- label: string
10
- value: string | number
11
- }[]
12
- }
@@ -1,113 +0,0 @@
1
- // Lib
2
- import React from "react"
3
- import { Controller, useFormContext } from "react-hook-form"
4
-
5
- // Images
6
- import expandArrowSVG from "../../assets/expand-arrow.svg"
7
- import closedSVG from "../../assets/closed.svg"
8
-
9
- // Include in project
10
- import "./index.scss"
11
- import InputBase from "../input-base"
12
- import Menu from "../../menu"
13
- import type { TOption, TValueOption } from "../.."
14
- import type { PropsInputSelect } from "./index.type"
15
-
16
- const InputSelect: React.FC<PropsInputSelect> = ({
17
- name,
18
- label,
19
- disabled = false,
20
- require = false,
21
- fullWidth = false,
22
- options,
23
- onChange,
24
- isHideClearIcon = true,
25
- placeholder,
26
- isInModal = false,
27
- }) => {
28
- const { control } = useFormContext()
29
-
30
- return (
31
- <Controller
32
- name={name}
33
- control={control}
34
- render={({ field, fieldState }) => {
35
- const { value, onChange: setValue } = field
36
- const { invalid, error } = fieldState
37
-
38
- const selected = options?.find((e) => e.value === value) as TOption | undefined
39
-
40
- const handleSelect = (val: TValueOption, close: () => void) => {
41
- setValue(val)
42
- onChange?.(val)
43
- close()
44
- }
45
-
46
- const handleClear = (e: React.MouseEvent<HTMLImageElement>) => {
47
- e.stopPropagation()
48
- setValue(null)
49
- onChange?.(null)
50
- }
51
-
52
- return (
53
- <InputBase
54
- name={name}
55
- label={label}
56
- require={require}
57
- fullWidth={fullWidth}
58
- isInvalid={invalid}
59
- errorMessage={error?.message}
60
- >
61
- <Menu
62
- isInModal={isInModal}
63
- disabled={disabled}
64
- trigger={({ isOpen }) => (
65
- <div className="DBui-wrapperInputSelect" data-invalid={invalid} data-disabled={disabled}>
66
- <div className="DBui-wrapperLabelInputSelect">
67
- {selected?.label ? (
68
- <p className="DBui-wrapperLabel">
69
- <small>{selected.label}</small>
70
- </p>
71
- ) : (
72
- <p className="DBui-placeholder">
73
- <small>{placeholder}</small>
74
- </p>
75
- )}
76
-
77
- <img
78
- src={closedSVG}
79
- className="DBui-clearIconInputSelect"
80
- onClick={handleClear}
81
- data-hidden={!value || disabled || isHideClearIcon}
82
- />
83
- </div>
84
-
85
- <img
86
- src={expandArrowSVG}
87
- className="DBui-arrowIconInputSelect"
88
- data-checked={disabled ? true : !isOpen}
89
- />
90
- </div>
91
- )}
92
- >
93
- {({ close }) =>
94
- options.map((data, index) => (
95
- <p
96
- key={index}
97
- className="DBui-menuItemInputSelect"
98
- onClick={() => handleSelect(data.value, close)}
99
- data-checked={value === data.value}
100
- >
101
- {data.label}
102
- </p>
103
- ))
104
- }
105
- </Menu>
106
- </InputBase>
107
- )
108
- }}
109
- />
110
- )
111
- }
112
-
113
- export default InputSelect
@@ -1,15 +0,0 @@
1
- import type { TOption, TValueOption } from "../.."
2
-
3
- export type PropsInputSelect = {
4
- name: string
5
- label?: string
6
- placeholder?: string
7
- disabled?: boolean
8
- require?: boolean
9
- fullWidth?: boolean
10
- options: TOption[]
11
- onChange?: (value: TValueOption | null) => void
12
- value?: string
13
- isHideClearIcon?: boolean
14
- isInModal?: boolean
15
- }
@@ -1,44 +0,0 @@
1
- // Lib
2
- import React from "react"
3
- import { Controller, useFormContext } from "react-hook-form"
4
-
5
- // Include in project
6
- import "./index.scss"
7
- import type { PropsInputSwitch } from "./index.type"
8
-
9
- const InputSwitch: React.FC<PropsInputSwitch> = ({ name, disabled }) => {
10
- const { control } = useFormContext()
11
-
12
- return (
13
- <Controller
14
- name={name}
15
- control={control}
16
- render={({ field }) => {
17
- const { value, onChange } = field
18
-
19
- return (
20
- <div className="DBui-wrapInputList" data-disabled={disabled}>
21
- <label className="DBui-switch">
22
- <span className="DBui-minus"></span>
23
- <span className="DBui-miniCircle"></span>
24
-
25
- <input
26
- type="checkbox"
27
- checked={Boolean(value)}
28
- disabled={disabled}
29
- onChange={(e) => {
30
- e.stopPropagation()
31
- onChange(!value)
32
- }}
33
- />
34
-
35
- <span className="DBui-slider"></span>
36
- </label>
37
- </div>
38
- )
39
- }}
40
- />
41
- )
42
- }
43
-
44
- export default InputSwitch
@@ -1,4 +0,0 @@
1
- export type PropsInputSwitch = {
2
- name: string
3
- disabled?: boolean
4
- }
@@ -1,48 +0,0 @@
1
- // Lib
2
- import React from "react"
3
- import { useFormContext, useFormState } from "react-hook-form"
4
-
5
- // Include in project
6
- import "./index.scss"
7
- import InputBase from "../input-base"
8
- import type { PropsInputTextarea } from "./index.type"
9
-
10
- const InputTextarea: React.FC<PropsInputTextarea> = ({
11
- name,
12
- label,
13
- placeholder,
14
- disabled = false,
15
- require = false,
16
- fullWidth = false,
17
- rows = 2,
18
- cols = 50,
19
- }) => {
20
- const { register, control } = useFormContext()
21
- const { errors } = useFormState({ control, name })
22
-
23
- const error = errors?.[name]
24
- const isInvalid = Boolean(error)
25
-
26
- return (
27
- <InputBase
28
- name={name}
29
- label={label}
30
- require={require}
31
- fullWidth={fullWidth}
32
- isInvalid={isInvalid}
33
- errorMessage={error?.message}
34
- >
35
- <textarea
36
- {...register(name)}
37
- className="DBui-inputTextarea"
38
- placeholder={placeholder}
39
- disabled={disabled}
40
- rows={rows}
41
- cols={cols}
42
- data-invalid={isInvalid}
43
- />
44
- </InputBase>
45
- )
46
- }
47
-
48
- export default InputTextarea
@@ -1,10 +0,0 @@
1
- export type PropsInputTextarea = {
2
- name: string
3
- label?: string
4
- placeholder?: string
5
- disabled?: boolean
6
- require?: boolean
7
- fullWidth?: boolean
8
- rows?: number
9
- cols?: number
10
- }
@@ -1,136 +0,0 @@
1
- // Lib
2
- import React, { useEffect, useRef, useState } from "react"
3
- import { createPortal } from "react-dom"
4
- import "./index.scss"
5
- import type { PropsMenu } from "./index.type"
6
-
7
- const Menu: React.FC<PropsMenu> = ({ children, trigger, disabled, isInModal = false }) => {
8
- const [isOpen, setIsOpen] = useState(false)
9
- const [menuStyle, setMenuStyle] = useState({})
10
-
11
- const triggerRef = useRef<HTMLDivElement>(null)
12
- const menuRef = useRef<HTMLDivElement>(null)
13
-
14
- // Toggle dropdown
15
- const toggleMenu = () => {
16
- if (!disabled) setIsOpen((prev) => !prev)
17
- }
18
-
19
- // ------------ MAIN POSITIONING FUNCTION ------------
20
- const calculateMenuPosition = () => {
21
- if (!triggerRef.current || !menuRef.current) return
22
-
23
- const triggerRect = triggerRef.current.getBoundingClientRect()
24
- const menu = menuRef.current
25
-
26
- const viewportH = window.innerHeight
27
- const viewportW = window.innerWidth
28
- const menuWidth = menu.offsetWidth
29
-
30
- const spaceAbove = triggerRect.top
31
- const spaceBelow = viewportH - triggerRect.bottom
32
-
33
- // วางล่างถ้าพื้นที่ล่างมากกว่า
34
- const placeBottom = spaceBelow >= spaceAbove
35
-
36
- let top = placeBottom
37
- ? triggerRect.bottom // show below
38
- : triggerRect.top - menu.offsetHeight // show above (height กำหนดจาก CSS max-height)
39
-
40
- // เปลี่ยน logic ตรงนี้ → กันเมนูล้นจอจาก CSS max-height
41
- // ถ้าด้านบนพื้นที่ไม่พอ ให้ clamp
42
- if (!placeBottom) {
43
- const minTop = 8
44
- if (top < minTop) top = minTop
45
- }
46
-
47
- let left = triggerRect.left
48
- if (left + menuWidth > viewportW - 8) {
49
- left = viewportW - menuWidth - 8
50
- }
51
- if (left < 8) left = 8
52
-
53
- setMenuStyle({
54
- position: "fixed",
55
- top,
56
- left,
57
- })
58
- }
59
-
60
- // ------------ Double RAF (ให้ DOM render ก่อน 100%) ------------
61
- useEffect(() => {
62
- if (!isOpen) return
63
-
64
- requestAnimationFrame(() => {
65
- requestAnimationFrame(() => {
66
- calculateMenuPosition()
67
- })
68
- })
69
- }, [isOpen, children])
70
-
71
- // ------------ Reposition when scroll/resize/orientation ------------
72
- useEffect(() => {
73
- if (!isOpen) return
74
-
75
- const handler = () => calculateMenuPosition()
76
-
77
- window.addEventListener("scroll", handler, true)
78
- window.addEventListener("resize", handler)
79
- window.addEventListener("orientationchange", handler)
80
-
81
- return () => {
82
- window.removeEventListener("scroll", handler, true)
83
- window.removeEventListener("resize", handler)
84
- window.removeEventListener("orientationchange", handler)
85
- }
86
- }, [isOpen, children])
87
-
88
- // ------------ Close On Click Outside ------------
89
- useEffect(() => {
90
- if (!isOpen) return
91
-
92
- const handleClick = (e: MouseEvent) => {
93
- const t = triggerRef.current
94
- const m = menuRef.current
95
- if (!t || !m) return
96
-
97
- if (!t.contains(e.target as Node) && !m.contains(e.target as Node)) {
98
- setIsOpen(false)
99
- }
100
- }
101
-
102
- document.addEventListener("mousedown", handleClick)
103
- return () => document.removeEventListener("mousedown", handleClick)
104
- }, [isOpen])
105
-
106
- // Portal root
107
- const portalRoot = document.getElementById("root") || document.getElementById("__next") || document.body
108
-
109
- return (
110
- <>
111
- <div ref={triggerRef} className="DBui-wrapperMenu" onClick={toggleMenu}>
112
- {trigger({ isOpen })}
113
- </div>
114
-
115
- {isOpen &&
116
- createPortal(
117
- <div
118
- ref={menuRef}
119
- className="DBui-wrapperMenuItem"
120
- style={menuStyle}
121
- data-hidden={!isOpen}
122
- data-is-in-modal={isInModal}
123
- >
124
- {typeof children === "function"
125
- ? children({
126
- close: () => setIsOpen(false),
127
- })
128
- : children}
129
- </div>,
130
- portalRoot,
131
- )}
132
- </>
133
- )
134
- }
135
-
136
- export default Menu
@@ -1,8 +0,0 @@
1
- import type { JSX } from "react"
2
-
3
- export type PropsMenu = {
4
- trigger: (props: { isOpen: boolean }) => JSX.Element
5
- children: (args: { close: () => void }) => JSX.Element | JSX.Element[]
6
- disabled?: boolean
7
- isInModal?: boolean
8
- }