Skip to content

Commit

Permalink
feat: add tooltip box and use it in calendars
Browse files Browse the repository at this point in the history
  • Loading branch information
jongomez committed Oct 16, 2023
1 parent d2b8ba7 commit f875ba8
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ModalStyles } from '../Modal/Modal.styles'
import { ModalFooterStyles } from '../ModalFooter/ModalFooter.styles'
import { ModalBodyStyles } from '../ModalBody/ModalBody.styles'
import { DateRangePickerStyles } from '../DateRangePicker/DateRangePicker.styles'
import { TooltipBaseStyles } from '../TooltipBase/TooltipBase.styles'

const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
[
Expand Down Expand Up @@ -82,6 +83,7 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
DateFieldStyles,
CalendarStyles,
DateRangePickerStyles,
TooltipBaseStyles,
]

export const CSSBaseline: React.FC<{ theme?: Theme }> = ({
Expand Down
4 changes: 2 additions & 2 deletions packages/lsd-react/src/components/Calendar/Calendar.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const CalendarStyles = css`
.${calendarClasses.root} {
border: 1px solid rgb(var(--lsd-border-primary));
visibility: hidden;
position: absolute;
position: absolute !important;
top: 0;
left: 0;
opacity: 0;
Expand All @@ -16,12 +16,12 @@ export const CalendarStyles = css`
background: rgb(var(--lsd-surface-primary));
user-select: none;
padding: 8px;
}
.${calendarClasses.container} {
display: flex;
flex-direction: column;
padding: 8px;
}
.${calendarClasses.open} {
Expand Down
10 changes: 7 additions & 3 deletions packages/lsd-react/src/components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
useCommonProps,
omitCommonProps,
} from '../../utils/useCommonProps'
import { TooltipBase } from '../TooltipBase'

export type CalendarType = null | 'endDate' | 'startDate'

Expand All @@ -34,6 +35,7 @@ export type CalendarProps = CommonProps &
endDate?: string
minDate?: Date
maxDate?: Date
tooltipArrowOffset?: number
}

