Skip to content

Commit 9e25510

Browse files
authored
Merge pull request #2023 from dxc-technology/gomezivann/date-input-fix
Adding collision detection to the Date Picker
2 parents d57c8a5 + 08c97e1 commit 9e25510

File tree

5 files changed

+146
-49
lines changed

5 files changed

+146
-49
lines changed

lib/src/bar-chart/BarChart.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { useState, useMemo, useCallback } from "react";
55
import BarChartProps, { DataTypes, InsetWrapperProps } from "./types";
66
import theme from "./theme";
77
import styled, { css } from "styled-components";
8-
import { DxcSpinner, DxcInset, DxcSelect, DxcButton, DxcHeading, DxcGrid } from "../main";
8+
import DxcSpinner from "../spinner/Spinner";
9+
import DxcInset from "../inset/Inset";
10+
import DxcSelect from "../select/Select";
11+
import DxcButton from "../button/Button";
12+
import DxcHeading from "../heading/Heading";
13+
import DxcGrid from "../grid/Grid";
914
import DxcIcon from "../icon/Icon";
1015
import CoreTokens from "../common/coreTokens";
1116
import useTranslatedLabels from "../useTranslatedLabels";

lib/src/date-input/DateInput.stories.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ThemeProvider } from "styled-components";
1212
import { HalstackProvider } from "../HalstackContext";
1313
import preview from "../../.storybook/preview";
1414
import { disabledRules } from "../../test/accessibility/rules/specific/date-input/disabledRules";
15+
import DxcContainer from "../container/Container";
1516

