Skip to content

Commit 0e533a5

Browse files
fix(core): MR-772 calendário do DateRange/DatePicker na largura dos dois campos (#1257)
* fix(core): calendário do daterange na largura dos dois campos (mr-772) O container do calendário deixa de ser filho da grid (ocupava só a primeira coluna) e passa a ser posicionado sob a linha dos campos, na largura inteira e sobreposto ao conteúdo seguinte (drawers/modais não sofrem reflow). Labels alinhadas ao Input vizinho. Stories para drawer, modal e linha com Input. Co-authored-by: Igor Monteiro <igor.monteiro@useblu.com.br> Co-Authored-By: igor.monteiro <igor.monteiro@useblu.com.br> * docs(core): story do daterange em container estreito (mr-772) Co-authored-by: Igor Monteiro <igor.monteiro@useblu.com.br> Co-Authored-By: igor.monteiro <igor.monteiro@useblu.com.br> * fix(react): erro por campo no daterange e fecha calendário ao exibir erro (mr-772) - error/helperText aceitam { from, to } além de boolean/string (compatível) - calendário fecha quando um erro chega, para a mensagem ficar visível - DatePicker (data única) mantém boolean/string Co-authored-by: Igor Monteiro <igor.monteiro@useblu.com.br> Co-Authored-By: igor.monteiro <igor.monteiro@useblu.com.br> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Igor Monteiro <igor.monteiro@useblu.com.br>
1 parent 92ffea6 commit 0e533a5

8 files changed

Lines changed: 192 additions & 29 deletions

File tree

‎packages/ocean-core/src/components/_date-picker.scss‎

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ $background-custom: rgba(176, 245, 245, 0.24) 50%,
1818
width: 100%;
1919

2020
&__calendar-container {
21-
position: relative;
21+
left: 0;
22+
position: absolute;
23+
right: 0;
24+
top: 100%;
25+
width: 100%;
26+
z-index: 1;
27+
28+
&--inline {
29+
position: relative;
30+
top: auto;
31+
z-index: auto;
32+
}
2233
}
2334

2435
&:active,
@@ -32,10 +43,12 @@ $background-custom: rgba(176, 245, 245, 0.24) 50%,
3243
display: grid;
3344
gap: $spacing-inline-xxs;
3445
grid-template-columns: repeat(2, 1fr);
46+
position: relative;
3547
width: 100%;
3648
}
3749

3850
&__form-row-datepicker {
51+
position: relative;
3952
width: 100%;
4053
}
4154

@@ -44,12 +57,15 @@ $background-custom: rgba(176, 245, 245, 0.24) 50%,
4457
margin: 0;
4558

4659
> label {
60+
align-items: center;
4761
color: $color-interface-dark-down;
62+
display: flex;
4863
font-family: $font-family-base;
4964
font-size: $font-size-xxs;
5065
font-weight: 400;
5166
line-height: 15px;
52-
margin: 0 0 $spacing-inline-xxs;
67+
margin: 0 $spacing-inline-xxxs $spacing-inline-xxs 0;
68+
min-height: 22px;
5369
padding: 0;
5470
text-transform: none;
5571
}
@@ -82,22 +98,21 @@ $background-custom: rgba(176, 245, 245, 0.24) 50%,
8298
border-radius: $border-radius-md;
8399
box-shadow: $shadow-level-2;
84100
box-sizing: border-box;
85-
margin: 0;
101+
left: 0;
102+
margin: $spacing-inline-xxxs 0 0;
86103
padding: 0 0 $spacing-inline-xs;
87104
position: absolute;
105+
right: 0;
88106
width: 100%;
89107

90108
.rdp-vhidden {
91109
display: none;
92110
}
93111
}
94112

95-
&_calendar_m1 {
96-
top: $spacing-inline-xxxs;
97-
}
98-
113+
&_calendar_m1,
99114
&_calendar_m2 {
100-
top: $spacing-inline-xxxs;
115+
top: 0;
101116
}
102117

103118
&__disabled-tooltip {
@@ -142,12 +157,6 @@ $background-custom: rgba(176, 245, 245, 0.24) 50%,
142157
}
143158
}
144159

