@platformatic/ui-components 0.2.15 → 0.2.18
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 +1 -1
- package/src/components/LoginButton.jsx +1 -1
- package/src/components/TooltipV2.jsx +0 -1
- package/src/components/forms/Select.jsx +24 -59
- package/src/components/forms/Select.module.css +5 -2
- package/src/components/forms/SelectWithInput.jsx +327 -0
- package/src/components/forms/SelectWithInput.module.css +40 -0
- package/src/components/forms/index.js +2 -0
- package/src/stories/forms/Select.stories.jsx +3 -16
- package/src/stories/forms/SelectWithInput.stories.jsx +176 -0
package/package.json
CHANGED
|
@@ -3,6 +3,6 @@ import Button from './Button'
|
|
|
3
3
|
|
|
4
4
|
export default function LoginButton ({ iconName, label, onClick, ...props }) {
|
|
5
5
|
return (
|
|
6
|
-
<Button backgroundColor='main-green' alt={label} label={label} platformaticIcon={{ iconName, size: 'medium', color: 'main-dark-blue' }} data-testid='login-button' size='extra-large' onClick={onClick} {...props}
|
|
6
|
+
<Button backgroundColor='main-green' alt={label} label={label} platformaticIcon={{ iconName, size: 'medium', color: 'main-dark-blue' }} data-testid='login-button' size='extra-large' onClick={onClick} {...props} />
|
|
7
7
|
)
|
|
8
8
|
}
|
|
@@ -13,7 +13,6 @@ function TooltipV2 ({ tooltipClassName, text, visible, alignment, elementClassNa
|
|
|
13
13
|
if (visible) {
|
|
14
14
|
setClassName(visibleClassName())
|
|
15
15
|
if (ref.current) {
|
|
16
|
-
console.log('elementClassName', elementClassName)
|
|
17
16
|
const referenceBoundingClientRect = document.getElementsByClassName(elementClassName)[0]?.getBoundingClientRect()
|
|
18
17
|
if (referenceBoundingClientRect) {
|
|
19
18
|
const topPosition = referenceBoundingClientRect.y - (referenceBoundingClientRect.height)
|
|
@@ -17,12 +17,9 @@ function Select ({
|
|
|
17
17
|
borderColor,
|
|
18
18
|
borderListColor,
|
|
19
19
|
errorMessage,
|
|
20
|
-
onChange,
|
|
21
20
|
onSelect,
|
|
22
|
-
onClear,
|
|
23
21
|
disabled,
|
|
24
22
|
mainColor,
|
|
25
|
-
optionSelected,
|
|
26
23
|
dataAttrName,
|
|
27
24
|
dataAttrValue,
|
|
28
25
|
backgroundColor,
|
|
@@ -30,18 +27,19 @@ function Select ({
|
|
|
30
27
|
}) {
|
|
31
28
|
const inputRef = useRef()
|
|
32
29
|
const [showOptions, setShowOptions] = useState(false)
|
|
30
|
+
// eslint-disable-next-line no-unused-vars
|
|
33
31
|
const [isSelected, setIsSelected] = useState(false)
|
|
34
32
|
const [isOnFocus, setIsOnFocus] = useState(false)
|
|
33
|
+
|
|
35
34
|
const showError = errorMessage.length > 0
|
|
36
35
|
const containerClassName = `${styles.container} ${defaultContainerClassName} `
|
|
37
36
|
let inputClassName = `${commonStyles.fullWidth} ${styles.select} ${inputTextClassName}`
|
|
38
37
|
inputClassName += ' ' + styles[`select-${mainColor}`]
|
|
39
38
|
inputClassName += ' ' + commonStyles[`text--${borderColor}`]
|
|
40
39
|
let optionsClassName = `${styles.options} ${defaultOptionsClassName} `
|
|
41
|
-
if (showError) inputClassName += ' ' + commonStyles['bordered--error-red']
|
|
42
|
-
if (disabled) inputClassName += ' ' + commonStyles['apply-opacity-30']
|
|
43
40
|
inputClassName += ' ' + commonStyles[`background-color-${backgroundColor}`]
|
|
44
41
|
optionsClassName += commonStyles[`background-color-${backgroundColor}`]
|
|
42
|
+
const optionSelectedClassName = styles.optionSelected
|
|
45
43
|
|
|
46
44
|
if (borderListColor) {
|
|
47
45
|
optionsClassName += ' ' + styles['bordered-options']
|
|
@@ -55,12 +53,20 @@ function Select ({
|
|
|
55
53
|
const optionSpanClassName = commonStyles[`text--${mainColor}`]
|
|
56
54
|
|
|
57
55
|
const [wrapperClassName, setWrapperClassName] = useState(normalClassName())
|
|
56
|
+
|
|
58
57
|
function onFocusClassName () {
|
|
59
58
|
return inputClassName + ' ' + commonStyles[`bordered--${borderColor}-100`]
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
function normalClassName () {
|
|
63
|
-
|
|
62
|
+
const baseClassName = inputClassName
|
|
63
|
+
if (showError) {
|
|
64
|
+
inputClassName += ' ' + commonStyles['bordered--error-red']
|
|
65
|
+
} else {
|
|
66
|
+
inputClassName += ' ' + commonStyles[`bordered--${borderColor}-70`]
|
|
67
|
+
}
|
|
68
|
+
if (disabled) inputClassName += ' ' + commonStyles['apply-opacity-30']
|
|
69
|
+
return baseClassName
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const dataProps = {}
|
|
@@ -87,24 +93,24 @@ function Select ({
|
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
useEffect(() => {
|
|
90
|
-
if (value.length > 0 && !showOptions && !isSelected) {
|
|
91
|
-
setShowOptions(true)
|
|
92
|
-
}
|
|
93
96
|
if (value.length === 0) {
|
|
94
97
|
setIsSelected(false)
|
|
95
98
|
}
|
|
96
99
|
}, [value])
|
|
97
100
|
|
|
98
101
|
useEffect(() => {
|
|
99
|
-
if (
|
|
100
|
-
|
|
102
|
+
if (disabled) {
|
|
103
|
+
const className = normalClassName() + ' ' + commonStyles['apply-opacity-30']
|
|
104
|
+
setWrapperClassName(className)
|
|
105
|
+
} else {
|
|
106
|
+
setWrapperClassName(normalClassName())
|
|
101
107
|
}
|
|
102
|
-
}, [
|
|
108
|
+
}, [disabled])
|
|
103
109
|
|
|
104
|
-
function renderLi (option, index) {
|
|
110
|
+
function renderLi (option, index, isOptionSelected) {
|
|
105
111
|
return (
|
|
106
112
|
<li
|
|
107
|
-
key={index} className={singleOptionClassName} onClick={() => {
|
|
113
|
+
key={index} className={`${isOptionSelected ? optionSelectedClassName : singleOptionClassName}`} onClick={() => {
|
|
108
114
|
if (option.notSelectable) {
|
|
109
115
|
return handleNotSelectable(option.onClick && option.onClick())
|
|
110
116
|
}
|
|
@@ -121,34 +127,15 @@ function Select ({
|
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
function renderOptions () {
|
|
124
|
-
if (value.length === 0) {
|
|
125
|
-
return (
|
|
126
|
-
<ul className={optionsClassName}>
|
|
127
|
-
{options.length > 0 ? options.map((option, index) => renderLi(option, index)) : <li className={singleOptionClassName}><div className={styles.liContent}><span className={commonStyles[`text--${mainColor}`]}>No data found</span></div></li>}
|
|
128
|
-
</ul>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
130
|
const notFilterableOptions = options.filter(option => option.notFilterable)
|
|
132
|
-
const filteredOptions = options.filter(option => !option.notFilterable).filter(option => option.label.toLowerCase().includes(value.toLowerCase()))
|
|
133
|
-
|
|
134
131
|
return (
|
|
135
132
|
<ul className={optionsClassName}>
|
|
136
|
-
{filteredOptions.length > 0 ? filteredOptions.map((option, index) => renderLi(option, index)) : <li className={singleOptionClassName}><div className={styles.liContent}><span className={commonStyles[`text--${mainColor}`]}>No data found</span></div></li>}
|
|
137
133
|
{notFilterableOptions.length > 0 && notFilterableOptions.map((option, index) => renderLi(option, index))}
|
|
134
|
+
{options.length > 0 ? options.map((option, index) => renderLi(option, index, option.label === value)) : <li className={singleOptionClassName}><div className={styles.liContent}><span className={commonStyles[`text--${mainColor}`]}>No data found</span></div></li>}
|
|
138
135
|
</ul>
|
|
139
136
|
)
|
|
140
137
|
}
|
|
141
138
|
|
|
142
|
-
// https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-change-or-input-event-in-react-js
|
|
143
|
-
function clearValue () {
|
|
144
|
-
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
|
|
145
|
-
nativeInputValueSetter.call(inputRef.current, '')
|
|
146
|
-
const ev2 = new Event('input', { bubbles: true })
|
|
147
|
-
ev2.simulated = true
|
|
148
|
-
inputRef.current.dispatchEvent(ev2)
|
|
149
|
-
onClear()
|
|
150
|
-
}
|
|
151
|
-
|
|
152
139
|
function handleFocus () {
|
|
153
140
|
if (!isOnFocus) {
|
|
154
141
|
setIsOnFocus(true)
|
|
@@ -161,7 +148,7 @@ function Select ({
|
|
|
161
148
|
event.preventDefault()
|
|
162
149
|
setTimeout(() => {
|
|
163
150
|
if (showOptions) {
|
|
164
|
-
setShowOptions(false)
|
|
151
|
+
// setShowOptions(false)
|
|
165
152
|
setIsOnFocus(false)
|
|
166
153
|
setWrapperClassName(normalClassName())
|
|
167
154
|
}
|
|
@@ -171,10 +158,9 @@ function Select ({
|
|
|
171
158
|
return (
|
|
172
159
|
<div className={containerClassName} {...dataProps}>
|
|
173
160
|
<div className={styles.selectContainer}>
|
|
174
|
-
<input type='text' name={name} value={value} className={wrapperClassName} ref={inputRef}
|
|
161
|
+
<input type='text' name={name} value={value} className={wrapperClassName} ref={inputRef} disabled={disabled} placeholder={placeholder} onFocus={() => handleFocus()} onBlur={(e) => handleBlur(e)} readOnly />
|
|
175
162
|
<div className={styles.icons}>
|
|
176
|
-
|
|
177
|
-
<PlatformaticIcon iconName={showOptions ? 'ArrowUpIcon' : 'ArrowDownIcon'} color={borderColor} onClick={() => disabled ? null : setShowOptions(!showOptions)} size={SMALL} />
|
|
163
|
+
<PlatformaticIcon iconName={showOptions ? 'ArrowUpIcon' : 'ArrowDownIcon'} color={borderColor} disabled={disabled} onClick={() => setShowOptions(!showOptions)} size={SMALL} />
|
|
178
164
|
</div>
|
|
179
165
|
</div>
|
|
180
166
|
{showOptions && !showError && renderOptions()}
|
|
@@ -236,18 +222,10 @@ Select.propTypes = {
|
|
|
236
222
|
* color of border UL
|
|
237
223
|
*/
|
|
238
224
|
borderListColor: PropTypes.oneOf([MAIN_GREEN, MAIN_DARK_BLUE, WHITE]),
|
|
239
|
-
/**
|
|
240
|
-
* onChange
|
|
241
|
-
*/
|
|
242
|
-
onChange: PropTypes.func,
|
|
243
225
|
/**
|
|
244
226
|
* onSelect
|
|
245
227
|
*/
|
|
246
228
|
onSelect: PropTypes.func,
|
|
247
|
-
/**
|
|
248
|
-
* onClear
|
|
249
|
-
*/
|
|
250
|
-
onClear: PropTypes.func,
|
|
251
229
|
/**
|
|
252
230
|
* Disabled
|
|
253
231
|
*/
|
|
@@ -256,16 +234,6 @@ Select.propTypes = {
|
|
|
256
234
|
* mainColor
|
|
257
235
|
*/
|
|
258
236
|
mainColor: PropTypes.oneOf([MAIN_DARK_BLUE, WHITE]),
|
|
259
|
-
/**
|
|
260
|
-
* optionSelected
|
|
261
|
-
*/
|
|
262
|
-
optionSelected: PropTypes.shape({
|
|
263
|
-
label: PropTypes.string,
|
|
264
|
-
value: PropTypes.oneOfType([
|
|
265
|
-
PropTypes.string,
|
|
266
|
-
PropTypes.number
|
|
267
|
-
])
|
|
268
|
-
}),
|
|
269
237
|
/**
|
|
270
238
|
* dataAttrName
|
|
271
239
|
*/
|
|
@@ -296,12 +264,9 @@ Select.defaultProps = {
|
|
|
296
264
|
borderColor: MAIN_GREEN,
|
|
297
265
|
borderListColor: '',
|
|
298
266
|
errorMessage: '',
|
|
299
|
-
onChange: () => {},
|
|
300
267
|
onSelect: () => {},
|
|
301
|
-
onClear: () => {},
|
|
302
268
|
disabled: false,
|
|
303
269
|
mainColor: MAIN_DARK_BLUE,
|
|
304
|
-
optionSelected: null,
|
|
305
270
|
dataAttrName: '',
|
|
306
271
|
dataAttrValue: '',
|
|
307
272
|
backgroundColor: WHITE,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
@apply outline-none;
|
|
18
18
|
}
|
|
19
19
|
.options {
|
|
20
|
-
@apply absolute left-0 top-[42px] w-
|
|
20
|
+
@apply absolute left-0 top-[42px] w-[calc(100%-1rem)] max-h-[216px] overflow-y-auto z-20 p-2;
|
|
21
21
|
}
|
|
22
22
|
.bordered-options {
|
|
23
23
|
@apply border border-solid rounded
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
.option {
|
|
26
26
|
@apply font-light cursor-pointer z-10 flex flex-row justify-between items-center px-4;
|
|
27
27
|
}
|
|
28
|
+
.optionSelected {
|
|
29
|
+
@apply font-light cursor-pointer z-10 flex flex-row justify-between items-center px-4 bg-white/15;
|
|
30
|
+
}
|
|
28
31
|
.bordered-bottom {
|
|
29
32
|
@apply border-b
|
|
30
33
|
}
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
transform: translateY(-50%);
|
|
34
37
|
}
|
|
35
38
|
.liContent {
|
|
36
|
-
@apply py-2 flex items-center gap-x-1 relative z-[-1] w-
|
|
39
|
+
@apply py-2 flex items-center gap-x-1 relative z-[-1] w-full;
|
|
37
40
|
}
|
|
38
41
|
.descriptionValue {
|
|
39
42
|
@apply opacity-70
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import styles from './SelectWithInput.module.css'
|
|
5
|
+
import commonStyles from '../Common.module.css'
|
|
6
|
+
import { MAIN_DARK_BLUE, MAIN_GREEN, RICH_BLACK, SMALL, WHITE } from '../constants'
|
|
7
|
+
import PlatformaticIcon from '../PlatformaticIcon'
|
|
8
|
+
|
|
9
|
+
function SelectWithInput ({
|
|
10
|
+
defaultContainerClassName,
|
|
11
|
+
placeholder,
|
|
12
|
+
name,
|
|
13
|
+
value,
|
|
14
|
+
options,
|
|
15
|
+
defaultOptionsClassName,
|
|
16
|
+
optionsBorderedBottom,
|
|
17
|
+
borderColor,
|
|
18
|
+
borderListColor,
|
|
19
|
+
errorMessage,
|
|
20
|
+
onChange,
|
|
21
|
+
onSelect,
|
|
22
|
+
onClear,
|
|
23
|
+
disabled,
|
|
24
|
+
mainColor,
|
|
25
|
+
optionSelected,
|
|
26
|
+
dataAttrName,
|
|
27
|
+
dataAttrValue,
|
|
28
|
+
backgroundColor,
|
|
29
|
+
inputTextClassName
|
|
30
|
+
}) {
|
|
31
|
+
const inputRef = useRef()
|
|
32
|
+
const [showOptions, setShowOptions] = useState(false)
|
|
33
|
+
const [isSelected, setIsSelected] = useState(false)
|
|
34
|
+
const [isOnFocus, setIsOnFocus] = useState(false)
|
|
35
|
+
|
|
36
|
+
const showError = errorMessage.length > 0
|
|
37
|
+
const containerClassName = `${styles.container} ${defaultContainerClassName} `
|
|
38
|
+
let inputClassName = `${commonStyles.fullWidth} ${styles.select} ${inputTextClassName}`
|
|
39
|
+
inputClassName += ' ' + styles[`select-${mainColor}`]
|
|
40
|
+
inputClassName += ' ' + commonStyles[`text--${borderColor}`]
|
|
41
|
+
let optionsClassName = `${styles.options} ${defaultOptionsClassName} `
|
|
42
|
+
inputClassName += ' ' + commonStyles[`background-color-${backgroundColor}`]
|
|
43
|
+
optionsClassName += commonStyles[`background-color-${backgroundColor}`]
|
|
44
|
+
|
|
45
|
+
if (borderListColor) {
|
|
46
|
+
optionsClassName += ' ' + styles['bordered-options']
|
|
47
|
+
optionsClassName += ' ' + commonStyles[`bordered--${borderListColor}-30`]
|
|
48
|
+
}
|
|
49
|
+
let singleOptionClassName = `${styles.option} ` + commonStyles[`bordered--${mainColor}-70`] + ' ' + commonStyles[`hover-background-color-opaque-${mainColor}`]
|
|
50
|
+
if (optionsBorderedBottom) {
|
|
51
|
+
singleOptionClassName += ` ${styles['bordered-bottom']}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const optionSpanClassName = commonStyles[`text--${mainColor}`]
|
|
55
|
+
|
|
56
|
+
const [wrapperClassName, setWrapperClassName] = useState(normalClassName())
|
|
57
|
+
|
|
58
|
+
function onFocusClassName () {
|
|
59
|
+
return inputClassName + ' ' + commonStyles[`bordered--${borderColor}-100`]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalClassName () {
|
|
63
|
+
const baseClassName = inputClassName
|
|
64
|
+
if (showError) {
|
|
65
|
+
inputClassName += ' ' + commonStyles['bordered--error-red']
|
|
66
|
+
} else {
|
|
67
|
+
inputClassName += ' ' + commonStyles[`bordered--${borderColor}-70`]
|
|
68
|
+
}
|
|
69
|
+
if (disabled) inputClassName += ' ' + commonStyles['apply-opacity-30']
|
|
70
|
+
return baseClassName
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dataProps = {}
|
|
74
|
+
if (dataAttrName && dataAttrValue) {
|
|
75
|
+
dataProps[`data-${dataAttrName}`] = dataAttrValue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function handleNotSelectable (callback = () => {}) {
|
|
79
|
+
setIsSelected(true)
|
|
80
|
+
setShowOptions(false)
|
|
81
|
+
callback()
|
|
82
|
+
}
|
|
83
|
+
function setSelected ({ label, value }) {
|
|
84
|
+
setIsSelected(true)
|
|
85
|
+
setShowOptions(false)
|
|
86
|
+
/* eslint-disable-next-line no-undef */
|
|
87
|
+
onSelect(new CustomEvent('valueSelected', {
|
|
88
|
+
detail: {
|
|
89
|
+
name,
|
|
90
|
+
label,
|
|
91
|
+
value
|
|
92
|
+
}
|
|
93
|
+
}))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (value.length > 0 && !showOptions && !isSelected) {
|
|
98
|
+
setShowOptions(true)
|
|
99
|
+
}
|
|
100
|
+
if (value.length === 0) {
|
|
101
|
+
setIsSelected(false)
|
|
102
|
+
}
|
|
103
|
+
}, [value])
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (optionSelected) {
|
|
107
|
+
setSelected(optionSelected)
|
|
108
|
+
}
|
|
109
|
+
}, [optionSelected])
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (disabled) {
|
|
113
|
+
const className = normalClassName() + ' ' + commonStyles['apply-opacity-30']
|
|
114
|
+
setWrapperClassName(className)
|
|
115
|
+
} else {
|
|
116
|
+
setWrapperClassName(normalClassName())
|
|
117
|
+
}
|
|
118
|
+
}, [disabled])
|
|
119
|
+
|
|
120
|
+
function renderLi (option, index) {
|
|
121
|
+
return (
|
|
122
|
+
<li
|
|
123
|
+
key={index} className={singleOptionClassName} onClick={() => {
|
|
124
|
+
if (option.notSelectable) {
|
|
125
|
+
return handleNotSelectable(option.onClick && option.onClick())
|
|
126
|
+
}
|
|
127
|
+
return setSelected(option)
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<div className={styles.liContent}>
|
|
131
|
+
{option.iconName && <PlatformaticIcon iconName={option.iconName} color={option.iconColor || mainColor} size={option.iconSize ?? SMALL} onClick={null} />}
|
|
132
|
+
<span className={optionSpanClassName}>{option.label}</span>
|
|
133
|
+
</div>
|
|
134
|
+
{option.descriptionValue && <span className={`${optionSpanClassName} ${styles.descriptionValue}`}>{option.descriptionValue}</span>}
|
|
135
|
+
</li>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderOptions () {
|
|
140
|
+
if (value.length === 0) {
|
|
141
|
+
return (
|
|
142
|
+
<ul className={optionsClassName}>
|
|
143
|
+
{options.length > 0 ? options.map((option, index) => renderLi(option, index)) : <li className={singleOptionClassName}><div className={styles.liContent}><span className={commonStyles[`text--${mainColor}`]}>No data found</span></div></li>}
|
|
144
|
+
</ul>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
const notFilterableOptions = options.filter(option => option.notFilterable)
|
|
148
|
+
const filteredOptions = options.filter(option => !option.notFilterable).filter(option => option.label.toLowerCase().includes(value.toLowerCase()))
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<ul className={optionsClassName}>
|
|
152
|
+
{filteredOptions.length > 0 ? filteredOptions.map((option, index) => renderLi(option, index)) : <li className={singleOptionClassName}><div className={styles.liContent}><span className={commonStyles[`text--${mainColor}`]}>No data found</span></div></li>}
|
|
153
|
+
{notFilterableOptions.length > 0 && notFilterableOptions.map((option, index) => renderLi(option, index))}
|
|
154
|
+
</ul>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-change-or-input-event-in-react-js
|
|
159
|
+
function clearValue () {
|
|
160
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
|
|
161
|
+
nativeInputValueSetter.call(inputRef.current, '')
|
|
162
|
+
const ev2 = new Event('input', { bubbles: true })
|
|
163
|
+
ev2.simulated = true
|
|
164
|
+
inputRef.current.dispatchEvent(ev2)
|
|
165
|
+
onClear()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleFocus () {
|
|
169
|
+
if (!isOnFocus) {
|
|
170
|
+
setIsOnFocus(true)
|
|
171
|
+
setWrapperClassName(onFocusClassName())
|
|
172
|
+
}
|
|
173
|
+
setShowOptions(true)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleBlur (event) {
|
|
177
|
+
event.preventDefault()
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
if (showOptions) {
|
|
180
|
+
setShowOptions(false)
|
|
181
|
+
setIsOnFocus(false)
|
|
182
|
+
setWrapperClassName(normalClassName())
|
|
183
|
+
}
|
|
184
|
+
}, 250)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className={containerClassName} {...dataProps}>
|
|
189
|
+
<div className={styles.selectContainer}>
|
|
190
|
+
<input type='text' name={name} value={value} className={wrapperClassName} ref={inputRef} onChange={onChange} disabled={disabled} placeholder={placeholder} onFocus={() => handleFocus()} onBlur={(e) => handleBlur(e)} />
|
|
191
|
+
<div className={styles.icons}>
|
|
192
|
+
{value?.length > 0 && !disabled && <PlatformaticIcon iconName='CircleCloseIcon' color={borderColor} onClick={() => clearValue()} size={SMALL} />}
|
|
193
|
+
<PlatformaticIcon iconName={showOptions ? 'ArrowUpIcon' : 'ArrowDownIcon'} color={borderColor} disabled={disabled} onClick={() => setShowOptions(!showOptions)} size={SMALL} />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
{showOptions && !showError && renderOptions()}
|
|
197
|
+
{showError && <span className={commonStyles['error-message']}>{errorMessage}</span>}
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
SelectWithInput.propTypes = {
|
|
203
|
+
/**
|
|
204
|
+
* defaultContainerClassName
|
|
205
|
+
*/
|
|
206
|
+
defaultContainerClassName: PropTypes.string,
|
|
207
|
+
/**
|
|
208
|
+
* placeholder
|
|
209
|
+
*/
|
|
210
|
+
placeholder: PropTypes.string,
|
|
211
|
+
/**
|
|
212
|
+
* name
|
|
213
|
+
*/
|
|
214
|
+
name: PropTypes.oneOfType([
|
|
215
|
+
PropTypes.string,
|
|
216
|
+
PropTypes.number
|
|
217
|
+
]),
|
|
218
|
+
/**
|
|
219
|
+
* value
|
|
220
|
+
*/
|
|
221
|
+
value: PropTypes.string,
|
|
222
|
+
/**
|
|
223
|
+
* options
|
|
224
|
+
*/
|
|
225
|
+
options: PropTypes.arrayOf(PropTypes.shape({
|
|
226
|
+
label: PropTypes.string,
|
|
227
|
+
value: PropTypes.oneOfType([
|
|
228
|
+
PropTypes.string,
|
|
229
|
+
PropTypes.number
|
|
230
|
+
]),
|
|
231
|
+
iconName: PropTypes.string,
|
|
232
|
+
iconSize: PropTypes.string,
|
|
233
|
+
iconColor: PropTypes.string,
|
|
234
|
+
notSelectable: PropTypes.bool,
|
|
235
|
+
notFilterable: PropTypes.bool,
|
|
236
|
+
onClick: PropTypes.func,
|
|
237
|
+
descriptionValue: PropTypes.string
|
|
238
|
+
})),
|
|
239
|
+
/**
|
|
240
|
+
* defaultOptionsClassName
|
|
241
|
+
*/
|
|
242
|
+
defaultOptionsClassName: PropTypes.string,
|
|
243
|
+
/**
|
|
244
|
+
* optionsBorderedBottom
|
|
245
|
+
*/
|
|
246
|
+
optionsBorderedBottom: PropTypes.bool,
|
|
247
|
+
/**
|
|
248
|
+
* color of border
|
|
249
|
+
*/
|
|
250
|
+
borderColor: PropTypes.oneOf([MAIN_GREEN, MAIN_DARK_BLUE, WHITE]),
|
|
251
|
+
/**
|
|
252
|
+
* color of border UL
|
|
253
|
+
*/
|
|
254
|
+
borderListColor: PropTypes.oneOf([MAIN_GREEN, MAIN_DARK_BLUE, WHITE]),
|
|
255
|
+
/**
|
|
256
|
+
* onChange
|
|
257
|
+
*/
|
|
258
|
+
onChange: PropTypes.func,
|
|
259
|
+
/**
|
|
260
|
+
* onSelect
|
|
261
|
+
*/
|
|
262
|
+
onSelect: PropTypes.func,
|
|
263
|
+
/**
|
|
264
|
+
* onClear
|
|
265
|
+
*/
|
|
266
|
+
onClear: PropTypes.func,
|
|
267
|
+
/**
|
|
268
|
+
* Disabled
|
|
269
|
+
*/
|
|
270
|
+
disabled: PropTypes.bool,
|
|
271
|
+
/**
|
|
272
|
+
* mainColor
|
|
273
|
+
*/
|
|
274
|
+
mainColor: PropTypes.oneOf([MAIN_DARK_BLUE, WHITE]),
|
|
275
|
+
/**
|
|
276
|
+
* optionSelected
|
|
277
|
+
*/
|
|
278
|
+
optionSelected: PropTypes.shape({
|
|
279
|
+
label: PropTypes.string,
|
|
280
|
+
value: PropTypes.oneOfType([
|
|
281
|
+
PropTypes.string,
|
|
282
|
+
PropTypes.number
|
|
283
|
+
])
|
|
284
|
+
}),
|
|
285
|
+
/**
|
|
286
|
+
* dataAttrName
|
|
287
|
+
*/
|
|
288
|
+
dataAttrName: PropTypes.string,
|
|
289
|
+
/**
|
|
290
|
+
* dataAttrValue
|
|
291
|
+
*/
|
|
292
|
+
dataAttrValue: PropTypes.string,
|
|
293
|
+
/**
|
|
294
|
+
* backgroundColor
|
|
295
|
+
*/
|
|
296
|
+
backgroundColor: PropTypes.oneOf([MAIN_DARK_BLUE, WHITE, RICH_BLACK]),
|
|
297
|
+
/**
|
|
298
|
+
* inputTextClassName
|
|
299
|
+
*/
|
|
300
|
+
inputTextClassName: PropTypes.string
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
SelectWithInput.defaultProps = {
|
|
304
|
+
defaultContainerClassName: '',
|
|
305
|
+
placeholder: 'this is the default',
|
|
306
|
+
name: '',
|
|
307
|
+
value: '',
|
|
308
|
+
id: '',
|
|
309
|
+
options: [],
|
|
310
|
+
defaultOptionsClassName: '',
|
|
311
|
+
optionsBorderedBottom: true,
|
|
312
|
+
borderColor: MAIN_GREEN,
|
|
313
|
+
borderListColor: '',
|
|
314
|
+
errorMessage: '',
|
|
315
|
+
onChange: () => {},
|
|
316
|
+
onSelect: () => {},
|
|
317
|
+
onClear: () => {},
|
|
318
|
+
disabled: false,
|
|
319
|
+
mainColor: MAIN_DARK_BLUE,
|
|
320
|
+
optionSelected: null,
|
|
321
|
+
dataAttrName: '',
|
|
322
|
+
dataAttrValue: '',
|
|
323
|
+
backgroundColor: WHITE,
|
|
324
|
+
inputTextClassName: ''
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default SelectWithInput
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
@apply flex flex-col w-full relative
|
|
3
|
+
}
|
|
4
|
+
.selectContainer {
|
|
5
|
+
@apply w-full flex items-center relative z-10;
|
|
6
|
+
}
|
|
7
|
+
.select {
|
|
8
|
+
@apply py-1.5 px-2 md:px-3 md:py-[10.5px] h-full border border-solid box-border rounded;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.select-main-dark-blue.active,
|
|
12
|
+
.select-main-dark-blue:focus {
|
|
13
|
+
@apply shadow-main-dark-blue outline-none;
|
|
14
|
+
}
|
|
15
|
+
.select-white.active,
|
|
16
|
+
.select-white:focus {
|
|
17
|
+
@apply outline-none;
|
|
18
|
+
}
|
|
19
|
+
.options {
|
|
20
|
+
@apply absolute left-0 top-[42px] w-full max-h-[216px] overflow-y-scroll z-20;
|
|
21
|
+
}
|
|
22
|
+
.bordered-options {
|
|
23
|
+
@apply border border-solid rounded
|
|
24
|
+
}
|
|
25
|
+
.option {
|
|
26
|
+
@apply font-light cursor-pointer z-10 flex flex-row justify-between items-center px-4;
|
|
27
|
+
}
|
|
28
|
+
.bordered-bottom {
|
|
29
|
+
@apply border-b
|
|
30
|
+
}
|
|
31
|
+
.icons {
|
|
32
|
+
@apply absolute top-[50%] right-[0.5rem] flex gap-x-2;
|
|
33
|
+
transform: translateY(-50%);
|
|
34
|
+
}
|
|
35
|
+
.liContent {
|
|
36
|
+
@apply py-2 flex items-center gap-x-1 relative z-[-1] w-auto basis-2/3;
|
|
37
|
+
}
|
|
38
|
+
.descriptionValue {
|
|
39
|
+
@apply opacity-70
|
|
40
|
+
}
|
|
@@ -5,6 +5,7 @@ import Password from './Password'
|
|
|
5
5
|
import Preview from './Preview'
|
|
6
6
|
import RadioGroup from './RadioGroup'
|
|
7
7
|
import Select from './Select'
|
|
8
|
+
import SelectWithInput from './SelectWithInput'
|
|
8
9
|
import TextArea from './TextArea'
|
|
9
10
|
import ToggleSwitch from './ToggleSwitch'
|
|
10
11
|
|
|
@@ -16,6 +17,7 @@ export default {
|
|
|
16
17
|
Preview,
|
|
17
18
|
RadioGroup,
|
|
18
19
|
Select,
|
|
20
|
+
SelectWithInput,
|
|
19
21
|
TextArea,
|
|
20
22
|
ToggleSwitch
|
|
21
23
|
}
|
|
@@ -35,18 +35,13 @@ Default.args = {
|
|
|
35
35
|
const TemplateBorderMainDarkBlue = (args) => {
|
|
36
36
|
const [value, setValue] = useState('')
|
|
37
37
|
|
|
38
|
-
function handleChange (event) {
|
|
39
|
-
setValue(event.target.value)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
38
|
function handleSelect (event) {
|
|
43
39
|
setValue(event.detail.label)
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
return (
|
|
47
43
|
<div style={{ backgroundColor: 'white', padding: '20px 10px' }}>
|
|
48
|
-
<
|
|
49
|
-
<Select {...args} value={value} onChange={handleChange} onSelect={handleSelect} />
|
|
44
|
+
<Select {...args} value={value} onSelect={handleSelect} />
|
|
50
45
|
</div>
|
|
51
46
|
)
|
|
52
47
|
}
|
|
@@ -54,10 +49,6 @@ const TemplateBorderMainDarkBlue = (args) => {
|
|
|
54
49
|
const TemplateBorderMainDarkBlue2 = (args) => {
|
|
55
50
|
const [value, setValue] = useState('')
|
|
56
51
|
|
|
57
|
-
function handleChange (event) {
|
|
58
|
-
setValue(event.target.value)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
52
|
function handleSelect (event) {
|
|
62
53
|
setValue(event.detail.label)
|
|
63
54
|
}
|
|
@@ -66,7 +57,7 @@ const TemplateBorderMainDarkBlue2 = (args) => {
|
|
|
66
57
|
<>
|
|
67
58
|
<div style={{ backgroundColor: 'white', padding: '20px 10px' }}>
|
|
68
59
|
<p>Value of the input {value}</p>
|
|
69
|
-
<Select {...args} value={value}
|
|
60
|
+
<Select {...args} value={value} onSelect={handleSelect} />
|
|
70
61
|
<br />
|
|
71
62
|
<Select placeholder='Disabled' disabled />
|
|
72
63
|
|
|
@@ -146,10 +137,6 @@ githubRepoExample.args = {
|
|
|
146
137
|
const TemplateTransparent = (args) => {
|
|
147
138
|
const [value, setValue] = useState('')
|
|
148
139
|
|
|
149
|
-
function handleChange (event) {
|
|
150
|
-
setValue(event.target.value)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
140
|
function handleSelect (event) {
|
|
154
141
|
setValue(event.detail.label)
|
|
155
142
|
}
|
|
@@ -157,7 +144,7 @@ const TemplateTransparent = (args) => {
|
|
|
157
144
|
return (
|
|
158
145
|
<div style={{ padding: '20px 10px' }}>
|
|
159
146
|
<p>Value of the input {value}</p>
|
|
160
|
-
<Select {...args} value={value}
|
|
147
|
+
<Select {...args} value={value} onSelect={handleSelect} />
|
|
161
148
|
</div>
|
|
162
149
|
)
|
|
163
150
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
import React, { useState } from 'react'
|
|
3
|
+
import SelectWithInput from '../../components/forms/SelectWithInput'
|
|
4
|
+
import { MAIN_DARK_BLUE, MAIN_GREEN, RICH_BLACK, WHITE } from '../../components/constants'
|
|
5
|
+
|
|
6
|
+
const divStyle = {
|
|
7
|
+
width: '100%',
|
|
8
|
+
height: 'auto',
|
|
9
|
+
padding: '20px',
|
|
10
|
+
backgroundColor: 'transparent'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
title: 'Platformatic/Forms/SelectWithInput',
|
|
15
|
+
component: SelectWithInput,
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (
|
|
18
|
+
<div style={divStyle}>
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Template = (args) => <SelectWithInput {...args} />
|
|
26
|
+
|
|
27
|
+
export const Default = Template.bind({})
|
|
28
|
+
|
|
29
|
+
Default.args = {
|
|
30
|
+
name: 'test',
|
|
31
|
+
placeholder: 'Platformatic empty select',
|
|
32
|
+
borderColor: MAIN_GREEN
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const TemplateBorderMainDarkBlue = (args) => {
|
|
36
|
+
const [value, setValue] = useState('')
|
|
37
|
+
|
|
38
|
+
function handleChange (event) {
|
|
39
|
+
setValue(event.target.value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleSelect (event) {
|
|
43
|
+
setValue(event.detail.label)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ backgroundColor: 'white', padding: '20px 10px' }}>
|
|
48
|
+
<p>Value of the input {value}</p>
|
|
49
|
+
<SelectWithInput {...args} value={value} onChange={handleChange} onSelect={handleSelect} />
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const TemplateBorderMainDarkBlue2 = (args) => {
|
|
55
|
+
const [value, setValue] = useState('')
|
|
56
|
+
|
|
57
|
+
function handleChange (event) {
|
|
58
|
+
setValue(event.target.value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handleSelect (event) {
|
|
62
|
+
setValue(event.detail.label)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<div style={{ backgroundColor: 'white', padding: '20px 10px' }}>
|
|
68
|
+
<p>Value of the input {value}</p>
|
|
69
|
+
<SelectWithInput {...args} value={value} onChange={handleChange} onSelect={handleSelect} />
|
|
70
|
+
<br />
|
|
71
|
+
<SelectWithInput placeholder='Disabled' disabled />
|
|
72
|
+
|
|
73
|
+
</div>
|
|
74
|
+
</>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const BorderMainDarkBlue = TemplateBorderMainDarkBlue.bind({})
|
|
79
|
+
|
|
80
|
+
BorderMainDarkBlue.args = {
|
|
81
|
+
name: 'test',
|
|
82
|
+
placeholder: 'Defaul option',
|
|
83
|
+
options: [...Array(20).keys()].map(ele => ({ label: `Option ${ele}`, value: `Value${ele}`, iconName: 'SocialGitHubIcon' })),
|
|
84
|
+
borderColor: MAIN_DARK_BLUE
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const DefaultInvalid = TemplateBorderMainDarkBlue.bind({})
|
|
88
|
+
|
|
89
|
+
DefaultInvalid.args = {
|
|
90
|
+
name: 'test',
|
|
91
|
+
placeholder: 'Platformatic empty select',
|
|
92
|
+
options: [{ label: 'Option 1', value: '1' }, { label: 'Option 2', value: '2' }, { label: 'Option 3', value: '3' }],
|
|
93
|
+
borderColor: MAIN_GREEN,
|
|
94
|
+
errorMessage: 'This is an error message'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const withDifferentOptions = TemplateBorderMainDarkBlue2.bind({})
|
|
98
|
+
|
|
99
|
+
withDifferentOptions.args = {
|
|
100
|
+
name: 'test',
|
|
101
|
+
placeholder: 'Platformatic',
|
|
102
|
+
options: [...Array(20).keys()].map(ele => ({
|
|
103
|
+
label: `Option ${ele}`,
|
|
104
|
+
value: `Value${ele}`,
|
|
105
|
+
iconName: 'SocialGitHubIcon'
|
|
106
|
+
})).concat({
|
|
107
|
+
label: 'Last but not least',
|
|
108
|
+
iconName: 'AddIcon',
|
|
109
|
+
value: 123123,
|
|
110
|
+
notSelectable: true,
|
|
111
|
+
onClick: () => alert('you clicked last but not least')
|
|
112
|
+
}).concat({
|
|
113
|
+
label: 'Play',
|
|
114
|
+
iconName: 'PlayIcon',
|
|
115
|
+
value: 123123,
|
|
116
|
+
notSelectable: true
|
|
117
|
+
}),
|
|
118
|
+
borderColor: MAIN_DARK_BLUE
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const githubRepoExample = TemplateBorderMainDarkBlue2.bind({})
|
|
122
|
+
|
|
123
|
+
githubRepoExample.args = {
|
|
124
|
+
name: 'test',
|
|
125
|
+
placeholder: 'Platformatic',
|
|
126
|
+
options: [...Array(4).keys()].map(ele => ({
|
|
127
|
+
label: `GitHub Repo ${ele}`,
|
|
128
|
+
value: `Value${ele}`,
|
|
129
|
+
iconName: 'SocialGitHubIcon'
|
|
130
|
+
})).concat({
|
|
131
|
+
label: 'this is an option that is not selectable',
|
|
132
|
+
iconName: 'AddIcon',
|
|
133
|
+
value: 123123,
|
|
134
|
+
notSelectable: true,
|
|
135
|
+
onClick: () => alert('you clicked last but not least')
|
|
136
|
+
}).concat({
|
|
137
|
+
label: 'Add Github repo',
|
|
138
|
+
iconName: 'PlayIcon',
|
|
139
|
+
value: 123123,
|
|
140
|
+
notSelectable: true,
|
|
141
|
+
notFilterable: true
|
|
142
|
+
}),
|
|
143
|
+
borderColor: MAIN_DARK_BLUE
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const TemplateTransparent = (args) => {
|
|
147
|
+
const [value, setValue] = useState('')
|
|
148
|
+
|
|
149
|
+
function handleChange (event) {
|
|
150
|
+
setValue(event.target.value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleSelect (event) {
|
|
154
|
+
setValue(event.detail.label)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div style={{ padding: '20px 10px' }}>
|
|
159
|
+
<p>Value of the input {value}</p>
|
|
160
|
+
<SelectWithInput {...args} value={value} onChange={handleChange} onSelect={handleSelect} />
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const TemplateBackgroundTransparent = TemplateTransparent.bind({})
|
|
166
|
+
|
|
167
|
+
TemplateBackgroundTransparent.args = {
|
|
168
|
+
name: 'test',
|
|
169
|
+
placeholder: 'Defaul option',
|
|
170
|
+
optionsBorderedBottom: false,
|
|
171
|
+
options: [...Array(20).keys()].map(ele => ({ label: `Option ${ele}`, value: `Value${ele}`, iconName: 'OrganizationIcon' })),
|
|
172
|
+
borderColor: WHITE,
|
|
173
|
+
mainColor: WHITE,
|
|
174
|
+
backgroundColor: RICH_BLACK,
|
|
175
|
+
borderListColor: WHITE
|
|
176
|
+
}
|