Skip to content
This repository was archived by the owner on Jul 28, 2024. It is now read-only.

Commit aa861f5

Browse files
rchubarkinRoman Chubarkin
andauthored
[DS-39] feat: implement time picker (#184)
* feat: implement time picker * feat(calendar): move logic to hook, rename view to format, dont use ref to store value * chore(calendar): use tsx playground in usage * fix(molecules/popover): always close on esc key * fix(timepicker/calendar): use data-element for stub, fix stub height Co-authored-by: Roman Chubarkin <[email protected]>
1 parent 7e21249 commit aa861f5

File tree

12 files changed

+462
-6
lines changed

12 files changed

+462
-6
lines changed

gatsby-node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ exports.onCreateWebpackConfig = ({ actions }) => {
44
actions.setWebpackConfig({
55
resolve: {
66
alias: {
7+
calendar: path.resolve(__dirname, './src/calendar/'),
78
dev: path.resolve(__dirname, './src/dev/'),
89
lib: path.resolve(__dirname, './src/lib/'),
910
static: path.resolve(__dirname, './src/static/'),

src/calendar/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { TimePicker } from './time-picker';

src/calendar/time-picker/index.tsx

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import styled from 'styled-components';
2+
import React, { useEffect, useRef } from 'react';
3+
import { Input, ListContainer, ListItem, Popover } from 'ui';
4+
import { Priority } from 'lib/types';
5+
6+
import { TimePickerProps } from './types';
7+
import { useTimePicker } from './use-time-picker';
8+
9+
export function TimePicker({
10+
format = 'HH:mm',
11+
value,
12+
name,
13+
hourStep = 1,
14+
minStep = 1,
15+
secStep = 1,
16+
priority = 'secondary',
17+
onChange,
18+
disabled = false,
19+
...rest
20+
}: TimePickerProps) {
21+
const { getInputProps, getPickers, onClose } = useTimePicker({
22+
format,
23+
value,
24+
hourStep,
25+
minStep,
26+
secStep,
27+
onChange,
28+
});
29+
30+
const scrollCallbacks = useRef<(() => void)[]>([]);
31+
32+
function scrollToSelectedItems() {
33+
for (const cb of scrollCallbacks.current) {
34+
cb();
35+
}
36+
}
37+
38+
function close() {
39+
onClose();
40+
scrollToSelectedItems();
41+
}
42+
43+
return (
44+
<Popover
45+
isOpen={false}
46+
priority={priority}
47+
position="bottom"
48+
fullWidth={false}
49+
disabled={disabled}
50+
onClose={close}
51+
content={
52+
<Content>
53+
{getPickers().map(({ name, value, options, onChange }) => (
54+
<RangeSelect
55+
key={name}
56+
value={value}
57+
priority={priority}
58+
options={options}
59+
setScrollCallback={(cb) => {
60+
scrollCallbacks.current.push(cb);
61+
}}
62+
onChange={onChange}
63+
/>
64+
))}
65+
</Content>
66+
}
67+
>
68+
<Input
69+
// eslint-disable-next-line react/jsx-props-no-spreading
70+
{...rest}
71+
// eslint-disable-next-line react/jsx-props-no-spreading
72+
{...getInputProps()}
73+
autoComplete="off"
74+
disabled={disabled}
75+
type="text"
76+
name={name}
77+
priority={priority}
78+
/>
79+
</Popover>
80+
);
81+
}
82+
interface RangeSelectProps {
83+
value: string;
84+
options: string[];
85+
onChange: (value: string) => void;
86+
setScrollCallback: (cb: () => void) => void;
87+
}
88+
89+
const RangeSelectItem = styled(ListItem)`
90+
--local-scroll-gap: 10px;
91+
92+
& > * {
93+
padding-right: calc(var(--local-horizontal) + var(--local-scroll-gap));
94+
}
95+
`;
96+
97+
const Content = styled.div`
98+
display: flex;
99+
--local-border-color: var(--woly-shape-default);
100+
101+
& > *:not(:last-child) {
102+
border-right: var(--woly-border-width) solid var(--woly-shape-default);
103+
}
104+
`;
105+
106+
const RangeSelectContainer = styled(ListContainer)`
107+
--local-height: 150px;
108+
--local-item-height: var(--woly-line-height);
109+
--local-gap: calc(var(--woly-border-width) * 2);
110+
height: var(--local-height);
111+
overflow-y: scroll;
112+
113+
[data-element='stub'] {
114+
height: calc(var(--local-height) - var(--local-item-height) - var(--local-gap));
115+
}
116+
`;
117+
118+
function RangeSelect({
119+
value,
120+
options,
121+
onChange,
122+
priority,
123+
setScrollCallback,
124+
}: RangeSelectProps & Priority) {
125+
const selectRef = useRef(null);
126+
127+
function scrollToSelected() {
128+
const select = selectRef.current as HTMLElement | null;
129+
if (!select) return;
130+
131+
scrollToSelectedItem(select);
132+
}
133+
134+
useEffect(scrollToSelected, [value]);
135+
136+
useEffect(() => {
137+
setScrollCallback(scrollToSelected);
138+
}, []);
139+
140+
return (
141+
<RangeSelectContainer priority={priority} as="ul" ref={selectRef}>
142+
{options.map((optionValue) => (
143+
<RangeSelectItem
144+
forwardedAs="li"
145+
key={optionValue}
146+
text={optionValue}
147+
priority={priority}
148+
selected={optionValue === value}
149+
onClick={() => onChange(optionValue)}
150+
/>
151+
))}
152+
<li data-element="stub" />
153+
</RangeSelectContainer>
154+
);
155+
}
156+
157+
function scrollToSelectedItem(selectEl: HTMLElement) {
158+
const selectedItem = selectEl.querySelector('[data-selected="true"]') as HTMLElement | null;
159+
if (!selectedItem) return;
160+
selectEl.scrollTo({ top: selectedItem.offsetTop, behavior: 'smooth' });
161+
}

src/calendar/time-picker/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { InputProps } from 'ui/atoms/input';
2+
import { Priority } from 'lib/types';
3+
4+
type ReservedProps = 'type' | 'autoComplete' | 'onChange' | 'value';
5+
6+
export type Format = 'HH:mm:ss' | 'hh:mm:ss' | 'HH:mm' | 'hh:mm';
7+
8+
export type TimePickerProps = Omit<InputProps, ReservedProps> &
9+
Priority & {
10+
format?: Format;
11+
value: string;
12+
hourStep?: number;
13+
minStep?: number;
14+
secStep?: number;
15+
onChange: (value: string) => void;
16+
};

src/calendar/time-picker/usage.mdx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
### Example
2+
3+
```tsx playground
4+
import { TimePicker } from 'calendar';
5+
6+
export function Example() {
7+
const [value, onChange] = React.useState('')
8+
return (
9+
<Playground>
10+
<div style={{ paddingBottom: '200px', display: 'flex' }}>
11+
<TimePicker priority="default" name="time" value={value} onChange={onChange} />
12+
</div>
13+
</Playground>
14+
)
15+
}
16+
```
17+
18+
### Custom steps
19+
20+
```tsx playground
21+
import { TimePicker } from 'calendar';
22+
23+
export function Example() {
24+
const [value, onChange] = React.useState('')
25+
return (
26+
<Playground>
27+
<div style={{ paddingBottom: '200px', display: 'flex' }}>
28+
<TimePicker
29+
format="HH:mm:ss"
30+
priority="default"
31+
name="time"
32+
value={value}
33+
onChange={onChange}
34+
hourStep={2}
35+
minStep={5}
36+
secStep={10}
37+
/>
38+
</div>
39+
</Playground>
40+
)
41+
}
42+
```
43+
44+
### Disabled
45+
46+
```tsx playground
47+
import { TimePicker } from 'calendar';
48+
49+
export function Example() {
50+
const [value, onChange] = React.useState('')
51+
return (
52+
<Playground>
53+
<div style={{ paddingBottom: '200px', display: 'flex' }}>
54+
<TimePicker priority="default" name="time" value={value} disabled onChange={onChange} />
55+
</div>
56+
</Playground>
57+
)
58+
}
59+
```
60+
61+
### Props
62+
63+
| Name | Type | Default | Description |
64+
| ---------- | --------------------------------------------- | --------- | ------------------------------------------------------------------------------------- |
65+
| `format` | `'HH:mm:ss' l 'hh:mm:ss' l 'HH:mm' l 'hh:mm'` | `'HH:mm'` | time format |
66+
| `hourStep` | `number` | `1` | define set of available values for hour (eg. hourStep = 3, options = [0, 3, 6, 9...]) |
67+
| `minStep` | `number` | `1` | define set of available values for min |
68+
| `secStep` | `number` | `1` | define set of available values for sec |
69+
| `onChange` | `(newValue: string) => void` | | change callback |
70+
| `...` | `InputProps` | `{}` | Props from Input component |

0 commit comments

Comments
 (0)