Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 203 additions & 181 deletions skills/carbon-react/components/date-input.md

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions src/__internal__/input/input-style-overrides.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@ import addFocusStyling from "../../style/utils/add-focus-styling";
export const dateStyleOverrides = css`
.date & {
.input-text-container input {
padding: 0 0 0 12px;
margin-right: -12px;
padding-inline-end: 0;
}
}

.date-typical [data-role="date-input-wrapper"] & {
&:focus-within {
box-shadow: none;
outline: none;
}

.input-text-container input:focus {
border-radius: var(--global-radius-action-m) 0 0
var(--global-radius-action-m);
${addFocusStyling()}
z-index: 2;
}
}
`;
Expand Down Expand Up @@ -144,7 +157,6 @@ const searchNewBaseStyles = css`

&:focus-within:has(:focus:not(button)) {
box-shadow: none;
-webkit-box-shadow: none;
}

.input-text-container input[type="search"]:focus {
Expand Down Expand Up @@ -331,7 +343,6 @@ export const passwordStyleOverrides = css`
[data-component="password"] & {
&:focus-within {
box-shadow: none !important;
-webkit-box-shadow: none !important;
outline: none !important;
}

Expand Down
10 changes: 4 additions & 6 deletions src/__internal__/label/label.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,14 @@ const StyledLabel = styled.label<StyledLabelProps>`
${({ $isRequired, $size, $disabled }) =>
$isRequired &&
css`
display: inline-flex;
align-items: center;

::after {
content: "*";
color: ${$disabled
? "var(--input-labelset-label-disabled)"
: "var(--input-labelset-label-required)"};
content: "*";
display: inline-block;
font: ${getFontToken($size)};
margin-left: 4px;
margin-inline-start: 4px;
}
`}

Expand All @@ -65,7 +63,7 @@ const StyledLabel = styled.label<StyledLabelProps>`
.fieldset-required-input & {
::after {
content: "";
margin-left: 0;
margin-inline-start: 0;
}
}
`;
Expand Down
18 changes: 18 additions & 0 deletions src/__internal__/label/label.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ test("should apply medium font to required indicator by default", () => {
);
});

test("should apply small font to label and required indicator", () => {
render(
<Label size="small" isRequired>
Small Required
</Label>,
);

expect(screen.getByText("Small Required")).toHaveStyleRule(
"font",
"var(--global-font-static-comp-medium-s)",
);
expect(screen.getByText("Small Required")).toHaveStyleRule(
"font",
"var(--global-font-static-comp-medium-s)",
{ modifier: "::after" },
);
});

test("should apply disabled colour and set aria-disabled when `disabled` prop is true", () => {
render(
<Label size="medium" disabled>
Expand Down
52 changes: 34 additions & 18 deletions src/components/date-range/date-range-interaction.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,22 @@ export const MinMaxDateRange: Story = {
play: async ({ canvasElement }) => {
if (!allowInteractions()) return;
const canvas = within(canvasElement);
const [startIcon, endIcon] = canvas.getAllByTestId("icon");
const [startCalendarButton, endCalendarButton] = canvas.getAllByRole(
"button",
{ name: "Open calendar" },
);
const startInput = canvas.getByRole("textbox", { name: /start date/i });
const endInput = canvas.getByRole("textbox", { name: /end date/i });

await userEvent.click(startIcon);
// Navigate to previous month — all days there are before minDate ("2025-07-14"),
// so pressing Enter on a disabled day should not commit a new value.
await userEvent.click(startCalendarButton);
await userEvent.keyboard("{PageUp}{End}{Enter}");
await expect(startInput).toHaveValue("17/07/2025");

await userEvent.click(endIcon);
// Navigate to next month — all days there are after maxDate ("2025-07-30"),
// so pressing Enter on a disabled day should not commit a new value.
await userEvent.click(endCalendarButton);
await userEvent.keyboard("{PageDown}{Home}{Enter}");
await expect(endInput).toHaveValue("20/07/2025");
},
Expand Down Expand Up @@ -158,13 +165,16 @@ export const DisabledDaysDateRange: Story = {
play: async ({ canvasElement }) => {
if (!allowInteractions()) return;
const canvas = within(canvasElement);
const [startIcon, endIcon] = canvas.getAllByTestId("icon");
const [startCalendarButton, endCalendarButton] = canvas.getAllByRole(
"button",
{ name: "Open calendar" },
);
const endInput = canvas.getByRole("textbox", { name: /end date/i });

await userEvent.click(startIcon);
await userEvent.click(startCalendarButton);
await userEvent.keyboard("{ArrowLeft}{Enter}");

await userEvent.click(endIcon);
await userEvent.click(endCalendarButton);
await userEvent.keyboard("{ArrowRight}{ArrowDown}");
await expect(endInput).toHaveValue("25/07/2025");
},
Expand All @@ -183,20 +193,20 @@ export const FocusStateDateRange: Story = {
if (!allowInteractions()) return;
const canvas = within(canvasElement);

const [startIcon] = canvas.getAllByTestId("icon");
await userEvent.click(startIcon);
const [startCalendarButton] = canvas.getAllByRole("button", {
name: "Open calendar",
});
await userEvent.click(startCalendarButton);

const selectedDayBtn = canvas.getByRole("button", { name: /selected$/i });

// The picker's internal tab order depth is variable, so we tab up to 6 times
// until focus lands on the selected day button.
const doc = canvasElement.ownerDocument as Document;
for (let i = 0; i < 6 && doc.activeElement !== selectedDayBtn; i += 1) {
await userEvent.tab();
}

if (doc.activeElement !== selectedDayBtn) {
(selectedDayBtn as HTMLElement).focus();
}

await expect(selectedDayBtn).toHaveFocus();
},
};
Expand Down Expand Up @@ -275,8 +285,10 @@ export const LocaleDeDateRange: Story = {
if (!allowInteractions()) return;

const canvas = within(canvasElement);
const [startIcon] = canvas.getAllByTestId("icon");
await userEvent.click(startIcon);
const [startCalendarButton] = canvas.getAllByRole("button", {
name: "Open calendar",
});
await userEvent.click(startCalendarButton);

const prevMonth = canvas.getByRole("button", { name: /de-DE-previous/i });
await userEvent.click(prevMonth);
Expand All @@ -298,8 +310,10 @@ export const TypingSyncDateRange: Story = {

const canvas = within(canvasElement);

const [startIcon] = canvas.getAllByTestId("icon");
await userEvent.click(startIcon);
const [startCalendarButton] = canvas.getAllByRole("button", {
name: "Open calendar",
});
await userEvent.click(startCalendarButton);

const startInput = canvas.getByRole("textbox", { name: /start date/i });
await userEvent.clear(startInput);
Expand All @@ -324,8 +338,10 @@ export const MonthYearNavigationDateRange: Story = {
play: async ({ canvasElement }) => {
if (!allowInteractions()) return;
const c = within(canvasElement);
const [startIcon] = c.getAllByTestId("icon");
await userEvent.click(startIcon);
const [startCalendarButton] = c.getAllByRole("button", {
name: "Open calendar",
});
await userEvent.click(startCalendarButton);
const next = c.getByRole("button", { name: /next month/i });
await userEvent.click(next);
await expect(next).toHaveFocus();
Expand Down
2 changes: 1 addition & 1 deletion src/components/date-range/date-range.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
formattedValue,
parseISODate,
checkISOFormatAndLength,
} from "../date/__internal__/utils";
} from "../date/__internal__/utils/utils";
import DateInput, { DateChangeEvent, DateInputProps } from "../date";
import { filterStyledSystemMarginProps } from "../../style/utils";
import tagComponent, {
Expand Down
8 changes: 5 additions & 3 deletions src/components/date-range/date-range.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ You can use the `required` prop to indicate if the fields are mandatory.
<Canvas of={DateRangeStories.Required} />

### With disabled dates
You can disable dates by using `startDateProps` and `endDateProps`, which allow you to pass properties directly to the start or end date.

You can disable dates by using `startDateProps` and `endDateProps`, which allow you to pass properties directly to the start or end date.
For example, you can set a `minDate` or `maxDate`, or use `pickerProps` with a `disabled` option that accepts a matcher or an array of [matchers](https://daypicker.dev/api/type-aliases/Matcher#example).

<Canvas of={DateRangeStories.WithDisabledDates} />

### Locale override

The examples below illustrates how to override the locale passed into the DateRange component. In this example it has been set up for
the French locale. Required locales can be imported like so `import { fr } from 'date-fns/locale';`
The example below illustrates how to override the locale passed into the DateRange component. Locale values are merged with `en-GB`,
so omitted accessibility labels fall back to British English. Applications should provide approved translations through
`I18nProvider` for every language they support. Required date locales can be imported like so `import { fr } from 'date-fns/locale';`

<Canvas of={DateRangeStories.LocaleOverrideExampleImplementation} />

Expand Down
57 changes: 36 additions & 21 deletions src/components/date-range/date-range.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -667,20 +667,24 @@ test("should close the open 'start' picker when the user moves focus to the `end
const startDate = screen.getByTestId("start");
const endDate = screen.getByTestId("end");

const startCalendarIcon = screen.getAllByTestId("input-icon-toggle")[0];
await user.click(startCalendarIcon);
const startCalendarButton = within(startDate).getByRole("button", {
name: "Open calendar",
});
await user.click(startCalendarButton);

expect(within(startDate).getByRole("grid")).toBeVisible();
expect(within(endDate).queryByRole("grid")).not.toBeInTheDocument();

const endCalendarIcon = screen.getAllByTestId("input-icon-toggle")[1];
await user.click(endCalendarIcon);
const endCalendarButton = within(endDate).getByRole("button", {
name: "Open calendar",
});
await user.click(endCalendarButton);

expect(within(endDate).getByRole("grid")).toBeVisible();
expect(within(startDate).queryByRole("grid")).not.toBeInTheDocument();
});