1617
export default {
1718
title: "Date Input",
@@ -37,6 +38,12 @@ const opinionatedTheme = {
3738

3839
const DateInputChromatic = () => (
3940
<>
41+
<ExampleContainer>
42+
<Title title="Year picker" theme="light" level={4} />
43+
<DxcContainer height="500px">
44+
<DxcDateInput label="Date input" defaultValue="06-04-1905" error="Error message" />
45+
</DxcContainer>
46+
</ExampleContainer>
4047
<ExampleContainer>
4148
<Title title="Complete date input" theme="light" level={4} />
4249
<DxcDateInput label="Date input" helperText="Help message" format="dd/mm/yy" placeholder optional />
@@ -105,17 +112,13 @@ const DateInputChromatic = () => (
105112
<Title title="FillParent size" theme="light" level={4} />
106113
<DxcDateInput label="FillParent" size="fillParent" />
107114
</ExampleContainer>
108-
<ExampleContainer expanded>
109-
<Title title="Year picker" theme="light" level={4} />
110-
<DxcDateInput label="Date input" defaultValue="06-04-1905" />
111-
</ExampleContainer>
112115
</>
113116
);
114117

115118
export const Chromatic = DateInputChromatic.bind({});
116119
Chromatic.play = async ({ canvasElement }) => {
117120
const canvas = within(canvasElement);
118-
await userEvent.click(canvas.getAllByRole("combobox")[canvas.getAllByRole("combobox").length - 1]);
121+
await userEvent.click(canvas.getAllByRole("combobox")[0]);
119122
await fireEvent.click(screen.getByText("April 1905"));
120123
};
121124

@@ -147,10 +150,12 @@ const DateInputOpinionatedTheme = () => (
147150
<DxcDateInput label="Error date input" error="Error message." placeholder />
148151
</HalstackProvider>
149152
</ExampleContainer>
150-
<ExampleContainer expanded>
153+
<ExampleContainer>
151154
<Title title="Date picker" theme="light" level={4} />
152155
<HalstackProvider theme={opinionatedTheme}>
153-
<DxcDateInput label="Date input" defaultValue="06-04-1905" />
156+
<div style={{ display: "flex", height: "400px", alignItems: "flex-end" }}>
157+
<DxcDateInput label="Date input" defaultValue="06-04-1905" error="Error message" />
158+
</div>
154159
</HalstackProvider>
155160
</ExampleContainer>
156161
</>
@@ -159,7 +164,7 @@ const DateInputOpinionatedTheme = () => (
159164
export const DateInputOpinionated = DateInputOpinionatedTheme.bind({});
160165
DateInputOpinionated.play = async ({ canvasElement }) => {
161166
const canvas = within(canvasElement);
162-
await userEvent.click(canvas.getAllByRole("combobox")[canvas.getAllByRole("combobox").length - 1]);
167+
await userEvent.click(canvas.getAllByRole("combobox")[3]);
163168
};
164169

165170
const YearPickerOpinionatedTheme = () => (

lib/src/date-input/DateInput.tsx

Lines changed: 121 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import React, { useState, useRef, useEffect, useId } from "react";
1+
import React, { useState, useRef, useEffect, useId, useCallback } from "react";
22
import dayjs from "dayjs";
33
import styled, { ThemeProvider } from "styled-components";
44
import useTheme from "../useTheme";
55
import useTranslatedLabels from "../useTranslatedLabels";
6-
import DxcTextInput from "../text-input/TextInput";
76
import DateInputPropsType, { RefType } from "./types";
8-
import DxcDatePicker from "./DatePicker";
7+
import DatePicker from "./DatePicker";
98
import * as Popover from "@radix-ui/react-popover";
109
import customParseFormat from "dayjs/plugin/customParseFormat";
10+
import { getMargin } from "../common/utils";
11+
import { spaces } from "../common/variables";
12+
import DxcTextInput from "../text-input/TextInput";
1113

1214
dayjs.extend(customParseFormat);
1315

16+
const SIDEOFFSET = 4;
17+
1418
const getValueForPicker = (value, format) => dayjs(value, format.toUpperCase(), true);
1519

1620
const getDate = (value, format, lastValidYear, setLastValidYear) => {
@@ -25,9 +29,7 @@ const getDate = (value, format, lastValidYear, setLastValidYear) => {
2529
setLastValidYear(1900 + +newDate.format("YY"));
2630
newDate = newDate.set("year", 1900 + +newDate.format("YY"));
2731
}
28-
} else {
29-
newDate = newDate.set("year", (lastValidYear <= 1999 ? 1900 : 2000) + +newDate.format("YY"));
30-
}
32+
} else newDate = newDate.set("year", (lastValidYear <= 1999 ? 1900 : 2000) + +newDate.format("YY"));
3133
return newDate;
3234
}
3335
};
@@ -67,26 +69,11 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
6769
: 1900
6870
: undefined
6971
);
72+
const [sideOffset, setSideOffset] = useState(SIDEOFFSET);
7073
const colorsTheme = useTheme();
7174
const translatedLabels = useTranslatedLabels();
7275
const dateRef = useRef(null);
73-
74-
useEffect(() => {
75-
if (value || value === "") setDayjsDate(getDate(value, format, lastValidYear, setLastValidYear));
76-
}, [value, format, lastValidYear]);
77-
78-
useEffect(() => {
79-
if (!disabled) {
80-
const actionButtonRef = dateRef?.current.querySelector("[title='Select date']");
81-
actionButtonRef?.setAttribute("aria-haspopup", true);
82-
actionButtonRef?.setAttribute("role", "combobox");
83-
actionButtonRef?.setAttribute("aria-expanded", isOpen);
84-
actionButtonRef?.setAttribute("aria-controls", calendarId);
85-
if (isOpen) {
86-
actionButtonRef?.setAttribute("aria-describedby", calendarId);
87-
}
88-
}
89-
}, [isOpen, disabled, calendarId]);
76+
const popoverContentRef = useRef(null);
9077

9178
const handleCalendarOnClick = (newDate) => {
9279
const newValue = newDate.format(format.toUpperCase());
@@ -139,8 +126,24 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
139126
: onBlur?.(callbackParams);
140127
};
141128

129+
const adjustSideOffset = useCallback(() => {
130+
if (error != null) {
131+
setTimeout(() => {
132+
if (popoverContentRef.current && dateRef.current) {
133+
const popoverRect = popoverContentRef.current.getBoundingClientRect();
134+
const triggerRect = dateRef.current.querySelector('[id^="input"]')?.getBoundingClientRect();
135+
const errorMessageHeight = dateRef.current
136+
.querySelector('[id^="error-input"]')
137+
?.getBoundingClientRect().height;
138+
setSideOffset(popoverRect.top > triggerRect.bottom ? -errorMessageHeight : SIDEOFFSET);
139+
}
140+
}, 0);
141+
}
142+
}, [error]);
143+
142144
const openCalendar = () => {
143145
setIsOpen(!isOpen);
146+
adjustSideOffset();
144147
};
145148
const closeCalendar = () => {
146149
setIsOpen(false);
@@ -158,17 +161,49 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
158161
if (!event?.currentTarget.contains(event.relatedTarget)) closeCalendar();
159162
};
160163

164+
useEffect(() => {
165+
window.addEventListener("scroll", adjustSideOffset);
166+
return () => {
167+
window.removeEventListener("scroll", adjustSideOffset);
168+
};
169+
}, [adjustSideOffset]);
170+
171+
useEffect(() => {
172+
if (value || value === "") setDayjsDate(getDate(value, format, lastValidYear, setLastValidYear));
173+
}, [value, format, lastValidYear]);
174+
175+
useEffect(() => {
176+
if (!disabled) {
177+
const actionButtonRef = dateRef?.current.querySelector("[title='Select date']");
178+
actionButtonRef?.setAttribute("aria-haspopup", true);
179+
actionButtonRef?.setAttribute("role", "combobox");
180+
actionButtonRef?.setAttribute("aria-expanded", isOpen);
181+
actionButtonRef?.setAttribute("aria-controls", calendarId);
182+
if (isOpen) {
183+
actionButtonRef?.setAttribute("aria-describedby", calendarId);
184+
}
185+
}
186+
}, [isOpen, disabled, calendarId]);
187+
161188
return (
162189
<ThemeProvider theme={colorsTheme}>
163-
<DateInputContainer size={size} ref={ref}>
190+
<DateInputContainer margin={margin} size={size} ref={ref}>
191+
{label && (
192+
<Label
193+
htmlFor={dateRef.current?.getElementsByTagName("input")[0].id}
194+
disabled={disabled}
195+
hasHelperText={helperText ? true : false}
196+
>
197+
{label} {optional && <OptionalLabel>{translatedLabels.formFields.optionalLabel}</OptionalLabel>}
198+
</Label>
199+
)}
200+
{helperText && <HelperText disabled={disabled}>{helperText}</HelperText>}
164201
<Popover.Root open={isOpen}>
165202
<Popover.Trigger asChild aria-controls={undefined}>
166203
<DxcTextInput
167-
label={label}
168204
name={name}
169205
defaultValue={defaultValue}
170206
value={value ?? innerValue}
171-
helperText={helperText}
172207
placeholder={placeholder ? format.toUpperCase() : null}
173208
action={{
174209
onClick: openCalendar,
@@ -183,22 +218,21 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
183218
onBlur={handleOnBlur}
184219
error={error}
185220
autocomplete={autocomplete}
186-
margin={margin}
187221
size={size}
188222
tabIndex={tabIndex}
189223
ref={dateRef}
190224
/>
191225
</Popover.Trigger>
192226
<Popover.Portal>
193227
<StyledPopoverContent
194-
sideOffset={error ? -18 : 2}
228+
sideOffset={sideOffset}
195229
align="end"
196230
aria-modal={true}
197231
onBlur={handleDatePickerOnBlur}
198232
onKeyDown={handleDatePickerEscKeydown}
199-
avoidCollisions={false}
233+
ref={popoverContentRef}
200234
>
201-
<DxcDatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
235+
<DatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
202236
</StyledPopoverContent>
203237
</Popover.Portal>
204238
</Popover.Root>
@@ -208,15 +242,68 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
208242
}
209243
);
210244

245+
const sizes = {
246+
small: "240px",
247+
medium: "360px",
248+
large: "480px",
249+
fillParent: "100%",
250+
};
251+
252+
const calculateWidth = (margin, size) =>
253+
size === "fillParent"
254+
? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`
255+
: sizes[size];
256+
257+
const DateInputContainer = styled.div<{ margin: DateInputPropsType["margin"]; size: DateInputPropsType["size"] }>`
258+
${(props) => props.size == "fillParent" && "width: 100%;"}
259+
display: flex;
260+
flex-direction: column;
261+
width: ${(props) => calculateWidth(props.margin, props.size)};
262+
${(props) => props.size !== "fillParent" && "min-width:" + calculateWidth(props.margin, props.size)};
263+
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
264+
margin-top: ${(props) =>
265+
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
266+
margin-right: ${(props) =>
267+
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
268+
margin-bottom: ${(props) =>
269+
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
270+
margin-left: ${(props) =>
271+
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
272+
font-family: ${(props) => props.theme.textInput.fontFamily};
273+
`;
274+
275+
const Label = styled.label<{
276+
disabled: DateInputPropsType["disabled"];
277+
hasHelperText: boolean;
278+
}>`
279+
color: ${(props) =>
280+
props.disabled ? props.theme.textInput.disabledLabelFontColor : props.theme.textInput.labelFontColor};
281+
font-size: ${(props) => props.theme.textInput.labelFontSize};
282+
font-style: ${(props) => props.theme.textInput.labelFontStyle};
283+
font-weight: ${(props) => props.theme.textInput.labelFontWeight};
284+
line-height: ${(props) => props.theme.textInput.labelLineHeight};
285+
${(props) => !props.hasHelperText && `margin-bottom: 0.25rem`}
286+
`;
287+
288+
const OptionalLabel = styled.span`
289+
font-weight: ${(props) => props.theme.textInput.optionalLabelFontWeight};
290+
`;
291+
292+
const HelperText = styled.span<{ disabled: DateInputPropsType["disabled"] }>`
293+
color: ${(props) =>
294+
props.disabled ? props.theme.textInput.disabledHelperTextFontColor : props.theme.textInput.helperTextFontColor};
295+
font-size: ${(props) => props.theme.textInput.helperTextFontSize};
296+
font-style: ${(props) => props.theme.textInput.helperTextFontStyle};
297+
font-weight: ${(props) => props.theme.textInput.helperTextFontWeight};
298+
line-height: ${(props) => props.theme.textInput.helperTextLineHeight};
299+
margin-bottom: 0.25rem;
300+
`;
301+
211302
const StyledPopoverContent = styled(Popover.Content)`
212303
z-index: 2147483647;
213304
&:focus-visible {
214305
outline: none;
215306
}
216307
`;
217308

218-
const DateInputContainer = styled.div<{ size: DateInputPropsType["size"] }>`
219-
${(props) => props.size == "fillParent" && "width: 100%;"}
220-
`;
221-
222309
export default DxcDateInput;

lib/src/date-input/DatePicker.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import DxcIcon from "../icon/Icon";
99

1010
const today = dayjs();
1111

12-
const DxcDatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Element => {
12+
const DatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Element => {
1313
const [innerDate, setInnerDate] = useState(date?.isValid() ? date : dayjs());
1414
const [content, setContent] = useState("calendar");
1515
const selectedDate = date?.isValid() ? date : null;
@@ -30,7 +30,7 @@ const DxcDatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Ele
3030
};
3131

3232
return (
33-
<DatePicker id={id}>
33+
<DatePickerContainer id={id}>
3434
<PickerHeader>
3535
<HeaderButton
3636
aria-label={translatedLabels.calendar.previousMonthTitle}
@@ -68,11 +68,11 @@ const DxcDatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Ele
6868
{content === "yearPicker" && (
6969
<YearPicker selectedDate={selectedDate} onYearSelect={handleOnYearSelect} today={today} />
7070
)}
71-
</DatePicker>
71+
</DatePickerContainer>
7272
);
7373
};
7474

75-
const DatePicker = styled.div`
75+
const DatePickerContainer = styled.div`
7676
padding-top: 16px;
7777
background-color: ${(props) => props.theme.dateInput.pickerBackgroundColor};
7878
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
@@ -144,4 +144,4 @@ const HeaderYearTriggerLabel = styled.span`
144144
font-size: ${(props) => props.theme.dateInput.pickerHeaderFontSize};
145145
`;
146146

147-
export default React.memo(DxcDatePicker);
147+
export default React.memo(DatePicker);

website/screens/components/accordion/specs/AccordionSpecsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const sections = [
5454
</DxcBulletedList.Item>
5555
<DxcBulletedList.Item>Title</DxcBulletedList.Item>
5656
<DxcBulletedList.Item>
57-
Helper text<em>(Optional)</em>
57+
Helper text <em>(Optional)</em>
5858
</DxcBulletedList.Item>
5959
<DxcBulletedList.Item>
6060
Caret icon <em>(Expand/collapse)</em>

0 commit comments

Comments
 (0)