Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/tall-schools-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clickhouse/click-ui': minor
---

Adds the property `hasCloseButton`, which shows a close button on the right side of the input to the date pickers.
153 changes: 141 additions & 12 deletions src/components/DatePicker/Common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { styled } from 'styled-components';
import { InputElement, InputStartContent, InputWrapper } from '@/components/InputWrapper';
import {
KeyboardEvent,
MouseEvent,
ReactNode,
useCallback,
useEffect,
Expand Down Expand Up @@ -45,9 +46,10 @@ const yearsOffset = Math.floor(totalYears / 2);
const HighlightedInputWrapper = styled(InputWrapper)<{
$isActive: boolean;
$width?: string;
$minWidth?: string;
error?: boolean;
}>`
${({ $isActive, $width, error, theme }) => {
${({ $isActive, $width, $minWidth, error, theme }) => {
let borderColor = $isActive
? theme.click.datePicker.dateOption.color.stroke.active
: theme.click.field.color.stroke.default;
Expand All @@ -58,21 +60,37 @@ const HighlightedInputWrapper = styled(InputWrapper)<{

return `border: ${theme.click.datePicker.dateOption.stroke} solid ${borderColor};
width: ${$width ? $width : explicitWidth};
${$width && `min-width: ${explicitWidth};`}
${$minWidth ? `min-width: ${$minWidth};` : ''}
`;
}}
}`;

interface DatePickerInputProps {
isActive: boolean;
disabled: boolean;
id?: string;
partialMonth?: number;
partialYear?: number;
placeholder?: string;
selectedDate?: Date;
timezone?: Timezone;
}
// This span is rendered in the Date*PickerInputs, which are children of a Popover Trigger
// which are buttons. So using an IconButton will cause React to throw an error since a button can't be inside a button
const ClearButton = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
${({ theme }) => `
padding: ${theme.click.button.iconButton.default.space.y} ${theme.click.button.iconButton.default.space.x};
border: 1px solid ${theme.click.button.iconButton.color.primary.stroke.default};
border-radius: ${theme.click.button.iconButton.radii.all};
color: ${theme.click.button.iconButton.color.primary.text.default};
background-color: ${theme.click.button.iconButton.color.primary.background.default};

&:hover {
border-color: ${theme.click.button.iconButton.color.primary.stroke.hover};
background-color: ${theme.click.button.iconButton.color.primary.background.hover};
}

&:active {
border-color: ${theme.click.button.iconButton.color.primary.stroke.active};
background-color: ${theme.click.button.iconButton.color.primary.background.active};
}
`}
`;

const formatPartialDate = (
timezone: Timezone,
Expand All @@ -99,10 +117,25 @@ const formatPartialDate = (
return '';
};

interface DatePickerInputProps {
isActive: boolean;
disabled: boolean;
hasClearButton?: boolean;
id?: string;
onClear?: () => void;
partialMonth?: number;
partialYear?: number;
placeholder?: string;
selectedDate?: Date;
timezone?: Timezone;
}

export const DatePickerInput = ({
isActive,
disabled,
hasClearButton,
id,
onClear,
partialMonth,
partialYear,
placeholder,
Expand All @@ -117,6 +150,17 @@ export const DatePickerInput = ({
partialMonth
);

// Stop propagation to keep the event from bubbling up to the dropdown trigger and opening the dropdown
const handleClear = useCallback(
(event: MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
if (onClear) {
onClear();
}
},
[onClear]
);

return (
<HighlightedInputWrapper
$isActive={isActive}
Expand All @@ -133,14 +177,29 @@ export const DatePickerInput = ({
readOnly
value={formattedSelectedDate}
/>
{hasClearButton && !disabled && (
<ClearButton
role="button"
aria-label="clear input"
data-testid="datepicker-input-clear"
onClick={handleClear}
>
<Icon
name="cross"
size="sm"
/>
</ClearButton>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why isn't this an IconButton?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ariser the hover styling on IconButton overflowed the input and couldn't be changed

)}
</HighlightedInputWrapper>
);
};

interface DateRangePickerInputProps {
isActive: boolean;
disabled: boolean;
hasClearButton?: boolean;
id?: string;
onClear?: () => void;
placeholder?: string;
selectedEndDate?: Date;
selectedStartDate?: Date;
Expand All @@ -150,14 +209,33 @@ interface DateRangePickerInputProps {
export const DateRangePickerInput = ({
isActive,
disabled,
hasClearButton,
id,
onClear,
placeholder,
selectedEndDate,
selectedStartDate,
timezone = 'system',
}: DateRangePickerInputProps) => {
const defaultId = useId();

// Stop propagation to keep the event from bubbling up to the dropdown trigger and opening the dropdown
const handleClear = useCallback(
(event: MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
if (onClear) {
onClear();
}
},
[onClear]
);

// The Dropdown.Trigger wrapping this input toggles on pointerdown, so stop
// pointerdown (and click) from bubbling up to it when clearing.
const handleClearPointerDown = useCallback((event: MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
}, []);

let formattedValue = (
<Text
color="muted"
Expand Down Expand Up @@ -192,6 +270,7 @@ export const DateRangePickerInput = ({
return (
<HighlightedInputWrapper
$isActive={isActive}
$width={hasClearButton ? 'max-content' : undefined}
Comment thread
hoorayimhelping marked this conversation as resolved.
disabled={disabled}
id={id ?? defaultId}
>
Expand All @@ -205,14 +284,30 @@ export const DateRangePickerInput = ({
>
{formattedValue}
</InputElement>
{hasClearButton && !disabled && (
<ClearButton
role="button"
aria-label="clear input"
data-testid="daterangepicker-input-clear"
onPointerDown={handleClearPointerDown}
onClick={handleClear}
>
<Icon
name="cross"
size="sm"
/>
</ClearButton>
)}
</HighlightedInputWrapper>
);
};

interface DateTimeRangePickerInputProps {
isActive: boolean;
disabled: boolean;
hasClearButton?: boolean;
id?: string;
onClear?: () => void;
placeholder?: string;
selectedEndDate?: Date;
selectedStartDate?: Date;
Expand All @@ -223,7 +318,9 @@ interface DateTimeRangePickerInputProps {
export const DateTimeRangePickerInput = ({
isActive,
disabled,
hasClearButton,
id,
onClear,
placeholder,
selectedEndDate,
selectedStartDate,
Expand All @@ -232,6 +329,23 @@ export const DateTimeRangePickerInput = ({
}: DateTimeRangePickerInputProps) => {
const defaultId = useId();

// Stop propagation to keep the event from bubbling up to the dropdown trigger and opening the dropdown
const handleClear = useCallback(
(event: MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
if (onClear) {
onClear();
}
},
[onClear]
);

// The Dropdown.Trigger wrapping this input toggles on pointerdown, so stop
// pointerdown (and click) from bubbling up to it when clearing.
const handleClearPointerDown = useCallback((event: MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
}, []);

const formatDateTime = useCallback(
(date: Date) => {
if (shouldShowSeconds) {
Expand Down Expand Up @@ -299,6 +413,7 @@ export const DateTimeRangePickerInput = ({
<HighlightedInputWrapper
$isActive={isActive}
$width="max-content"
$minWidth={explicitWidth}
disabled={disabled}
error={startDateIsAfterEndDate}
id={id ?? defaultId}
Expand All @@ -313,6 +428,20 @@ export const DateTimeRangePickerInput = ({
>
{formattedValue}
</InputElement>
{hasClearButton && !disabled && (
<ClearButton
role="button"
aria-label="clear input"
data-testid="datetimepicker-input-clear"
onPointerDown={handleClearPointerDown}
onClick={handleClear}
>
<Icon
name="cross"
size="sm"
/>
</ClearButton>
)}
</HighlightedInputWrapper>
);
};
Expand Down
16 changes: 16 additions & 0 deletions src/components/DatePicker/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,19 @@ export const TimezoneLocalVsUTC = {
);
},
};

export const HasClearButton = {
...defaultStory,
render: (args: Args) => {
const date = args.date ? new Date(args.date) : new Date('2026-04-15');
return (
<DatePicker
date={date}
hasClearButton
onSelectDate={args.onSelectDate}
placeholder={args.placeholder}
responsivePositioning={false}
/>
);
},
};
63 changes: 63 additions & 0 deletions src/components/DatePicker/DatePicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,67 @@ describe('DatePicker', () => {
});
});
});

describe('clearing the selected date', () => {
it('hides the clear control until it is explicitly enabled', () => {
const { queryByTestId } = renderCUI(<DatePicker onSelectDate={vi.fn()} />);

expect(queryByTestId('datepicker-input-clear')).not.toBeInTheDocument();
});

it('offers a clear control when enabled', () => {
const { getByTestId } = renderCUI(
<DatePicker
hasClearButton
onSelectDate={vi.fn()}
/>
);

expect(getByTestId('datepicker-input-clear')).toBeInTheDocument();
});

it('withholds the clear control while the picker is disabled', () => {
const { queryByTestId } = renderCUI(
<DatePicker
disabled
hasClearButton
onSelectDate={vi.fn()}
/>
);

expect(queryByTestId('datepicker-input-clear')).not.toBeInTheDocument();
});

describe('when the clear control is clicked', () => {
it('removes the currently selected date', async () => {
const { getByTestId } = renderCUI(
<DatePicker
hasClearButton
date={new Date('07-04-2020')}
onSelectDate={vi.fn()}
/>
);

expect(getByTestId('datepicker-input')).toHaveValue('Jul 04, 2020');

await userEvent.click(getByTestId('datepicker-input-clear'));

expect(getByTestId('datepicker-input')).toHaveValue('');
});

it('leaves the calendar closed', async () => {
const { getByTestId, queryByTestId } = renderCUI(
<DatePicker
hasClearButton
date={new Date('07-04-2020')}
onSelectDate={vi.fn()}
/>
);

await userEvent.click(getByTestId('datepicker-input-clear'));

expect(queryByTestId('datepicker-calendar-container')).not.toBeInTheDocument();
});
});
});
});
Loading
Loading