Skip to content

Commit 1185119

Browse files
fix(print): align event date fields in printed PDF
Refs: PS-275
1 parent 08cdc0c commit 1185119

4 files changed

Lines changed: 177 additions & 31 deletions

File tree

src/components/form/EventForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ const EventForm: React.FC<EventFormProps> = ({
280280
/>
281281
</Col>
282282
</Row>
283-
<Row>
283+
<Row className="event-form-print-skip">
284284
<Col sm="12" md={{ size: 8, offset: 1 }} lg={{ size: 8, offset: 1 }}>
285285
<Input
286286
type="textarea"

src/components/form/partitions/DateRange.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { addDays, addHours, setHours, startOfDay, subDays } from 'date-fns';
2-
import { fi, sv } from 'date-fns/locale';
1+
import { addDays, addHours, format, setHours, startOfDay, subDays } from 'date-fns';
2+
import fi from 'date-fns/locale/fi';
3+
import sv from 'date-fns/locale/sv';
34
import { FormikErrors, FormikTouched } from 'formik';
45
import React from 'react';
56
import { Event } from '../../../store/types';
@@ -8,7 +9,9 @@ import { useIntl } from 'react-intl';
89
import { Row, Col } from 'reactstrap';
910

1011
import DatePicker from '../fields/DatePicker';
12+
import PrintValue from '../fields/PrintValue';
1113
import TimePicker from '../fields/timePicker/TimePicker';
14+
import formatTime from '../../../utils/formatTime';
1215

1316
import 'react-datepicker/dist/react-datepicker.css';
1417
import './dateRange.scss';
@@ -30,7 +33,7 @@ interface EventWithDateObjects extends Omit<Event, 'start_time' | 'end_time'> {
3033

3134
interface Props {
3235
errors: FormikErrors<Event>;
33-
handleBlur: (_event: React.FocusEvent<HTMLElement>) => void;
36+
handleBlur: (_event: React.FormEvent<HTMLElement>) => void;
3437
handleChange: (
3538
event:
3639
| { target: { id: string; value: unknown } }
@@ -51,8 +54,7 @@ const DateRange: React.FC<Props> = ({
5154
unavailableDates,
5255
values,
5356
}) => {
54-
const onChange = (id: string) => (value: Date | null) => {
55-
if (!value) return;
57+
const onChange = (id: string) => (value: Date) => {
5658
handleChange({
5759
target: {
5860
id,
@@ -75,8 +77,7 @@ const DateRange: React.FC<Props> = ({
7577

7678
const handleDateChange =
7779
(id: string, oldDate: Date | string | undefined) =>
78-
(value: Date | null) => {
79-
if (!value) return;
80+
(value: Date) => {
8081
const oldDateObj = ensureDate(oldDate);
8182
onChange(id)(
8283
oldDateObj ? setHours(value, oldDateObj.getHours()) : addHours(value, 9)
@@ -87,7 +88,7 @@ const DateRange: React.FC<Props> = ({
8788
const syntheticEvent = {
8889
currentTarget: { id },
8990
preventDefault: () => {},
90-
} as React.FocusEvent<HTMLElement>;
91+
} as React.FormEvent<HTMLElement>;
9192

9293
handleBlur(syntheticEvent);
9394
};
@@ -139,9 +140,13 @@ const DateRange: React.FC<Props> = ({
139140
);
140141
const dateFormat = getDateFormat(locale);
141142
const timeFormat = getTimeFormat(locale);
143+
const formatDateValue = (value: Date | undefined) =>
144+
value ? format(value, dateFormat) : '';
145+
const formatTimeValue = (value: Date | undefined) =>
146+
value ? formatTime(value, timeFormat, locale) : '';
142147

143148
return (
144-
<>
149+
<div className="printable">
145150
<Row>
146151
<Col
147152
sm="12"
@@ -150,6 +155,10 @@ const DateRange: React.FC<Props> = ({
150155
>
151156
<DatePicker
152157
id="date_range_start_date"
158+
// @ts-ignore
159+
chooseDayAriaLabelPrefix={formatMessage({
160+
id: 'form.event.partitions.date_range.dayAriaLabelPrefix',
161+
})}
153162
label="form.event.partitions.date_range.start_date.label"
154163
placeholder="form.event.partitions.date_range.start_date.placeholder"
155164
locale={locale}
@@ -169,6 +178,7 @@ const DateRange: React.FC<Props> = ({
169178
showMonthDropdown
170179
useShortMonthInDropdown
171180
/>
181+
<PrintValue value={formatDateValue(selectedStartTime)} />
172182
</Col>
173183
<Col sm="12" md={{ size: 4 }}>
174184
<TimePicker
@@ -186,6 +196,7 @@ const DateRange: React.FC<Props> = ({
186196
timeIntervals={timeIntervals}
187197
touched={!!touched.start_time}
188198
/>
199+
<PrintValue value={formatTimeValue(selectedStartTime)} />
189200
</Col>
190201
</Row>
191202
<Row>
@@ -196,6 +207,10 @@ const DateRange: React.FC<Props> = ({
196207
>
197208
<DatePicker
198209
id="date_range_end_date"
210+
// @ts-ignore
211+
chooseDayAriaLabelPrefix={formatMessage({
212+
id: 'form.event.partitions.date_range.dayAriaLabelPrefix',
213+
})}
199214
label="form.event.partitions.date_range.end_date.label"
200215
placeholder="form.event.partitions.date_range.end_date.placeholder"
201216
locale={locale}
@@ -214,6 +229,7 @@ const DateRange: React.FC<Props> = ({
214229
showMonthDropdown
215230
useShortMonthInDropdown
216231
/>
232+
<PrintValue value={formatDateValue(selectedEndTime)} />
217233
</Col>
218234
<Col sm="12" md={{ size: 4 }}>
219235
<TimePicker
@@ -231,9 +247,10 @@ const DateRange: React.FC<Props> = ({
231247
timeIntervals={timeIntervals}
232248
touched={!!touched.end_time}
233249
/>
250+
<PrintValue value={formatTimeValue(selectedEndTime)} />
234251
</Col>
235252
</Row>
236-
</>
253+
</div>
237254
);
238255
};
239256

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React from 'react';
2+
import { addDays, setHours, startOfDay, format } from 'date-fns';
3+
import { render, screen } from '@testing-library/react';
4+
import { IntlProvider } from 'react-intl';
5+
import { vi } from 'vitest';
6+
7+
import DateRange from '../DateRange';
8+
import formatTime from '../../../../utils/formatTime';
9+
10+
const datePickerMock = vi.fn();
11+
const timePickerMock = vi.fn();
12+
const printValueMock = vi.fn();
13+
14+
vi.mock('../../fields/DatePicker', () => ({
15+
default: (props: Record<string, unknown>) => {
16+
datePickerMock(props);
17+
return (
18+
<div
19+
data-testid={String(props.id)}
20+
data-exclude-count={
21+
Array.isArray(props.excludeDates) ? props.excludeDates.length : 0
22+
}
23+
/>
24+
);
25+
},
26+
}));
27+
28+
vi.mock('../../fields/timePicker/TimePicker', () => ({
29+
default: (props: Record<string, unknown>) => {
30+
timePickerMock(props);
31+
return <div data-testid={String(props.id)} />;
32+
},
33+
}));
34+
35+
vi.mock('../../fields/PrintValue', () => ({
36+
default: (props: Record<string, unknown>) => {
37+
printValueMock(props);
38+
return <div data-testid="print-value" data-value={String(props.value)} />;
39+
},
40+
}));
41+
42+
const renderComponent = () => {
43+
const startTime = setHours(addDays(startOfDay(new Date()), 8), 9);
44+
const endTime = setHours(addDays(startOfDay(new Date()), 10), 17);
45+
const unavailableDates = [addDays(startOfDay(new Date()), 12)];
46+
47+
return render(
48+
<IntlProvider
49+
locale="en"
50+
messages={{
51+
'form.event.partitions.date_range.dayAriaLabelPrefix': 'Day',
52+
'form.event.partitions.date_range.timeCaption': 'Time',
53+
}}
54+
>
55+
<DateRange
56+
errors={{} as never}
57+
handleBlur={vi.fn()}
58+
handleChange={vi.fn()}
59+
touched={{} as never}
60+
unavailableDates={unavailableDates}
61+
values={
62+
{
63+
start_time: startTime,
64+
end_time: endTime,
65+
} as never
66+
}
67+
/>
68+
</IntlProvider>
69+
);
70+
};
71+
72+
describe('<DateRange />', () => {
73+
it('keeps unavailable dates blocked and renders print-only date and time mirrors', () => {
74+
datePickerMock.mockClear();
75+
timePickerMock.mockClear();
76+
printValueMock.mockClear();
77+
78+
const { container } = renderComponent();
79+
80+
const startPickerProps = datePickerMock.mock.calls[0][0] as {
81+
excludeDates?: Date[];
82+
id: string;
83+
};
84+
const endPickerProps = datePickerMock.mock.calls[1][0] as {
85+
excludeDates?: Date[];
86+
id: string;
87+
};
88+
89+
expect(screen.getByTestId('date_range_start_date')).toBeInTheDocument();
90+
expect(screen.getByTestId('date_range_end_date')).toBeInTheDocument();
91+
expect(startPickerProps.id).toBe('date_range_start_date');
92+
expect(endPickerProps.id).toBe('date_range_end_date');
93+
expect(startPickerProps.excludeDates).toHaveLength(1);
94+
expect(endPickerProps.excludeDates).toHaveLength(1);
95+
96+
const expectedStartDate = format(
97+
setHours(addDays(startOfDay(new Date()), 8), 9),
98+
'dd/MM/yyyy'
99+
);
100+
const expectedStartTime = formatTime(
101+
setHours(addDays(startOfDay(new Date()), 8), 9),
102+
'h:mm a',
103+
'en'
104+
);
105+
const expectedEndDate = format(
106+
setHours(addDays(startOfDay(new Date()), 10), 17),
107+
'dd/MM/yyyy'
108+
);
109+
const expectedEndTime = formatTime(
110+
setHours(addDays(startOfDay(new Date()), 10), 17),
111+
'h:mm a',
112+
'en'
113+
);
114+
115+
expect(screen.getAllByTestId('print-value')).toHaveLength(4);
116+
expect(container.querySelectorAll('.printable')).toHaveLength(1);
117+
expect(printValueMock.mock.calls[0][0]).toMatchObject({
118+
value: expectedStartDate,
119+
});
120+
expect(printValueMock.mock.calls[1][0]).toMatchObject({
121+
value: expectedStartTime,
122+
});
123+
expect(printValueMock.mock.calls[2][0]).toMatchObject({
124+
value: expectedEndDate,
125+
});
126+
expect(printValueMock.mock.calls[3][0]).toMatchObject({
127+
value: expectedEndTime,
128+
});
129+
});
130+
});

src/components/pages/events/print.scss

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
* div called `printable-area`. When someone prints or saves a PDF, the
66
* rules below hide most of the screen so only that div shows up on paper.
77
* Answers are shown as plain text in `.print-only` spots while the real
8-
* input boxes are hidden. See Input.tsx for how that works.
8+
* input boxes are hidden. See Input.tsx and DateRange.tsx for how that works.
99
*/
1010

1111
.print-only {
1212
display: none;
13+
margin: 0;
1314
}
1415

1516
@media print {
@@ -48,10 +49,9 @@
4849
}
4950

5051
/*
51-
* Event form print: EventForm.tsx puts three short help paragraphs in
52-
* `.event-form-print-skip` (text from form.event.subtitle.* in the
53-
* language files). The user still sees them on the website when editing.
54-
* They do not show on paper or in a PDF.
52+
* Event form print: EventForm.tsx uses `.event-form-print-skip` for
53+
* helper copy and optional blocks that are useful on screen but should
54+
* stay out of the contractor-facing PDF.
5555
*/
5656
.printable-area .event-form .event-form-print-skip {
5757
display: none;
@@ -65,6 +65,7 @@
6565

6666
.printable .print-only {
6767
display: block;
68+
margin: 0;
6869
}
6970

7071
.form-check-input,
@@ -125,21 +126,19 @@
125126
margin: 0;
126127
}
127128

128-
.col,
129-
.col-1,
130-
.col-2,
131-
.col-3,
132-
.col-4,
133-
.col-5,
134-
.col-6,
135-
.col-7,
136-
.col-8,
137-
.col-9,
138-
.col-10,
139-
.col-11,
140-
.col-12 {
129+
/*
130+
* Event form print: the form uses responsive Bootstrap column classes
131+
* (`col-md-*`, `col-lg-*`, etc.) in addition to the base `col-*` helpers.
132+
* Reset all of them here so every row starts from the same left edge.
133+
*/
134+
.printable-area .row > .col,
135+
.printable-area .row > [class^='col-'],
136+
.printable-area .row > [class*=' col-'] {
141137
width: 100%;
142-
padding: 0;
138+
max-width: 100%;
139+
padding-left: 0;
140+
padding-right: 0;
141+
flex: 0 0 100%;
143142
}
144143

145144
/*
@@ -167,4 +166,4 @@
167166
margin-top: 0.5rem;
168167
margin-bottom: 0.5rem;
169168
}
170-
}
169+
}

0 commit comments

Comments
 (0)