Skip to content

Commit

Permalink
Merge pull request #2023 from dxc-technology/gomezivann/date-input-fix
Browse files Browse the repository at this point in the history
Adding collision detection to the Date Picker
  • Loading branch information
Jialecl committed Jun 18, 2024
2 parents d57c8a5 + 08c97e1 commit 9e25510
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 49 deletions.
7 changes: 6 additions & 1 deletion lib/src/bar-chart/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { useState, useMemo, useCallback } from "react";
import BarChartProps, { DataTypes, InsetWrapperProps } from "./types";
import theme from "./theme";
import styled, { css } from "styled-components";
import { DxcSpinner, DxcInset, DxcSelect, DxcButton, DxcHeading, DxcGrid } from "../main";
import DxcSpinner from "../spinner/Spinner";
import DxcInset from "../inset/Inset";
import DxcSelect from "../select/Select";
import DxcButton from "../button/Button";
import DxcHeading from "../heading/Heading";
import DxcGrid from "../grid/Grid";
import DxcIcon from "../icon/Icon";
import CoreTokens from "../common/coreTokens";
import useTranslatedLabels from "../useTranslatedLabels";
Expand Down
21 changes: 13 additions & 8 deletions lib/src/date-input/DateInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ThemeProvider } from "styled-components";
import { HalstackProvider } from "../HalstackContext";
import preview from "../../.storybook/preview";
import { disabledRules } from "../../test/accessibility/rules/specific/date-input/disabledRules";
import DxcContainer from "../container/Container";

export default {
title: "Date Input",
Expand All @@ -37,6 +38,12 @@ const opinionatedTheme = {

const DateInputChromatic = () => (
<>
<ExampleContainer>
<Title title="Year picker" theme="light" level={4} />
<DxcContainer height="500px">
<DxcDateInput label="Date input" defaultValue="06-04-1905" error="Error message" />
</DxcContainer>
</ExampleContainer>
<ExampleContainer>
<Title title="Complete date input" theme="light" level={4} />
<DxcDateInput label="Date input" helperText="Help message" format="dd/mm/yy" placeholder optional />
Expand Down Expand Up @@ -105,17 +112,13 @@ const DateInputChromatic = () => (
<Title title="FillParent size" theme="light" level={4} />
<DxcDateInput label="FillParent" size="fillParent" />
</ExampleContainer>
<ExampleContainer expanded>
<Title title="Year picker" theme="light" level={4} />
<DxcDateInput label="Date input" defaultValue="06-04-1905" />
</ExampleContainer>
</>
);

export const Chromatic = DateInputChromatic.bind({});
Chromatic.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getAllByRole("combobox")[canvas.getAllByRole("combobox").length - 1]);
await userEvent.click(canvas.getAllByRole("combobox")[0]);
await fireEvent.click(screen.getByText("April 1905"));
};

Expand Down Expand Up @@ -147,10 +150,12 @@ const DateInputOpinionatedTheme = () => (
<DxcDateInput label="Error date input" error="Error message." placeholder />
</HalstackProvider>
</ExampleContainer>
<ExampleContainer expanded>
<ExampleContainer>
<Title title="Date picker" theme="light" level={4} />
<HalstackProvider theme={opinionatedTheme}>
<DxcDateInput label="Date input" defaultValue="06-04-1905" />
<div style={{ display: "flex", height: "400px", alignItems: "flex-end" }}>
<DxcDateInput label="Date input" defaultValue="06-04-1905" error="Error message" />
</div>
</HalstackProvider>
</ExampleContainer>
</>
Expand All @@ -159,7 +164,7 @@ const DateInputOpinionatedTheme = () => (
export const DateInputOpinionated = DateInputOpinionatedTheme.bind({});
DateInputOpinionated.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getAllByRole("combobox")[canvas.getAllByRole("combobox").length - 1]);
await userEvent.click(canvas.getAllByRole("combobox")[3]);
};

const YearPickerOpinionatedTheme = () => (
Expand Down
155 changes: 121 additions & 34 deletions lib/src/date-input/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React, { useState, useRef, useEffect, useId } from "react";
import React, { useState, useRef, useEffect, useId, useCallback } from "react";
import dayjs from "dayjs";
import styled, { ThemeProvider } from "styled-components";
import useTheme from "../useTheme";
import useTranslatedLabels from "../useTranslatedLabels";
import DxcTextInput from "../text-input/TextInput";
import DateInputPropsType, { RefType } from "./types";
import DxcDatePicker from "./DatePicker";
import DatePicker from "./DatePicker";
import * as Popover from "@radix-ui/react-popover";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { getMargin } from "../common/utils";
import { spaces } from "../common/variables";
import DxcTextInput from "../text-input/TextInput";

dayjs.extend(customParseFormat);

const SIDEOFFSET = 4;

const getValueForPicker = (value, format) => dayjs(value, format.toUpperCase(), true);

const getDate = (value, format, lastValidYear, setLastValidYear) => {
Expand All @@ -25,9 +29,7 @@ const getDate = (value, format, lastValidYear, setLastValidYear) => {
setLastValidYear(1900 + +newDate.format("YY"));
newDate = newDate.set("year", 1900 + +newDate.format("YY"));
}
} else {
newDate = newDate.set("year", (lastValidYear <= 1999 ? 1900 : 2000) + +newDate.format("YY"));
}
} else newDate = newDate.set("year", (lastValidYear <= 1999 ? 1900 : 2000) + +newDate.format("YY"));
return newDate;
}
};
Expand Down Expand Up @@ -67,26 +69,11 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
: 1900
: undefined
);
const [sideOffset, setSideOffset] = useState(SIDEOFFSET);
const colorsTheme = useTheme();
const translatedLabels = useTranslatedLabels();
const dateRef = useRef(null);

