Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/components/RelativeDatePicker/RelativeDatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function RelativeDatePicker(props: RelativeDatePickerProps) {
disabled: isMobile && state.mode === 'absolute',
className: b('input', {mobile: isMobile && state.mode === 'absolute'}),
}}
hasClear={props.hasClear && !(isMobile && state.mode === 'absolute')}
hasClear={fieldProps.hasClear && !(isMobile && state.mode === 'absolute')}
startContent={
<Button {...modeSwitcherProps}>
<Icon data={FunctionIcon} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ export const Default = meta.story({
},
},
timeZone: timeZoneControl,
allowNullableValues: {
control: {
type: 'boolean',
},
},
withPresets: {
control: {
type: 'boolean',
},
},
},
});

Expand Down Expand Up @@ -144,6 +154,22 @@ export const CustomPresets = Default.extend({
{to: 'now', from: 'now-5y', title: 'Last five years'},
],
},
{
id: 'unlimited',
title: 'Unlimited',
presets: [
{
from: null,
to: 'now',
title: 'Past',
},
{
from: 'now',
to: null,
title: 'Future',
},
],
},
],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react';

import {describe, expect, it, vitest} from 'vitest';
import {userEvent} from 'vitest/browser';

import {render} from '#test-utils/utils';

import {RelativeRangeDatePicker} from '../RelativeRangeDatePicker';
import type {Preset} from '../components/Presets/defaultPresets';
import type {PresetTab} from '../components/Presets/utils';

const presetTabs: PresetTab[] = [
{
id: 'main',
title: 'Presets',
presets: [
{from: 'now-5d', to: 'now', title: 'Last five days'},
{from: 'not-a-date', to: 'now', title: 'Broken preset'},
],
},
{
id: 'unlimited',
title: 'Unlimited',
presets: [
{from: null, to: 'now', title: 'Past'},
{from: 'now', to: null, title: 'Future'},
],
},
];

const docsWithNullableRows: Preset[] = [
{title: 'Past', from: null, to: 'now'},
{title: 'Future', from: 'now', to: null},
{title: 'Last 5 minutes', from: 'now - 5m', to: 'now'},
];

async function openPicker(screen: Awaited<ReturnType<typeof render>>) {
await userEvent.click(screen.getByLabelText('picker', {exact: true}));
}

async function openUnlimitedTab(screen: Awaited<ReturnType<typeof render>>) {
await userEvent.click(screen.getByRole('tab', {name: 'Unlimited'}));
}

function ToggleNullablePicker() {
const [allowNullableValues, setAllowNullableValues] = React.useState(true);

return (
<React.Fragment>
<button
type="button"
onClick={() => {
setAllowNullableValues((value) => !value);
}}
>
toggle-nullable
</button>
<RelativeRangeDatePicker
withPresets
presetTabs={presetTabs}
allowNullableValues={allowNullableValues}
label="picker"
/>
</React.Fragment>
);
}

