From f20e9c6bdb4262f1ed2d3e3221905679a3ce302d Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com> Date: Tue, 7 May 2024 09:48:27 -0700 Subject: [PATCH] [Mobile] Schedule name and date in mobile scheduled transaction modal (#2664) * [Mobile] Add schedule name and date to scheduled transaction modal * useRef to prevent rerenders * Release notes * Fix typecheck error * Fix lint errors * Update schedule font * Heavier font weight --- .../src/components/NotesButton.tsx | 2 +- .../src/components/common/Modal.tsx | 110 ++++++++++++------ .../src/components/common/Tooltip.tsx | 33 +++--- .../modals/ScheduledTransactionMenuModal.tsx | 39 ++++++- upcoming-release-notes/2664.md | 6 + 5 files changed, 132 insertions(+), 58 deletions(-) create mode 100644 upcoming-release-notes/2664.md diff --git a/packages/desktop-client/src/components/NotesButton.tsx b/packages/desktop-client/src/components/NotesButton.tsx index bbd42b33c..901442b8c 100644 --- a/packages/desktop-client/src/components/NotesButton.tsx +++ b/packages/desktop-client/src/components/NotesButton.tsx @@ -66,7 +66,7 @@ export function NotesButton({ setIsOpen(true); }} > - <SvgCustomNotesPaper style={{ width, height }} /> + <SvgCustomNotesPaper style={{ width, height, flexShrink: 0 }} /> </Button> </View> diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx index ddd498717..64ec91e6d 100644 --- a/packages/desktop-client/src/components/common/Modal.tsx +++ b/packages/desktop-client/src/components/common/Modal.tsx @@ -12,15 +12,20 @@ import React, { import { useHotkeysContext } from 'react-hotkeys-hook'; import ReactModal from 'react-modal'; +import { useHover } from 'usehooks-ts'; + +import { useMergedRefs } from '../../hooks/useMergedRefs'; import { AnimatedLoading } from '../../icons/AnimatedLoading'; import { SvgLogo } from '../../icons/logo'; import { SvgDelete } from '../../icons/v0'; +import { SvgClose } from '../../icons/v1'; import { type CSSProperties, styles, theme } from '../../style'; import { tokens } from '../../tokens'; import { Button } from './Button'; import { Input } from './Input'; import { Text } from './Text'; +import { Tooltip } from './Tooltip'; import { View } from './View'; export type ModalProps = { @@ -372,14 +377,42 @@ export function ModalTitle({ shrinkOnOverflow = false, }: ModalTitleProps) { const [isEditing, setIsEditing] = useState(false); - const style = getStyle?.(isEditing); + const tooltipTriggerRef = useRef(); - const _onEdit = () => { - if (!isEditable) { - return; + // Dynamic font size to avoid ellipsis. + const textRef = useRef<HTMLSpanElement>(); + const [textOverflowed, setTextOverflowed] = useState(false); + const [textFontSize, setTextFontSize] = useState(25); + + useEffect(() => { + const containerWidth = textRef.current.offsetWidth; + const textWidth = textRef.current.scrollWidth; + + if (textWidth > containerWidth) { + setTextOverflowed(true); + } else { + setTextOverflowed(false); } + }, [title]); - setIsEditing(true); + useEffect(() => { + if (textOverflowed && shrinkOnOverflow) { + const containerWidth = textRef.current.offsetWidth; + const textWidth = textRef.current.scrollWidth; + const newFontSize = Math.floor( + (containerWidth / textWidth) * textFontSize, + ); + setTextFontSize(newFontSize); + } + }, [title, textFontSize, shrinkOnOverflow, textOverflowed]); + + const mergedTextRef = useMergedRefs(textRef, tooltipTriggerRef); + const isHovered = useHover(tooltipTriggerRef); + + const _onEdit = () => { + if (isEditable) { + setIsEditing(true); + } }; const _onTitleUpdate = newTitle => { @@ -398,22 +431,7 @@ export function ModalTitle({ } }, [isEditing]); - // Dynamic font size to avoid ellipsis. - const textRef = useRef<HTMLSpanElement>(); - const [textFontSize, setTextFontSize] = useState(25); - useEffect(() => { - if (shrinkOnOverflow) { - const containerWidth = textRef.current.offsetWidth; - const textWidth = textRef.current.scrollWidth; - - if (textWidth > containerWidth) { - const newFontSize = Math.floor( - (containerWidth / textWidth) * textFontSize, - ); - setTextFontSize(newFontSize); - } - } - }, [textFontSize, shrinkOnOverflow]); + const style = getStyle?.(isEditing); return isEditing ? ( <Input @@ -435,22 +453,42 @@ export function ModalTitle({ }} /> ) : ( - <Text - innerRef={textRef} - style={{ - fontSize: textFontSize, - fontWeight: 700, - textAlign: 'center', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - ...(isEditable && styles.underlinedText), - ...style, - }} - onClick={_onEdit} + <Tooltip + content={ + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + gap: 10, + maxWidth: '90vw', + }} + > + <SvgClose style={{ width: 10, height: 10, flexShrink: 0 }} /> + <Text style={styles.mediumText}>{title}</Text> + </View> + } + placement="top" + triggerRef={tooltipTriggerRef} + isOpen={textOverflowed && isHovered} + offset={10} > - {title} - </Text> + <Text + innerRef={mergedTextRef} + style={{ + fontSize: textFontSize, + fontWeight: 700, + textAlign: 'center', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + ...(isEditable && styles.underlinedText), + ...style, + }} + onClick={_onEdit} + > + {title} + </Text> + </Tooltip> ); } diff --git a/packages/desktop-client/src/components/common/Tooltip.tsx b/packages/desktop-client/src/components/common/Tooltip.tsx index 050741284..4b9f68c47 100644 --- a/packages/desktop-client/src/components/common/Tooltip.tsx +++ b/packages/desktop-client/src/components/common/Tooltip.tsx @@ -25,44 +25,39 @@ export const Tooltip = ({ ...props }: TooltipProps) => { const triggerRef = useRef(null); - const [hover, setHover] = useState(false); + const [isHovered, setIsHover] = useState(false); - const [delayHandler, setDelayHandler] = useState<ReturnType< - typeof setTimeout - > | null>(null); + const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>(); - const handleMouseEnter = useCallback(() => { + const handlePointerEnter = useCallback(() => { const timeout = setTimeout(() => { - setHover(true); + setIsHover(true); }, triggerProps.delay ?? 300); - setDelayHandler(timeout); - return () => { - clearTimeout(timeout); - }; + hoverTimeoutRef.current = timeout; }, [triggerProps.delay]); - const handleMouseLeave = useCallback(() => { - if (delayHandler) { - clearTimeout(delayHandler); + const handlePointerLeave = useCallback(() => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); } - setHover(false); - }, [delayHandler]); + setIsHover(false); + }, []); // Force closing the tooltip whenever the disablement state changes useEffect(() => { - setHover(false); + setIsHover(false); }, [triggerProps.isDisabled]); return ( <View ref={triggerRef} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} + onPointerEnter={handlePointerEnter} + onPointerLeave={handlePointerLeave} > <TooltipTrigger - isOpen={hover && !triggerProps.isDisabled} + isOpen={isHovered && !triggerProps.isDisabled} {...triggerProps} > {children} diff --git a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx index bac14eeb5..b0857d863 100644 --- a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx @@ -1,8 +1,14 @@ -import React, { type ComponentPropsWithoutRef } from 'react'; +import React, { useCallback, type ComponentPropsWithoutRef } from 'react'; + +import { useSchedules } from 'loot-core/client/data-hooks/schedules'; +import { format } from 'loot-core/shared/months'; +import { type Query } from 'loot-core/shared/query'; import { type CSSProperties, theme, styles } from '../../style'; import { Menu } from '../common/Menu'; import { Modal } from '../common/Modal'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; import { type CommonModalProps } from '../Modals'; type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps & { @@ -21,9 +27,38 @@ export function ScheduledTransactionMenuModal({ borderRadius: 0, borderTop: `1px solid ${theme.pillBorder}`, }; + const scheduleId = transactionId?.split('/')?.[1]; + const scheduleData = useSchedules({ + transform: useCallback( + (q: Query) => q.filter({ id: scheduleId }), + [scheduleId], + ), + }); + const schedule = scheduleData?.schedules?.[0]; + + if (!schedule) { + return; + } return ( - <Modal showHeader focusAfterClose={false} {...modalProps}> + <Modal + title={schedule.name} + showHeader + focusAfterClose={false} + {...modalProps} + > + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <Text style={{ fontSize: 17, fontWeight: 400 }}>Scheduled date</Text> + <Text style={{ fontSize: 17, fontWeight: 700 }}> + {format(schedule.next_date, 'MMMM dd, yyyy')} + </Text> + </View> <ScheduledTransactionMenu transactionId={transactionId} onPost={onPost} diff --git a/upcoming-release-notes/2664.md b/upcoming-release-notes/2664.md new file mode 100644 index 000000000..623f2ba47 --- /dev/null +++ b/upcoming-release-notes/2664.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Add schedule name and date to mobile scheduled transaction modal. \ No newline at end of file -- GitLab