From 51744cde4e73e28ad97cb34138f35e5f01462325 Mon Sep 17 00:00:00 2001 From: Mukul Bansal Date: Sun, 31 Dec 2023 11:53:38 +0530 Subject: [PATCH] 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 --- .../TimePicker/TimePicker.stories.mdx | 51 + .../TimePicker/TimePicker.stories.tsx | 3 + .../TimePicker/TimePicker.styled.tsx | 14 + .../components/TimePicker/TimePicker.test.tsx | 59 + .../src/components/TimePicker/TimePicker.tsx | 49 + .../TimePickerPopup.styled.tsx | 133 ++ .../TimePickerPopup/TimePickerPopup.tsx | 138 ++ .../TimePicker/TimePickerPopup/index.ts | 1 + .../TimePicker/TimePickerPopup/types.ts | 5 + .../__snapshots__/TimePicker.test.tsx.snap | 1281 +++++++++++++++++ .../core/src/components/TimePicker/types.ts | 12 + packages/theme/src/core/index.ts | 2 + packages/theme/src/core/timePicker/index.ts | 24 + packages/theme/src/core/timePicker/types.ts | 28 + packages/theme/src/core/types.ts | 1 + packages/theme/src/types.ts | 1 + 16 files changed, 1802 insertions(+) create mode 100644 packages/core/src/components/TimePicker/TimePicker.stories.mdx create mode 100644 packages/core/src/components/TimePicker/TimePicker.stories.tsx create mode 100644 packages/core/src/components/TimePicker/TimePicker.styled.tsx create mode 100644 packages/core/src/components/TimePicker/TimePicker.test.tsx create mode 100644 packages/core/src/components/TimePicker/TimePicker.tsx create mode 100644 packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.styled.tsx create mode 100644 packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.tsx create mode 100644 packages/core/src/components/TimePicker/TimePickerPopup/index.ts create mode 100644 packages/core/src/components/TimePicker/TimePickerPopup/types.ts create mode 100644 packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap create mode 100644 packages/core/src/components/TimePicker/types.ts create mode 100644 packages/theme/src/core/timePicker/index.ts create mode 100644 packages/theme/src/core/timePicker/types.ts diff --git a/packages/core/src/components/TimePicker/TimePicker.stories.mdx b/packages/core/src/components/TimePicker/TimePicker.stories.mdx new file mode 100644 index 000000000..f5c82a2e5 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePicker.stories.mdx @@ -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'; + + + +# 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 ; +}; +``` + + + + {() => { + const [time, setTime] = useState(''); + return ( + + ); + }} + + diff --git a/packages/core/src/components/TimePicker/TimePicker.stories.tsx b/packages/core/src/components/TimePicker/TimePicker.stories.tsx new file mode 100644 index 000000000..7af5647ac --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePicker.stories.tsx @@ -0,0 +1,3 @@ +import { TimePickerProps } from './types'; + +export const variants: Required['variant'][] = ['outlined', 'filled', 'fusion']; diff --git a/packages/core/src/components/TimePicker/TimePicker.styled.tsx b/packages/core/src/components/TimePicker/TimePicker.styled.tsx new file mode 100644 index 000000000..f68ce2d54 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePicker.styled.tsx @@ -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; +`; diff --git a/packages/core/src/components/TimePicker/TimePicker.test.tsx b/packages/core/src/components/TimePicker/TimePicker.test.tsx new file mode 100644 index 000000000..1ef9fc2e6 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePicker.test.tsx @@ -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(); + fireEvent.click(screen.getByLabelText('Time')); + expect(container).toMatchSnapshot(); + }); + + it('should give time entered in the textfield', () => { + const mockOnChange = jest.fn(); + render(); + 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(); + 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(); + 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(); + fireEvent.click(screen.getByTitle('time-icon')); + expect(screen.queryByText('hour-arrow-down')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/components/TimePicker/TimePicker.tsx b/packages/core/src/components/TimePicker/TimePicker.tsx new file mode 100644 index 000000000..25b6b9c03 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePicker.tsx @@ -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 = 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) => onChange(e.target.value); + + return ( + + + + + {!disabled && !isMobile && } + + ); + }) +); +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 & WithStyle = Object.assign(Component, { Style: TimePickerWrapper }); diff --git a/packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.styled.tsx b/packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.styled.tsx new file mode 100644 index 000000000..a55f7d9f5 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.styled.tsx @@ -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; +`; diff --git a/packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.tsx b/packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.tsx new file mode 100644 index 000000000..70cba7cd5 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePickerPopup/TimePickerPopup.tsx @@ -0,0 +1,138 @@ +import { ExpandLessIcon, ExpandMoreIcon } from '@medly-components/icons'; +import { WithStyle } from '@medly-components/utils'; +import { FC, RefObject, useContext, useEffect, useRef, useState } from 'react'; +import { ThemeContext } from 'styled-components'; +import Button from '../../Button'; +import Popover from '../../Popover'; +import Popup from '../../Popover/Popup'; +import Text from '../../Text'; +import { + Colon, + PeriodListItem, + PeriodPicker, + PeriodUList, + TimeLabels, + TimePicker, + TimePickerActions, + TimePickerCard, + TimePickerWrapper, + TimeUList +} from './TimePickerPopup.styled'; +import { Period, TimePickerPopupProps } from './types'; + +const PERIOD: Period[] = ['AM', 'PM']; + +export const Component: FC = ({ value, onChange, popoverPlacement }) => { + const hourRef = useRef(null); + const minutesRef = useRef(null); + const periodRef = useRef(null); + const theme = useContext(ThemeContext); + const [open, setPopupState] = useContext(Popover.Context); + const [selectedPeriod, setSelectedPeriod] = useState('AM'); + + const scrollTime = (ref: RefObject, direction: 'UP' | 'DOWN') => + ref.current?.scrollTo({ + top: ref.current.scrollTop + (direction === 'UP' ? -ref.current.clientHeight : ref.current.clientHeight), + behavior: 'smooth' + }); + + const scrollPeriod = (period: Period) => { + if (periodRef.current) { + periodRef.current.style.transform = + period === 'AM' ? 'translateY(0)' : `translateY(calc(-${theme.timePicker.nonSelectedOption.lineHeight} - 2.4rem))`; + } + }; + + const handleCancel = () => { + hourRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); + minutesRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); + setSelectedPeriod('AM'); + setPopupState(false); + }; + + const handleSubmit = () => { + const hour = Math.round((hourRef.current?.scrollTop || 0) / (hourRef.current?.clientHeight || 1)); + const minutes = Math.round((minutesRef.current?.scrollTop || 0) / (minutesRef.current?.clientHeight || 1)); + const hourString = `0${selectedPeriod === 'AM' ? hour : hour + 12}`.slice(-2); + const minutesString = `0${minutes}`.slice(-2); + onChange(`${hourString}:${minutesString}`); + setPopupState(false); + }; + + useEffect(() => { + if (value) { + const time = value.split(':'); + const hour = Number(time[0]); + const minutes = Number(time[1]); + const period = hour < 12 ? 'AM' : 'PM'; + hourRef.current?.scrollTo({ top: (hour % 12) * hourRef.current.clientHeight, behavior: 'smooth' }); + minutesRef.current?.scrollTo({ top: minutes * minutesRef.current.clientHeight, behavior: 'smooth' }); + setSelectedPeriod(period); + scrollPeriod(period); + } + }, [open, value]); + + useEffect(() => { + scrollPeriod(selectedPeriod); + }, [selectedPeriod]); + + return ( + + + + Hour + + Minutes + + + + + scrollTime(hourRef, 'UP')} /> + + {Array.from({ length: 12 }, (_, index) => ( +
  • {`0${index}`.slice(-2)}
  • + ))} +
    + scrollTime(hourRef, 'DOWN')} /> +
    + + + scrollTime(minutesRef, 'UP')} /> + + {Array.from({ length: 60 }, (_, index) => ( +
  • {`0${index}`.slice(-2)}
  • + ))} +
    + scrollTime(minutesRef, 'DOWN')} /> +
    + + + {PERIOD.map(period => ( + setSelectedPeriod(period)} + > + {period} + + ))} + + +
    + + + + +
    +
    + ); +}; +Component.displayName = 'TimePickerPopup'; +Component.defaultProps = { + popoverPlacement: 'bottom-start' +}; +export const TimePickerPopup: FC & WithStyle = Object.assign(Component, { Style: TimePickerCard }); diff --git a/packages/core/src/components/TimePicker/TimePickerPopup/index.ts b/packages/core/src/components/TimePicker/TimePickerPopup/index.ts new file mode 100644 index 000000000..0124a454a --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePickerPopup/index.ts @@ -0,0 +1 @@ +export { TimePickerPopup as default } from './TimePickerPopup'; diff --git a/packages/core/src/components/TimePicker/TimePickerPopup/types.ts b/packages/core/src/components/TimePicker/TimePickerPopup/types.ts new file mode 100644 index 000000000..907529cd8 --- /dev/null +++ b/packages/core/src/components/TimePicker/TimePickerPopup/types.ts @@ -0,0 +1,5 @@ +import { Placement } from '../../Popover/types'; + +export type Period = 'AM' | 'PM'; + +export type TimePickerPopupProps = { value: string; onChange: (time: string) => void; popoverPlacement?: Placement }; diff --git a/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap b/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap new file mode 100644 index 000000000..de43c949d --- /dev/null +++ b/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap @@ -0,0 +1,1281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TimePicker should render properly 1`] = ` +.c0 { + position: relative; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c13 { + z-index: 4; + position: absolute; + background-color: white; + top: calc(100% + 0px); + left: 0%; +} + +.c9 { + overflow: visible; + font-size: 2.4rem; + -webkit-transition: all 100ms linear; + transition: all 100ms linear; + cursor: inherit; +} + +.c9 * { + fill-opacity: 1; + -webkit-transition: all 100ms linear; + transition: all 100ms linear; + fill: #607890; +} + +.c20 { + overflow: visible; + font-size: 2.4rem; + -webkit-transition: all 100ms linear; + transition: all 100ms linear; + cursor: pointer; +} + +.c20 * { + fill-opacity: 1; + -webkit-transition: all 100ms linear; + transition: all 100ms linear; + fill: #607890; +} + +.c7 { + top: 50%; + left: 0; + cursor: text; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + position: absolute; + pointer-events: none; + -webkit-transition: all 150ms cubic-bezier(0.4,0,0.2,1); + transition: all 150ms cubic-bezier(0.4,0,0.2,1); + font-size: 1.6rem; + font-weight: 400; + -webkit-letter-spacing: 0rem; + -moz-letter-spacing: 0rem; + -ms-letter-spacing: 0rem; + letter-spacing: 0rem; + line-height: 2.6rem; + font-family: Open Sans,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + touch-action: manipulation; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + opacity: 1; + z-index: 1; + width: 100%; + max-width: -webkit-min-content; + max-width: -moz-min-content; + max-width: min-content; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c12 { + pointer-events: none; + margin-left: 1.6rem; +} + +.c3 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + box-sizing: border-box; + height: 5.6rem; + padding: 0 1.6rem; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; + cursor: text; + border-radius: 0.4rem; + background-color: #ffffff; +} + +.c3 .c6 { + color: #607890; +} + +.c3 .sc-1ubn8hn-0, +.c3 .c11 { + color: #607890; +} + +.c3 .sc-1ubn8hn-0 *, +.c3 .c11 * { + fill: #607890; +} + +.c3 ~ .sc-avlt8d-0 { + color: #546A7F; +} + +.c3 input { + box-shadow: 0 0 0 100000px #ffffff inset; +} + +.c3::after { + content: ''; + box-sizing: border-box; + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + background-color: transparent; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; + border-radius: 0.4rem; + border-width: 0.4rem; + border: 0.1rem solid #98A7B7; +} + +.c3:hover { + box-shadow: 0px 0.2rem 0.8rem rgba(96,120,144,0.2); +} + +.c3:focus-within { + border-radius: 0.4rem; + box-shadow: 0 0.2rem 0.8rem rgba(56,114,210,0.2); +} + +.c3:hover::after { + border-color: #607890; + border-width: 0.15rem; +} + +.c3:focus-within::after { + border-color: #3872D2; + border-radius: 0.4rem; + border-width: 0.15rem; +} + +.c3:focus-within, +.c3:focus-within:hover { + background-color: #ffffff; +} + +.c3:focus-within::after, +.c3:focus-within:hover::after { + border-color: #3872D2; + border-radius: 0.4rem; +} + +.c3:focus-within .c6, +.c3:focus-within:hover .c6 { + color: #3872D2; +} + +.c3:focus-within .sc-1ubn8hn-0, +.c3:focus-within:hover .sc-1ubn8hn-0, +.c3:focus-within .c11, +.c3:focus-within:hover .c11 { + color: #3872D2; +} + +.c3:focus-within .sc-1ubn8hn-0 *, +.c3:focus-within:hover .sc-1ubn8hn-0 *, +.c3:focus-within .c11 *, +.c3:focus-within:hover .c11 * { + fill: #3872D2; +} + +.c3:focus-within input, +.c3:focus-within:hover input { + box-shadow: 0 0 0 100000px #ffffff inset; +} + +.c3:focus-within input::-webkit-input-placeholder, +.c3:focus-within:hover input::-webkit-input-placeholder { + color: #C7D0D8; +} + +.c3:focus-within input::-moz-placeholder, +.c3:focus-within:hover input::-moz-placeholder { + color: #C7D0D8; +} + +.c3:focus-within input:-ms-input-placeholder, +.c3:focus-within:hover input:-ms-input-placeholder { + color: #C7D0D8; +} + +.c3:focus-within input::placeholder, +.c3:focus-within:hover input::placeholder { + color: #C7D0D8; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + position: relative; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: transparent; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + height: 100%; +} + +.c1 { + position: relative; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + width: -webkit-min-content; + width: -moz-min-content; + width: min-content; + min-width: 20rem; + margin: 0.8rem 0.8rem 0.8rem 0; +} + +.c5 { + color: #13181D; + width: 100%; + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; + background-color: transparent; + border: none; + text-overflow: ellipsis; + resize: none; + z-index: 1; + -webkit-align-self: flex-end; + -ms-flex-item-align: end; + align-self: flex-end; + margin-bottom: 0.6rem; +} + +.c5, +.c5 ~ .sc-1r5xr2a-0 { + font-size: 1.6rem; + font-weight: 400; + -webkit-letter-spacing: 0rem; + -moz-letter-spacing: 0rem; + -ms-letter-spacing: 0rem; + letter-spacing: 0rem; + line-height: 2.6rem; + font-family: Open Sans,Helvetica Neue,Helvetica,Arial,sans-serif; +} + +.c5.c5.c5:focus { + outline: none; + box-shadow: unset; +} + +.c5.c5.c5:focus::-webkit-input-placeholder { + opacity: 1; +} + +.c5.c5.c5:focus::-moz-placeholder { + opacity: 1; +} + +.c5.c5.c5:focus:-ms-input-placeholder { + opacity: 1; +} + +.c5.c5.c5:focus::placeholder { + opacity: 1; +} + +.c5:disabled { + color: #435465; +} + +.c5:disabled ~ .c6, +.c5:disabled ~ .sc-1r5xr2a-0 { + cursor: not-allowed; +} + +.c5::-webkit-input-placeholder { + opacity: 0; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; +} + +.c5::-moz-placeholder { + opacity: 0; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; +} + +.c5:-ms-input-placeholder { + opacity: 0; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; +} + +.c5::placeholder { + opacity: 0; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; +} + +.c5:not(:placeholder-shown) ~ .c6, +.c5:focus ~ .c6 { + -webkit-transform: translateY(-87%) scale(0.75); + -ms-transform: translateY(-87%) scale(0.75); + transform: translateY(-87%) scale(0.75); +} + +.c5:not(:placeholder-shown) ~ .sc-1r5xr2a-0 { + opacity: 1; +} + +.c2 input[type='time']::-webkit-calendar-picker-indicator { + background: none; + display: none; +} + +.c10 { + cursor: pointer; +} + +.c17 { + margin: 0; + color: inherit; + font-size: 1.4rem; + font-weight: 400; + -webkit-letter-spacing: 0rem; + -moz-letter-spacing: 0rem; + -ms-letter-spacing: 0rem; + letter-spacing: 0rem; + line-height: 2.2rem; + text-align: initial; + display: inline-block; +} + +.c29 { + margin: 0; + color: inherit; + font-size: 1.4rem; + font-weight: 600; + -webkit-letter-spacing: 0rem; + -moz-letter-spacing: 0rem; + -ms-letter-spacing: 0rem; + letter-spacing: 0rem; + line-height: 2.2rem; + text-align: initial; + display: inline-block; +} + +.c28 { + border: none; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-family: inherit; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; + padding: 0.8rem 2.4rem 1rem; + border-radius: 0.8rem; + background-color: transparent; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c28, +.c28 .c16, +.c28 .c8, +.c28 .c8 * { + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; +} + +.c28:hover { + cursor: pointer; +} + +.c28:focus { + outline: none; +} + +.c28:disabled { + cursor: not-allowed; +} + +.c28::after { + content: ''; + display: block; + position: absolute; + width: 0; + bottom: 0.85rem; + left: 50%; + height: 0.15rem; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; + border-radius: 0.15rem; + -webkit-transform: translate(-50%); + -ms-transform: translate(-50%); + transform: translate(-50%); +} + +.c28:disabled { + color: #98A7B7; +} + +.c28:disabled::after { + background-color: #98A7B7; +} + +.c28:disabled .c8 * { + fill: #98A7B7; +} + +.c28:not(:disabled):not(:hover) { + color: #3872D2; +} + +.c28:not(:disabled):not(:hover)::after { + background-color: #3872D2; +} + +.c28:not(:disabled):not(:hover) .c8 * { + fill: #3872D2; +} + +.c28:not(:disabled):active { + color: #275093; +} + +.c28:not(:disabled):active::after { + background-color: #275093; +} + +.c28:not(:disabled):active .c8 * { + fill: #275093; +} + +.c28:not(:disabled):active::after { + width: calc(100% - 4.8rem); +} + +.c28:not(:disabled):not(:active):hover { + color: #3061B3; +} + +.c28:not(:disabled):not(:active):hover::after { + background-color: #3061B3; +} + +.c28:not(:disabled):not(:active):hover .c8 * { + fill: #3061B3; +} + +.c28:not(:disabled):not(:active):hover::after { + width: calc(100% - 4.8rem); +} + +.c28 .c8 + .c16 { + margin-left: 0.8rem; +} + +.c28 .c16 + .c8 { + margin-left: 0.8rem; +} + +.c30 { + border: none; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-family: inherit; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; + padding: 0.8rem 2.4rem 1rem; + border-radius: 0.8rem; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c30, +.c30 .c16, +.c30 .c8, +.c30 .c8 * { + -webkit-transition: all 100ms ease-out; + transition: all 100ms ease-out; +} + +.c30:hover { + cursor: pointer; +} + +.c30:focus { + outline: none; +} + +.c30:disabled { + cursor: not-allowed; +} + +.c30:disabled { + color: #98A7B7; + background: #dfe4e9; +} + +.c30:disabled .c8 * { + fill: #98A7B7; +} + +.c30:not(:disabled):not(:hover) { + color: #ffffff; + background: #3872D2; +} + +.c30:not(:disabled):not(:hover) .c8 * { + fill: #ffffff; +} + +.c30:not(:disabled):active { + color: #ffffff; + background: #275093; +} + +.c30:not(:disabled):active .c8 * { + fill: #ffffff; +} + +.c30:not(:disabled):not(:active):hover { + color: #ffffff; + background: #3061B3; + box-shadow: 0 0.2rem 0.8rem rgba(48,97,179,0.35); +} + +.c30:not(:disabled):not(:active):hover .c8 * { + fill: #ffffff; +} + +.c30 .c8 + .c16 { + margin-left: 0.8rem; +} + +.c30 .c16 + .c8 { + margin-left: 0.8rem; +} + +.c14 { + padding: 1.2rem; + background: #ffffff; + box-shadow: 0 0.2rem 0.8rem #b0bcc8; + border-radius: 0.4rem; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + height: -webkit-max-content; + height: -moz-max-content; + height: max-content; +} + +.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c15 > * { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-align: center; +} + +.c15 > *:nth-child(2) { + -webkit-flex: 0.5; + -ms-flex: 0.5; + flex: 0.5; +} + +.c18 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin: 1.8rem 0 2.4rem; + position: relative; +} + +.c18 > * { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c18::before, +.c18::after { + content: ''; + position: absolute; + left: -1.2rem; + width: calc(100% + 2.4rem); + height: 0.1rem; + background-color: #C7D0D8; +} + +.c18::before { + top: 3.2rem; +} + +.c18::after { + bottom: 3.2rem; +} + +.c19 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 2.4rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c21 { + padding: 0; + margin: 0; + height: 2.6rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + overflow: auto; + -webkit-scroll-snap-type: y mandatory; + -moz-scroll-snap-type: y mandatory; + -ms-scroll-snap-type: y mandatory; + scroll-snap-type: y mandatory; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + list-style: none; +} + +.c21::-webkit-scrollbar { + display: none; +} + +.c21 > li { + -webkit-scroll-snap-align: center; + -moz-scroll-snap-align: center; + -ms-scroll-snap-align: center; + scroll-snap-align: center; + font-size: 1.6rem; + font-weight: Medium; + line-height: 2.6rem; + color: #13181D; +} + +.c23 { + position: relative; + max-height: 2.4rem; +} + +.c24 { + padding: 0; + margin: 0; + gap: 2.4rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + list-style: none; + -webkit-transition: -webkit-transform 200ms ease; + -webkit-transition: transform 200ms ease; + transition: transform 200ms ease; +} + +.c25 { + font-size: 1.4rem; + font-weight: Regular; + line-height: 2.2rem; + color: #546A7F; + cursor: pointer; + -webkit-transition: all 200ms ease; + transition: all 200ms ease; +} + +.c26 { + font-size: 1.6rem; + font-weight: Medium; + line-height: 2.6rem; + color: #13181D; + cursor: pointer; + -webkit-transition: all 200ms ease; + transition: all 200ms ease; +} + +.c22 { + -webkit-flex: 0.5; + -ms-flex: 0.5; + flex: 0.5; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c27 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +
    +
    +
    +
    +
    + + +
    + + + +
    + +
    +
    +
    +
    + + Hour + + + + Minutes + + +
    +
    +
    + + + +
      +
    • + 00 +
    • +
    • + 01 +
    • +
    • + 02 +
    • +
    • + 03 +
    • +
    • + 04 +
    • +
    • + 05 +
    • +
    • + 06 +
    • +
    • + 07 +
    • +
    • + 08 +
    • +
    • + 09 +
    • +
    • + 10 +
    • +
    • + 11 +
    • +
    + + + +
    + + : + +
    + + + +
      +
    • + 00 +
    • +
    • + 01 +
    • +
    • + 02 +
    • +
    • + 03 +
    • +
    • + 04 +
    • +
    • + 05 +
    • +
    • + 06 +
    • +
    • + 07 +
    • +
    • + 08 +
    • +
    • + 09 +
    • +
    • + 10 +
    • +
    • + 11 +
    • +
    • + 12 +
    • +
    • + 13 +
    • +
    • + 14 +
    • +
    • + 15 +
    • +
    • + 16 +
    • +
    • + 17 +
    • +
    • + 18 +
    • +
    • + 19 +
    • +
    • + 20 +
    • +
    • + 21 +
    • +
    • + 22 +
    • +
    • + 23 +
    • +
    • + 24 +
    • +
    • + 25 +
    • +
    • + 26 +
    • +
    • + 27 +
    • +
    • + 28 +
    • +
    • + 29 +
    • +
    • + 30 +
    • +
    • + 31 +
    • +
    • + 32 +
    • +
    • + 33 +
    • +
    • + 34 +
    • +
    • + 35 +
    • +
    • + 36 +
    • +
    • + 37 +
    • +
    • + 38 +
    • +
    • + 39 +
    • +
    • + 40 +
    • +
    • + 41 +
    • +
    • + 42 +
    • +
    • + 43 +
    • +
    • + 44 +
    • +
    • + 45 +
    • +
    • + 46 +
    • +
    • + 47 +
    • +
    • + 48 +
    • +
    • + 49 +
    • +
    • + 50 +
    • +
    • + 51 +
    • +
    • + 52 +
    • +
    • + 53 +
    • +
    • + 54 +
    • +
    • + 55 +
    • +
    • + 56 +
    • +
    • + 57 +
    • +
    • + 58 +
    • +
    • + 59 +
    • +
    + + + +
    +
    +
      +
    • + AM +
    • +
    • + PM +
    • +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; diff --git a/packages/core/src/components/TimePicker/types.ts b/packages/core/src/components/TimePicker/types.ts new file mode 100644 index 000000000..e8514fcfc --- /dev/null +++ b/packages/core/src/components/TimePicker/types.ts @@ -0,0 +1,12 @@ +import { Omit } from '@medly-components/utils'; +import { Placement } from '../Popover/types'; +import { TextFieldProps } from '../TextField/types'; + +export interface TimePickerProps extends Omit { + /** Current Time */ + value: string; + /** Function to be called on changing the time */ + onChange: (time: string) => void; + /** Popover placement */ + popoverPlacement?: Placement; +} diff --git a/packages/theme/src/core/index.ts b/packages/theme/src/core/index.ts index df1f95ea8..f0e23ed22 100644 --- a/packages/theme/src/core/index.ts +++ b/packages/theme/src/core/index.ts @@ -29,6 +29,7 @@ import stepper from './stepper'; import table from './table'; import tabs from './tabs'; import textField from './textField'; +import timePicker from './timePicker'; import toast from './toast'; import toggle from './toggle'; @@ -63,6 +64,7 @@ export const coreDefaultTheme = { textField, toast, toggle, + timePicker, pagination, dialogBox, helperAndErrorTextTooltip diff --git a/packages/theme/src/core/timePicker/index.ts b/packages/theme/src/core/timePicker/index.ts new file mode 100644 index 000000000..2daf4d8bf --- /dev/null +++ b/packages/theme/src/core/timePicker/index.ts @@ -0,0 +1,24 @@ +import colors from '../colors'; +import variants from '../font/variants'; +import { TimePickerTheme } from './types'; + +const timePickerTheme: TimePickerTheme = { + bgColor: colors.white, + borderRadius: `0.4rem`, + shadowColor: colors.grey[400], + label: { + ...variants.body2, + color: colors.grey[700] + }, + selectedOption: { + ...variants.body1, + fontWeight: 'Medium', + color: colors.black + }, + nonSelectedOption: { + ...variants.body2, + color: colors.grey[700] + } +}; + +export default timePickerTheme; diff --git a/packages/theme/src/core/timePicker/types.ts b/packages/theme/src/core/timePicker/types.ts new file mode 100644 index 000000000..196e44efb --- /dev/null +++ b/packages/theme/src/core/timePicker/types.ts @@ -0,0 +1,28 @@ +import { FontWeights } from '../types'; + +export interface TimePickerTheme { + bgColor: string; + borderRadius: string; + shadowColor: string; + label: { + color: string; + fontSize: string; + fontWeight: FontWeights; + lineHeight: string; + letterSpacing: string; + }; + selectedOption: { + color: string; + fontSize: string; + fontWeight: FontWeights; + lineHeight: string; + letterSpacing: string; + }; + nonSelectedOption: { + color: string; + fontSize: string; + fontWeight: FontWeights; + lineHeight: string; + letterSpacing: string; + }; +} diff --git a/packages/theme/src/core/types.ts b/packages/theme/src/core/types.ts index c77a0b3a9..f45d2cc62 100644 --- a/packages/theme/src/core/types.ts +++ b/packages/theme/src/core/types.ts @@ -28,5 +28,6 @@ export * from './stepper/types'; export * from './table/types'; export * from './tabs/types'; export * from './textField/types'; +export * from './timePicker/types'; export * from './toast/types'; export * from './toggle/types'; diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c97574b66..89b8bc913 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -32,6 +32,7 @@ export interface CoreTheme { toggle: CoreThemes.ToggleTheme; tabs: CoreThemes.TabsTheme; textField: CoreThemes.TextFieldTheme; + timePicker: CoreThemes.TimePickerTheme; toast: CoreThemes.ToastTheme; pagination: CoreThemes.PaginationTheme; helperAndErrorTextTooltip: CoreThemes.HelperAndErrorTextTooltipTheme;