Skip to content

Commit

Permalink
feat(RangeDateSelection): add new component
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS committed Aug 14, 2024
1 parent 3767684 commit eafb388
Show file tree
Hide file tree
Showing 28 changed files with 2,121 additions and 0 deletions.
44 changes: 44 additions & 0 deletions src/components/RangeDateSelection/RangeDateSelection.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@use '../variables';
@use '../mixins';

$block: '.#{variables.$ns}range-date-selection';

#{$block} {
display: grid;
align-items: center;
grid-template-areas: 'buttons-start ruler buttons-end';
grid-template-columns: auto 1fr auto;

border-block: 1px solid var(--g-color-line-generic);

&__ruler {
grid-area: ruler;

&_dragging #{$block}__selection {
pointer-events: none;
}
}

&__buttons {
display: flex;
align-items: center;

height: 22px;

&_position_start {
grid-area: buttons-start;

padding-inline-end: var(--g-spacing-half);

border-inline-end: 1px solid var(--g-color-line-generic);
}

&_position_end {
grid-area: buttons-end;

padding-inline-start: var(--g-spacing-half);

border-inline-start: 1px solid var(--g-color-line-generic);
}
}
}
115 changes: 115 additions & 0 deletions src/components/RangeDateSelection/RangeDateSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use client';

import React from 'react';

import type {DateTime} from '@gravity-ui/date-utils';
import {Minus, Plus} from '@gravity-ui/icons';
import {Button, Icon} from '@gravity-ui/uikit';

import {block} from '../../utils/cn';
import type {AccessibilityProps, DomProps, StyleProps} from '../types';
import {filterDOMProps} from '../utils/filterDOMProps';

import {DateTimeRuler} from './components/Ruler/Ruler';
import {SelectionControl} from './components/SelectionControl/SelectionControl';
import {useRangeDateSelectionState} from './hooks/useRangeDateSelectionState';
import type {RangeDateSelectionOptions} from './hooks/useRangeDateSelectionState';
import {i18n} from './i18n';

import './RangeDateSelection.scss';

const b = block('range-date-selection');

export interface RangeDateSelectionProps
extends RangeDateSelectionOptions,
DomProps,
StyleProps,
AccessibilityProps {
/** Formats time ticks */
formatTime?: (time: DateTime) => string;
/** Displays now line */
displayNow?: boolean;
/** Enables dragging ruler */
draggableRuler?: boolean;
/** Displays buttons to scale selection */
hasScaleButtons?: boolean;
/** Position of scale buttons */
scaleButtonsPosition?: 'start' | 'end';
}

