@widergy/mobile-ui 2.16.0 → 2.17.1
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/CHANGELOG.md +18 -3
- package/lib/components/UTBanner/README.md +124 -11
- package/lib/components/UTBanner/constants.js +5 -0
- package/lib/components/UTBanner/index.js +119 -52
- package/lib/components/UTBanner/proptypes.js +36 -13
- package/lib/components/UTBanner/theme.js +137 -11
- package/lib/components/UTBottomSheet/index.js +35 -32
- package/lib/components/UTBottomSheet/styles.js +4 -3
- package/lib/components/UTDatePicker/README.md +73 -0
- package/lib/components/UTDatePicker/components/Calendar/constants.js +3 -0
- package/lib/components/UTDatePicker/components/Calendar/index.js +197 -0
- package/lib/components/UTDatePicker/components/Day/index.js +44 -0
- package/lib/components/UTDatePicker/components/Day/styles.js +8 -0
- package/lib/components/UTDatePicker/components/PickerColumn/index.js +57 -0
- package/lib/components/UTDatePicker/constants.js +48 -0
- package/lib/components/UTDatePicker/index.js +135 -0
- package/lib/components/UTDatePicker/layout.js +108 -0
- package/lib/components/UTDatePicker/proptypes.js +20 -0
- package/lib/components/UTDatePicker/styles.js +63 -0
- package/lib/components/UTDatePicker/theme.js +18 -0
- package/lib/components/UTDatePicker/utils.js +52 -0
- package/lib/constants/testIds.js +13 -0
- package/lib/index.js +1 -0
- package/package.json +5 -4
|
@@ -96,6 +96,7 @@ const UTBottomSheet = ({
|
|
|
96
96
|
}, [height]);
|
|
97
97
|
|
|
98
98
|
const ChildrenContainer = scrolleable ? ScrollView : View;
|
|
99
|
+
const hasHeader = !!(title || buttonText || description);
|
|
99
100
|
|
|
100
101
|
const actionAlignment =
|
|
101
102
|
actionAlignment_ || [primaryAction, secondaryAction, tertiaryAction].filter(Boolean).length === 2
|
|
@@ -118,44 +119,46 @@ const UTBottomSheet = ({
|
|
|
118
119
|
>
|
|
119
120
|
<Animated.View style={[styles.animatedContainer(theme), { height: modalHeight }, pan.getLayout()]}>
|
|
120
121
|
<SafeAreaView onLayout={onLayout} style={styles.content(adjustableHeight)}>
|
|
121
|
-
<View style={styles.dragHandle}>
|
|
122
|
+
<View style={styles.dragHandle(hasHeader)}>
|
|
122
123
|
<View style={styles.handleIndicator(theme)} />
|
|
123
124
|
</View>
|
|
124
125
|
<View style={styles.content(adjustableHeight)}>
|
|
125
|
-
|
|
126
|
-
<View style={styles.
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{title}
|
|
136
|
-
</UTFieldLabel>
|
|
137
|
-
{buttonText && (
|
|
138
|
-
<UTButton
|
|
139
|
-
colorTheme="primary"
|
|
140
|
-
onPress={onButtonPress ?? onClose}
|
|
141
|
-
variant="semitransparent"
|
|
142
|
-
dataTestId={dataTestId ? `${dataTestId}.${closeButton}` : undefined}
|
|
143
|
-
{...onCloseProps}
|
|
126
|
+
{hasHeader && (
|
|
127
|
+
<View style={styles.header}>
|
|
128
|
+
<View style={styles.headerTopRow}>
|
|
129
|
+
<UTFieldLabel
|
|
130
|
+
required={required}
|
|
131
|
+
style={styles.title}
|
|
132
|
+
variant="subtitle1"
|
|
133
|
+
weight="medium"
|
|
134
|
+
dataTestId={dataTestId ? `${dataTestId}.${titleTestId}` : undefined}
|
|
135
|
+
{...titleProps}
|
|
144
136
|
>
|
|
145
|
-
{
|
|
146
|
-
</
|
|
137
|
+
{title}
|
|
138
|
+
</UTFieldLabel>
|
|
139
|
+
{buttonText && (
|
|
140
|
+
<UTButton
|
|
141
|
+
colorTheme="primary"
|
|
142
|
+
onPress={onButtonPress ?? onClose}
|
|
143
|
+
variant="semitransparent"
|
|
144
|
+
dataTestId={dataTestId ? `${dataTestId}.${closeButton}` : undefined}
|
|
145
|
+
{...onCloseProps}
|
|
146
|
+
>
|
|
147
|
+
{buttonText}
|
|
148
|
+
</UTButton>
|
|
149
|
+
)}
|
|
150
|
+
</View>
|
|
151
|
+
{description && (
|
|
152
|
+
<UTLabel
|
|
153
|
+
colorTheme="gray"
|
|
154
|
+
variant="small"
|
|
155
|
+
dataTestId={dataTestId ? `${dataTestId}.${descriptionTestId}` : undefined}
|
|
156
|
+
>
|
|
157
|
+
{description}
|
|
158
|
+
</UTLabel>
|
|
147
159
|
)}
|
|
148
160
|
</View>
|
|
149
|
-
|
|
150
|
-
<UTLabel
|
|
151
|
-
colorTheme="gray"
|
|
152
|
-
variant="small"
|
|
153
|
-
dataTestId={dataTestId ? `${dataTestId}.${descriptionTestId}` : undefined}
|
|
154
|
-
>
|
|
155
|
-
{description}
|
|
156
|
-
</UTLabel>
|
|
157
|
-
)}
|
|
158
|
-
</View>
|
|
161
|
+
)}
|
|
159
162
|
<ChildrenContainer
|
|
160
163
|
keyboardShouldPersistTaps="handled"
|
|
161
164
|
style={styles.childrenContainer(adjustableHeight, withBodyPadding)}
|
|
@@ -42,11 +42,12 @@ const styles = StyleSheet.create({
|
|
|
42
42
|
content: adjustableHeight => ({
|
|
43
43
|
flexGrow: adjustableHeight ? 0 : 1
|
|
44
44
|
}),
|
|
45
|
-
dragHandle: {
|
|
45
|
+
dragHandle: hasHeader => ({
|
|
46
46
|
alignItems: 'center',
|
|
47
47
|
backgroundColor: 'transparent',
|
|
48
|
-
padding: 16
|
|
49
|
-
|
|
48
|
+
padding: 16,
|
|
49
|
+
paddingBottom: hasHeader ? 16 : 0
|
|
50
|
+
}),
|
|
50
51
|
handleIndicator: theme => ({
|
|
51
52
|
backgroundColor: theme.Palette.light['04'],
|
|
52
53
|
borderRadius: 2.5,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# UTDatePicker
|
|
2
|
+
|
|
3
|
+
Selector de fechas con calendario modal y soporte de entrada manual. Adaptación mobile del componente `UTDatePicker` de Energy-UI web (V1).
|
|
4
|
+
|
|
5
|
+
## Props
|
|
6
|
+
|
|
7
|
+
| Nombre | Tipo | Default | Descripción |
|
|
8
|
+
|-------------|---------------------------|---------------|-----------------------------------------------------------------------|
|
|
9
|
+
| clearable | `bool` | `true` | Muestra botón para limpiar la fecha seleccionada |
|
|
10
|
+
| CustomIcon | `elementType` | — | Ícono personalizado para el botón del calendario |
|
|
11
|
+
| disabled | `bool` | — | Deshabilita toda interacción |
|
|
12
|
+
| error | `string` | `''` | Mensaje de error externo |
|
|
13
|
+
| helpText | `string` | — | Texto de ayuda debajo del campo |
|
|
14
|
+
| modalProps | `object` | — | Props adicionales para el Modal de React Native (equivalente mobile de `popoverProps` en web) |
|
|
15
|
+
| onChange | `func` | `() => {}` | Callback al seleccionar una fecha. Recibe string en formato `DD/MM/YYYY` |
|
|
16
|
+
| placeholder | `string` | `'DD/MM/AAAA'`| Texto placeholder del campo |
|
|
17
|
+
| range | `{ maxDate, minDate }` | — | Restricción de rango. Fechas en formato `DD/MM/YYYY` |
|
|
18
|
+
| readOnly | `bool` | `false` | Modo solo lectura |
|
|
19
|
+
| required | `bool` | — | Marca el título como requerido |
|
|
20
|
+
| size | `'sm' \| 'md'` | `'sm'` | Tamaño del campo de entrada |
|
|
21
|
+
| style | `{ root: ViewStyle }` | — | Estilos personalizados para el contenedor raíz |
|
|
22
|
+
| title | `string` | — | Etiqueta superior del campo |
|
|
23
|
+
| value | `string` | `''` | Fecha seleccionada en formato `DD/MM/YYYY` |
|
|
24
|
+
| variant | `'select' \| 'picker'` | `'select'` | Variante visual del campo. `'picker'` usa fondo transparente sin borde |
|
|
25
|
+
|
|
26
|
+
## Variantes
|
|
27
|
+
|
|
28
|
+
### `variant='select'` (default)
|
|
29
|
+
Campo con borde y fondo blanco. Muestra el mensaje de error debajo del campo.
|
|
30
|
+
|
|
31
|
+
### `variant='picker'`
|
|
32
|
+
Fondo transparente sin borde. El error no se muestra debajo (se maneja externamente).
|
|
33
|
+
|
|
34
|
+
## Adaptaciones mobile vs web
|
|
35
|
+
|
|
36
|
+
- El calendario se abre mediante el ícono de calendario (no al hacer foco en el input, como en web).
|
|
37
|
+
- El `Popover` de MUI fue reemplazado por un `Modal` de React Native. Usar `modalProps` en lugar de `popoverProps`.
|
|
38
|
+
- Los estilos usan `StyleSheet` y el sistema de theming de la librería (no CSS Modules).
|
|
39
|
+
- La entrada de texto usa teclado numérico (`type='number'`).
|
|
40
|
+
|
|
41
|
+
## Ejemplos
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
// Básico
|
|
45
|
+
<UTDatePicker
|
|
46
|
+
onChange={date => console.log(date)}
|
|
47
|
+
title="Fecha de nacimiento"
|
|
48
|
+
value={selectedDate}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
// Con rango y error
|
|
52
|
+
<UTDatePicker
|
|
53
|
+
error="La fecha no es válida"
|
|
54
|
+
onChange={setDate}
|
|
55
|
+
range={{ minDate: '01/01/2020', maxDate: '31/12/2030' }}
|
|
56
|
+
title="Seleccioná una fecha"
|
|
57
|
+
value={date}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
// Variante picker (transparente)
|
|
61
|
+
<UTDatePicker
|
|
62
|
+
onChange={setDate}
|
|
63
|
+
value={date}
|
|
64
|
+
variant="picker"
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
// Solo lectura
|
|
68
|
+
<UTDatePicker
|
|
69
|
+
readOnly
|
|
70
|
+
title="Fecha registrada"
|
|
71
|
+
value="15/06/2024"
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import 'dayjs/locale/es';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import { func, object, string } from 'prop-types';
|
|
6
|
+
|
|
7
|
+
import UTButton from '../../../UTButton';
|
|
8
|
+
import UTLabel from '../../../UTLabel';
|
|
9
|
+
import { TEST_IDS } from '../../../../constants/testIds';
|
|
10
|
+
import {
|
|
11
|
+
CALENDAR_VIEWS,
|
|
12
|
+
DAY_UNIT,
|
|
13
|
+
MONTH_LABELS,
|
|
14
|
+
MONTH_UNIT,
|
|
15
|
+
MONTH_YEAR_FORMAT,
|
|
16
|
+
OUTPUT_LABEL_MASK,
|
|
17
|
+
WEEKDAYS_LABELS
|
|
18
|
+
} from '../../constants';
|
|
19
|
+
import styles from '../../styles';
|
|
20
|
+
import { getWeeks, getYearRange } from '../../utils';
|
|
21
|
+
import Day from '../Day';
|
|
22
|
+
import PickerColumn from '../PickerColumn';
|
|
23
|
+
|
|
24
|
+
import { CALENDAR_LOCALE, PICKER_ITEM_HEIGHT, WEEK_KEY_FORMAT } from './constants';
|
|
25
|
+
|
|
26
|
+
const { UTDatePicker: datePickerTestIds } = TEST_IDS;
|
|
27
|
+
|
|
28
|
+
const Calendar = ({ dataTestId, maxDate, minDate, onDaySelect, pickedDate }) => {
|
|
29
|
+
const monthScrollRef = useRef(null);
|
|
30
|
+
const yearScrollRef = useRef(null);
|
|
31
|
+
const [currentMonth, setCurrentMonth] = useState(pickedDate?.isValid() ? pickedDate : dayjs());
|
|
32
|
+
const [viewMode, setViewMode] = useState(CALENDAR_VIEWS.calendar);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (pickedDate?.isValid()) setCurrentMonth(pickedDate);
|
|
36
|
+
}, [pickedDate]);
|
|
37
|
+
|
|
38
|
+
const weeks = getWeeks(currentMonth);
|
|
39
|
+
const years = useMemo(() => getYearRange(minDate, maxDate), [minDate, maxDate]);
|
|
40
|
+
const selectedYear = pickedDate?.isValid() ? pickedDate.year() : null;
|
|
41
|
+
const selectedMonth = pickedDate?.isValid() ? pickedDate.month() : null;
|
|
42
|
+
|
|
43
|
+
const visibleMonths = useMemo(() => {
|
|
44
|
+
const viewedYear = currentMonth.year();
|
|
45
|
+
const parsedMin = minDate ? dayjs(minDate, OUTPUT_LABEL_MASK) : null;
|
|
46
|
+
const parsedMax = maxDate ? dayjs(maxDate, OUTPUT_LABEL_MASK) : null;
|
|
47
|
+
const minMonthBound = parsedMin?.year() === viewedYear ? parsedMin.month() : 0;
|
|
48
|
+
const maxMonthBound = parsedMax?.year() === viewedYear ? parsedMax.month() : 11;
|
|
49
|
+
return MONTH_LABELS.map((label, index) => ({ index, label })).filter(
|
|
50
|
+
({ index }) => index >= minMonthBound && index <= maxMonthBound
|
|
51
|
+
);
|
|
52
|
+
}, [currentMonth, minDate, maxDate]);
|
|
53
|
+
|
|
54
|
+
const handlePrevMonth = () => setCurrentMonth(prev => prev.subtract(1, MONTH_UNIT));
|
|
55
|
+
const handleNextMonth = () => setCurrentMonth(prev => prev.add(1, MONTH_UNIT));
|
|
56
|
+
|
|
57
|
+
const handleMonthSelect = month => {
|
|
58
|
+
setCurrentMonth(prev => prev.month(month));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleYearSelect = year => {
|
|
62
|
+
setCurrentMonth(prev => prev.year(year));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isInYearView = viewMode === CALENDAR_VIEWS.year;
|
|
66
|
+
const rawMonthLabel = currentMonth.locale(CALENDAR_LOCALE).format(MONTH_YEAR_FORMAT);
|
|
67
|
+
const monthYearLabel = rawMonthLabel.charAt(0).toUpperCase() + rawMonthLabel.slice(1);
|
|
68
|
+
|
|
69
|
+
const toggleViewMode = () =>
|
|
70
|
+
setViewMode(prev => (prev === CALENDAR_VIEWS.calendar ? CALENDAR_VIEWS.year : CALENDAR_VIEWS.calendar));
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isInYearView) return;
|
|
74
|
+
const targetMonth = selectedMonth ?? currentMonth.month();
|
|
75
|
+
const monthScrollPos = visibleMonths.findIndex(m => m.index === targetMonth);
|
|
76
|
+
const yearIdx = years.findIndex(y => y === (selectedYear ?? currentMonth.year()));
|
|
77
|
+
const id = setTimeout(() => {
|
|
78
|
+
if (monthScrollPos >= 0) {
|
|
79
|
+
monthScrollRef.current?.scrollTo({ animated: false, y: monthScrollPos * PICKER_ITEM_HEIGHT });
|
|
80
|
+
}
|
|
81
|
+
if (yearIdx >= 0) yearScrollRef.current?.scrollTo({ animated: false, y: yearIdx * PICKER_ITEM_HEIGHT });
|
|
82
|
+
}, 50);
|
|
83
|
+
return () => clearTimeout(id);
|
|
84
|
+
}, [isInYearView, visibleMonths, years, selectedMonth, selectedYear, currentMonth]);
|
|
85
|
+
|
|
86
|
+
const isDisabled = date => {
|
|
87
|
+
if (minDate && date.isBefore(dayjs(minDate, OUTPUT_LABEL_MASK), DAY_UNIT)) return true;
|
|
88
|
+
if (maxDate && date.isAfter(dayjs(maxDate, OUTPUT_LABEL_MASK), DAY_UNIT)) return true;
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const monthItems = visibleMonths.map(({ index, label }) => ({
|
|
93
|
+
key: label,
|
|
94
|
+
label,
|
|
95
|
+
onPress: () => handleMonthSelect(index),
|
|
96
|
+
selected: index === currentMonth.month(),
|
|
97
|
+
testID: `${datePickerTestIds.monthItem}.${label}`
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
const yearItems = years.map(year => ({
|
|
101
|
+
key: year,
|
|
102
|
+
label: String(year),
|
|
103
|
+
onPress: () => handleYearSelect(year),
|
|
104
|
+
selected: year === currentMonth.year(),
|
|
105
|
+
testID: `${datePickerTestIds.yearItem}.${year}`
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<View style={styles.calendarRoot} testID={dataTestId || datePickerTestIds.calendar}>
|
|
110
|
+
<View style={styles.nav}>
|
|
111
|
+
{isInYearView ? (
|
|
112
|
+
<View style={styles.navSpacer} />
|
|
113
|
+
) : (
|
|
114
|
+
<UTButton
|
|
115
|
+
colorTheme="secondary"
|
|
116
|
+
dataTestId={datePickerTestIds.calendarPrevBtn}
|
|
117
|
+
Icon="IconChevronLeft"
|
|
118
|
+
onPress={handlePrevMonth}
|
|
119
|
+
size="small"
|
|
120
|
+
variant="text"
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<UTButton
|
|
125
|
+
colorTheme="secondary"
|
|
126
|
+
dataTestId={datePickerTestIds.calendarMonthYear}
|
|
127
|
+
Icon={isInYearView ? 'IconChevronUp' : 'IconChevronDown'}
|
|
128
|
+
iconPlacement="right"
|
|
129
|
+
onPress={toggleViewMode}
|
|
130
|
+
size="small"
|
|
131
|
+
style={{ root: styles.navLabel }}
|
|
132
|
+
variant="text"
|
|
133
|
+
>
|
|
134
|
+
{monthYearLabel}
|
|
135
|
+
</UTButton>
|
|
136
|
+
|
|
137
|
+
{isInYearView ? (
|
|
138
|
+
<View style={styles.navSpacer} />
|
|
139
|
+
) : (
|
|
140
|
+
<UTButton
|
|
141
|
+
colorTheme="secondary"
|
|
142
|
+
dataTestId={datePickerTestIds.calendarNextBtn}
|
|
143
|
+
Icon="IconChevronRight"
|
|
144
|
+
onPress={handleNextMonth}
|
|
145
|
+
size="small"
|
|
146
|
+
variant="text"
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
{isInYearView ? (
|
|
152
|
+
<View style={styles.pickerContainer}>
|
|
153
|
+
<PickerColumn items={monthItems} scrollRef={monthScrollRef} testID={datePickerTestIds.monthView} />
|
|
154
|
+
<PickerColumn items={yearItems} scrollRef={yearScrollRef} testID={datePickerTestIds.yearView} />
|
|
155
|
+
</View>
|
|
156
|
+
) : (
|
|
157
|
+
<View style={styles.month}>
|
|
158
|
+
<View style={styles.week}>
|
|
159
|
+
{WEEKDAYS_LABELS.map(label => (
|
|
160
|
+
<UTLabel colorTheme="gray" key={label} style={styles.dayCell} variant="small" weight="medium">
|
|
161
|
+
{label}
|
|
162
|
+
</UTLabel>
|
|
163
|
+
))}
|
|
164
|
+
</View>
|
|
165
|
+
|
|
166
|
+
{weeks.map(week => (
|
|
167
|
+
<View key={week[0].format(WEEK_KEY_FORMAT)} style={styles.week}>
|
|
168
|
+
{week.map(date => {
|
|
169
|
+
const dayInCurrentMonth = date.month() === currentMonth.month();
|
|
170
|
+
return (
|
|
171
|
+
<Day
|
|
172
|
+
date={date}
|
|
173
|
+
dayInCurrentMonth={dayInCurrentMonth}
|
|
174
|
+
isDisabled={isDisabled(date)}
|
|
175
|
+
key={date.format(WEEK_KEY_FORMAT)}
|
|
176
|
+
onClick={onDaySelect}
|
|
177
|
+
pickedDate={pickedDate}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</View>
|
|
182
|
+
))}
|
|
183
|
+
</View>
|
|
184
|
+
)}
|
|
185
|
+
</View>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
Calendar.propTypes = {
|
|
190
|
+
dataTestId: string,
|
|
191
|
+
maxDate: string,
|
|
192
|
+
minDate: string,
|
|
193
|
+
onDaySelect: func,
|
|
194
|
+
pickedDate: object
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default Calendar;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { bool, func, object } from 'prop-types';
|
|
5
|
+
|
|
6
|
+
import { useTheme } from '../../../../theming';
|
|
7
|
+
import UTButton from '../../../UTButton';
|
|
8
|
+
import { DAY_NUMBER_FORMAT } from '../../constants';
|
|
9
|
+
import { getDayCellButtonStyle } from '../../theme';
|
|
10
|
+
import { isSelected } from '../../utils';
|
|
11
|
+
|
|
12
|
+
import styles from './styles';
|
|
13
|
+
|
|
14
|
+
const Day = ({ date, dayInCurrentMonth, isDisabled, onClick, pickedDate }) => {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const selected = isSelected(date, pickedDate);
|
|
17
|
+
|
|
18
|
+
if (!dayInCurrentMonth) return <View style={styles.otherMonthCell} />;
|
|
19
|
+
|
|
20
|
+
const buttonStyle = getDayCellButtonStyle(selected, theme);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<UTButton
|
|
24
|
+
colorTheme={selected ? 'primary' : 'secondary'}
|
|
25
|
+
disabled={isDisabled}
|
|
26
|
+
onPress={() => onClick(date)}
|
|
27
|
+
size="small"
|
|
28
|
+
style={buttonStyle}
|
|
29
|
+
variant={selected ? 'filled' : 'text'}
|
|
30
|
+
>
|
|
31
|
+
{dayjs(date).format(DAY_NUMBER_FORMAT)}
|
|
32
|
+
</UTButton>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Day.propTypes = {
|
|
37
|
+
date: object,
|
|
38
|
+
dayInCurrentMonth: bool,
|
|
39
|
+
isDisabled: bool,
|
|
40
|
+
onClick: func,
|
|
41
|
+
pickedDate: object
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default memo(Day);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
import { arrayOf, bool, func, number, object, oneOfType, shape, string } from 'prop-types';
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '../../../../theming';
|
|
6
|
+
import UTButton from '../../../UTButton';
|
|
7
|
+
import { getYearItemStyle } from '../../theme';
|
|
8
|
+
import styles from '../../styles';
|
|
9
|
+
|
|
10
|
+
const PickerColumn = ({ items, scrollRef, testID }) => {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<View style={styles.pickerColumn}>
|
|
15
|
+
<ScrollView
|
|
16
|
+
contentContainerStyle={styles.pickerScrollContent}
|
|
17
|
+
ref={scrollRef}
|
|
18
|
+
showsVerticalScrollIndicator={false}
|
|
19
|
+
style={styles.pickerScroll}
|
|
20
|
+
testID={testID}
|
|
21
|
+
>
|
|
22
|
+
{items.map(({ key, label, onPress, selected, testID: itemTestID }) => {
|
|
23
|
+
const itemTheme = getYearItemStyle(selected, theme);
|
|
24
|
+
return (
|
|
25
|
+
<UTButton
|
|
26
|
+
colorTheme={selected ? 'primary' : 'secondary'}
|
|
27
|
+
dataTestId={itemTestID}
|
|
28
|
+
key={key}
|
|
29
|
+
onPress={onPress}
|
|
30
|
+
size="small"
|
|
31
|
+
style={{ root: { ...styles.yearItem, ...itemTheme } }}
|
|
32
|
+
variant={selected ? 'filled' : 'text'}
|
|
33
|
+
>
|
|
34
|
+
{label}
|
|
35
|
+
</UTButton>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</ScrollView>
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
PickerColumn.propTypes = {
|
|
44
|
+
items: arrayOf(
|
|
45
|
+
shape({
|
|
46
|
+
key: oneOfType([number, string]),
|
|
47
|
+
label: string,
|
|
48
|
+
onPress: func,
|
|
49
|
+
selected: bool,
|
|
50
|
+
testID: string
|
|
51
|
+
})
|
|
52
|
+
),
|
|
53
|
+
scrollRef: object,
|
|
54
|
+
testID: string
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default PickerColumn;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const OUTPUT_LABEL_MASK = 'DD/MM/YYYY';
|
|
2
|
+
|
|
3
|
+
export const WEEKDAYS_LABELS = ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'];
|
|
4
|
+
|
|
5
|
+
export const MONTH_YEAR_FORMAT = 'MMMM YYYY';
|
|
6
|
+
|
|
7
|
+
export const DAY_NUMBER_FORMAT = 'DD';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_PLACEHOLDER = 'DD/MM/AAAA';
|
|
10
|
+
|
|
11
|
+
export const TYPING_ERROR_MESSAGE = 'Fecha inválida';
|
|
12
|
+
|
|
13
|
+
export const VARIANTS = {
|
|
14
|
+
picker: 'picker',
|
|
15
|
+
select: 'select'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const SIZES = {
|
|
19
|
+
md: 'md',
|
|
20
|
+
sm: 'sm'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DAY_UNIT = 'day';
|
|
24
|
+
export const MONTH_UNIT = 'month';
|
|
25
|
+
|
|
26
|
+
export const CALENDAR_VIEWS = {
|
|
27
|
+
calendar: 'calendar',
|
|
28
|
+
year: 'year'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const MONTH_LABELS = [
|
|
32
|
+
'Ene',
|
|
33
|
+
'Feb',
|
|
34
|
+
'Mar',
|
|
35
|
+
'Abr',
|
|
36
|
+
'May',
|
|
37
|
+
'Jun',
|
|
38
|
+
'Jul',
|
|
39
|
+
'Ago',
|
|
40
|
+
'Sep',
|
|
41
|
+
'Oct',
|
|
42
|
+
'Nov',
|
|
43
|
+
'Dic'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const YEAR_RANGE_BEFORE = 50;
|
|
47
|
+
export const YEAR_RANGE_AFTER = 10;
|
|
48
|
+
export const YEARS_PER_ROW = 3;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Keyboard } from 'react-native';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
5
|
+
|
|
6
|
+
import { COMPONENT_KEYS } from '../UTBaseInputField/constants';
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_PLACEHOLDER, OUTPUT_LABEL_MASK, VARIANTS } from './constants';
|
|
9
|
+
import UTDatePickerLayout from './layout';
|
|
10
|
+
import { propTypes } from './proptypes';
|
|
11
|
+
import { dateMatchesFormat, getFinalDate } from './utils';
|
|
12
|
+
|
|
13
|
+
dayjs.extend(customParseFormat);
|
|
14
|
+
|
|
15
|
+
const UTDatePicker = ({
|
|
16
|
+
modalProps,
|
|
17
|
+
clearable = true,
|
|
18
|
+
CustomIcon,
|
|
19
|
+
disabled,
|
|
20
|
+
error = '',
|
|
21
|
+
helpText,
|
|
22
|
+
onChange = () => {},
|
|
23
|
+
placeholder,
|
|
24
|
+
range,
|
|
25
|
+
readOnly = false,
|
|
26
|
+
required,
|
|
27
|
+
style,
|
|
28
|
+
title,
|
|
29
|
+
value = '',
|
|
30
|
+
variant = VARIANTS.select
|
|
31
|
+
}) => {
|
|
32
|
+
const { maxDate, minDate } = range || {};
|
|
33
|
+
|
|
34
|
+
const [pickedDate, setPickedDate] = useState(null);
|
|
35
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!value && pickedDate?.isValid()) setPickedDate(null);
|
|
39
|
+
else if (dateMatchesFormat(value, OUTPUT_LABEL_MASK)) setPickedDate(dayjs(value, OUTPUT_LABEL_MASK));
|
|
40
|
+
else setPickedDate(null);
|
|
41
|
+
}, [value]);
|
|
42
|
+
|
|
43
|
+
const openCalendar = () => {
|
|
44
|
+
if (disabled || readOnly) return;
|
|
45
|
+
Keyboard.dismiss();
|
|
46
|
+
setIsOpen(true);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleClose = () => {
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleDaySelect = date => {
|
|
54
|
+
const finalDate = getFinalDate({ date, maxDate, minDate });
|
|
55
|
+
setPickedDate(finalDate);
|
|
56
|
+
if (finalDate.isValid()) onChange(finalDate.format(OUTPUT_LABEL_MASK));
|
|
57
|
+
else onChange('');
|
|
58
|
+
setIsOpen(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleClear = () => {
|
|
62
|
+
setPickedDate(null);
|
|
63
|
+
onChange('');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const displayValue = pickedDate?.isValid() ? pickedDate.format(OUTPUT_LABEL_MASK) : '';
|
|
67
|
+
|
|
68
|
+
const hasError = !!error;
|
|
69
|
+
const hasValue = !!pickedDate?.isValid();
|
|
70
|
+
const displayPlaceholder = placeholder || DEFAULT_PLACEHOLDER;
|
|
71
|
+
const errorMessage = error;
|
|
72
|
+
const isTransparent = variant === VARIANTS.picker;
|
|
73
|
+
|
|
74
|
+
const showClearButton = clearable && hasValue && !disabled && !readOnly;
|
|
75
|
+
const showCalendarIcon = !readOnly && !disabled;
|
|
76
|
+
|
|
77
|
+
const calendarAction = {
|
|
78
|
+
colorTheme: hasError ? 'error' : isOpen ? 'primary' : 'gray',
|
|
79
|
+
Icon: CustomIcon || 'IconCalendarEvent',
|
|
80
|
+
onPress: openCalendar,
|
|
81
|
+
size: 'small',
|
|
82
|
+
variant: 'text'
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const clearAction = {
|
|
86
|
+
colorTheme: 'secondary',
|
|
87
|
+
Icon: 'IconX',
|
|
88
|
+
onPress: handleClear,
|
|
89
|
+
size: 'small',
|
|
90
|
+
variant: 'text'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const rightAdornments = showCalendarIcon
|
|
94
|
+
? [
|
|
95
|
+
{
|
|
96
|
+
name: COMPONENT_KEYS.ACTION,
|
|
97
|
+
props: { action: showClearButton ? clearAction : calendarAction }
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
: [];
|
|
101
|
+
|
|
102
|
+
const inputStyle = isTransparent
|
|
103
|
+
? { container: { backgroundColor: 'transparent', borderColor: 'transparent' } }
|
|
104
|
+
: { container: { alignItems: 'stretch', paddingHorizontal: 12 } };
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<UTDatePickerLayout
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
displayPlaceholder={displayPlaceholder}
|
|
110
|
+
displayValue={displayValue}
|
|
111
|
+
errorMessage={errorMessage}
|
|
112
|
+
handleClose={handleClose}
|
|
113
|
+
handleDaySelect={handleDaySelect}
|
|
114
|
+
hasError={hasError}
|
|
115
|
+
helpText={helpText}
|
|
116
|
+
inputStyle={inputStyle}
|
|
117
|
+
isOpen={isOpen}
|
|
118
|
+
maxDate={maxDate}
|
|
119
|
+
minDate={minDate}
|
|
120
|
+
modalProps={modalProps}
|
|
121
|
+
onPress={openCalendar}
|
|
122
|
+
pickedDate={pickedDate}
|
|
123
|
+
readOnly={readOnly}
|
|
124
|
+
required={required}
|
|
125
|
+
rightAdornments={rightAdornments}
|
|
126
|
+
style={style}
|
|
127
|
+
title={title}
|
|
128
|
+
variant={variant}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
UTDatePicker.propTypes = propTypes;
|
|
134
|
+
|
|
135
|
+
export default UTDatePicker;
|