diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index 8330aa8..f5a2019 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -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 | SerializedStyles> = [ @@ -82,6 +83,7 @@ const componentStyles: Array | SerializedStyles> = DateFieldStyles, CalendarStyles, DateRangePickerStyles, + TooltipBaseStyles, ] export const CSSBaseline: React.FC<{ theme?: Theme }> = ({ diff --git a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts index 8cb1907..596e833 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts @@ -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; @@ -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} { diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index d6c4547..ff86d14 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -15,6 +15,7 @@ import { useCommonProps, omitCommonProps, } from '../../utils/useCommonProps' +import { TooltipBase } from '../TooltipBase' export type CalendarType = null | 'endDate' | 'startDate' @@ -34,6 +35,7 @@ export type CalendarProps = CommonProps & endDate?: string minDate?: Date maxDate?: Date + tooltipArrowOffset?: number } export const Calendar: React.FC & { @@ -54,6 +56,7 @@ export const Calendar: React.FC & { // 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) @@ -184,7 +187,7 @@ export const Calendar: React.FC & { goToPreviousYear, }} > -
& { open && calendarClasses.open, disabled && calendarClasses.disabled, )} - ref={ref} + rootRef={ref} style={{ ...style, ...(props.style ?? {}) }} + arrowOffset={tooltipArrowOffset} >
{activeMonths.map((month, idx) => ( @@ -208,7 +212,7 @@ export const Calendar: React.FC & { /> ))}
-
+ ) } diff --git a/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.styles.ts b/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.styles.ts index 42d4222..18fe6e4 100644 --- a/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.styles.ts +++ b/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.styles.ts @@ -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} { @@ -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} { diff --git a/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.tsx b/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.tsx index d098020..dec9f64 100644 --- a/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.tsx +++ b/packages/lsd-react/src/components/DateRangePicker/DateRangePicker.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx' import React, { ChangeEvent, ChangeEventHandler, useRef, useState } from 'react' import { dateToISODateString, + getCalendarTooltipArrowOffset, isValidRange, removeDateTimezoneOffset, switchCalendar, @@ -23,9 +24,11 @@ import { } from '../../utils/useCommonProps' export type DateRangePickerProps = CommonProps & - Omit & { + Omit & { startValue?: string endValue?: string + onStartDateChange: DatePickerProps['onChange'] + onEndDateChange: DatePickerProps['onChange'] } export const DateRangePicker: React.FC & { @@ -33,7 +36,8 @@ export const DateRangePicker: React.FC & { } = ({ startValue: startValueProp, endValue: endValueProp, - onChange, + onStartDateChange, + onEndDateChange, size = 'large', variant = 'outlined-bottom', withCalendar = true, @@ -53,7 +57,7 @@ export const DateRangePicker: React.FC & { const startInput = useInput({ value: startValueProp, defaultValue: '', - onChange, + onChange: onStartDateChange, getInput: () => ref.current?.querySelectorAll( `input.${DateField.classes.input}`, @@ -63,7 +67,7 @@ export const DateRangePicker: React.FC & { const endInput = useInput({ value: endValueProp, defaultValue: '', - onChange, + onChange: onEndDateChange, getInput: () => ref.current?.querySelectorAll( `input.${DateField.classes.input}`, @@ -73,6 +77,9 @@ export const DateRangePicker: React.FC & { const onStartInputChange = (e: ChangeEvent) => { if (!endInput.value || isValidRange(e.target.value, endInput.value)) { startInput.onChange(e) + + // Switch to endDate calendar when the startDate is set. + setCalendarType('endDate') } } @@ -206,6 +213,10 @@ export const DateRangePicker: React.FC & { startDate={startInput.value} endDate={endInput.value} className={dateRangePickerClasses.calendar} + tooltipArrowOffset={getCalendarTooltipArrowOffset( + calendarType, + size, + )} /> )} diff --git a/packages/lsd-react/src/components/TooltipBase/TooltipBase.classes.ts b/packages/lsd-react/src/components/TooltipBase/TooltipBase.classes.ts new file mode 100644 index 0000000..7fe3658 --- /dev/null +++ b/packages/lsd-react/src/components/TooltipBase/TooltipBase.classes.ts @@ -0,0 +1,5 @@ +export const tooltipBaseClasses = { + root: `lsd-tooltip-base`, + arrowTip: `lsd-tooltip-base__arrow-tip`, + content: `lsd-tooltip-base__content`, +} diff --git a/packages/lsd-react/src/components/TooltipBase/TooltipBase.stories.tsx b/packages/lsd-react/src/components/TooltipBase/TooltipBase.stories.tsx new file mode 100644 index 0000000..30219e7 --- /dev/null +++ b/packages/lsd-react/src/components/TooltipBase/TooltipBase.stories.tsx @@ -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 = ({ + ...args +}: TooltipBaseProps) => { + return +} + +Root.args = { + arrowPosition: 'top', + arrowOffset: 50, + arrowSize: 10, +} diff --git a/packages/lsd-react/src/components/TooltipBase/TooltipBase.styles.ts b/packages/lsd-react/src/components/TooltipBase/TooltipBase.styles.ts new file mode 100644 index 0000000..5e1d6ec --- /dev/null +++ b/packages/lsd-react/src/components/TooltipBase/TooltipBase.styles.ts @@ -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; + } +` diff --git a/packages/lsd-react/src/components/TooltipBase/TooltipBase.tsx b/packages/lsd-react/src/components/TooltipBase/TooltipBase.tsx new file mode 100644 index 0000000..40929d7 --- /dev/null +++ b/packages/lsd-react/src/components/TooltipBase/TooltipBase.tsx @@ -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 & { + arrowOffset?: number + arrowPosition?: 'top' | 'bottom' | 'left' | 'right' + arrowSize?: number + rootRef?: React.Ref + } + +export const TooltipBase: React.FC & { + 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 ( +
+ {!arrowOffset ? ( + children + ) : ( + <> +
+
{children}
+ + )} +
+ ) +} + +TooltipBase.classes = tooltipBaseClasses diff --git a/packages/lsd-react/src/components/TooltipBase/index.ts b/packages/lsd-react/src/components/TooltipBase/index.ts new file mode 100644 index 0000000..dacef1e --- /dev/null +++ b/packages/lsd-react/src/components/TooltipBase/index.ts @@ -0,0 +1 @@ +export * from './TooltipBase' diff --git a/packages/lsd-react/src/index.ts b/packages/lsd-react/src/index.ts index ddbd413..6e14a87 100644 --- a/packages/lsd-react/src/index.ts +++ b/packages/lsd-react/src/index.ts @@ -38,3 +38,4 @@ export * from './components/DateField' export * from './components/DatePicker' export * from './components/Calendar' export * from './components/DateRangePicker' +export * from './components/TooltipBase' diff --git a/packages/lsd-react/src/utils/date.utils.ts b/packages/lsd-react/src/utils/date.utils.ts index ae79f46..6d8d97f 100644 --- a/packages/lsd-react/src/utils/date.utils.ts +++ b/packages/lsd-react/src/utils/date.utils.ts @@ -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 @@ -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 +}