useEffect(() => {
if (value || value === "") setDayjsDate(getDate(value, format, lastValidYear, setLastValidYear));
}, [value, format, lastValidYear]);

useEffect(() => {
if (!disabled) {
const actionButtonRef = dateRef?.current.querySelector("[title='Select date']");
actionButtonRef?.setAttribute("aria-haspopup", true);
actionButtonRef?.setAttribute("role", "combobox");
actionButtonRef?.setAttribute("aria-expanded", isOpen);
actionButtonRef?.setAttribute("aria-controls", calendarId);
if (isOpen) {
actionButtonRef?.setAttribute("aria-describedby", calendarId);
}
}
}, [isOpen, disabled, calendarId]);
const popoverContentRef = useRef(null);

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

const adjustSideOffset = useCallback(() => {
if (error != null) {
setTimeout(() => {
if (popoverContentRef.current && dateRef.current) {
const popoverRect = popoverContentRef.current.getBoundingClientRect();
const triggerRect = dateRef.current.querySelector('[id^="input"]')?.getBoundingClientRect();
const errorMessageHeight = dateRef.current
.querySelector('[id^="error-input"]')
?.getBoundingClientRect().height;
setSideOffset(popoverRect.top > triggerRect.bottom ? -errorMessageHeight : SIDEOFFSET);
}
}, 0);
}
}, [error]);

const openCalendar = () => {
setIsOpen(!isOpen);
adjustSideOffset();
};
const closeCalendar = () => {
setIsOpen(false);
Expand All @@ -158,17 +161,49 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
if (!event?.currentTarget.contains(event.relatedTarget)) closeCalendar();
};

useEffect(() => {
window.addEventListener("scroll", adjustSideOffset);
return () => {
window.removeEventListener("scroll", adjustSideOffset);
};
}, [adjustSideOffset]);

useEffect(() => {
if (value || value === "") setDayjsDate(getDate(value, format, lastValidYear, setLastValidYear));
}, [value, format, lastValidYear]);

useEffect(() => {
if (!disabled) {
const actionButtonRef = dateRef?.current.querySelector("[title='Select date']");
actionButtonRef?.setAttribute("aria-haspopup", true);
actionButtonRef?.setAttribute("role", "combobox");
actionButtonRef?.setAttribute("aria-expanded", isOpen);
actionButtonRef?.setAttribute("aria-controls", calendarId);
if (isOpen) {
actionButtonRef?.setAttribute("aria-describedby", calendarId);
}
}
}, [isOpen, disabled, calendarId]);