145-
@media (max-width: 321px) {
146-
&_calendar_m2 {
147-
top: 150px;
148-
}
149-
}
150-
151160
&__caption {
152161
align-items: center;
153162
color: $color-interface-dark-deep;

‎packages/ocean-react/src/Date/DatePicker.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ const DatePickerSingle = React.forwardRef<
117117
{!disabled && calendarOpen && (
118118
<div
119119
data-testid="datepicker-calendar"
120-
className="ods-date__calendar-container"
120+
className={classNames('ods-date__calendar-container', {
121+
'ods-date__calendar-container--inline': inline,
122+
})}
121123
role="presentation"
122124
onClick={handleCalendarClick}
123125
>

‎packages/ocean-react/src/Date/DateRange.tsx‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,19 @@ const DatePickerRange = React.forwardRef<HTMLDivElement, DatePickerProps>(
5858
startsToday,
5959
locale,
6060
disabledDaysMessage,
61+
error,
6162
});
6263

64+
const fieldError = (field: 'from' | 'to'): boolean =>
65+
!disabled && (typeof error === 'object' ? !!error[field] : !!error);
66+
67+
const fieldHelperText = (field: 'from' | 'to'): string | undefined => {
68+
if (disabled || showDayPicker) return undefined;
69+
const text =
70+
typeof helperText === 'object' ? helperText[field] : helperText;
71+
return text || undefined;
72+
};
73+
6374
return (
6475
<div>
6576
{showDayPicker && (
@@ -95,11 +106,9 @@ const DatePickerRange = React.forwardRef<HTMLDivElement, DatePickerProps>(
95106
autoComplete="off"
96107
readOnly={!editable}
97108
disabled={disabled}
98-
error={!disabled && error}
109+
error={fieldError('from')}
99110
inputMode="numeric"
100-
helperText={
101-
(!disabled && !showDayPicker && helperText) || undefined
102-
}
111+
helperText={fieldHelperText('from')}
103112
maxLength={10}
104113
/>
105114
</div>
@@ -128,11 +137,9 @@ const DatePickerRange = React.forwardRef<HTMLDivElement, DatePickerProps>(
128137
autoComplete="off"
129138
readOnly={!editable}
130139
disabled={disabled}
131-
error={!disabled && error}
140+
error={fieldError('to')}
132141
inputMode="numeric"
133-
helperText={
134-
(!disabled && !showDayPicker && helperText) || undefined
135-
}
142+
helperText={fieldHelperText('to')}
136143
maxLength={10}
137144
/>
138145
</div>

‎packages/ocean-react/src/Date/__tests__/DateRange.test.tsx‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,3 +882,52 @@ test('tooltip does not appear when clicked date has no entry in disabledDaysMess
882882
screen.queryByTestId('datepicker-disabled-tooltip')
883883
).not.toBeInTheDocument();
884884
});
885+
886+
test('shows error and helper text only on the field passed per field', () => {
887+
render(
888+
<DatePicker
889+
labels={{ from: 'first-label', to: 'second-label' }}
890+
values={{ from: '31/02/2026', to: '09/09/2026' }}
891+
onSelect={jest.fn()}
892+
editable
893+
error={{ from: true }}
894+
helperText={{ from: 'Essa data não existe' }}
895+
/>
896+
);
897+
898+
expect(screen.getByText('Essa data não existe')).toBeInTheDocument();
899+
expect(
900+
screen.getByTestId('datepicker-input-1').closest('.ods-input')
901+
).toHaveClass('ods-input--error');
902+
expect(
903+
screen.getByTestId('datepicker-input-2').closest('.ods-input')
904+
).not.toHaveClass('ods-input--error');
905+
});
906+
907+
test('closes the calendar when an error arrives so the message is visible', () => {
908+
const { rerender } = render(
909+
<DatePicker
910+
labels={{ from: 'first-label', to: 'second-label' }}
911+
values={{ from: '31/02/2026', to: '' }}
912+
onSelect={jest.fn()}
913+
editable
914+
/>
915+
);
916+
917+
fireEvent.click(screen.getByTestId('datepicker-input-1'));
918+
expect(screen.getByTestId('datepicker-calendar')).toBeInTheDocument();
919+
920+
rerender(
921+
<DatePicker
922+
labels={{ from: 'first-label', to: 'second-label' }}
923+
values={{ from: '31/02/2026', to: '' }}
924+
onSelect={jest.fn()}
925+
editable
926+
error={{ from: true }}
927+
helperText={{ from: 'Essa data não existe' }}
928+
/>
929+
);
930+
931+
expect(screen.queryByTestId('datepicker-calendar')).not.toBeInTheDocument();
932+
expect(screen.getByText('Essa data não existe')).toBeInTheDocument();
933+
});

‎packages/ocean-react/src/Date/hooks/useDateRange.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default function useDatePicker({
2525
startsToday,
2626
locale,
2727
disabledDaysMessage,
28+
error,
2829
}: IDatePickerProps): IDatePickerReturn {
2930
const localeOption = locale || ptBr;
3031
const localeDateFormat =
@@ -169,6 +170,16 @@ export default function useDatePicker({
169170
}
170171
};
171172

173+
const hasError =
174+
typeof error === 'object' ? !!(error.from || error.to) : !!error;
175+
176+
React.useEffect(() => {
177+
if (hasError) {
178+
setShowDayPicker(false);
179+
setCurrentField('');
180+
}
181+
}, [hasError]);
182+
172183
const handleCloseByOutside = () => {
173184
if (showDayPicker) {
174185
setShowDayPicker(false);

‎packages/ocean-react/src/Date/stories/DateRange.stories.tsx‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { Meta, StoryObj } from '@storybook/react';
22
import React, { useState } from 'react';
33
import { ptBR, enUS } from 'date-fns/locale';
44
import DateRange from '../DateRange';
5+
import Drawer from '../../Drawer';
6+
import Modal from '../../Modal';
7+
import Input from '../../Input';
58
import type { DatePickerFields } from '../types/DateRange.types';
69
import {
710
createDateDecorator,
@@ -110,6 +113,17 @@ export const States: Story = {
110113
/>
111114
</div>
112115

116+
<div>
117+
<h4>Com erro em um só campo</h4>
118+
<DateRangeWrapper
119+
labels={{ from: 'Data inicial', to: 'Data final' }}
120+
values={{ from: '31/02/2026', to: '09/09/2026' }}
121+
editable
122+
error={{ from: true }}
123+
helperText={{ from: 'Essa data não existe. Confira o dia e o mês.' }}
124+
/>
125+
</div>
126+
113127
<div>
114128
<h4>Desabilitado</h4>
115129
<DateRangeWrapper
@@ -232,3 +246,56 @@ export const Localization: Story = {
232246
</div>
233247
),
234248
};
249+
250+
const contentBelow = (
251+
<div style={{ marginTop: 16 }}>
252+
<h4>Seção seguinte</h4>
253+
<p>Este conteúdo não deve se mover quando o calendário abre.</p>
254+
</div>
255+
);
256+
257+
export const InsideDrawer: Story = {
258+
parameters: noControlsParameters,
259+
render: () => (
260+
<Drawer open size="small" overlayClose={() => undefined}>
261+
<div style={{ padding: 16 }}>
262+
<DateRangeWrapper labels={{ from: 'De', to: 'Até' }} />
263+
{contentBelow}
264+
</div>
265+
</Drawer>
266+
),
267+
};
268+
269+
export const InsideModal: Story = {
270+
parameters: noControlsParameters,
271+
render: () => (
272+
<Modal isOpen onRequestClose={() => undefined}>
273+
<div style={{ width: 400 }}>
274+
<DateRangeWrapper labels={{ from: 'Data inicial', to: 'Data final' }} />
275+
{contentBelow}
276+
</div>
277+
</Modal>
278+
),
279+
};
280+
281+
export const NarrowContainer: Story = {
282+
parameters: noControlsParameters,
283+
render: () => (
284+
<div style={{ width: 320 }}>
285+
<DateRangeWrapper labels={{ from: 'De', to: 'Até' }} />
286+
{contentBelow}
287+
</div>
288+
),
289+
};
290+
291+
export const InlineWithInput: Story = {
292+
parameters: noControlsParameters,
293+
render: () => (
294+
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
295+
<div style={{ width: 240 }}>
296+
<Input label="Nome do cliente" placeholder="Buscar" />
297+
</div>
298+
<DateRangeWrapper labels={{ from: 'De', to: 'Até' }} />
299+
</div>
300+
),
301+
};

‎packages/ocean-react/src/Date/types/DatePicker.types.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ export type DatePickerSingleProps = {
1818
*/
1919
disabledDaysMessage?: DisabledDaysMessageProp;
2020
inline?: boolean;
21-
} & Omit<DatePickerProps, 'labels' | 'values' | 'onSelect'>;
21+
error?: boolean;
22+
helperText?: string;
23+
} & Omit<
24+
DatePickerProps,
25+
'labels' | 'values' | 'onSelect' | 'error' | 'helperText'
26+
>;
2227

2328
export type IDatePickerProps = Pick<
2429
DatePickerSingleProps,

‎packages/ocean-react/src/Date/types/DateRange.types.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export type DatePickerFields = {
1717
to: string;
1818
};
1919

20+
/**
21+
* Estado por campo (from/to). Um booleano/string único vale para os dois.
22+
*/
23+
export type DateFieldFlags = { from?: boolean; to?: boolean };
24+
export type DateFieldTexts = { from?: string; to?: string };
25+
2026
export type DatePickerProps = {
2127
/**
2228
* Determines names of inputs (from/to)
@@ -61,16 +67,18 @@ export type DatePickerProps = {
6167
disabledDaysMessage?: DisabledDaysMessageProp;
6268

6369
/**
64-
* Determines error os inputs
70+
* Determines error on the inputs. A boolean applies to both fields;
71+
* `{ from, to }` marks only the field(s) with an invalid date.
6572
* @default false
6673
*/
67-
error?: boolean;
74+
error?: boolean | DateFieldFlags;
6875

6976
/**
70-
* Determines error message
77+
* Determines error message. A string applies to both fields;
78+
* `{ from, to }` shows a message under a specific field.
7179
* @default null
7280
*/
73-
helperText?: string;
81+
helperText?: string | DateFieldTexts;
7482

7583
/**
7684
* Determines if date seleting starts today
@@ -93,7 +101,12 @@ export type DatePickerProps = {
93101

94102
export type IDatePickerProps = Pick<
95103
DatePickerProps,
96-
'values' | 'onSelect' | 'startsToday' | 'locale' | 'disabledDaysMessage'
104+
| 'values'
105+
| 'onSelect'
106+
| 'startsToday'
107+
| 'locale'
108+
| 'disabledDaysMessage'
109+
| 'error'
97110
>;
98111

99112
export type IDatePickerReturn = {

0 commit comments

Comments
 (0)