export function RangeDateSelection(props: RangeDateSelectionProps) {
const state = useRangeDateSelectionState(props);

const [isDraggingRuler, setDraggingRuler] = React.useState(false);

const handleRulerMoveStart = () => {
state.setDraggingValue(state.value);
setDraggingRuler(true);
};
const handleRulerMove = (d: number) => {
const intervalWidth = state.viewportInterval.end.diff(state.viewportInterval.start);
const delta = -Math.floor((d * intervalWidth) / 100);
state.move(delta);
};
const handleRulerMoveEnd = () => {
setDraggingRuler(false);
state.endDragging();
};

let id = React.useId();
id = props.id ?? id;

return (
<div
{...filterDOMProps(props, {labelable: true})}
id={id}
className={b(null, props.className)}
style={props.style}
dir="ltr" // TODO: RTL support
>
<DateTimeRuler
className={b('ruler', {dragging: isDraggingRuler})}
{...state.viewportInterval}
onMoveStart={handleRulerMoveStart}
onMove={props.draggableRuler ? handleRulerMove : undefined}
onMoveEnd={handleRulerMoveEnd}
dragDisabled={state.isDragging}
displayNow={props.displayNow}
minValue={props.minValue}
maxValue={props.maxValue}
formatTime={props.formatTime}
timeZone={state.timeZone}
>
<SelectionControl className={b('selection')} state={state} aria-labelledby={id} />
</DateTimeRuler>
{props.hasScaleButtons ? (
<div className={b('buttons', {position: props.scaleButtonsPosition ?? 'start'})}>
<Button
view="flat-secondary"
size="xs"
onClick={() => {
state.startDragging();
state.scale(0.5);
state.endDragging();
}}
extraProps={{'aria-label': i18n('Decrease range')}}
>
<Icon data={Minus} />
</Button>
<Button
view="flat-secondary"
size="xs"
onClick={() => {
state.startDragging();
state.scale(1.5);
state.endDragging();
}}
extraProps={{'aria-label': i18n('Increase range')}}
>
<Icon data={Plus} />
</Button>
</div>
) : null}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React from 'react';

import {dateTimeParse} from '@gravity-ui/date-utils';
import type {DateTime} from '@gravity-ui/date-utils';
import {Button} from '@gravity-ui/uikit';
import {action} from '@storybook/addon-actions';
import type {Meta, StoryObj} from '@storybook/react';

import {timeZoneControl} from '../../../demo/utils/zones';
import {RelativeRangeDatePicker} from '../../RelativeRangeDatePicker';
import type {RelativeRangeDatePickerValue} from '../../RelativeRangeDatePicker';
import {RangeDateSelection} from '../RangeDateSelection';

const meta: Meta<typeof RangeDateSelection> = {
title: 'Components/RangeDateSelection',
component: RangeDateSelection,
tags: ['autodocs'],
args: {
onUpdate: action('onUpdate'),
},
};

export default meta;

type Story = StoryObj<typeof RangeDateSelection>;

export const Default = {
render: (args) => {
const timeZone = args.timeZone;
const props = {
...args,
minValue: args.minValue ? dateTimeParse(args.minValue, {timeZone}) : undefined,
maxValue: args.maxValue ? dateTimeParse(args.maxValue, {timeZone}) : undefined,
placeholderValue: args.placeholderValue
? dateTimeParse(args.placeholderValue, {timeZone})
: undefined,
};
return <RangeDateSelection {...props} />;
},
argTypes: {
minValue: {
control: {
type: 'text',
},
},
maxValue: {
control: {
type: 'text',
},
},
placeholderValue: {
control: {
type: 'text',
},
},
timeZone: timeZoneControl,
},
} satisfies Story;

export const WithControls = {
...Default,
render: function WithControls(args) {
const timeZone = args.timeZone;
const minValue = args.minValue ? dateTimeParse(args.minValue, {timeZone}) : undefined;
const maxValue = args.maxValue ? dateTimeParse(args.maxValue, {timeZone}) : undefined;
const placeholderValue = args.placeholderValue
? dateTimeParse(args.placeholderValue, {timeZone})
: undefined;

const [value, setValue] = React.useState<RelativeRangeDatePickerValue>({
start: {
type: 'relative',
value: 'now - 1d',
},
end: {
type: 'relative',
value: 'now',
},
});

const {start, end} = toAbsoluteRange(value, timeZone);

const [, rerender] = React.useState({});
React.useEffect(() => {
const hasRelative = value.start?.type === 'relative' || value.end?.type === 'relative';
if (hasRelative) {
const timer = setInterval(() => {
rerender({});
}, 1000);
return () => clearInterval(timer);
}
return undefined;
}, [value]);

return (
<div>
<div
style={{
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end',
paddingBlock: '1rem',
}}
>
<RelativeRangeDatePicker
style={{width: '20rem'}}
value={value}
onUpdate={(v) => {
if (v) {
setValue(v);
}
}}
format="L LTS"
withApplyButton
withPresets
minValue={minValue}
maxValue={maxValue}
placeholderValue={placeholderValue}
/>
<div style={{display: 'flex', gap: '2px'}}>
<Button
view="flat"
onClick={() => setValue(getRelativeInterval('now - 30m', 'now'))}
>
30m
</Button>
<Button
view="flat"
onClick={() => setValue(getRelativeInterval('now - 1h', 'now'))}
>
1h
</Button>
<Button
view="flat"
onClick={() => setValue(getRelativeInterval('now - 1d', 'now'))}
>
1d
</Button>
<Button
view="flat"
onClick={() => setValue(getRelativeInterval('now - 1w', 'now'))}
>
1w
</Button>
</div>
</div>
<RangeDateSelection
{...args}
value={{start, end}}
onUpdate={(value) => {

Check warning on line 150 in src/components/RangeDateSelection/__stories__/RangeDateSelection.stories.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'value' is already declared in the upper scope on line 70 column 16
setValue({
start: {type: 'absolute', value: value.start},
end: {type: 'absolute', value: value.end},
});
}}
minValue={minValue}
maxValue={maxValue}
placeholderValue={placeholderValue}
/>
</div>
);
},
} satisfies Story;

function getRelativeInterval(start: string, end: string): RelativeRangeDatePickerValue {
return {
start: {type: 'relative', value: start},
end: {type: 'relative', value: end},
};
}

function toAbsoluteRange(interval: RelativeRangeDatePickerValue, timeZone?: string) {
const start: DateTime =
interval.start?.type === 'relative'
? dateTimeParse(interval.start.value, {timeZone})!

Check warning on line 175 in src/components/RangeDateSelection/__stories__/RangeDateSelection.stories.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Forbidden non-null assertion
: interval.start!.value;

Check warning on line 176 in src/components/RangeDateSelection/__stories__/RangeDateSelection.stories.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Forbidden non-null assertion

const end: DateTime =
interval.end?.type === 'relative'
? dateTimeParse(interval.end.value, {roundUp: true, timeZone})!

Check warning on line 180 in src/components/RangeDateSelection/__stories__/RangeDateSelection.stories.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Forbidden non-null assertion
: interval.end!.value;

Check warning on line 181 in src/components/RangeDateSelection/__stories__/RangeDateSelection.stories.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Forbidden non-null assertion

return {start, end};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@use '../../../variables';

$block: '.#{variables.$ns}timeline-now-line';

#{$block} {
stroke: var(--g-date-thin-timeline-now-color);
stroke-width: 2px;
}
Loading

0 comments on commit eafb388

Please sign in to comment.