return (
<ThemeProvider theme={colorsTheme}>
<DateInputContainer size={size} ref={ref}>
<DateInputContainer margin={margin} size={size} ref={ref}>
{label && (
<Label
htmlFor={dateRef.current?.getElementsByTagName("input")[0].id}
disabled={disabled}
hasHelperText={helperText ? true : false}
>
{label} {optional && <OptionalLabel>{translatedLabels.formFields.optionalLabel}</OptionalLabel>}
</Label>
)}
{helperText && <HelperText disabled={disabled}>{helperText}</HelperText>}
<Popover.Root open={isOpen}>
<Popover.Trigger asChild aria-controls={undefined}>
<DxcTextInput
label={label}
name={name}
defaultValue={defaultValue}
value={value ?? innerValue}
helperText={helperText}
placeholder={placeholder ? format.toUpperCase() : null}
action={{
onClick: openCalendar,
Expand All @@ -183,22 +218,21 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
onBlur={handleOnBlur}
error={error}
autocomplete={autocomplete}
margin={margin}
size={size}
tabIndex={tabIndex}
ref={dateRef}
/>
</Popover.Trigger>
<Popover.Portal>
<StyledPopoverContent
sideOffset={error ? -18 : 2}
sideOffset={sideOffset}
align="end"
aria-modal={true}
onBlur={handleDatePickerOnBlur}
onKeyDown={handleDatePickerEscKeydown}
avoidCollisions={false}
ref={popoverContentRef}
>
<DxcDatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
<DatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
Expand All @@ -208,15 +242,68 @@ const DxcDateInput = React.forwardRef<RefType, DateInputPropsType>(
}
);

const sizes = {
small: "240px",
medium: "360px",
large: "480px",
fillParent: "100%",
};

const calculateWidth = (margin, size) =>
size === "fillParent"
? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`
: sizes[size];

const DateInputContainer = styled.div<{ margin: DateInputPropsType["margin"]; size: DateInputPropsType["size"] }>`
${(props) => props.size == "fillParent" && "width: 100%;"}
display: flex;
flex-direction: column;
width: ${(props) => calculateWidth(props.margin, props.size)};
${(props) => props.size !== "fillParent" && "min-width:" + calculateWidth(props.margin, props.size)};
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
margin-top: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
margin-right: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
margin-bottom: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
margin-left: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
font-family: ${(props) => props.theme.textInput.fontFamily};
`;

const Label = styled.label<{
disabled: DateInputPropsType["disabled"];
hasHelperText: boolean;
}>`
color: ${(props) =>
props.disabled ? props.theme.textInput.disabledLabelFontColor : props.theme.textInput.labelFontColor};
font-size: ${(props) => props.theme.textInput.labelFontSize};
font-style: ${(props) => props.theme.textInput.labelFontStyle};
font-weight: ${(props) => props.theme.textInput.labelFontWeight};
line-height: ${(props) => props.theme.textInput.labelLineHeight};
${(props) => !props.hasHelperText && `margin-bottom: 0.25rem`}
`;

const OptionalLabel = styled.span`
font-weight: ${(props) => props.theme.textInput.optionalLabelFontWeight};
`;

const HelperText = styled.span<{ disabled: DateInputPropsType["disabled"] }>`
color: ${(props) =>
props.disabled ? props.theme.textInput.disabledHelperTextFontColor : props.theme.textInput.helperTextFontColor};
font-size: ${(props) => props.theme.textInput.helperTextFontSize};
font-style: ${(props) => props.theme.textInput.helperTextFontStyle};
font-weight: ${(props) => props.theme.textInput.helperTextFontWeight};
line-height: ${(props) => props.theme.textInput.helperTextLineHeight};
margin-bottom: 0.25rem;
`;

const StyledPopoverContent = styled(Popover.Content)`
z-index: 2147483647;
&:focus-visible {
outline: none;
}
`;

const DateInputContainer = styled.div<{ size: DateInputPropsType["size"] }>`
${(props) => props.size == "fillParent" && "width: 100%;"}
`;

export default DxcDateInput;
10 changes: 5 additions & 5 deletions lib/src/date-input/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DxcIcon from "../icon/Icon";

const today = dayjs();

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

return (
<DatePicker id={id}>
<DatePickerContainer id={id}>
<PickerHeader>
<HeaderButton
aria-label={translatedLabels.calendar.previousMonthTitle}
Expand Down Expand Up @@ -68,11 +68,11 @@ const DxcDatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Ele
{content === "yearPicker" && (
<YearPicker selectedDate={selectedDate} onYearSelect={handleOnYearSelect} today={today} />
)}
</DatePicker>
</DatePickerContainer>
);
};

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

export default React.memo(DxcDatePicker);
export default React.memo(DatePicker);
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const sections = [
</DxcBulletedList.Item>
<DxcBulletedList.Item>Title</DxcBulletedList.Item>
<DxcBulletedList.Item>
Helper text<em>(Optional)</em>
Helper text <em>(Optional)</em>
</DxcBulletedList.Item>
<DxcBulletedList.Item>
Caret icon <em>(Expand/collapse)</em>
Expand Down

0 comments on commit 9e25510

Please sign in to comment.