-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add new TimePicker component (#738)
* feat: add new TimePicker component affects: @medly-components/core, @medly-components/theme * docs: add TimePicker test to the docs affects: @medly-components/core * fix: lint issues affects: @medly-components/theme * fix: static code analysis error affects: @medly-components/core
- Loading branch information
Showing
16 changed files
with
1,802 additions
and
0 deletions.
There are no files selected for viewing
51 changes: 51 additions & 0 deletions
51
packages/core/src/components/TimePicker/TimePicker.stories.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { TimePicker } from './TimePicker'; | ||
import { Preview, Story, Meta, Props } from '@storybook/addon-docs/blocks'; | ||
import { boolean, select, text } from '@storybook/addon-knobs'; | ||
import { placements } from '../Popover/Popover.stories.tsx'; | ||
import { variants } from './TimePicker.stories'; | ||
import { useState } from 'react'; | ||
|
||
<Meta | ||
title="Core" | ||
component={TimePicker} | ||
parameters={{ | ||
jest: ['TimePicker.test.tsx'] | ||
}} | ||
/> | ||
|
||
# TimePicker Component | ||
|
||
The `TimePicker` is a controlled component that allows you to select a time through input text or using a dialog. This component is built on top of the `TextField` component. It accepts all the props of `TextField` component. This component will return the time in **24-hour format**. | ||
|
||
This component will use the native dialog on the mobile devices. | ||
|
||
## Usage | ||
|
||
```tsx | ||
import { TimePicker } from '@medly-components/core'; | ||
|
||
const Component = () => { | ||
const [time, setTime] = useState(''); | ||
return <TimePicker label="Time" value={time} onChange={setTime} />; | ||
}; | ||
``` | ||
|
||
<Preview withToolbar> | ||
<Story name="TimePicker"> | ||
{() => { | ||
const [time, setTime] = useState(''); | ||
return ( | ||
<TimePicker | ||
id="dob" | ||
value={time} | ||
onChange={setTime} | ||
disabled={boolean('Disabled', false)} | ||
label={text('Label', 'Time')} | ||
required={boolean('Required', false)} | ||
variant={select('variant', variants, 'outlined')} | ||
popoverPlacement={select('Popover placement', placements, 'bottom-start')} | ||
/> | ||
); | ||
}} | ||
</Story> | ||
</Preview> |
3 changes: 3 additions & 0 deletions
3
packages/core/src/components/TimePicker/TimePicker.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { TimePickerProps } from './types'; | ||
|
||
export const variants: Required<TimePickerProps>['variant'][] = ['outlined', 'filled', 'fusion']; |
14 changes: 14 additions & 0 deletions
14
packages/core/src/components/TimePicker/TimePicker.styled.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { AccessTimeIcon } from '@medly-components/icons'; | ||
import { InjectClassName } from '@medly-components/utils'; | ||
import styled from 'styled-components'; | ||
|
||
export const TimePickerWrapper = styled(InjectClassName)` | ||
input[type='time']::-webkit-calendar-picker-indicator { | ||
background: none; | ||
display: none; | ||
} | ||
`; | ||
|
||
export const TimeIcon = styled(AccessTimeIcon).attrs({ title: 'time-icon' })` | ||
cursor: pointer; | ||
`; |
59 changes: 59 additions & 0 deletions
59
packages/core/src/components/TimePicker/TimePicker.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { fireEvent, render, screen } from '@test-utils'; | ||
import { TimePicker } from './TimePicker'; | ||
|
||
describe('TimePicker', () => { | ||
beforeAll(() => { | ||
Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 24 }); | ||
// @ts-ignore | ||
Element.prototype.scrollTo = function ({ top }) { | ||
this.scrollTop = top; | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
Object.defineProperty(global?.navigator, 'userAgent', { configurable: true, value: { indexOf: () => -1 } }); | ||
}); | ||
|
||
it('should render properly', () => { | ||
const { container } = render(<TimePicker label="Time" value="13:11" onChange={jest.fn()} />); | ||
fireEvent.click(screen.getByLabelText('Time')); | ||
expect(container).toMatchSnapshot(); | ||
}); | ||
|
||
it('should give time entered in the textfield', () => { | ||
const mockOnChange = jest.fn(); | ||
render(<TimePicker label="Time" value="13:11" onChange={mockOnChange} />); | ||
fireEvent.change(screen.getByLabelText('Time'), { target: { value: '22:00' } }); | ||
expect(mockOnChange).toBeCalledWith('22:00'); | ||
}); | ||
|
||
it('should give the expected time on selecting time from dialog', () => { | ||
const mockOnChange = jest.fn(); | ||
render(<TimePicker label="Time" value="" onChange={mockOnChange} />); | ||
fireEvent.click(screen.getByTitle('time-icon')); | ||
fireEvent.click(screen.getByTitle('hour-arrow-down')); | ||
fireEvent.click(screen.getByTitle('minutes-arrow-down')); | ||
fireEvent.click(screen.getByText('PM')); | ||
fireEvent.click(screen.getByText('Apply')); | ||
expect(mockOnChange).toBeCalledWith('13:01'); | ||
}); | ||
|
||
it('should reset the values on clicking on cancel button', () => { | ||
const mockOnChange = jest.fn(); | ||
render(<TimePicker label="Time" value="" onChange={mockOnChange} />); | ||
fireEvent.click(screen.getByTitle('time-icon')); | ||
fireEvent.click(screen.getByTitle('hour-arrow-down')); | ||
fireEvent.click(screen.getByTitle('minutes-arrow-down')); | ||
fireEvent.click(screen.getByText('PM')); | ||
fireEvent.click(screen.getByText('Cancel')); | ||
expect(mockOnChange).not.toBeCalled(); | ||
expect(screen.queryByText('hour-arrow-down')).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should not render dialog for mobile devices', () => { | ||
Object.defineProperty(global?.navigator, 'userAgent', { configurable: true, value: { indexOf: () => 1 } }); | ||
render(<TimePicker label="Time" value="" onChange={jest.fn()} />); | ||
fireEvent.click(screen.getByTitle('time-icon')); | ||
expect(screen.queryByText('hour-arrow-down')).not.toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { WithStyle } from '@medly-components/utils'; | ||
import type { FC } from 'react'; | ||
import { forwardRef, memo } from 'react'; | ||
import Popover from '../Popover'; | ||
import TextField from '../TextField'; | ||
import { TimeIcon, TimePickerWrapper } from './TimePicker.styled'; | ||
import TimePickerPopup from './TimePickerPopup'; | ||
import { TimePickerProps } from './types'; | ||
|
||
const Component: FC<TimePickerProps> = memo( | ||
forwardRef((props, ref) => { | ||
const { value, onChange, disabled, className, popoverPlacement, ...restProps } = props; | ||
const id = props.id || props.label?.toLowerCase().replace(/\s/g, '') || 'medly-timepicker'; | ||
const isMobile = navigator?.userAgent?.indexOf('Mobi') > -1; | ||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value); | ||
|
||
return ( | ||
<Popover className={className} interactionType="click"> | ||
<TimePickerWrapper> | ||
<TextField | ||
fullWidth | ||
type="time" | ||
suffix={TimeIcon} | ||
id={id} | ||
ref={ref} | ||
disabled={disabled} | ||
onChange={handleChange} | ||
value={value} | ||
{...restProps} | ||
/> | ||
</TimePickerWrapper> | ||
{!disabled && !isMobile && <TimePickerPopup value={value} onChange={onChange} popoverPlacement={popoverPlacement} />} | ||
</Popover> | ||
); | ||
}) | ||
); | ||
Component.defaultProps = { | ||
size: 'M', | ||
label: 'Time', | ||
variant: 'outlined', | ||
disabled: false, | ||
required: false, | ||
fullWidth: false, | ||
minWidth: '20rem', | ||
showDecorators: true | ||
}; | ||
Component.displayName = 'TimePicker'; | ||
export const TimePicker: FC<TimePickerProps> & WithStyle = Object.assign(Component, { Style: TimePickerWrapper }); |
133 changes: 133 additions & 0 deletions
133
packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.styled.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import styled from 'styled-components'; | ||
import Text from '../../Text'; | ||
|
||
export const TimePickerCard = styled('div')` | ||
padding: 1.2rem; | ||
background: ${({ theme }) => theme.timePicker.bgColor}; | ||
box-shadow: 0 0.2rem 0.8rem ${({ theme }) => theme.timePicker.shadowColor}; | ||
border-radius: ${({ theme }) => theme.timePicker.borderRadius}; | ||
width: max-content; | ||
height: max-content; | ||
`; | ||
|
||
export const TimeLabels = styled('div')` | ||
display: flex; | ||
flex-direction: row; | ||
& > * { | ||
flex: 1; | ||
user-select: none; | ||
text-align: center; | ||
} | ||
& > *:nth-child(2) { | ||
flex: 0.5; | ||
} | ||
`; | ||
|
||
export const TimePickerWrapper = styled.div` | ||
width: 100%; | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
justify-content: space-between; | ||
margin: 1.8rem 0 2.4rem; | ||
position: relative; | ||
& > * { | ||
flex: 1; | ||
} | ||
&::before, | ||
&::after { | ||
content: ''; | ||
position: absolute; | ||
left: -1.2rem; | ||
width: calc(100% + 2.4rem); | ||
height: 0.1rem; | ||
background-color: ${({ theme }) => theme.colors.grey[300]}; | ||
} | ||
&::before { | ||
top: 3.2rem; | ||
} | ||
&::after { | ||
bottom: 3.2rem; | ||
} | ||
`; | ||
|
||
export const TimePicker = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
gap: 2.4rem; | ||
align-items: center; | ||
justify-content: center; | ||
`; | ||
|
||
export const TimeUList = styled.ul` | ||
padding: 0; | ||
margin: 0; | ||
height: ${({ theme }) => theme.timePicker.selectedOption.lineHeight}; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: flex-start; | ||
overflow: auto; | ||
scroll-snap-type: y mandatory; | ||
user-select: none; | ||
list-style: none; | ||
&::-webkit-scrollbar { | ||
display: none; | ||
} | ||
& > li { | ||
scroll-snap-align: center; | ||
font-size: ${({ theme }) => theme.timePicker.selectedOption.fontSize}; | ||
font-weight: ${({ theme }) => theme.timePicker.selectedOption.fontWeight}; | ||
line-height: ${({ theme }) => theme.timePicker.selectedOption.lineHeight}; | ||
letter-spacing: ${({ theme }) => theme.timePicker.selectedOption.LetterSpacing}; | ||
color: ${({ theme }) => theme.timePicker.selectedOption.color}; | ||
} | ||
`; | ||
|
||
export const PeriodPicker = styled('div')` | ||
position: relative; | ||
max-height: 2.4rem; | ||
`; | ||
|
||
export const PeriodUList = styled('ul')` | ||
padding: 0; | ||
margin: 0; | ||
gap: 2.4rem; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: flex-start; | ||
user-select: none; | ||
list-style: none; | ||
transition: transform 200ms ease; | ||
`; | ||
|
||
export const PeriodListItem = styled('li')<{ isSelected: boolean }>` | ||
font-size: ${({ isSelected, theme }) => theme.timePicker[isSelected ? 'selectedOption' : 'nonSelectedOption'].fontSize}; | ||
font-weight: ${({ isSelected, theme }) => theme.timePicker[isSelected ? 'selectedOption' : 'nonSelectedOption'].fontWeight}; | ||
line-height: ${({ isSelected, theme }) => theme.timePicker[isSelected ? 'selectedOption' : 'nonSelectedOption'].lineHeight}; | ||
letter-spacing: ${({ isSelected, theme }) => theme.timePicker[isSelected ? 'selectedOption' : 'nonSelectedOption'].LetterSpacing}; | ||
color: ${({ isSelected, theme }) => theme.timePicker[isSelected ? 'selectedOption' : 'nonSelectedOption'].color}; | ||
cursor: pointer; | ||
transition: all 200ms ease; | ||
`; | ||
|
||
export const Colon = styled(Text).attrs({ children: ':' })` | ||
flex: 0.5; | ||
display: flex; | ||
justify-content: flex-end; | ||
user-select: none; | ||
`; | ||
|
||
export const TimePickerActions = styled.div` | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: space-between; | ||
align-items: center; | ||
`; |
Oops, something went wrong.