test("should apply aria-label and aria-labelledby to the date picker region", async () => {
test("should apply aria-label and aria-labelledby to the date picker dialog", async () => {
const user = userEvent.setup();
render(
<DateRange
Expand All @@ -699,30 +703,34 @@ test("should apply aria-label and aria-labelledby to the date picker region", as
const startDate = screen.getByTestId("start");
const endDate = screen.getByTestId("end");

const startCalendarIcon = screen.getAllByTestId("input-icon-toggle")[0];
await user.click(startCalendarIcon);
const startCalendarButton = within(startDate).getByRole("button", {
name: "Open calendar",
});
await user.click(startCalendarButton);

expect(within(startDate).getByRole("region")).toBeVisible();
expect(within(startDate).getByRole("region")).toHaveAttribute(
expect(within(startDate).getByRole("dialog")).toBeVisible();
expect(within(startDate).getByRole("dialog")).toHaveAttribute(
"aria-label",
"start aria-label",
);
expect(within(startDate).getByRole("region")).toHaveAttribute(
expect(within(startDate).getByRole("dialog")).toHaveAttribute(
"aria-labelledby",
"start aria-labelledby",
expect.stringContaining("start aria-labelledby"),
);

const endCalendarIcon = screen.getAllByTestId("input-icon-toggle")[1];
await user.click(endCalendarIcon);
const endCalendarButton = within(endDate).getByRole("button", {
name: "Open calendar",
});
await user.click(endCalendarButton);

expect(within(endDate).getByRole("region")).toBeVisible();
expect(within(endDate).getByRole("region")).toHaveAttribute(
expect(within(endDate).getByRole("dialog")).toBeVisible();
expect(within(endDate).getByRole("dialog")).toHaveAttribute(
"aria-label",
"end aria-label",
);
expect(within(endDate).getByRole("region")).toHaveAttribute(
expect(within(endDate).getByRole("dialog")).toHaveAttribute(
"aria-labelledby",
"end aria-labelledby",
expect.stringContaining("end aria-labelledby"),
);
});

Expand All @@ -741,14 +749,18 @@ test("should close the open 'end' picker when the user moves focus to the `start
const startDate = screen.getByTestId("start");
const endDate = screen.getByTestId("end");

const endCalendarIcon = screen.getAllByTestId("input-icon-toggle")[1];
await user.click(endCalendarIcon);
const endCalendarButton = within(endDate).getByRole("button", {
name: "Open calendar",
});
await user.click(endCalendarButton);

expect(within(endDate).getByRole("grid")).toBeVisible();
expect(within(startDate).queryByRole("grid")).not.toBeInTheDocument();

const startCalendarIcon = screen.getAllByTestId("input-icon-toggle")[0];
await user.click(startCalendarIcon);
const startCalendarButton = within(startDate).getByRole("button", {
name: "Open calendar",
});
await user.click(startCalendarButton);

expect(within(startDate).getByRole("grid")).toBeVisible();
expect(within(endDate).queryByRole("grid")).not.toBeInTheDocument();
Expand Down Expand Up @@ -1197,6 +1209,9 @@ describe("Locale formatting overrides", () => {
ariaLabels: {
nextMonthButton: () => "foo",
previousMonthButton: () => "foo",
chooseMonth: () => "Choose the month",
chooseYear: () => "Choose the year",
closeButton: () => "Close",
},
dateFnsLocale: () => enGBLocale,
dateFormatOverride: "y-m-ddd",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";

import StyledCalendarCloseButton from "./calendar-close-button.style";

interface CalendarCloseButtonProps {
children: React.ReactNode;
onClick: (
event:
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLButtonElement>,
) => void;
}

const CalendarCloseButton = ({
children,
onClick,
}: CalendarCloseButtonProps) => (
<StyledCalendarCloseButton
data-role="date-picker-close"
type="button"
variant="default"
variantType="subtle"
size="small"
onClick={onClick}
>
{children}
</StyledCalendarCloseButton>
);

export default CalendarCloseButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from "styled-components";

import Button from "../../../button/__next__";

const StyledCalendarCloseButton = styled(Button)`
align-self: flex-end;

@media (max-width: 480px) {
align-self: stretch;
width: 100%;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why centre the close button? Is that standard practice on small screens? Feels odd to move it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me remove centring

}
`;

export default StyledCalendarCloseButton;
Loading
Loading