Skip to content

Commit

Permalink
New travel calendar (#753)
Browse files Browse the repository at this point in the history
* - Add support for horizontally connected LaballedTextInput.
- Started working on new calendar.

* - Working on TravelDateInput.

* - Working on TravelDateInput.

* - Some hover highlight improvements.

* - Working on input mask and localized date format in inputs.

* - Update date-fns to 3.6.0.

* - Add dependency on input-mask.

* - Fix eslint warnings.

* - Fix errors.

* - Create input masks from date format.

* - Localized inputs now working.

* - Add input label props.

* - Refactor to use Date for state instead of custom date data objects.
- Localize month picker button.

* - Some styling fixes when invalid range is selected.
- Do not parse text input until length matches date format.

* - Improved keyboard usage.

* - Remove invalid prop.

* - Align width of inputs with the rest of the calendar.

* - Add story with all locales.

* StoryFn.

* - Update visible month properly when typing date in input fields.

* - Esc now closes month picker.

* - Add aria-label to prev/next month buttons, with default values in english if omitted.

* - Hovered cell has white background, except when part of selected range.

* - Set aria-live polite on month picker button.

* - Fix better outline.

* - Add abbr prop to th with full length week day names.
- Add aria-selected to td days.

* - Cursor is pointer only on days in current month.

* - Highlight todays date.

* - When selecting month, focus goes back to month picker button.
- When opening month picker, selected month is autofocused.

* - TabIndex is set to first day of month if neither today nor selected date is in visible month.
- MonthPicker uses Date as model instead of custom. This makes it easier to use date-fns to mutate it.
- Add keyboard navigation in MonthPicker, based on rows and columns. This data is precalculated.

* - Add support for PageUp, PageDown, Home and End.

* - Improve keyboard navigation in month picker. Can now go to next and previous row with arrow left and right.
- Can also navigate past empty cells.

* - Hide first year in month picker, if it is same as current year.

* - Use globally unique DOM id's for td elements.

* - Fix space. ev.key === "Space" was wrong, checking ev.code instead.

* - Dates before today are now disabled.
- Previous month stepper button is disabled if showing today's month.

* - Entering invalid date (such as feb 30) no longer crashes the calendar.

* - Remove heading, should be part of app (or parent container), not the calendar.

* - Apply prettier.

* - Visible calendar can be today's month at the earliest. Manually entering an earlier month will focus on today's month.
- Entering a date with missing zeroes will now be reformatted to correct date format (if valid) on blur.
- getDefaultLocaleForFormatting() can now return undefined, if no matching locale is found, instead of default sv.
- parseLocalizedDateString() returns undefined if parse fails, instead of "Invalid date". This should be less error prone since type system will enforce null checks.

* - Working on popover.

* - Refactor and extract into hooks and components, so that we can have multiple variants.

* - More refactoring to enable external input component and calendar component.

* - Working on calendar with overlay. Not working yet.

* - Add animation to TravelDateInput popover.
- TravelDateInput now takes up correct in-line size in DOM, even though the inputs are absolute positioned.

* - Rename to "date range calendar" instead of "date calendar".
- Fix styling of table.
- Change order of absolute positioned elements, so that tabbing works correctly again.
- Add zIndex prop, since zIndex is now needed when DOM element order is correct for tabbing, but not for rendering order.
- Reduce animation time, is now "fast".

* - Use props spread for internal components, to decrease the amount of code that is just noise.

* - Use props spread for internal components, to decrease the amount of code that is just noise.

* - Update .gitignore to include IntelliJ log files.

* - Add heading to calendar component as well, for consistency.
- Make heading optional, and align absolute positioning properly when heading is missing.
- Add separate z-index when calendar popover is not visible, so that z-index doesn't interfere when having multiple inputs.
- Change selection highlight as per design.

* - Add stories to show how to parse localized date.

* - Add render prop for rendering custom content below calendar in popover.
- Add stories with render prop examples.
- Add `onHideCalendar` callback.
- Fix comment typo.
- Add `firstMonthInMonthPicker` and `numMonthsInMonthPicker` props.
  • Loading branch information
mattias800 authored Jul 4, 2024
1 parent 06b7fdd commit 5f373cf
Show file tree
Hide file tree
Showing 42 changed files with 2,244 additions and 116 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ build
lerna-debug.log
.vscode/settings.json

ti-*.log
.log*
1 change: 1 addition & 0 deletions packages/calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@stenajs-webui/core": "20.11.6",
"@stenajs-webui/elements": "20.11.6",
"@stenajs-webui/forms": "20.11.6",
"@stenajs-webui/input-mask": "20.11.6",
"@stenajs-webui/theme": "20.11.6",
"@stenajs-webui/tooltip": "20.11.6",
"classnames": "^2.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { StoryFn } from "@storybook/react";
import * as React from "react";
import { useState } from "react";
import { TravelDateRangeCalendar } from "./TravelDateRangeCalendar";
import { Column, Row, Spacing } from "@stenajs-webui/core";
import { Banner, Label } from "@stenajs-webui/elements";
import { TravelDateRangeInputValue } from "../../../features/travel-calendar/types";
import { parseLocalizedDateString } from "../../../features/localize-date-format/LocalizedDateParser";

export default {
title: "calendar/Calendar/TravelDateRangeCalendar",
component: TravelDateRangeCalendar,
decorators: [
(TheStory: StoryFn) => (
<div style={{ marginBottom: "400px" }}>
<TheStory />
</div>
),
],
};

export const Standard = () => {
const [value, setValue] = useState<TravelDateRangeInputValue | undefined>(
undefined
);

return (
<div style={{ display: "inline-block" }}>
<TravelDateRangeCalendar
value={value}
onValueChange={setValue}
localeCode={"sv"}
/>
</div>
);
};

export const WithHeading = () => {
const [value, setValue] = useState<TravelDateRangeInputValue | undefined>(
undefined
);

return (
<div style={{ display: "inline-block" }}>
<TravelDateRangeCalendar
value={value}
onValueChange={setValue}
localeCode={"sv"}
heading={"Select dates"}
/>
</div>
);
};

export const Locales = () => {
const locales = [
"sv",
"da",
"en-GB",
"pl",
"nl",
"en-US",
"de-AT",
"de-DE",
"fr",
"de",
"es",
"nb",
];

return (
<Column gap={4}>
{locales.map((locale) => (
<LocaleDemo localeCode={locale} />
))}
</Column>
);
};

const LocaleDemo = ({ localeCode }: { localeCode: string }) => {
const [value, setValue] = useState<TravelDateRangeInputValue | undefined>(
undefined
);

return (
<Column gap={2}>
<Row>
<Banner headerText={"Locale: " + localeCode} variant={"info"} />
</Row>
<Row>
<TravelDateRangeCalendar
value={value}
onValueChange={setValue}
localeCode={localeCode}
/>
</Row>
</Column>
);
};

export const ParseDate = () => {
const [startDate, setStartDate] = useState<Date | undefined>(undefined);

const [endDate, setEndDate] = useState<Date | undefined>(undefined);

const [value, setValue] = useState<TravelDateRangeInputValue | undefined>(
undefined
);

const setValueHandler = (value: TravelDateRangeInputValue) => {
setValue(value);
if (value.startDate) {
setStartDate(parseLocalizedDateString(value.startDate, "sv"));
}
if (value.endDate) {
setEndDate(parseLocalizedDateString(value.endDate, "sv"));
}
};

return (
<div style={{ display: "inline-block" }}>
<TravelDateRangeCalendar
value={value}
onValueChange={setValueHandler}
localeCode={"sv"}
heading={"Select dates"}
/>
<Spacing num={2} />
<Row gap={4}>
<Label text={"Start date"}>{startDate?.toDateString() ?? "-"}</Label>
<Label text={"End date"}>{endDate?.toDateString() ?? "-"}</Label>
</Row>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as React from "react";
import { Column, Heading, HeadingVariant } from "@stenajs-webui/core";
import { ValueAndOnValueChangeProps } from "@stenajs-webui/forms";
import { TravelDateTextInputFields } from "../../../features/travel-calendar/components/TravelDateTextInputFields";
import { MonthPicker } from "../../../features/month-picker/MonthPicker";
import { useTravelDateRangeInput } from "../../../features/travel-calendar/hooks/UseTravelDateRangeInput";
import { MonthHeader } from "../../../features/travel-calendar/components/MonthHeader";
import { TravelCalendar } from "../../../features/travel-calendar/components/TravelCalendar";
import { TravelDateRangeInputValue } from "../../../features/travel-calendar/types";

export interface TravelDateRangeCalendarProps
extends ValueAndOnValueChangeProps<TravelDateRangeInputValue> {
localeCode?: string;
initialMonthInFocus?: Date;
startDateLabel?: string;
endDateLabel?: string;
previousMonthButtonAriaLabel?: string;
nextMonthButtonAriaLabel?: string;
heading?: string;
headingLevel?: HeadingVariant;
firstMonthInMonthPicker?: Date;
numMonthsInMonthPicker?: number;
}

export const TravelDateRangeCalendar: React.FC<
TravelDateRangeCalendarProps
> = ({
value,
onValueChange,
startDateLabel,
endDateLabel,
localeCode = "sv",
initialMonthInFocus,
previousMonthButtonAriaLabel = "Previous month",
nextMonthButtonAriaLabel = "Next month",
heading,
headingLevel,
numMonthsInMonthPicker = 12,
firstMonthInMonthPicker = new Date(),
}) => {
const inputProps = useTravelDateRangeInput(
value,
onValueChange,
localeCode,
initialMonthInFocus
);

const {
visiblePanel,
visibleMonth,
onValueChangeByInputs,
setVisibleMonth,
setVisiblePanel,
monthPickerButtonRef,
} = inputProps;

return (
<Column gap={3}>
{heading && (
<Heading variant={"h2"} as={headingLevel}>
{heading}
</Heading>
)}
<TravelDateTextInputFields
value={value}
onValueChange={onValueChangeByInputs}
localeCode={localeCode}
startDateLabel={startDateLabel}
endDateLabel={endDateLabel}
/>

<MonthHeader
{...inputProps}
previousMonthButtonAriaLabel={previousMonthButtonAriaLabel}
nextMonthButtonAriaLabel={nextMonthButtonAriaLabel}
/>

{visiblePanel === "calendar" && <TravelCalendar {...inputProps} />}

{visiblePanel === "month-picker" && (
<MonthPicker
firstMonth={firstMonthInMonthPicker}
numMonths={numMonthsInMonthPicker}
value={visibleMonth}
onValueChange={(v) => {
setVisibleMonth(v);
setVisiblePanel("calendar");
monthPickerButtonRef.current?.focus();
}}
onCancel={() => {
setVisiblePanel("calendar");
monthPickerButtonRef.current?.focus();
}}
/>
)}
</Column>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.travelDateRangeInput {
.overlay {
transition: opacity var(--swui-animation-time-fast) ease-in-out;

opacity: 1;
&:not(&.calendarVisible) {
opacity: 0;
}
}
}
Loading

0 comments on commit 5f373cf

Please sign in to comment.