cpk-ui 0.0.1 → 0.0.4

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.
@@ -0,0 +1,275 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { forwardRef, isValidElement, useCallback, useMemo, useRef, useState, } from 'react';
3
+ import { Platform, Text, TextInput, TouchableWithoutFeedback, View, } from 'react-native';
4
+ import { useHover } from 'react-native-web-hooks';
5
+ import { css } from '@emotion/native';
6
+ import { useCPK } from '../../../providers';
7
+ import { Icon } from '../Icon/Icon';
8
+ import { cloneElemWithDefaultColors } from '../../../utils/guards';
9
+ import { Global } from '@emotion/react';
10
+ export const EditText = forwardRef(({ testID, textInputProps, style, styles, label, error, startElement, endElement, multiline = false, value = '', placeholder, placeholderColor, onChange, onChangeText, onFocus, onBlur, onSubmitEditing, numberOfLines, maxLength, hideCounter = false, autoComplete, autoCapitalize = 'none', secureTextEntry = false, editable = true, direction = 'column', decoration = 'underline', colors = {}, required = false, }, ref) => {
11
+ EditText.displayName = 'EditText';
12
+ const { theme } = useCPK();
13
+ const webRef = useRef(null);
14
+ const [focused, setFocused] = useState(false);
15
+ const defaultInputRef = useRef(null);
16
+ const inputRef = ref || defaultInputRef;
17
+ const hovered = useHover(webRef);
18
+ const defaultContainerStyle = css `
19
+ flex-direction: ${direction};
20
+ `;
21
+ const defaultColor = !editable
22
+ ? colors.disabled || theme.text.disabled
23
+ : error
24
+ ? colors.error || theme.text.validation
25
+ : focused
26
+ ? colors.focused || theme.text.basic
27
+ : hovered
28
+ ? colors.hovered || theme.text.basic
29
+ : colors.placeholder || theme.text.placeholder;
30
+ // Default label placeholder color has different value compared to default input placeholder color
31
+ const labelPlaceholderColor = useMemo(() => defaultColor === (colors.placeholder || theme.text.placeholder) && {
32
+ color: colors.placeholder || theme.text.disabled,
33
+ }, [
34
+ colors.placeholder,
35
+ defaultColor,
36
+ theme.text.disabled,
37
+ theme.text.placeholder,
38
+ ]);
39
+ const status = !editable
40
+ ? 'disabled'
41
+ : error
42
+ ? 'error'
43
+ : hovered
44
+ ? 'hovered'
45
+ : focused
46
+ ? 'focused'
47
+ : 'basic';
48
+ const renderLabel = useCallback(() => {
49
+ // eslint-disable-next-line react/no-unstable-nested-components
50
+ function Wrapper({ children }) {
51
+ return (_jsxs(View, { style: [
52
+ css `
53
+ margin-bottom: ${decoration === 'boxed' ? '14px' : 0};
54
+
55
+ flex-direction: row;
56
+ align-items: center;
57
+ `,
58
+ styles?.labelContainer,
59
+ ], children: [children, required ? (_jsx(Icon, { name: "AsteriskBold", style: css `
60
+ color: ${theme.role.danger};
61
+ opacity: ${focused ? '1' : '0.5'};
62
+ ` })) : null] }));
63
+ }
64
+ return typeof label === 'string' ? (_jsx(Wrapper, { children: _jsx(Text, { style: [
65
+ css `
66
+ font-family: Pretendard-Bold;
67
+ color: ${defaultColor};
68
+ margin-right: 4px;
69
+ font-size: 16px;
70
+ `,
71
+ labelPlaceholderColor,
72
+ styles?.label,
73
+ ], children: label }) })) : label ? (_jsx(Wrapper, { children: label(status) })) : null;
74
+ }, [
75
+ decoration,
76
+ defaultColor,
77
+ focused,
78
+ label,
79
+ labelPlaceholderColor,
80
+ required,
81
+ status,
82
+ styles?.label,
83
+ styles?.labelContainer,
84
+ theme.role.danger,
85
+ ]);
86
+ const renderContainer = useCallback((children) => {
87
+ return (_jsx(TouchableWithoutFeedback, { onPress: () => inputRef.current?.focus(), testID: "container-touch", children: _jsx(View, { style: [
88
+ defaultContainerStyle,
89
+ css `
90
+ flex-direction: ${direction};
91
+ align-items: ${direction === 'row' ? 'center' : 'flex-start'};
92
+ justify-content: ${direction === 'row'
93
+ ? 'flex-start'
94
+ : 'space-between'};
95
+ border-color: ${labelPlaceholderColor
96
+ ? labelPlaceholderColor.color
97
+ : defaultColor};
98
+ `,
99
+ decoration === 'boxed'
100
+ ? css `
101
+ border-radius: 4px;
102
+ border-width: 1px;
103
+ padding-left: 12px;
104
+ padding-right: 12px;
105
+ `
106
+ : css `
107
+ border-bottom-width: 1px;
108
+ `,
109
+ styles?.container,
110
+ ], testID: "container", children: children }) }));
111
+ }, [
112
+ decoration,
113
+ defaultColor,
114
+ defaultContainerStyle,
115
+ direction,
116
+ inputRef,
117
+ labelPlaceholderColor,
118
+ styles?.container,
119
+ ]);
120
+ const renderInput = useCallback(() => {
121
+ return (_jsx(View, { style: [
122
+ direction === 'row'
123
+ ? css `
124
+ flex: 1;
125
+ `
126
+ : css `
127
+ align-self: stretch;
128
+ `,
129
+ css `
130
+ padding: ${decoration === 'boxed' ? '4px 0' : '2px 0'};
131
+
132
+ flex-direction: row;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ `,
136
+ styles?.inputContainer,
137
+ ], children: _jsxs(_Fragment, { children: [isValidElement(startElement)
138
+ ? cloneElemWithDefaultColors({
139
+ element: startElement,
140
+ color: defaultColor,
141
+ style: css `
142
+ margin-left: -4px;
143
+ margin-right: 4px;
144
+ `,
145
+ })
146
+ : startElement, _jsx(TextInput, { autoCapitalize: autoCapitalize, autoComplete: autoComplete, editable: editable, maxLength: maxLength, multiline: multiline, numberOfLines: numberOfLines, onBlur: (e) => {
147
+ setFocused(false);
148
+ onBlur?.(e);
149
+ }, onChange: onChange, onChangeText: onChangeText, onFocus: (e) => {
150
+ setFocused(true);
151
+ onFocus?.(e);
152
+ }, onSubmitEditing: onSubmitEditing, placeholder: placeholder, placeholderTextColor: placeholderColor || theme.text.placeholder, ref: inputRef, secureTextEntry: secureTextEntry, selectionColor: theme.role.underlay, style: [
153
+ // Stretch input in order to make remaining space clickable
154
+ css `
155
+ font-family: Pretendard;
156
+ flex: 1;
157
+ font-size: 16px;
158
+ text-align-vertical: ${multiline ? 'top' : 'center'};
159
+ `,
160
+ Platform.OS === 'web' &&
161
+ css `
162
+ outline-width: 0;
163
+ `,
164
+ direction === 'column'
165
+ ? css `
166
+ padding-top: 12px;
167
+ `
168
+ : css `
169
+ padding-left: 12px;
170
+ `,
171
+ css `
172
+ color: ${defaultColor};
173
+ padding: 10px 0 12px 0;
174
+ `,
175
+ styles?.input,
176
+ ], testID: testID, value: value, ...textInputProps }), isValidElement(endElement)
177
+ ? cloneElemWithDefaultColors({
178
+ element: endElement,
179
+ color: defaultColor,
180
+ style: css `
181
+ margin-left: 4px;
182
+ margin-right: ${decoration === 'boxed' ? '-8px' : '-4px'};
183
+ `,
184
+ })
185
+ : endElement] }) }));
186
+ }, [
187
+ autoCapitalize,
188
+ autoComplete,
189
+ decoration,
190
+ defaultColor,
191
+ direction,
192
+ editable,
193
+ endElement,
194
+ inputRef,
195
+ maxLength,
196
+ multiline,
197
+ numberOfLines,
198
+ onBlur,
199
+ onChange,
200
+ onChangeText,
201
+ onFocus,
202
+ onSubmitEditing,
203
+ placeholder,
204
+ placeholderColor,
205
+ secureTextEntry,
206
+ startElement,
207
+ styles?.input,
208
+ styles?.inputContainer,
209
+ testID,
210
+ textInputProps,
211
+ theme.role.underlay,
212
+ theme.text.placeholder,
213
+ value,
214
+ ]);
215
+ const renderError = useCallback(() => {
216
+ return error ? (typeof error === 'string' ? (_jsx(Text, { style: [
217
+ css `
218
+ flex: 1;
219
+ color: ${theme.text.validation};
220
+ font-size: 12px;
221
+ `,
222
+ styles?.error,
223
+ ], children: error })) : (error?.(status))) : null;
224
+ }, [error, status, styles?.error, theme.text.validation]);
225
+ const renderCounter = useCallback(() => {
226
+ if (hideCounter) {
227
+ return null;
228
+ }
229
+ return maxLength ? (_jsx(Text, { style: [
230
+ css `
231
+ color: ${theme.text.placeholder};
232
+ font-size: 12px;
233
+ `,
234
+ styles?.counter,
235
+ ], children: `${value.length}/${maxLength}` })) : null;
236
+ }, [
237
+ hideCounter,
238
+ maxLength,
239
+ styles?.counter,
240
+ theme.text.placeholder,
241
+ value.length,
242
+ ]);
243
+ return (_jsxs(_Fragment, { children: [Platform.OS === 'web' ? _jsx(WebStyles, {}) : null, _jsxs(View, { ref: Platform.select({ web: webRef, default: undefined }), style: [
244
+ css `
245
+ width: 100%;
246
+ `,
247
+ style,
248
+ ], testID: "edit-text", children: [renderLabel(), renderContainer(renderInput()), renderError() || renderCounter() ? (_jsxs(View, { style: css `
249
+ margin-top: 6px;
250
+
251
+ flex-direction: ${!renderError() && renderCounter()
252
+ ? 'row-reverse'
253
+ : 'row'};
254
+ gap: 4px;
255
+ `, children: [renderError(), renderCounter()] })) : null] })] }));
256
+ });
257
+ function WebStyles() {
258
+ return (_jsx(Global
259
+ // @ts-ignore
260
+ , {
261
+ // @ts-ignore
262
+ styles: css `
263
+ input:autofill,
264
+ input:autofill:hover,
265
+ input:autofill:focus,
266
+ input:autofill:active {
267
+ -webkit-text-fill-color: #787878;
268
+ box-shadow: 0 0 0px 1000px #ededed inset;
269
+ transition: background-color 5000s ease-in-out 0s;
270
+ }
271
+ ::-webkit-scrollbar {
272
+ display: none;
273
+ }
274
+ ` }));
275
+ }
@@ -0,0 +1,46 @@
1
+ import type {ComponentProps} from 'react';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import {withThemeProvider} from '../../../../.storybook/decorators';
4
+ import {EditText} from './EditText';
5
+
6
+ const meta = {
7
+ title: 'EditText',
8
+ component: (props) => <EditText {...props} />,
9
+ argTypes: {
10
+ required: {type: 'boolean'},
11
+ label: {type: 'string'},
12
+ error: {type: 'string'},
13
+ value: {type: 'string'},
14
+ multiline: {type: 'boolean'},
15
+ placeholder: {type: 'string'},
16
+ placeholderColor: {type: 'string'},
17
+ editable: {type: 'boolean'},
18
+ secureTextEntry: {type: 'boolean'},
19
+ numberOfLines: {type: 'number'},
20
+ maxLength: {type: 'number'},
21
+ hideCounter: {type: 'boolean'},
22
+ direction: {
23
+ control: 'select',
24
+ options: ['row', 'column'],
25
+ },
26
+ decoration: {
27
+ control: 'select',
28
+ options: ['underline', 'boxed'],
29
+ },
30
+ },
31
+ decorators: [withThemeProvider],
32
+ } satisfies Meta<typeof EditText>;
33
+
34
+ export default meta;
35
+
36
+ type Story = StoryObj<typeof meta>;
37
+
38
+ export const Basic: Story = {
39
+ args: {
40
+ onChangeText: () => {},
41
+ direction: 'column',
42
+ decoration: 'boxed',
43
+ placeholder: 'Write something...',
44
+ editable: true,
45
+ },
46
+ };
@@ -0,0 +1,406 @@
1
+ import React from 'react';
2
+ import {Text} from 'react-native';
3
+ import RNWebHooks from 'react-native-web-hooks';
4
+ import type {RenderAPI} from '@testing-library/react-native';
5
+ import {act, fireEvent, render} from '@testing-library/react-native';
6
+
7
+ import {createComponent} from '../../../../test/testUtils';
8
+ import type {EditTextProps} from './EditText';
9
+ import {EditText} from './EditText';
10
+ import { light } from '../../../utils/colors';
11
+
12
+ jest.mock('react-native-web-hooks', () => ({
13
+ useHover: () => false,
14
+ }));
15
+
16
+ let testingLib: RenderAPI;
17
+
18
+ const Component = (editProps?: EditTextProps): JSX.Element =>
19
+ createComponent(<EditText {...editProps} />);
20
+
21
+ describe('[EditText]', () => {
22
+ jest.spyOn(console, 'error').mockImplementation(() => {});
23
+
24
+ beforeAll(() => {
25
+ testingLib = render(Component());
26
+ });
27
+
28
+ describe('hovered', () => {
29
+ beforeAll(() => {
30
+ jest.spyOn(RNWebHooks, 'useHover').mockImplementation(() => true);
31
+ });
32
+
33
+ describe('label', () => {
34
+ it('renders label text', async () => {
35
+ testingLib = render(Component({label: 'label text'}));
36
+
37
+ const label = testingLib.getByText('label text');
38
+
39
+ expect(label).toBeTruthy();
40
+ });
41
+
42
+ it('renders label style', async () => {
43
+ testingLib = render(
44
+ Component({
45
+ label: 'label text',
46
+ colors: {
47
+ basic: 'blue',
48
+ placeholder: 'green',
49
+ disabled: 'red',
50
+ error: 'yellow',
51
+ focused: 'purple',
52
+ hovered: 'orange',
53
+ },
54
+ }),
55
+ );
56
+
57
+ const label = testingLib.getByText('label text');
58
+
59
+ expect(label).toHaveStyle({color: 'orange'});
60
+ });
61
+
62
+ it('renders custom label style', async () => {
63
+ const renderCustomLabel = (): JSX.Element => {
64
+ return (
65
+ <Text
66
+ style={{
67
+ color: 'blue',
68
+ fontSize: 12,
69
+ fontWeight: 'bold',
70
+ }}
71
+ >
72
+ Custom label
73
+ </Text>
74
+ );
75
+ };
76
+
77
+ testingLib = render(
78
+ Component({
79
+ label: renderCustomLabel,
80
+ }),
81
+ );
82
+
83
+ const label = testingLib.getByText('Custom label');
84
+
85
+ expect(label).toBeTruthy();
86
+ });
87
+
88
+ describe('unhovered', () => {
89
+ beforeAll(() => {
90
+ jest.spyOn(RNWebHooks, 'useHover').mockImplementation(() => false);
91
+ });
92
+
93
+ it('should contain `focusColor` when focused', async () => {
94
+ testingLib = render(
95
+ Component({
96
+ testID: 'INPUT_TEST',
97
+ label: 'label text',
98
+ colors: {
99
+ basic: 'blue',
100
+ placeholder: 'green',
101
+ disabled: 'red',
102
+ error: 'yellow',
103
+ focused: 'purple',
104
+ hovered: 'orange',
105
+ },
106
+ }),
107
+ );
108
+
109
+ const input = testingLib.getByTestId('INPUT_TEST');
110
+ expect(input).toHaveStyle({color: 'green'});
111
+
112
+ act(() => {
113
+ input.props.onFocus();
114
+ });
115
+
116
+ const label = testingLib.getByText('label text');
117
+
118
+ expect(label).toHaveStyle({color: 'purple'});
119
+ });
120
+
121
+ it('renders error element when provided', async () => {
122
+ testingLib = render(
123
+ Component({
124
+ testID: 'INPUT_TEST',
125
+ label: 'label text',
126
+ error: 'error text',
127
+ styles: {
128
+ label: {
129
+ color: 'green',
130
+ },
131
+ },
132
+ }),
133
+ );
134
+
135
+ const input = testingLib.getByTestId('INPUT_TEST');
136
+
137
+ act(() => {
138
+ input.props.onFocus();
139
+ });
140
+
141
+ const error = testingLib.getByText('error text');
142
+ expect(error).toBeTruthy();
143
+ });
144
+ });
145
+ });
146
+ });
147
+
148
+ describe('layout', () => {
149
+ it('renders [direction] row', () => {
150
+ testingLib = render(
151
+ Component({
152
+ testID: 'INPUT_TEST',
153
+ direction: 'row',
154
+ }),
155
+ );
156
+
157
+ const input = testingLib.getByTestId('INPUT_TEST');
158
+ // const container = testingLib.getByTestId('container');
159
+
160
+ expect(input).toBeTruthy();
161
+ // expect(container).toHaveStyle({flexDirection: 'row'});
162
+ });
163
+
164
+ it('renders [decoration] boxed', () => {
165
+ testingLib = render(
166
+ Component({
167
+ testID: 'INPUT_TEST',
168
+ decoration: 'boxed',
169
+ }),
170
+ );
171
+
172
+ const input = testingLib.getByTestId('INPUT_TEST');
173
+ // const container = testingLib.getByTestId('container');
174
+
175
+ expect(input).toBeTruthy();
176
+ // expect(container).toHaveStyle({borderWidth: 1});
177
+ });
178
+ });
179
+
180
+ describe('start and end elements', () => {
181
+ it('renders start element', () => {
182
+ testingLib = render(
183
+ Component({
184
+ testID: 'INPUT_TEST',
185
+ startElement: <Text>Start</Text>,
186
+ }),
187
+ );
188
+
189
+ const input = testingLib.getByText('Start');
190
+ expect(input).toBeTruthy();
191
+ });
192
+
193
+ it('renders end element', () => {
194
+ testingLib = render(
195
+ Component({
196
+ testID: 'INPUT_TEST',
197
+ endElement: <Text>End</Text>,
198
+ }),
199
+ );
200
+
201
+ const input = testingLib.getByText('End');
202
+ expect(input).toBeTruthy();
203
+ });
204
+ });
205
+
206
+ describe('web', () => {
207
+ beforeAll(() => {
208
+ jest.mock('react-native/Libraries/Utilities/Platform', () => ({
209
+ OS: 'web',
210
+ select: () => null,
211
+ }));
212
+ });
213
+
214
+ it('renders web style', () => {
215
+ testingLib = render(
216
+ Component({
217
+ testID: 'INPUT_TEST',
218
+ }),
219
+ );
220
+
221
+ const input = testingLib.getByTestId('INPUT_TEST');
222
+ expect(input).toBeTruthy();
223
+ // @ts-ignore
224
+ expect(input).toHaveStyle({outlineWidth: 0});
225
+ });
226
+ });
227
+
228
+ describe('input', () => {
229
+ it('should trigger text changes', () => {
230
+ const CHANGE_TEXT = 'content';
231
+ const mockedFn = jest.fn();
232
+
233
+ const onChangeTextMock = (str: string): void => {
234
+ mockedFn(str);
235
+ };
236
+
237
+ testingLib = render(
238
+ Component({
239
+ testID: 'INPUT_TEST',
240
+ editable: false,
241
+ onChangeText: onChangeTextMock,
242
+ }),
243
+ );
244
+
245
+ const input = testingLib.getByTestId('INPUT_TEST');
246
+ expect(input).toBeTruthy();
247
+
248
+ fireEvent.changeText(input, CHANGE_TEXT);
249
+ expect(mockedFn).toBeCalledWith(CHANGE_TEXT);
250
+ });
251
+
252
+ it('should have value', () => {
253
+ testingLib = render(
254
+ Component({
255
+ testID: 'INPUT_TEST',
256
+ value: 'text123',
257
+ }),
258
+ );
259
+
260
+ const input = testingLib.getByTestId('INPUT_TEST');
261
+
262
+ expect(input).toBeTruthy();
263
+
264
+ expect(input).toHaveProp('value', 'text123');
265
+ });
266
+
267
+ it('should have render counter', () => {
268
+ testingLib = render(
269
+ Component({
270
+ testID: 'INPUT_TEST',
271
+ value: 'text123',
272
+ maxLength: 100,
273
+ }),
274
+ );
275
+
276
+ const counter = testingLib.getByText('7/100');
277
+
278
+ expect(counter).toBeTruthy();
279
+ });
280
+
281
+ it('should have render custom error', () => {
282
+ const renderCustomError = (): JSX.Element => <Text>custom error</Text>;
283
+
284
+ testingLib = render(
285
+ Component({
286
+ testID: 'INPUT_TEST',
287
+ value: 'text123',
288
+ error: renderCustomError,
289
+ }),
290
+ );
291
+
292
+ const error = testingLib.getByText('custom error');
293
+
294
+ expect(error).toBeTruthy();
295
+ });
296
+ });
297
+
298
+ describe('disabled', () => {
299
+ it('renders [default] disabled style', () => {
300
+ testingLib = render(
301
+ Component({
302
+ testID: 'INPUT_TEST',
303
+ editable: false,
304
+ }),
305
+ );
306
+
307
+ const input = testingLib.getByTestId('INPUT_TEST');
308
+
309
+ expect(input).toBeTruthy();
310
+
311
+ expect(input).toHaveStyle({color: light.text.disabled});
312
+ });
313
+
314
+ it('renders [custom] disabled style', () => {
315
+ testingLib = render(
316
+ Component({
317
+ testID: 'INPUT_TEST',
318
+ colors: {disabled: 'yellow'},
319
+ editable: false,
320
+ }),
321
+ );
322
+
323
+ const input = testingLib.getByTestId('INPUT_TEST');
324
+
325
+ expect(input).toBeTruthy();
326
+
327
+ expect(input).toHaveStyle({color: 'yellow'});
328
+ });
329
+ });
330
+
331
+ describe('focus', () => {
332
+ it('should trigger `onFocus`', async () => {
333
+ testingLib = render(
334
+ Component({
335
+ testID: 'INPUT_TEST',
336
+ onFocus: jest.fn(),
337
+ }),
338
+ );
339
+
340
+ const input = testingLib.getByTestId('INPUT_TEST');
341
+
342
+ expect(input).toBeTruthy();
343
+
344
+ fireEvent(input, 'focus');
345
+
346
+ expect(input).toHaveStyle({color: light.text.basic});
347
+ });
348
+
349
+ it('should trigger `onFocus` and render custom color', async () => {
350
+ testingLib = render(
351
+ Component({
352
+ testID: 'INPUT_TEST',
353
+ onFocus: jest.fn(),
354
+ colors: {focused: 'yellow'},
355
+ }),
356
+ );
357
+
358
+ const input = testingLib.getByTestId('INPUT_TEST');
359
+
360
+ expect(input).toBeTruthy();
361
+
362
+ fireEvent(input, 'focus');
363
+
364
+ expect(input).toHaveStyle({color: 'yellow'});
365
+ });
366
+
367
+ it('should trigger `onFocus` when touching container', async () => {
368
+ const focusFn = jest.fn();
369
+
370
+ testingLib = render(
371
+ Component({
372
+ onFocus: focusFn,
373
+ colors: {focused: 'yellow'},
374
+ }),
375
+ );
376
+
377
+ const touch = testingLib.getByTestId('container-touch');
378
+
379
+ expect(touch).toBeTruthy();
380
+
381
+ fireEvent(touch, 'press');
382
+ // Below should work but no luck in testing-library
383
+ // expect(focusFn).toBeCalled();
384
+ });
385
+
386
+ describe('onBlur (focused === false)', () => {
387
+ it('should trigger blur without errorText', async () => {
388
+ testingLib = render(
389
+ Component({
390
+ onBlur: () => {},
391
+ testID: 'INPUT_TEST',
392
+ colors: {placeholder: 'green'},
393
+ }),
394
+ );
395
+
396
+ const input = testingLib.getByTestId('INPUT_TEST');
397
+
398
+ expect(input).toBeTruthy();
399
+
400
+ fireEvent(input, 'blur');
401
+
402
+ expect(input).toHaveStyle({color: 'green'});
403
+ });
404
+ });
405
+ });
406
+ });
@@ -0,0 +1,535 @@
1
+ import type {MutableRefObject, ReactNode, RefObject} from 'react';
2
+ import React, {
3
+ forwardRef,
4
+ isValidElement,
5
+ useCallback,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import type {
11
+ StyleProp,
12
+ TextInputProps,
13
+ TextStyle,
14
+ ViewStyle,
15
+ } from 'react-native';
16
+ import {
17
+ Platform,
18
+ Text,
19
+ TextInput,
20
+ TouchableWithoutFeedback,
21
+ View,
22
+ } from 'react-native';
23
+ import {useHover} from 'react-native-web-hooks';
24
+ import {css} from '@emotion/native';
25
+
26
+ import {useCPK} from '../../../providers';
27
+ import {Icon} from '../Icon/Icon';
28
+ import {cloneElemWithDefaultColors} from '../../../utils/guards';
29
+ import {Global} from '@emotion/react';
30
+
31
+ export type EditTextStyles = {
32
+ container?: StyleProp<ViewStyle>;
33
+ labelContainer?: StyleProp<ViewStyle>;
34
+ label?: StyleProp<TextStyle>;
35
+ inputContainer?: StyleProp<TextStyle>;
36
+ input?: StyleProp<TextStyle>;
37
+ error?: StyleProp<TextStyle>;
38
+ counter?: StyleProp<TextStyle>;
39
+ };
40
+
41
+ export type EditTextStatus =
42
+ | 'disabled'
43
+ | 'error'
44
+ | 'focused'
45
+ | 'hovered'
46
+ | 'basic';
47
+
48
+ type RenderType = (stats: EditTextStatus) => JSX.Element;
49
+
50
+ type CustomRenderType =
51
+ | (({color, status}: {color: string; status: EditTextStatus}) => JSX.Element)
52
+ | null;
53
+
54
+ export type EditTextProps = {
55
+ testID?: TextInputProps['testID'];
56
+ inputRef?: MutableRefObject<TextInput | undefined> | RefObject<TextInput>;
57
+ style?: StyleProp<ViewStyle>;
58
+ styles?: EditTextStyles;
59
+
60
+ // Component
61
+ startElement?: JSX.Element | CustomRenderType;
62
+ endElement?: JSX.Element | CustomRenderType;
63
+ required?: boolean;
64
+ label?: string | RenderType;
65
+ error?: string | RenderType;
66
+ direction?: 'row' | 'column';
67
+ decoration?: 'underline' | 'boxed';
68
+ value?: TextInputProps['value'];
69
+ multiline?: TextInputProps['multiline'];
70
+ onChange?: TextInputProps['onChange'];
71
+ onChangeText?: TextInputProps['onChangeText'];
72
+ placeholder?: TextInputProps['placeholder'];
73
+ placeholderColor?: TextInputProps['placeholderTextColor'];
74
+ onFocus?: TextInputProps['onFocus'] | undefined;
75
+ onBlur?: TextInputProps['onBlur'] | undefined;
76
+ editable?: TextInputProps['editable'];
77
+ autoComplete?: TextInputProps['autoComplete'];
78
+ autoCapitalize?: TextInputProps['autoCapitalize'];
79
+ secureTextEntry?: TextInputProps['secureTextEntry'];
80
+ onSubmitEditing?: TextInputProps['onSubmitEditing'];
81
+ numberOfLines?: TextInputProps['numberOfLines'];
82
+ maxLength?: TextInputProps['maxLength'];
83
+ hideCounter?: boolean;
84
+
85
+ textInputProps?: Omit<
86
+ TextInputProps,
87
+ | 'value'
88
+ | 'onChange'
89
+ | 'numberOfLines'
90
+ | 'multiline'
91
+ | 'onChange'
92
+ | 'onChangeText'
93
+ | 'placeholder'
94
+ | 'placeholderTextColor'
95
+ | 'onFocus'
96
+ | 'onBlur'
97
+ | 'editable'
98
+ | 'autoComplete'
99
+ | 'autoCapitalize'
100
+ | 'secureTextEntry'
101
+ | 'onSubmitEditing'
102
+ | 'maxLength'
103
+ >;
104
+
105
+ colors?: {
106
+ basic?: string;
107
+ disabled?: string;
108
+ error?: string;
109
+ focused?: string;
110
+ hovered?: string;
111
+ placeholder?: string;
112
+ };
113
+ };
114
+
115
+ export const EditText = forwardRef<TextInput, EditTextProps>(
116
+ (
117
+ {
118
+ testID,
119
+ textInputProps,
120
+ style,
121
+ styles,
122
+ label,
123
+ error,
124
+ startElement,
125
+ endElement,
126
+ multiline = false,
127
+ value = '',
128
+ placeholder,
129
+ placeholderColor,
130
+ onChange,
131
+ onChangeText,
132
+ onFocus,
133
+ onBlur,
134
+ onSubmitEditing,
135
+ numberOfLines,
136
+ maxLength,
137
+ hideCounter = false,
138
+ autoComplete,
139
+ autoCapitalize = 'none',
140
+ secureTextEntry = false,
141
+ editable = true,
142
+ direction = 'column',
143
+ decoration = 'underline',
144
+ colors = {},
145
+ required = false,
146
+ }: EditTextProps,
147
+ ref,
148
+ ): JSX.Element => {
149
+ EditText.displayName = 'EditText';
150
+
151
+ const {theme} = useCPK();
152
+ const webRef = useRef<View>(null);
153
+ const [focused, setFocused] = useState(false);
154
+ const defaultInputRef = useRef(null);
155
+ const inputRef = (ref as MutableRefObject<TextInput>) || defaultInputRef;
156
+ const hovered = useHover(webRef);
157
+
158
+ const defaultContainerStyle = css`
159
+ flex-direction: ${direction};
160
+ `;
161
+
162
+ const defaultColor = !editable
163
+ ? colors.disabled || theme.text.disabled
164
+ : error
165
+ ? colors.error || theme.text.validation
166
+ : focused
167
+ ? colors.focused || theme.text.basic
168
+ : hovered
169
+ ? colors.hovered || theme.text.basic
170
+ : colors.placeholder || theme.text.placeholder;
171
+
172
+ // Default label placeholder color has different value compared to default input placeholder color
173
+ const labelPlaceholderColor = useMemo(
174
+ () =>
175
+ defaultColor === (colors.placeholder || theme.text.placeholder) && {
176
+ color: colors.placeholder || theme.text.disabled,
177
+ },
178
+ [
179
+ colors.placeholder,
180
+ defaultColor,
181
+ theme.text.disabled,
182
+ theme.text.placeholder,
183
+ ],
184
+ );
185
+
186
+ const status: EditTextStatus = !editable
187
+ ? 'disabled'
188
+ : error
189
+ ? 'error'
190
+ : hovered
191
+ ? 'hovered'
192
+ : focused
193
+ ? 'focused'
194
+ : 'basic';
195
+
196
+ const renderLabel = useCallback((): JSX.Element | null => {
197
+ // eslint-disable-next-line react/no-unstable-nested-components
198
+ function Wrapper({children}: {children: ReactNode}): JSX.Element {
199
+ return (
200
+ <View
201
+ style={[
202
+ css`
203
+ margin-bottom: ${decoration === 'boxed' ? '14px' : 0};
204
+
205
+ flex-direction: row;
206
+ align-items: center;
207
+ `,
208
+ styles?.labelContainer,
209
+ ]}
210
+ >
211
+ {children}
212
+ {required ? (
213
+ <Icon
214
+ name="AsteriskBold"
215
+ style={css`
216
+ color: ${theme.role.danger};
217
+ opacity: ${focused ? '1' : '0.5'};
218
+ `}
219
+ />
220
+ ) : null}
221
+ </View>
222
+ );
223
+ }
224
+
225
+ return typeof label === 'string' ? (
226
+ <Wrapper>
227
+ <Text
228
+ style={[
229
+ css`
230
+ font-family: Pretendard-Bold;
231
+ color: ${defaultColor};
232
+ margin-right: 4px;
233
+ font-size: 16px;
234
+ `,
235
+ labelPlaceholderColor,
236
+ styles?.label,
237
+ ]}
238
+ >
239
+ {label}
240
+ </Text>
241
+ </Wrapper>
242
+ ) : label ? (
243
+ <Wrapper>{label(status)}</Wrapper>
244
+ ) : null;
245
+ }, [
246
+ decoration,
247
+ defaultColor,
248
+ focused,
249
+ label,
250
+ labelPlaceholderColor,
251
+ required,
252
+ status,
253
+ styles?.label,
254
+ styles?.labelContainer,
255
+ theme.role.danger,
256
+ ]);
257
+
258
+ const renderContainer = useCallback(
259
+ (children: ReactNode): JSX.Element => {
260
+ return (
261
+ <TouchableWithoutFeedback
262
+ onPress={() => inputRef.current?.focus()}
263
+ testID="container-touch"
264
+ >
265
+ <View
266
+ style={[
267
+ defaultContainerStyle,
268
+ css`
269
+ flex-direction: ${direction};
270
+ align-items: ${direction === 'row' ? 'center' : 'flex-start'};
271
+ justify-content: ${direction === 'row'
272
+ ? 'flex-start'
273
+ : 'space-between'};
274
+ border-color: ${labelPlaceholderColor
275
+ ? labelPlaceholderColor.color
276
+ : defaultColor};
277
+ `,
278
+ decoration === 'boxed'
279
+ ? css`
280
+ border-radius: 4px;
281
+ border-width: 1px;
282
+ padding-left: 12px;
283
+ padding-right: 12px;
284
+ `
285
+ : css`
286
+ border-bottom-width: 1px;
287
+ `,
288
+ styles?.container,
289
+ ]}
290
+ testID="container"
291
+ >
292
+ {children}
293
+ </View>
294
+ </TouchableWithoutFeedback>
295
+ );
296
+ },
297
+ [
298
+ decoration,
299
+ defaultColor,
300
+ defaultContainerStyle,
301
+ direction,
302
+ inputRef,
303
+ labelPlaceholderColor,
304
+ styles?.container,
305
+ ],
306
+ );
307
+
308
+ const renderInput = useCallback((): JSX.Element | null => {
309
+ return (
310
+ <View
311
+ style={[
312
+ direction === 'row'
313
+ ? css`
314
+ flex: 1;
315
+ `
316
+ : css`
317
+ align-self: stretch;
318
+ `,
319
+ css`
320
+ padding: ${decoration === 'boxed' ? '4px 0' : '2px 0'};
321
+
322
+ flex-direction: row;
323
+ align-items: center;
324
+ justify-content: space-between;
325
+ `,
326
+ styles?.inputContainer,
327
+ ]}
328
+ >
329
+ <>
330
+ {isValidElement(startElement)
331
+ ? cloneElemWithDefaultColors({
332
+ element: startElement,
333
+ color: defaultColor,
334
+ style: css`
335
+ margin-left: -4px;
336
+ margin-right: 4px;
337
+ `,
338
+ })
339
+ : startElement}
340
+ <TextInput
341
+ autoCapitalize={autoCapitalize}
342
+ autoComplete={autoComplete}
343
+ editable={editable}
344
+ maxLength={maxLength}
345
+ multiline={multiline}
346
+ numberOfLines={numberOfLines}
347
+ onBlur={(e) => {
348
+ setFocused(false);
349
+ onBlur?.(e);
350
+ }}
351
+ onChange={onChange}
352
+ onChangeText={onChangeText}
353
+ onFocus={(e) => {
354
+ setFocused(true);
355
+ onFocus?.(e);
356
+ }}
357
+ onSubmitEditing={onSubmitEditing}
358
+ placeholder={placeholder}
359
+ placeholderTextColor={placeholderColor || theme.text.placeholder}
360
+ ref={inputRef}
361
+ secureTextEntry={secureTextEntry}
362
+ selectionColor={theme.role.underlay}
363
+ style={[
364
+ // Stretch input in order to make remaining space clickable
365
+ css`
366
+ font-family: Pretendard;
367
+ flex: 1;
368
+ font-size: 16px;
369
+ text-align-vertical: ${multiline ? 'top' : 'center'};
370
+ `,
371
+ Platform.OS === 'web' &&
372
+ css`
373
+ outline-width: 0;
374
+ `,
375
+ direction === 'column'
376
+ ? css`
377
+ padding-top: 12px;
378
+ `
379
+ : css`
380
+ padding-left: 12px;
381
+ `,
382
+ css`
383
+ color: ${defaultColor};
384
+ padding: 10px 0 12px 0;
385
+ `,
386
+ styles?.input,
387
+ ]}
388
+ testID={testID}
389
+ value={value}
390
+ {...textInputProps}
391
+ />
392
+ {isValidElement(endElement)
393
+ ? cloneElemWithDefaultColors({
394
+ element: endElement,
395
+ color: defaultColor,
396
+ style: css`
397
+ margin-left: 4px;
398
+ margin-right: ${decoration === 'boxed' ? '-8px' : '-4px'};
399
+ `,
400
+ })
401
+ : endElement}
402
+ </>
403
+ </View>
404
+ );
405
+ }, [
406
+ autoCapitalize,
407
+ autoComplete,
408
+ decoration,
409
+ defaultColor,
410
+ direction,
411
+ editable,
412
+ endElement,
413
+ inputRef,
414
+ maxLength,
415
+ multiline,
416
+ numberOfLines,
417
+ onBlur,
418
+ onChange,
419
+ onChangeText,
420
+ onFocus,
421
+ onSubmitEditing,
422
+ placeholder,
423
+ placeholderColor,
424
+ secureTextEntry,
425
+ startElement,
426
+ styles?.input,
427
+ styles?.inputContainer,
428
+ testID,
429
+ textInputProps,
430
+ theme.role.underlay,
431
+ theme.text.placeholder,
432
+ value,
433
+ ]);
434
+
435
+ const renderError = useCallback((): JSX.Element | null => {
436
+ return error ? (
437
+ typeof error === 'string' ? (
438
+ <Text
439
+ style={[
440
+ css`
441
+ flex: 1;
442
+ color: ${theme.text.validation};
443
+ font-size: 12px;
444
+ `,
445
+ styles?.error,
446
+ ]}
447
+ >
448
+ {error}
449
+ </Text>
450
+ ) : (
451
+ error?.(status)
452
+ )
453
+ ) : null;
454
+ }, [error, status, styles?.error, theme.text.validation]);
455
+
456
+ const renderCounter = useCallback((): JSX.Element | null => {
457
+ if (hideCounter) {
458
+ return null;
459
+ }
460
+
461
+ return maxLength ? (
462
+ <Text
463
+ style={[
464
+ css`
465
+ color: ${theme.text.placeholder};
466
+ font-size: 12px;
467
+ `,
468
+ styles?.counter,
469
+ ]}
470
+ >{`${value.length}/${maxLength}`}</Text>
471
+ ) : null;
472
+ }, [
473
+ hideCounter,
474
+ maxLength,
475
+ styles?.counter,
476
+ theme.text.placeholder,
477
+ value.length,
478
+ ]);
479
+
480
+ return (
481
+ <>
482
+ {Platform.OS === 'web' ? <WebStyles /> : null}
483
+ <View
484
+ ref={Platform.select({web: webRef, default: undefined})}
485
+ style={[
486
+ css`
487
+ width: 100%;
488
+ `,
489
+ style,
490
+ ]}
491
+ testID="edit-text"
492
+ >
493
+ {renderLabel()}
494
+ {renderContainer(renderInput())}
495
+ {renderError() || renderCounter() ? (
496
+ <View
497
+ style={css`
498
+ margin-top: 6px;
499
+
500
+ flex-direction: ${!renderError() && renderCounter()
501
+ ? 'row-reverse'
502
+ : 'row'};
503
+ gap: 4px;
504
+ `}
505
+ >
506
+ {renderError()}
507
+ {renderCounter()}
508
+ </View>
509
+ ) : null}
510
+ </View>
511
+ </>
512
+ );
513
+ },
514
+ );
515
+
516
+ function WebStyles(): JSX.Element {
517
+ return (
518
+ <Global
519
+ // @ts-ignore
520
+ styles={css`
521
+ input:autofill,
522
+ input:autofill:hover,
523
+ input:autofill:focus,
524
+ input:autofill:active {
525
+ -webkit-text-fill-color: #787878;
526
+ box-shadow: 0 0 0px 1000px #ededed inset;
527
+ transition: background-color 5000s ease-in-out 0s;
528
+ }
529
+ ::-webkit-scrollbar {
530
+ display: none;
531
+ }
532
+ `}
533
+ />
534
+ );
535
+ }
@@ -0,0 +1,12 @@
1
+ import styled from '@emotion/native';
2
+ import { isEmptyObject } from '../../../utils/theme';
3
+ export const Hr = styled.View `
4
+ height: 0.5px;
5
+ width: 100%;
6
+ background-color: ${({ theme }) => {
7
+ if (isEmptyObject(theme)) {
8
+ return theme.role.border;
9
+ }
10
+ return theme.role.border;
11
+ }};
12
+ `;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import {View} from 'react-native';
3
+ import type {RenderAPI} from '@testing-library/react-native';
4
+ import {render} from '@testing-library/react-native';
5
+
6
+ import {createComponent} from '../../../../test/testUtils';
7
+ import {Hr} from './Hr';
8
+
9
+ let testingLib: RenderAPI;
10
+
11
+ const Component = (): JSX.Element =>
12
+ createComponent(
13
+ <View>
14
+ <Hr />
15
+ </View>,
16
+ );
17
+
18
+ describe('[Hr]', () => {
19
+ it('should render without crashing', () => {
20
+ testingLib = render(Component());
21
+
22
+ const json = testingLib.toJSON();
23
+
24
+ expect(json).toBeTruthy();
25
+ });
26
+ });
@@ -0,0 +1,14 @@
1
+ import styled from '@emotion/native';
2
+ import {isEmptyObject} from '../../../utils/theme';
3
+
4
+ export const Hr = styled.View`
5
+ height: 0.5px;
6
+ width: 100%;
7
+ background-color: ${({theme}) => {
8
+ if (isEmptyObject(theme)) {
9
+ return theme.role.border;
10
+ }
11
+
12
+ return theme.role.border;
13
+ }};
14
+ `;
@@ -15,11 +15,10 @@ const createBaseText = (colorResolver, fallbackColor) => styled.Text `
15
15
  return colorResolver(theme);
16
16
  }};
17
17
  `;
18
- // Common Text Component Factory
19
- const createTextComponent = (BaseText, fontSize, lineHeight) => withTheme(({ style, children, theme, ...props }) => (_jsx(BaseText, { ...props, theme: theme, style: [
18
+ const createTextComponent = (BaseText, fontSize, lineHeight) => withTheme(({ style, children, theme, ...props }) => (_jsx(BaseText, { ...props, style: [
20
19
  css `
21
- font-size: ${fontSize + 'px'};
22
- line-height: ${lineHeight + 'px'};
20
+ font-size: ${fontSize}px;
21
+ line-height: ${lineHeight}px;
23
22
  `,
24
23
  { includeFontPadding: false },
25
24
  style,
@@ -22,8 +22,10 @@ const createBaseText = (
22
22
  `;
23
23
 
24
24
  // Common Text Component Factory
25
+ type TextComponentType = ReturnType<typeof styled.Text>;
26
+
25
27
  const createTextComponent = (
26
- BaseText: ReturnType<typeof styled.Text>,
28
+ BaseText: TextComponentType,
27
29
  fontSize: number,
28
30
  lineHeight: number,
29
31
  ) =>
@@ -40,11 +42,10 @@ const createTextComponent = (
40
42
  }) => (
41
43
  <BaseText
42
44
  {...props}
43
- theme={theme}
44
45
  style={[
45
46
  css`
46
- font-size: ${fontSize + 'px'};
47
- line-height: ${lineHeight + 'px'};
47
+ font-size: ${fontSize}px;
48
+ line-height: ${lineHeight}px;
48
49
  `,
49
50
  {includeFontPadding: false},
50
51
  style,
@@ -53,7 +54,7 @@ const createTextComponent = (
53
54
  {children}
54
55
  </BaseText>
55
56
  ),
56
- );
57
+ ) as unknown as TextComponentType;
57
58
 
58
59
  // Standard and Inverted Base Components
59
60
  const StandardBaseText = createBaseText((theme) => theme.text.basic, 'gray');
package/index.js CHANGED
@@ -7,6 +7,7 @@ export * from './providers';
7
7
  export * from './components/uis/Accordion/Accordion';
8
8
  export * from './components/uis/Button/Button';
9
9
  export * from './components/uis/Checkbox/Checkbox';
10
+ export * from './components/uis/EditText/EditText';
10
11
  export * from './components/uis/Icon/Icon';
11
12
  export * from './components/uis/IconButton/IconButton';
12
13
  export * from './components/uis/LoadingIndicator/LoadingIndicator';
package/index.tsx CHANGED
@@ -9,6 +9,7 @@ export * from './providers';
9
9
  export * from './components/uis/Accordion/Accordion';
10
10
  export * from './components/uis/Button/Button';
11
11
  export * from './components/uis/Checkbox/Checkbox';
12
+ export * from './components/uis/EditText/EditText';
12
13
  export * from './components/uis/Icon/Icon';
13
14
  export * from './components/uis/IconButton/IconButton';
14
15
  export * from './components/uis/LoadingIndicator/LoadingIndicator';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpk-ui",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "main": "index",
5
5
  "react-native": "index",
6
6
  "module": "index",