@herca/rn-kit 0.1.64 → 0.1.65
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/README.md +8 -0
- package/lib/module/DatePicker/DatePicker.js +2 -2
- package/lib/module/DatePicker/DatePicker.js.map +1 -1
- package/lib/module/Input/InputDate.js +170 -0
- package/lib/module/Input/InputDate.js.map +1 -0
- package/lib/module/Input/README_INPUT_DATE.md +265 -0
- package/lib/module/Ui/README_VIEWINSETS.md +126 -0
- package/lib/module/Ui/ViewInsets.js +33 -0
- package/lib/module/Ui/ViewInsets.js.map +1 -0
- package/lib/module/function/README_DATE_FORMATTER.md +397 -0
- package/lib/module/function/dateFormatter.js +38 -0
- package/lib/module/function/dateFormatter.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/Calendar/CalendarPropsType.d.ts +5 -0
- package/lib/typescript/src/Calendar/CalendarPropsType.d.ts.map +1 -1
- package/lib/typescript/src/Input/InputDate.d.ts +27 -0
- package/lib/typescript/src/Input/InputDate.d.ts.map +1 -0
- package/lib/typescript/src/Ui/ViewInsets.d.ts +13 -0
- package/lib/typescript/src/Ui/ViewInsets.d.ts.map +1 -0
- package/lib/typescript/src/function/dateFormatter.d.ts +14 -0
- package/lib/typescript/src/function/dateFormatter.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Calendar/CalendarPropsType.tsx +6 -0
- package/src/DatePicker/DatePicker.tsx +2 -2
- package/src/Input/InputDate.tsx +216 -0
- package/src/Input/README_INPUT_DATE.md +265 -0
- package/src/Ui/README_VIEWINSETS.md +126 -0
- package/src/Ui/ViewInsets.tsx +46 -0
- package/src/function/README_DATE_FORMATTER.md +397 -0
- package/src/function/dateFormatter.ts +54 -0
- package/src/index.tsx +8 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Color,
|
|
3
|
+
DatePicker,
|
|
4
|
+
Icon,
|
|
5
|
+
Typography,
|
|
6
|
+
type CalendarModeType,
|
|
7
|
+
type DateRangeProps,
|
|
8
|
+
} from '@herca/rn-kit';
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
11
|
+
import type { FormattedDateRangeProps } from '../Calendar/CalendarPropsType';
|
|
12
|
+
import { dateFormatter } from '../function/dateFormatter';
|
|
13
|
+
|
|
14
|
+
type DatePickerProps = React.ComponentProps<typeof DatePicker>;
|
|
15
|
+
type DatePickerPropsWithoutOnChange = Omit<DatePickerProps, 'onChange'>;
|
|
16
|
+
|
|
17
|
+
interface BaseProps {
|
|
18
|
+
label: string;
|
|
19
|
+
placeholder: string;
|
|
20
|
+
datePickerProps?: DatePickerPropsWithoutOnChange;
|
|
21
|
+
value?: string;
|
|
22
|
+
valueDateEnd?: string;
|
|
23
|
+
onSelectClick?: () => void;
|
|
24
|
+
onDatePickerClose?: () => void;
|
|
25
|
+
onDateChange?: (value: DateRangeProps) => void;
|
|
26
|
+
mode?: CalendarModeType;
|
|
27
|
+
language?: 'en' | 'id';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type SingleModeProps = BaseProps & {
|
|
31
|
+
mode?: 'single';
|
|
32
|
+
placeholderDateEnd?: never;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type RangeModeProps = BaseProps & {
|
|
36
|
+
mode: 'range';
|
|
37
|
+
placeholderDateEnd: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type Props = SingleModeProps | RangeModeProps;
|
|
41
|
+
|
|
42
|
+
export default function InputDate({
|
|
43
|
+
label,
|
|
44
|
+
placeholder = 'Select here...',
|
|
45
|
+
placeholderDateEnd = 'Select here...',
|
|
46
|
+
datePickerProps,
|
|
47
|
+
value,
|
|
48
|
+
valueDateEnd,
|
|
49
|
+
onSelectClick,
|
|
50
|
+
onDatePickerClose,
|
|
51
|
+
onDateChange,
|
|
52
|
+
mode = 'single',
|
|
53
|
+
language,
|
|
54
|
+
...props
|
|
55
|
+
}: Props) {
|
|
56
|
+
const [isDatePickerOpen, setSelectOpen] = useState<boolean>(false);
|
|
57
|
+
const [dateValue, setDateValue] = useState<FormattedDateRangeProps>();
|
|
58
|
+
const [raw, setRaw] = useState<DateRangeProps>();
|
|
59
|
+
|
|
60
|
+
const handleOpenSelect = () => {
|
|
61
|
+
setSelectOpen(true);
|
|
62
|
+
onSelectClick?.();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleCloseDatePicker = () => {
|
|
66
|
+
setSelectOpen(false);
|
|
67
|
+
onDatePickerClose?.();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleDateChange = (val: any) => {
|
|
71
|
+
onDateChange?.({
|
|
72
|
+
date: val.date ?? null,
|
|
73
|
+
startDate: val.startDate ?? null,
|
|
74
|
+
endDate: val.endDate ?? null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const formattedValue = dateFormatter({
|
|
78
|
+
date: val.date,
|
|
79
|
+
options: {
|
|
80
|
+
language,
|
|
81
|
+
format: language ? 'localized' : 'default',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const formattedStartDate = dateFormatter({
|
|
86
|
+
date: val.startDate,
|
|
87
|
+
options: {
|
|
88
|
+
language,
|
|
89
|
+
format: language ? 'localized' : 'default',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const formattedEndDate = dateFormatter({
|
|
94
|
+
date: val.endDate,
|
|
95
|
+
options: {
|
|
96
|
+
language,
|
|
97
|
+
format: language ? 'localized' : 'default',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
setDateValue({
|
|
102
|
+
date: formattedValue,
|
|
103
|
+
startDate: formattedStartDate,
|
|
104
|
+
endDate: formattedEndDate,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
setRaw(val);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const startDate = () => {
|
|
111
|
+
if (!value) {
|
|
112
|
+
if (mode === 'single') {
|
|
113
|
+
return dateValue?.date;
|
|
114
|
+
} else if (mode === 'range') {
|
|
115
|
+
return dateValue?.startDate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const endDate = () => {
|
|
123
|
+
if (mode === 'single') return null;
|
|
124
|
+
|
|
125
|
+
if (!valueDateEnd) {
|
|
126
|
+
return dateValue?.endDate;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return valueDateEnd;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const rawValue = () => {
|
|
133
|
+
if (mode === 'single') {
|
|
134
|
+
return {
|
|
135
|
+
date: datePickerProps?.value?.date ?? raw?.date,
|
|
136
|
+
};
|
|
137
|
+
} else {
|
|
138
|
+
return {
|
|
139
|
+
startDate: datePickerProps?.value?.startDate ?? raw?.startDate,
|
|
140
|
+
endDate: datePickerProps?.value?.endDate ?? raw?.endDate,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<View style={styles.gap4}>
|
|
147
|
+
<Typography variant="t2" weight="semibold" color={Color.gray[900]}>
|
|
148
|
+
{label}
|
|
149
|
+
</Typography>
|
|
150
|
+
|
|
151
|
+
<View style={styles.wrapper}>
|
|
152
|
+
{/* sart date */}
|
|
153
|
+
<Pressable
|
|
154
|
+
style={[styles.pickerTrigger, value !== '' && styles.hasValue]}
|
|
155
|
+
onPress={handleOpenSelect}
|
|
156
|
+
{...props}
|
|
157
|
+
>
|
|
158
|
+
<Typography variant="t2" color={Color.gray[900]} weight="medium">
|
|
159
|
+
{startDate() ?? placeholder}
|
|
160
|
+
</Typography>
|
|
161
|
+
<Icon name="ArrowDown" size={14} color={Color.gray[700]} />
|
|
162
|
+
</Pressable>
|
|
163
|
+
|
|
164
|
+
{/* end date */}
|
|
165
|
+
{mode === 'range' && (
|
|
166
|
+
<Pressable
|
|
167
|
+
style={[styles.pickerTrigger, value !== '' && styles.hasValue]}
|
|
168
|
+
onPress={handleOpenSelect}
|
|
169
|
+
{...props}
|
|
170
|
+
>
|
|
171
|
+
<Typography variant="t2" color={Color.gray[900]} weight="medium">
|
|
172
|
+
{endDate() ?? placeholderDateEnd}
|
|
173
|
+
</Typography>
|
|
174
|
+
<Icon name="ArrowDown" size={14} color={Color.gray[700]} />
|
|
175
|
+
</Pressable>
|
|
176
|
+
)}
|
|
177
|
+
</View>
|
|
178
|
+
|
|
179
|
+
<DatePicker
|
|
180
|
+
{...datePickerProps}
|
|
181
|
+
mode={mode}
|
|
182
|
+
isOpen={isDatePickerOpen}
|
|
183
|
+
onClose={handleCloseDatePicker}
|
|
184
|
+
value={rawValue()}
|
|
185
|
+
onChange={(val: DateRangeProps) => {
|
|
186
|
+
handleDateChange(val);
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
</View>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
gap4: {
|
|
195
|
+
gap: 4,
|
|
196
|
+
},
|
|
197
|
+
pickerTrigger: {
|
|
198
|
+
paddingVertical: 10,
|
|
199
|
+
paddingHorizontal: 12,
|
|
200
|
+
borderRadius: 8,
|
|
201
|
+
flexDirection: 'row',
|
|
202
|
+
justifyContent: 'space-between',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
borderWidth: 1,
|
|
205
|
+
borderColor: Color.gray[200],
|
|
206
|
+
flex: 1,
|
|
207
|
+
},
|
|
208
|
+
hasValue: {
|
|
209
|
+
borderColor: Color.primary[300],
|
|
210
|
+
},
|
|
211
|
+
wrapper: {
|
|
212
|
+
flexDirection: 'row',
|
|
213
|
+
gap: 8,
|
|
214
|
+
alignItems: 'center',
|
|
215
|
+
},
|
|
216
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# InputDate Component
|
|
2
|
+
|
|
3
|
+
Komponen React Native untuk input tanggal dengan dukungan mode single date dan date range. Terintegrasi dengan DatePicker dari `@herca/rn-kit` dan menyediakan UI yang user-friendly untuk pemilihan tanggal.
|
|
4
|
+
|
|
5
|
+
## Penggunaan Dasar
|
|
6
|
+
|
|
7
|
+
### Single Date Mode
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
import InputDate from './InputDate';
|
|
11
|
+
|
|
12
|
+
function MyForm() {
|
|
13
|
+
const [date, setDate] = useState('');
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<InputDate
|
|
17
|
+
label="Tanggal Lahir"
|
|
18
|
+
placeholder="Pilih tanggal"
|
|
19
|
+
value={date}
|
|
20
|
+
onDateChange={(val) => setDate(val.date)}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Date Range Mode
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
import InputDate from './InputDate';
|
|
30
|
+
|
|
31
|
+
function MyForm() {
|
|
32
|
+
const [startDate, setStartDate] = useState('');
|
|
33
|
+
const [endDate, setEndDate] = useState('');
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<InputDate
|
|
37
|
+
mode="range"
|
|
38
|
+
label="Periode"
|
|
39
|
+
placeholder="Tanggal mulai"
|
|
40
|
+
placeholderDateEnd="Tanggal selesai"
|
|
41
|
+
value={startDate}
|
|
42
|
+
valueDateEnd={endDate}
|
|
43
|
+
onDateChange={(val) => {
|
|
44
|
+
setStartDate(val.startDate);
|
|
45
|
+
setEndDate(val.endDate);
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Props
|
|
53
|
+
|
|
54
|
+
### Base Props
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Deskripsi |
|
|
57
|
+
| ------------------- | --------------------------------- | ------------------ | -------------------------------------------------- |
|
|
58
|
+
| `label` | `string` | **Required** | Label yang ditampilkan di atas input |
|
|
59
|
+
| `placeholder` | `string` | `"Select here..."` | Placeholder untuk tanggal pertama/single |
|
|
60
|
+
| `value` | `string` | `undefined` | Nilai tanggal terpilih (formatted) |
|
|
61
|
+
| `valueDateEnd` | `string` | `undefined` | Nilai tanggal akhir untuk mode range (formatted) |
|
|
62
|
+
| `datePickerProps` | `DatePickerPropsWithoutOnChange` | `undefined` | Props tambahan untuk DatePicker (kecuali onChange) |
|
|
63
|
+
| `onSelectClick` | `() => void` | `undefined` | Callback ketika input diklik |
|
|
64
|
+
| `onDatePickerClose` | `() => void` | `undefined` | Callback ketika DatePicker ditutup |
|
|
65
|
+
| `onDateChange` | `(value: DateRangeProps) => void` | `undefined` | Callback ketika tanggal berubah |
|
|
66
|
+
| `mode` | `'single' \| 'range'` | `'single'` | Mode pemilihan tanggal |
|
|
67
|
+
| `language` | `'en' \| 'id'` | `undefined` | Bahasa untuk format tanggal |
|
|
68
|
+
|
|
69
|
+
### Single Mode Specific Props
|
|
70
|
+
|
|
71
|
+
Ketika `mode` tidak diset atau `mode="single"`:
|
|
72
|
+
|
|
73
|
+
| Prop | Type | Deskripsi |
|
|
74
|
+
| -------------------- | ------- | ----------------------------- |
|
|
75
|
+
| `placeholderDateEnd` | `never` | Tidak tersedia di single mode |
|
|
76
|
+
|
|
77
|
+
### Range Mode Specific Props
|
|
78
|
+
|
|
79
|
+
Ketika `mode="range"`:
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Deskripsi |
|
|
82
|
+
| -------------------- | -------- | ---------------------------------------------- |
|
|
83
|
+
| `placeholderDateEnd` | `string` | **Required** - Placeholder untuk tanggal akhir |
|
|
84
|
+
|
|
85
|
+
## Return Value dari onDateChange
|
|
86
|
+
|
|
87
|
+
Callback `onDateChange` menerima objek dengan struktur:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
{
|
|
91
|
+
date: string | null; // Untuk single mode
|
|
92
|
+
startDate: string | null; // Untuk range mode (tanggal mulai)
|
|
93
|
+
endDate: string | null; // Untuk range mode (tanggal selesai)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Contoh Penggunaan
|
|
98
|
+
|
|
99
|
+
### Single Date dengan Language
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
<InputDate
|
|
103
|
+
label="Select Date"
|
|
104
|
+
placeholder="Choose a date"
|
|
105
|
+
mode="single"
|
|
106
|
+
language="id"
|
|
107
|
+
onDateChange={(val) => console.log(val.date)}
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Date Range dengan Controlled Values
|
|
112
|
+
|
|
113
|
+
```jsx
|
|
114
|
+
function BookingForm() {
|
|
115
|
+
const [dateRange, setDateRange] = useState({
|
|
116
|
+
start: '',
|
|
117
|
+
end: '',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<InputDate
|
|
122
|
+
mode="range"
|
|
123
|
+
label="Periode Booking"
|
|
124
|
+
placeholder="Check-in"
|
|
125
|
+
placeholderDateEnd="Check-out"
|
|
126
|
+
value={dateRange.start}
|
|
127
|
+
valueDateEnd={dateRange.end}
|
|
128
|
+
onDateChange={(val) => {
|
|
129
|
+
setDateRange({
|
|
130
|
+
start: val.startDate || '',
|
|
131
|
+
end: val.endDate || '',
|
|
132
|
+
});
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Dengan Custom DatePicker Props
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
<InputDate
|
|
143
|
+
label="Tanggal Lahir"
|
|
144
|
+
placeholder="Pilih tanggal"
|
|
145
|
+
datePickerProps={{
|
|
146
|
+
minDate: new Date('1900-01-01'),
|
|
147
|
+
maxDate: new Date(),
|
|
148
|
+
value: { date: new Date() },
|
|
149
|
+
}}
|
|
150
|
+
onDateChange={(val) => console.log(val)}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Dengan Event Callbacks
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
<InputDate
|
|
158
|
+
label="Appointment Date"
|
|
159
|
+
placeholder="Select date"
|
|
160
|
+
onSelectClick={() => console.log('Picker opened')}
|
|
161
|
+
onDatePickerClose={() => console.log('Picker closed')}
|
|
162
|
+
onDateChange={(val) => {
|
|
163
|
+
console.log('Date changed:', val);
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### English Language Format
|
|
169
|
+
|
|
170
|
+
```jsx
|
|
171
|
+
<InputDate
|
|
172
|
+
label="Birth Date"
|
|
173
|
+
placeholder="Select date"
|
|
174
|
+
language="en"
|
|
175
|
+
onDateChange={(val) => console.log(val.date)}
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Indonesian Language Format
|
|
180
|
+
|
|
181
|
+
```jsx
|
|
182
|
+
<InputDate
|
|
183
|
+
label="Tanggal Lahir"
|
|
184
|
+
placeholder="Pilih tanggal"
|
|
185
|
+
language="id"
|
|
186
|
+
onDateChange={(val) => console.log(val.date)}
|
|
187
|
+
/>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Fitur
|
|
191
|
+
|
|
192
|
+
- ✅ Dua mode: Single date dan Date range
|
|
193
|
+
- ✅ Support multi-bahasa (English & Indonesian)
|
|
194
|
+
- ✅ Controlled component dengan value props
|
|
195
|
+
- ✅ Auto-formatting tanggal berdasarkan bahasa
|
|
196
|
+
- ✅ Visual feedback untuk nilai yang sudah dipilih
|
|
197
|
+
- ✅ Terintegrasi dengan DatePicker dari @herca/rn-kit
|
|
198
|
+
- ✅ TypeScript support dengan discriminated unions untuk type safety
|
|
199
|
+
- ✅ Customizable melalui datePickerProps
|
|
200
|
+
- ✅ Event callbacks untuk lifecycle DatePicker
|
|
201
|
+
|
|
202
|
+
## Styling
|
|
203
|
+
|
|
204
|
+
Komponen menggunakan styling internal dengan karakteristik:
|
|
205
|
+
|
|
206
|
+
- Border color berubah ke `primary[300]` ketika ada nilai terpilih
|
|
207
|
+
- Layout responsive dengan flexbox
|
|
208
|
+
- Gap 8px antara input start date dan end date (mode range)
|
|
209
|
+
- Border radius 8px dengan padding yang konsisten
|
|
210
|
+
- Typography menggunakan variant `t2` dari @herca/rn-kit
|
|
211
|
+
|
|
212
|
+
## State Management Internal
|
|
213
|
+
|
|
214
|
+
Komponen mengelola beberapa state internal:
|
|
215
|
+
|
|
216
|
+
- `isDatePickerOpen`: Status buka/tutup DatePicker
|
|
217
|
+
- `dateValue`: Nilai tanggal yang sudah diformat
|
|
218
|
+
- `raw`: Nilai tanggal raw dari DatePicker
|
|
219
|
+
|
|
220
|
+
State internal ini memungkinkan komponen bekerja baik sebagai controlled maupun uncontrolled component.
|
|
221
|
+
|
|
222
|
+
## TypeScript
|
|
223
|
+
|
|
224
|
+
Komponen menggunakan discriminated unions untuk type safety antara single dan range mode:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
type SingleModeProps = BaseProps & {
|
|
228
|
+
mode?: 'single';
|
|
229
|
+
placeholderDateEnd?: never;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
type RangeModeProps = BaseProps & {
|
|
233
|
+
mode: 'range';
|
|
234
|
+
placeholderDateEnd: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export type Props = SingleModeProps | RangeModeProps;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Ini memastikan `placeholderDateEnd` hanya required ketika `mode="range"`.
|
|
241
|
+
|
|
242
|
+
## Dependencies
|
|
243
|
+
|
|
244
|
+
- `@herca/rn-kit` - Untuk komponen DatePicker, Icon, Typography, dan Color
|
|
245
|
+
- `react-native` - Core components (View, Pressable, StyleSheet)
|
|
246
|
+
|
|
247
|
+
## Best Practices
|
|
248
|
+
|
|
249
|
+
1. **Selalu provide label yang jelas** untuk accessibility
|
|
250
|
+
2. **Gunakan language prop** untuk konsistensi format tanggal di aplikasi
|
|
251
|
+
3. **Handle onDateChange** untuk update state parent component
|
|
252
|
+
4. **Gunakan controlled values** untuk form validation
|
|
253
|
+
5. **Provide meaningful placeholders** yang menjelaskan expected input
|
|
254
|
+
|
|
255
|
+
## Catatan
|
|
256
|
+
|
|
257
|
+
- Props tambahan akan diteruskan ke Pressable trigger
|
|
258
|
+
- Nilai yang dikembalikan via `onDateChange` selalu dalam format yang sudah diproses menggunakan `dateFormatter`
|
|
259
|
+
- Raw value dari DatePicker tetap tersimpan untuk keperluan internal
|
|
260
|
+
- Komponen dapat bekerja sebagai controlled maupun uncontrolled component
|
|
261
|
+
- Border color otomatis berubah ketika ada nilai terpilih untuk visual feedback
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ViewInsets Component
|
|
2
|
+
|
|
3
|
+
Komponen React Native yang menyediakan padding otomatis berdasarkan safe area insets untuk menghindari area sistem seperti notch, status bar, dan navigation bar.
|
|
4
|
+
|
|
5
|
+
## Instalasi
|
|
6
|
+
|
|
7
|
+
Komponen ini membutuhkan dependency berikut:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install react-native-safe-area-context
|
|
11
|
+
# atau
|
|
12
|
+
yarn add react-native-safe-area-context
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Pastikan untuk membungkus aplikasi Anda dengan `SafeAreaProvider`:
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return <SafeAreaProvider>{/* Konten aplikasi Anda */}</SafeAreaProvider>;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Penggunaan Dasar
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
import ViewInsets from './ViewInsets';
|
|
29
|
+
|
|
30
|
+
function MyScreen() {
|
|
31
|
+
return (
|
|
32
|
+
<ViewInsets useTop useBottom>
|
|
33
|
+
{/* Konten Anda */}
|
|
34
|
+
</ViewInsets>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Props
|
|
40
|
+
|
|
41
|
+
| Prop | Type | Default | Deskripsi |
|
|
42
|
+
| ----------- | ---------------------- | ------------ | ------------------------------------------------------- |
|
|
43
|
+
| `children` | `React.ReactNode` | **Required** | Konten yang akan ditampilkan di dalam container |
|
|
44
|
+
| `useTop` | `boolean` | `false` | Menambahkan padding top sesuai safe area inset atas |
|
|
45
|
+
| `useBottom` | `boolean` | `false` | Menambahkan padding bottom sesuai safe area inset bawah |
|
|
46
|
+
| `useLeft` | `boolean` | `false` | Menambahkan padding left sesuai safe area inset kiri |
|
|
47
|
+
| `useRight` | `boolean` | `false` | Menambahkan padding right sesuai safe area inset kanan |
|
|
48
|
+
| `style` | `StyleProp<ViewStyle>` | `undefined` | Style tambahan untuk container |
|
|
49
|
+
| `...rest` | `ViewProps` | - | Props lainnya akan diteruskan ke komponen View |
|
|
50
|
+
|
|
51
|
+
## Contoh Penggunaan
|
|
52
|
+
|
|
53
|
+
### Layar dengan Safe Area Atas dan Bawah
|
|
54
|
+
|
|
55
|
+
```jsx
|
|
56
|
+
<ViewInsets useTop useBottom>
|
|
57
|
+
<Text>Konten aman dari notch dan navigation bar</Text>
|
|
58
|
+
</ViewInsets>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Layar dengan Safe Area Atas Saja
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
<ViewInsets useTop>
|
|
65
|
+
<Text>Konten aman dari status bar/notch</Text>
|
|
66
|
+
</ViewInsets>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Layar dengan Safe Area di Semua Sisi
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
<ViewInsets useTop useBottom useLeft useRight>
|
|
73
|
+
<Text>Konten aman dari semua area sistem</Text>
|
|
74
|
+
</ViewInsets>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Dengan Custom Style
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
<ViewInsets
|
|
81
|
+
useTop
|
|
82
|
+
useBottom
|
|
83
|
+
style={{ backgroundColor: '#fff', paddingHorizontal: 16 }}
|
|
84
|
+
>
|
|
85
|
+
<Text>Konten dengan style tambahan</Text>
|
|
86
|
+
</ViewInsets>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Dengan Props View Lainnya
|
|
90
|
+
|
|
91
|
+
```jsx
|
|
92
|
+
<ViewInsets useTop useBottom testID="main-container" accessible={true}>
|
|
93
|
+
<Text>Konten dengan props accessibility</Text>
|
|
94
|
+
</ViewInsets>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Fitur
|
|
98
|
+
|
|
99
|
+
- ✅ Otomatis menangani safe area insets
|
|
100
|
+
- ✅ Kontrol granular untuk setiap sisi (atas, bawah, kiri, kanan)
|
|
101
|
+
- ✅ Performa optimal dengan `useMemo` untuk menghindari re-render tidak perlu
|
|
102
|
+
- ✅ Mendukung semua props dari komponen View React Native
|
|
103
|
+
- ✅ TypeScript support dengan type definitions lengkap
|
|
104
|
+
- ✅ Default `flex: 1` untuk kemudahan layout
|
|
105
|
+
|
|
106
|
+
## Catatan
|
|
107
|
+
|
|
108
|
+
- Komponen ini secara default menggunakan `flex: 1` pada container
|
|
109
|
+
- Safe area insets akan disesuaikan secara otomatis berdasarkan perangkat
|
|
110
|
+
- Berguna untuk menghindari area yang tertutup oleh notch, status bar, home indicator, dll
|
|
111
|
+
- Dapat dikombinasikan dengan style custom untuk kebutuhan layout yang lebih kompleks
|
|
112
|
+
|
|
113
|
+
## TypeScript
|
|
114
|
+
|
|
115
|
+
Komponen ini sudah dilengkapi dengan TypeScript definitions. Interface `ViewInsetsProps` extends dari `ViewProps` sehingga mendukung semua props dari View React Native.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
interface ViewInsetsProps extends ViewProps {
|
|
119
|
+
children: React.ReactNode;
|
|
120
|
+
useTop?: boolean;
|
|
121
|
+
useBottom?: boolean;
|
|
122
|
+
useLeft?: boolean;
|
|
123
|
+
useRight?: boolean;
|
|
124
|
+
bottomType?: 'inset' | 'tabbar'; // Prop ini didefinisikan tapi belum diimplementasikan
|
|
125
|
+
}
|
|
126
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
|
|
5
|
+
interface ViewInsetsProps extends ViewProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
useTop?: boolean;
|
|
8
|
+
useBottom?: boolean;
|
|
9
|
+
useLeft?: boolean;
|
|
10
|
+
useRight?: boolean;
|
|
11
|
+
bottomType?: 'inset' | 'tabbar';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ViewInsets: React.FC<ViewInsetsProps> = ({
|
|
15
|
+
children,
|
|
16
|
+
style,
|
|
17
|
+
useTop = false,
|
|
18
|
+
useBottom = false,
|
|
19
|
+
useLeft = false,
|
|
20
|
+
useRight = false,
|
|
21
|
+
...rest
|
|
22
|
+
}) => {
|
|
23
|
+
const insets = useSafeAreaInsets();
|
|
24
|
+
|
|
25
|
+
const defaultStyle = useMemo(
|
|
26
|
+
() =>
|
|
27
|
+
StyleSheet.create({
|
|
28
|
+
container: {
|
|
29
|
+
paddingTop: useTop ? insets.top : 0,
|
|
30
|
+
paddingBottom: useBottom ? insets.bottom : 0,
|
|
31
|
+
paddingLeft: useLeft ? insets.left : 0,
|
|
32
|
+
paddingRight: useRight ? insets.right : 0,
|
|
33
|
+
flex: 1,
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
[insets, useTop, useBottom, useLeft, useRight]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View style={[defaultStyle.container, style]} {...rest}>
|
|
41
|
+
{children}
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default ViewInsets;
|