describe('RelativeRangeDatePicker: nullable presets', () => {
it('filters nullable and malformed presets when nullable values are disabled', async () => {
const screen = await render(
<RelativeRangeDatePicker
withPresets
presetTabs={presetTabs}
allowNullableValues={false}
label="picker"
/>,
);

await openPicker(screen);

expect(screen.getByText('Last five days')).toBeInTheDocument();
expect(screen.getByText('Broken preset')).not.toBeInTheDocument();
expect(screen.getByRole('tab', {name: 'Unlimited'})).not.toBeInTheDocument();
});

it('shows nullable presets and still hides malformed presets when nullable values are enabled', async () => {
const screen = await render(
<RelativeRangeDatePicker
withPresets
presetTabs={presetTabs}
allowNullableValues
label="picker"
/>,
);

await openPicker(screen);

expect(screen.getByRole('tab', {name: 'Unlimited'})).toBeInTheDocument();
expect(screen.getByText('Broken preset')).not.toBeInTheDocument();

await openUnlimitedTab(screen);

expect(screen.getByText('Past')).toBeInTheDocument();
expect(screen.getByText('Future')).toBeInTheDocument();
});

it('submits nullable preset values through hidden form inputs', async () => {
let value: FormDataEntryValue[] = [];
const onSubmit = vitest.fn((e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
value = [...formData.values()];
});

const screen = await render(
<form onSubmit={onSubmit}>
<RelativeRangeDatePicker
name="date-field"
withPresets
presetTabs={presetTabs}
allowNullableValues
label="picker"
/>
<button type="submit">submit</button>
</form>,
);

await openPicker(screen);
await openUnlimitedTab(screen);
await userEvent.click(screen.getByText('Future'));

await expect.element(screen.getByLabelText('picker', {exact: true})).toHaveValue('Future');

await userEvent.click(screen.getByRole('button', {name: 'submit'}));

expect(onSubmit).toHaveBeenCalledTimes(1);
expect(value).toEqual(['relative', 'now', '', '', 'default']);
});

it('recomputes the control title when nullable mode toggles', async () => {
const screen = await render(<ToggleNullablePicker />);

await openPicker(screen);
await openUnlimitedTab(screen);
await userEvent.click(screen.getByText('Future'));

await expect.element(screen.getByLabelText('picker', {exact: true})).toHaveValue('Future');

await userEvent.click(screen.getByRole('button', {name: 'toggle-nullable'}));

await expect.element(screen.getByLabelText('picker', {exact: true})).toHaveValue('now — ');

await openPicker(screen);

expect(screen.getByRole('tab', {name: 'Unlimited'})).not.toBeInTheDocument();
expect(screen.getByText('Future')).not.toBeInTheDocument();
expect(screen.getByText('Last five days')).toBeInTheDocument();
});

it('hides nullable docs rows when nullable values are disabled', async () => {
const screen = await render(
<RelativeRangeDatePicker
withHeader
allowNullableValues={false}
docs={docsWithNullableRows}
label="picker"
/>,
);

await userEvent.tab();
await userEvent.tab();
await userEvent.keyboard('{Enter}');

expect(screen.getByText('Last 5 minutes')).toBeInTheDocument();
expect(screen.getByText('Past')).not.toBeInTheDocument();
expect(screen.getByText('Future')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const Control = React.forwardRef<HTMLInputElement, ControlProps>(
},
ref,
) => {
const {alwaysShowAsAbsolute, presetTabs, getRangeTitle} = props;
const {alwaysShowAsAbsolute, presetTabs, getRangeTitle, allowNullableValues} = props;
const format = props.format || 'L';

const {t} = i18n.useTranslation();
Expand All @@ -60,12 +60,14 @@ export const Control = React.forwardRef<HTMLInputElement, ControlProps>(
value: state.value,
timeZone: state.timeZone,
alwaysShowAsAbsolute,
allowNullableValues,
format,
presets: presetTabs?.flatMap(({presets}) => presets),
presetsTranslations,
lang,
}),
[
allowNullableValues,
alwaysShowAsAbsolute,
format,
getRangeTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ interface PresetsDocProps {
className?: string;
size?: 's' | 'm' | 'l' | 'xl';
docs?: Preset[];
onStartUpdate: (start: string) => void;
onEndUpdate: (end: string) => void;
onRangeUpdate: (start: string, end: string) => void;
onStartUpdate: (start: string | null) => void;
onEndUpdate: (end: string | null) => void;
onRangeUpdate: (start: string | null, end: string | null) => void;
}

export function PickerDoc({docs = data, ...props}: PresetsDocProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import React from 'react';

import type {DateTime} from '@gravity-ui/date-utils';
import {Button, Text} from '@gravity-ui/uikit';
import type {TextInputSize} from '@gravity-ui/uikit';
Expand All @@ -11,6 +13,7 @@ import type {DomProps, StyleProps} from '../../../types';
import type {RelativeRangeDatePickerStateOptions} from '../../hooks/useRelativeRangeDatePickerState';
import {Presets} from '../Presets/Presets';
import type {Preset} from '../Presets/defaultPresets';
import {filterPresetList} from '../Presets/utils';
import type {PresetTab} from '../Presets/utils';
import {Zones} from '../Zones/Zones';

Expand Down Expand Up @@ -56,6 +59,24 @@ export function PickerForm(
},
) {
const state = useRelativeRangeDatePickerDialogState(props);
const docs = React.useMemo(
() =>
props.docs
? filterPresetList(props.docs, {
minValue: props.minValue,
allowNullableValues: props.allowNullableValues,
})
: props.docs,
[props.allowNullableValues, props.docs, props.minValue],
);

const toRelativeValue = (value: string | null) => {
if (value === null && !props.allowNullableValues) {
return undefined;
}

return value === null ? null : {type: 'relative' as const, value};
};

const fieldProps: RelativeDatePickerProps = {
timeZone: state.timeZone,
Expand All @@ -78,18 +99,25 @@ export function PickerForm(
</Text>
<PickerDoc
size={props.size}
docs={props.docs}
docs={docs}
onStartUpdate={(start) => {
state.setStart({type: 'relative', value: start});
const nextStart = toRelativeValue(start);
if (nextStart !== undefined) {
state.setStart(nextStart);
}
}}
onEndUpdate={(end) => {
state.setEnd({type: 'relative', value: end});
const nextEnd = toRelativeValue(end);
if (nextEnd !== undefined) {
state.setEnd(nextEnd);
}
}}
onRangeUpdate={(start, end) => {
state.setRange(
{type: 'relative', value: start},
{type: 'relative', value: end},
);
const nextStart = toRelativeValue(start);
const nextEnd = toRelativeValue(end);
if (nextStart !== undefined && nextEnd !== undefined) {
state.setRange(nextStart, nextEnd);
}
}}
/>
</div>
Expand Down Expand Up @@ -146,15 +174,16 @@ export function PickerForm(
presetTabs={props.presetTabs}
onChoosePreset={(start, end) => {
state.setRange(
{type: 'relative', value: start},
{type: 'relative', value: end},
start === null ? null : {type: 'relative', value: start},
end === null ? null : {type: 'relative', value: end},
props.applyPresetsImmediately,
);
if (!props.withApplyButton || props.applyPresetsImmediately) {
props.onApply();
}
}}
minValue={props.minValue}
allowNullableValues={props.allowNullableValues}
className={b('presets')}
/>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function useRelativeRangeDatePickerDialogState(props: PickerFormProps) {
}
}

function setRange(newStart: Value, newEnd: Value, force?: boolean) {
function setRange(newStart: Value | null, newEnd: Value | null, force?: boolean) {
if (props.readOnly) {
return;
}
Expand Down
Loading
Loading