@widergy/mobile-ui 2.17.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 +9 -3
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
## [2.17.1](https://github.com/widergy/mobile-ui/compare/v2.17.0...v2.17.1) (2026-06-05)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* [DIS-1103] ut date picker ([#507](https://github.com/widergy/mobile-ui/issues/507)) ([ffd6db3](https://github.com/widergy/mobile-ui/commit/ffd6db3c847dca170a5df955c1149a880c7f42cb))
|
|
7
|
+
|
|
8
|
+
# [2.17.0](https://github.com/widergy/mobile-ui/compare/v2.16.0...v2.17.0) (2026-06-03)
|
|
9
|
+
|
|
10
|
+
### Novedades y Mejoras
|
|
5
11
|
|
|
6
|
-
*
|
|
12
|
+
* Se amplió el componente de banner con nuevas opciones de presentación: categoría encima del título, texto de ayuda debajo de la descripción, disposición del ícono y los botones en fila o columna, un segundo botón opcional y la posibilidad de cerrarlo desde una esquina. [#505](https://github.com/widergy/Energy-UI-Mobile/pull/505) [DIS-1102](https://widergy.atlassian.net/browse/DIS-1102)
|
|
7
13
|
|
|
8
14
|
# [2.16.0](https://github.com/widergy/mobile-ui/compare/v2.15.0...v2.16.0) (2026-05-15)
|
|
9
15
|
|
|
@@ -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;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
import { array, bool, func, object, string } from 'prop-types';
|
|
4
|
+
|
|
5
|
+
import UTBaseInputField from '../UTBaseInputField';
|
|
6
|
+
import UTBottomSheet from '../UTBottomSheet';
|
|
7
|
+
import UTFieldLabel from '../UTFieldLabel';
|
|
8
|
+
import UTLabel from '../UTLabel';
|
|
9
|
+
import UTValidation from '../UTValidation';
|
|
10
|
+
import { TEST_IDS } from '../../constants/testIds';
|
|
11
|
+
|
|
12
|
+
import Calendar from './components/Calendar';
|
|
13
|
+
import { VARIANTS } from './constants';
|
|
14
|
+
import styles from './styles';
|
|
15
|
+
|
|
16
|
+
const { UTDatePicker: datePickerTestIds } = TEST_IDS;
|
|
17
|
+
|
|
18
|
+
const UTDatePickerLayout = ({
|
|
19
|
+
disabled,
|
|
20
|
+
displayPlaceholder,
|
|
21
|
+
displayValue,
|
|
22
|
+
errorMessage,
|
|
23
|
+
handleClose,
|
|
24
|
+
handleDaySelect,
|
|
25
|
+
hasError,
|
|
26
|
+
helpText,
|
|
27
|
+
inputStyle,
|
|
28
|
+
isOpen,
|
|
29
|
+
maxDate,
|
|
30
|
+
minDate,
|
|
31
|
+
modalProps,
|
|
32
|
+
onPress,
|
|
33
|
+
pickedDate,
|
|
34
|
+
readOnly,
|
|
35
|
+
required,
|
|
36
|
+
rightAdornments,
|
|
37
|
+
style,
|
|
38
|
+
title,
|
|
39
|
+
variant
|
|
40
|
+
}) => (
|
|
41
|
+
<View style={[styles.root, style?.root]} testID={datePickerTestIds.root}>
|
|
42
|
+
{!!title && <UTFieldLabel required={required}>{title}</UTFieldLabel>}
|
|
43
|
+
|
|
44
|
+
<Pressable onPress={onPress} style={styles.pressable}>
|
|
45
|
+
<UTBaseInputField
|
|
46
|
+
alwaysShowPlaceholder
|
|
47
|
+
dataTestId={datePickerTestIds.input}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
editable={false}
|
|
50
|
+
error={hasError}
|
|
51
|
+
placeholder={displayPlaceholder}
|
|
52
|
+
readOnly={readOnly}
|
|
53
|
+
rightAdornments={rightAdornments}
|
|
54
|
+
style={inputStyle}
|
|
55
|
+
value={displayValue}
|
|
56
|
+
/>
|
|
57
|
+
</Pressable>
|
|
58
|
+
|
|
59
|
+
{!!helpText && (
|
|
60
|
+
<UTLabel colorTheme="gray" variant="small">
|
|
61
|
+
{helpText}
|
|
62
|
+
</UTLabel>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{variant === VARIANTS.select && !!errorMessage && (
|
|
66
|
+
<UTValidation
|
|
67
|
+
dataTestId={datePickerTestIds.errorMessage}
|
|
68
|
+
validationData={[{ items: [{ status: 'error', text: errorMessage }] }]}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
<UTBottomSheet
|
|
73
|
+
adjustableHeight
|
|
74
|
+
onClose={handleClose}
|
|
75
|
+
visible={isOpen}
|
|
76
|
+
withBodyPadding={false}
|
|
77
|
+
{...modalProps}
|
|
78
|
+
>
|
|
79
|
+
<Calendar maxDate={maxDate} minDate={minDate} onDaySelect={handleDaySelect} pickedDate={pickedDate} />
|
|
80
|
+
</UTBottomSheet>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
UTDatePickerLayout.propTypes = {
|
|
85
|
+
disabled: bool,
|
|
86
|
+
displayPlaceholder: string,
|
|
87
|
+
displayValue: string,
|
|
88
|
+
errorMessage: string,
|
|
89
|
+
handleClose: func,
|
|
90
|
+
handleDaySelect: func,
|
|
91
|
+
hasError: bool,
|
|
92
|
+
helpText: string,
|
|
93
|
+
inputStyle: object,
|
|
94
|
+
isOpen: bool,
|
|
95
|
+
maxDate: string,
|
|
96
|
+
minDate: string,
|
|
97
|
+
modalProps: object,
|
|
98
|
+
onPress: func,
|
|
99
|
+
pickedDate: object,
|
|
100
|
+
readOnly: bool,
|
|
101
|
+
required: bool,
|
|
102
|
+
rightAdornments: array,
|
|
103
|
+
style: object,
|
|
104
|
+
title: string,
|
|
105
|
+
variant: string
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default UTDatePickerLayout;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { bool, elementType, func, object, oneOf, shape, string } from 'prop-types';
|
|
2
|
+
|
|
3
|
+
export const propTypes = {
|
|
4
|
+
clearable: bool,
|
|
5
|
+
CustomIcon: elementType,
|
|
6
|
+
disabled: bool,
|
|
7
|
+
error: string,
|
|
8
|
+
helpText: string,
|
|
9
|
+
modalProps: object,
|
|
10
|
+
onChange: func,
|
|
11
|
+
placeholder: string,
|
|
12
|
+
range: shape({ maxDate: string, minDate: string }),
|
|
13
|
+
readOnly: bool,
|
|
14
|
+
required: bool,
|
|
15
|
+
size: oneOf(['sm', 'md']),
|
|
16
|
+
style: shape({ root: object }),
|
|
17
|
+
title: string,
|
|
18
|
+
value: string,
|
|
19
|
+
variant: oneOf(['select', 'picker'])
|
|
20
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
const styles = StyleSheet.create({
|
|
4
|
+
calendarRoot: {
|
|
5
|
+
paddingHorizontal: 24,
|
|
6
|
+
paddingTop: 16
|
|
7
|
+
},
|
|
8
|
+
dayCell: {
|
|
9
|
+
flex: 1,
|
|
10
|
+
paddingVertical: 2,
|
|
11
|
+
textAlign: 'center'
|
|
12
|
+
},
|
|
13
|
+
month: {
|
|
14
|
+
gap: 2,
|
|
15
|
+
minHeight: 260
|
|
16
|
+
},
|
|
17
|
+
nav: {
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
flexDirection: 'row',
|
|
20
|
+
justifyContent: 'space-between',
|
|
21
|
+
marginBottom: 24
|
|
22
|
+
},
|
|
23
|
+
navLabel: {
|
|
24
|
+
flex: 1,
|
|
25
|
+
textAlign: 'center'
|
|
26
|
+
},
|
|
27
|
+
navSpacer: {
|
|
28
|
+
width: 36
|
|
29
|
+
},
|
|
30
|
+
pickerColumn: {
|
|
31
|
+
flex: 1,
|
|
32
|
+
overflow: 'hidden'
|
|
33
|
+
},
|
|
34
|
+
pickerContainer: {
|
|
35
|
+
flexDirection: 'row',
|
|
36
|
+
gap: 8,
|
|
37
|
+
height: 260
|
|
38
|
+
},
|
|
39
|
+
pickerScroll: {
|
|
40
|
+
height: 260
|
|
41
|
+
},
|
|
42
|
+
pickerScrollContent: {
|
|
43
|
+
paddingBottom: 220
|
|
44
|
+
},
|
|
45
|
+
pressable: {
|
|
46
|
+
width: '100%'
|
|
47
|
+
},
|
|
48
|
+
root: {
|
|
49
|
+
gap: 8
|
|
50
|
+
},
|
|
51
|
+
week: {
|
|
52
|
+
flexDirection: 'row'
|
|
53
|
+
},
|
|
54
|
+
yearItem: {
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
borderRadius: 8,
|
|
57
|
+
height: 40,
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
width: '100%'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default styles;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const getDayCellButtonStyle = (selected, theme) => ({
|
|
2
|
+
root: {
|
|
3
|
+
alignItems: 'center',
|
|
4
|
+
flex: 1,
|
|
5
|
+
height: 36,
|
|
6
|
+
justifyContent: 'center',
|
|
7
|
+
paddingHorizontal: 0,
|
|
8
|
+
paddingVertical: 0,
|
|
9
|
+
...(selected && {
|
|
10
|
+
backgroundColor: theme.Palette.accent['04'],
|
|
11
|
+
borderRadius: 18
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const getYearItemStyle = (selected, theme) => ({
|
|
17
|
+
backgroundColor: selected ? theme.Palette.accent['04'] : undefined
|
|
18
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
3
|
+
|
|
4
|
+
import { DAY_UNIT, MONTH_UNIT, OUTPUT_LABEL_MASK, YEAR_RANGE_AFTER, YEAR_RANGE_BEFORE } from './constants';
|
|
5
|
+
|
|
6
|
+
dayjs.extend(customParseFormat);
|
|
7
|
+
|
|
8
|
+
export const isSelected = (date, selectedDate) => selectedDate && date.isSame(selectedDate, DAY_UNIT);
|
|
9
|
+
|
|
10
|
+
export const dateMatchesFormat = (date, targetFormat) => dayjs(date, targetFormat, true).isValid();
|
|
11
|
+
|
|
12
|
+
export const getFinalDate = ({ date, maxDate, minDate }) => {
|
|
13
|
+
const formattedDate = dayjs(date);
|
|
14
|
+
if (maxDate && formattedDate?.isAfter(dayjs(maxDate, OUTPUT_LABEL_MASK)))
|
|
15
|
+
return dayjs(maxDate, OUTPUT_LABEL_MASK);
|
|
16
|
+
if (minDate && formattedDate?.isBefore(dayjs(minDate, OUTPUT_LABEL_MASK)))
|
|
17
|
+
return dayjs(minDate, OUTPUT_LABEL_MASK);
|
|
18
|
+
return formattedDate;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getWeeks = month => {
|
|
22
|
+
const firstDay = month.startOf(MONTH_UNIT);
|
|
23
|
+
const lastDay = month.endOf(MONTH_UNIT);
|
|
24
|
+
const startDate = firstDay.subtract(firstDay.day(), DAY_UNIT);
|
|
25
|
+
|
|
26
|
+
const weeks = [];
|
|
27
|
+
let current = startDate;
|
|
28
|
+
|
|
29
|
+
do {
|
|
30
|
+
const weekStart = current;
|
|
31
|
+
weeks.push(Array.from({ length: 7 }, (_, i) => weekStart.add(i, DAY_UNIT)));
|
|
32
|
+
current = current.add(7, DAY_UNIT);
|
|
33
|
+
} while (current.isBefore(lastDay) || current.isSame(lastDay, DAY_UNIT));
|
|
34
|
+
|
|
35
|
+
return weeks;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getYearRange = (minDateStr, maxDateStr) => {
|
|
39
|
+
const currentYear = dayjs().year();
|
|
40
|
+
const minYear = minDateStr ? dayjs(minDateStr, OUTPUT_LABEL_MASK).year() : currentYear - YEAR_RANGE_BEFORE;
|
|
41
|
+
const maxYear = maxDateStr ? dayjs(maxDateStr, OUTPUT_LABEL_MASK).year() : currentYear + YEAR_RANGE_AFTER;
|
|
42
|
+
const years = [];
|
|
43
|
+
for (let y = maxYear; y >= minYear; y--) years.push(y);
|
|
44
|
+
return years;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const autoFormatDate = val => {
|
|
48
|
+
const digits = val.replace(/\D/g, '').slice(0, 8);
|
|
49
|
+
if (digits.length <= 2) return digits;
|
|
50
|
+
if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
|
|
51
|
+
return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
|
|
52
|
+
};
|
package/lib/constants/testIds.js
CHANGED
|
@@ -44,6 +44,19 @@ export const TEST_ID_CONSTANTS = {
|
|
|
44
44
|
|
|
45
45
|
export const TEST_IDS = {
|
|
46
46
|
modal: 'modal',
|
|
47
|
+
UTDatePicker: {
|
|
48
|
+
calendar: 'UTDatePicker.calendar',
|
|
49
|
+
calendarMonthYear: 'UTDatePicker.calendar.monthYear',
|
|
50
|
+
calendarNextBtn: 'UTDatePicker.calendar.nextBtn',
|
|
51
|
+
calendarPrevBtn: 'UTDatePicker.calendar.prevBtn',
|
|
52
|
+
errorMessage: 'UTDatePicker.errorMessage',
|
|
53
|
+
input: 'UTDatePicker.input',
|
|
54
|
+
monthItem: 'UTDatePicker.monthItem',
|
|
55
|
+
monthView: 'UTDatePicker.monthView',
|
|
56
|
+
root: 'UTDatePicker.root',
|
|
57
|
+
yearItem: 'UTDatePicker.yearItem',
|
|
58
|
+
yearView: 'UTDatePicker.yearView'
|
|
59
|
+
},
|
|
47
60
|
roundView: 'roundView',
|
|
48
61
|
skeletonLoader: 'skeletonLoader',
|
|
49
62
|
topbar: {
|
package/lib/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export { default as UTCuit } from './components/UTCuit';
|
|
|
49
49
|
export { default as UTCheckList } from './components/UTCheckList';
|
|
50
50
|
export { default as UTDataCategory } from './components/UTDataCategory';
|
|
51
51
|
export { default as UTDataElement } from './components/UTDataElement';
|
|
52
|
+
export { default as UTDatePicker } from './components/UTDatePicker';
|
|
52
53
|
export { default as UTDetailDrawer } from './components/UTDetailDrawer';
|
|
53
54
|
export { default as UTIcon } from './components/UTIcon';
|
|
54
55
|
export { default as UTImage } from './components/UTImage';
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@widergy/mobile-ui",
|
|
3
3
|
"description": "Widergy Mobile Components",
|
|
4
4
|
"author": "widergy",
|
|
5
|
-
"version": "2.17.
|
|
5
|
+
"version": "2.17.1",
|
|
6
6
|
"repository": "https://github.com/widergy/mobile-ui.git",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"files": [
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@tabler/icons-react-native": "^3.34.1",
|
|
44
44
|
"@widergy/web-utils": "^2.0.0",
|
|
45
45
|
"core-js": "3",
|
|
46
|
+
"dayjs": "^1.11.10",
|
|
46
47
|
"deprecated-react-native-prop-types": "^5.0.0",
|
|
47
48
|
"expo-document-picker": "^13.1.6",
|
|
48
49
|
"expo-image-manipulator": "^13.1.7",
|
|
@@ -73,12 +74,12 @@
|
|
|
73
74
|
"@commitlint/config-conventional": "^17.7.0",
|
|
74
75
|
"@eslint/compat": "^1.4.0",
|
|
75
76
|
"@eslint/eslintrc": "^3.3.0",
|
|
76
|
-
"@widergy/semantic-release-package-config": "^1.0.0",
|
|
77
|
-
"@widergy/eslint-config": "^1.0.0",
|
|
78
77
|
"@react-native/babel-preset": "0.73.0",
|
|
78
|
+
"@widergy/eslint-config": "^1.0.0",
|
|
79
|
+
"@widergy/semantic-release-package-config": "^1.0.0",
|
|
80
|
+
"babel-jest": "^29.6.2",
|
|
79
81
|
"babel-plugin-import-glob": "^2.0.0",
|
|
80
82
|
"babel-plugin-module-resolver": "^5.0.0",
|
|
81
|
-
"babel-jest": "^29.6.2",
|
|
82
83
|
"babel-preset-minify": "^0.5.2",
|
|
83
84
|
"eslint": "^9.38.0",
|
|
84
85
|
"eslint-config-airbnb": "^19.0.4",
|