export const Calendar: React.FC<CalendarProps> & {
Expand All @@ -54,6 +56,7 @@ export const Calendar: React.FC<CalendarProps> & {
// minDate and maxDate are necessary because onDateFocus freaks out with small/large date values.
minDate = new Date(1950, 0, 1),
maxDate = new Date(2100, 0, 1),
tooltipArrowOffset,
...props
}) => {
const commonProps = useCommonProps(props)
Expand Down Expand Up @@ -184,7 +187,7 @@ export const Calendar: React.FC<CalendarProps> & {
goToPreviousYear,
}}
>
<div
<TooltipBase
{...props}
className={clsx(
{ ...omitCommonProps(props) },
Expand All @@ -194,8 +197,9 @@ export const Calendar: React.FC<CalendarProps> & {
open && calendarClasses.open,
disabled && calendarClasses.disabled,
)}
ref={ref}
rootRef={ref}
style={{ ...style, ...(props.style ?? {}) }}
arrowOffset={tooltipArrowOffset}
>
<div className={clsx(calendarClasses.container)}>
{activeMonths.map((month, idx) => (
Expand All @@ -208,7 +212,7 @@ export const Calendar: React.FC<CalendarProps> & {
/>
))}
</div>
</div>
</TooltipBase>
</CalendarContext.Provider>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css } from '@emotion/react'
import { dateRangePickerClasses } from './DateRangePicker.classes'
import { dateFieldClasses } from '../DateField/DateField.classes'
import { tooltipBaseClasses } from '../TooltipBase/TooltipBase.classes'

export const DateRangePickerStyles = css`
.${dateRangePickerClasses.root} {
Expand Down Expand Up @@ -31,6 +32,10 @@ export const DateRangePickerStyles = css`
.${dateRangePickerClasses.calendar} {
border-top: none !important;
.${tooltipBaseClasses.arrowTip} {
transition: left 0.2s ease-in-out;
}
}
.${dateRangePickerClasses.openCalendar} {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import clsx from 'clsx'
import React, { ChangeEvent, ChangeEventHandler, useRef, useState } from 'react'
import {
dateToISODateString,
getCalendarTooltipArrowOffset,
isValidRange,
removeDateTimezoneOffset,
switchCalendar,
Expand All @@ -23,17 +24,20 @@ import {
} from '../../utils/useCommonProps'

export type DateRangePickerProps = CommonProps &
Omit<DatePickerProps, 'value' | 'clearButton'> & {
Omit<DatePickerProps, 'value' | 'clearButton' | 'onChange'> & {
startValue?: string
endValue?: string
onStartDateChange: DatePickerProps['onChange']
onEndDateChange: DatePickerProps['onChange']
}

export const DateRangePicker: React.FC<DateRangePickerProps> & {
classes: typeof dateRangePickerClasses
} = ({
startValue: startValueProp,
endValue: endValueProp,
onChange,
onStartDateChange,
onEndDateChange,
size = 'large',
variant = 'outlined-bottom',
withCalendar = true,
Expand All @@ -53,7 +57,7 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
const startInput = useInput({
value: startValueProp,
defaultValue: '',
onChange,
onChange: onStartDateChange,
getInput: () =>
ref.current?.querySelectorAll(
`input.${DateField.classes.input}`,
Expand All @@ -63,7 +67,7 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
const endInput = useInput({
value: endValueProp,
defaultValue: '',
onChange,
onChange: onEndDateChange,
getInput: () =>
ref.current?.querySelectorAll(
`input.${DateField.classes.input}`,
Expand All @@ -73,6 +77,9 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
const onStartInputChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!endInput.value || isValidRange(e.target.value, endInput.value)) {
startInput.onChange(e)

// Switch to endDate calendar when the startDate is set.
setCalendarType('endDate')
}
}

Expand Down Expand Up @@ -206,6 +213,10 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
startDate={startInput.value}
endDate={endInput.value}
className={dateRangePickerClasses.calendar}
tooltipArrowOffset={getCalendarTooltipArrowOffset(
calendarType,
size,
)}
/>
</Portal>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const tooltipBaseClasses = {
root: `lsd-tooltip-base`,
arrowTip: `lsd-tooltip-base__arrow-tip`,
content: `lsd-tooltip-base__content`,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { StoryObj, Meta } from '@storybook/react'
import { TooltipBase, TooltipBaseProps } from './TooltipBase'

export default {
title: 'TooltipBase',
component: TooltipBase,
argTypes: {
arrowPosition: {
type: {
name: 'enum',
value: ['top', 'bottom', 'left', 'right'],
},
},
},
} as Meta

export const Root: StoryObj<TooltipBaseProps> = ({
...args
}: TooltipBaseProps) => {
return <TooltipBase {...args} style={{ height: 200, width: 200 }} />
}

Root.args = {
arrowPosition: 'top',
arrowOffset: 50,
arrowSize: 10,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { css } from '@emotion/react'
import { tooltipBaseClasses } from './TooltipBase.classes'

export const TooltipBaseStyles = css`
.${tooltipBaseClasses.root} {
border: 1px solid rgb(var(--lsd-border-primary));
position: relative;
}
.${tooltipBaseClasses.arrowTip} {
border: 1px solid rgb(var(--lsd-border-primary));
position: absolute;
background: rgb(var(--lsd-surface-primary));
}
.${tooltipBaseClasses.content} {
background: rgb(var(--lsd-surface-primary));
width: 100%;
height: 100%;
/* Position relative is only used so the z-index works. */
position: relative;
/* Make sure the arrow tip div is behind the tooltip's content. */
z-index: 1;
}
`
68 changes: 68 additions & 0 deletions packages/lsd-react/src/components/TooltipBase/TooltipBase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import clsx from 'clsx'
import React from 'react'
import {
CommonProps,
omitCommonProps,
useCommonProps,
} from '../../utils/useCommonProps'
import { tooltipBaseClasses } from './TooltipBase.classes'

export type TooltipBaseProps = CommonProps &
React.HTMLAttributes<HTMLDivElement> & {
arrowOffset?: number
arrowPosition?: 'top' | 'bottom' | 'left' | 'right'
arrowSize?: number
rootRef?: React.Ref<HTMLDivElement>
}

export const TooltipBase: React.FC<TooltipBaseProps> & {
classes: typeof tooltipBaseClasses
} = ({
children,
arrowOffset,
arrowPosition = 'top',
arrowSize = 10,
rootRef,
...props
}) => {
const commonProps = useCommonProps(props)

// Calculate arrow tip style based on position and offset.
const arrowTipStyle: React.CSSProperties = {
width: `${arrowSize}px`,
height: `${arrowSize}px`,
transform: 'rotate(45deg)',
}

// Adjust the arrow tip's position along the tooltip border based on the arrowPosition and arrowOffset.
if (['top', 'bottom'].includes(arrowPosition)) {
arrowTipStyle.left = `${arrowOffset}px`
arrowTipStyle[arrowPosition] = `-${arrowSize / 2}px` // Halfway above the root box's border
} else {
arrowTipStyle.top = `${arrowOffset}px`
arrowTipStyle[arrowPosition] = `-${arrowSize / 2}px` // Halfway outside the root box's border
}

return (
<div
ref={rootRef}
{...omitCommonProps(props)}
className={clsx(
commonProps.className,
props.className,
tooltipBaseClasses.root,
)}
>
{!arrowOffset ? (
children
) : (
<>
<div className={tooltipBaseClasses.arrowTip} style={arrowTipStyle} />
<div className={tooltipBaseClasses.content}>{children}</div>
</>
)}
</div>
)
}

TooltipBase.classes = tooltipBaseClasses
1 change: 1 addition & 0 deletions packages/lsd-react/src/components/TooltipBase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TooltipBase'
1 change: 1 addition & 0 deletions packages/lsd-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ export * from './components/DateField'
export * from './components/DatePicker'
export * from './components/Calendar'
export * from './components/DateRangePicker'
export * from './components/TooltipBase'
32 changes: 32 additions & 0 deletions packages/lsd-react/src/utils/date.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OnDatesChangeProps, UseMonthResult } from '@datepicker-react/hooks'
import { calendarClasses } from '../components/Calendar/Calendar.classes'
import { CalendarType } from '../components/Calendar'
import { DateRangePickerProps } from '../components/DateRangePicker'

type SafeConvertDateResult = {
isValid: boolean
Expand Down Expand Up @@ -231,3 +232,34 @@ export function isValidRange(
// Check if the end date is after the start date
return endDate > startDate
}

export const getCalendarTooltipArrowOffset = (
calendarType: CalendarType,
size: DateRangePickerProps['size'],
): number => {
if (size === 'large') {
if (calendarType === 'startDate') {
return 130
} else {
return 290
}
}

if (size === 'medium') {
if (calendarType === 'startDate') {
return 120
} else {
return 267
}
}

if (size === 'small') {
if (calendarType === 'startDate') {
return 107
} else {
return 239
}
}

return 0
}

0 comments on commit f875ba8

Please sign in to comment.