From e0b33c9d5f28d9bc6e28e09d2de4d9317c4b38c0 Mon Sep 17 00:00:00 2001 From: suryakumara Date: Mon, 8 Dec 2025 10:26:55 +0800 Subject: [PATCH] feat: new components --- src/assets/SvgIcon/InfoCircleSolidSvgIcon.tsx | 22 ++ src/components/Dropdown/Dropdown.stories.tsx | 67 ++++-- src/components/Dropdown/DropdownV3.tsx | 227 ++++++++++++++++++ src/components/Tooltip/Tooltip.stories.tsx | 20 ++ src/components/Tooltip/TooltipIcon.tsx | 50 ++++ 5 files changed, 363 insertions(+), 23 deletions(-) create mode 100644 src/assets/SvgIcon/InfoCircleSolidSvgIcon.tsx create mode 100644 src/components/Dropdown/DropdownV3.tsx create mode 100644 src/components/Tooltip/Tooltip.stories.tsx create mode 100644 src/components/Tooltip/TooltipIcon.tsx diff --git a/src/assets/SvgIcon/InfoCircleSolidSvgIcon.tsx b/src/assets/SvgIcon/InfoCircleSolidSvgIcon.tsx new file mode 100644 index 0000000..5fe8c57 --- /dev/null +++ b/src/assets/SvgIcon/InfoCircleSolidSvgIcon.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const InfoCircleSolidSvgIcon = (props: SvgIconProps) => ( + + + +); + +export default InfoCircleSolidSvgIcon; diff --git a/src/components/Dropdown/Dropdown.stories.tsx b/src/components/Dropdown/Dropdown.stories.tsx index f26b6f3..6107782 100644 --- a/src/components/Dropdown/Dropdown.stories.tsx +++ b/src/components/Dropdown/Dropdown.stories.tsx @@ -5,32 +5,32 @@ import Button from '../Button'; import Dropdown from './Dropdown'; import { DropDownItem } from './Dropdown.type'; import DropdownV2 from './DropdownV2'; +import DropdownV3 from './DropdownV3'; const list: DropDownItem[] = [ + { id: 'A001', value: 'A001', name: 'Distributor 1' }, + { id: 'A002', value: 'A002', name: 'Distributor 2' }, + { id: 'A003', value: 'A003', name: 'Distributor 3' }, + { id: 'A004', value: 'A004', name: 'Distributor 4' }, + { id: 'A005', value: 'A005', name: 'Distributor 5' }, + { id: 'A006', value: 'A006', name: 'Distributor 6' }, + { id: 'A007', value: 'A007', name: 'Distributor 7' }, + { id: 'A008', value: 'A008', name: 'Distributor 8' }, + { id: 'A009', value: 'A009', name: 'Distributor 9' }, + { id: 'A010', value: 'A010', name: 'Distributor 10' }, + { id: 'A011', value: 'A011', name: 'Distributor 11' }, + { id: 'A012', value: 'A012', name: 'Distributor 12' }, + { id: 'A013', value: 'A013', name: 'Distributor 13' }, + { id: 'A014', value: 'A014', name: 'Distributor 14' }, + { id: 'A015', value: 'A015', name: 'Distributor 15' }, + { id: 'A016', value: 'A016', name: 'Distributor 16' }, + { id: 'A017', value: 'A017', name: 'Distributor 17' }, + { id: 'A018', value: 'A018', name: 'Distributor 18' }, + { id: 'A019', value: 'A019', name: 'Distributor 19' }, { - id: 'A001', - value: 'A001', - name: 'Distributor', - }, - { - id: 'A002', - value: 'A002', - name: 'Distributor A', - }, - { - id: 'A003', - value: 'A003', - name: 'Distributor B', - }, - { - id: 'A004', - value: 'A004', - name: 'Distributor C', - }, - { - id: 'A004', - value: 'A004', - name: 'Very long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long', + id: 'A020', + value: 'A020', + name: 'Very long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long', }, ]; @@ -231,3 +231,24 @@ export const Version2: Story = { ); }, }; + +export const Version3: Story = { + args: { + mode: 'dark', + list, + placeholder: 'Please Select Item', + }, + render: (args) => { + const [selectedId, setSelectedId] = useState(); + + return ( +
+ setSelectedId(value as string)} + /> +
+ ); + }, +}; diff --git a/src/components/Dropdown/DropdownV3.tsx b/src/components/Dropdown/DropdownV3.tsx new file mode 100644 index 0000000..33734ec --- /dev/null +++ b/src/components/Dropdown/DropdownV3.tsx @@ -0,0 +1,227 @@ +import React, { useState, useEffect, useRef } from 'react'; +import classNames from 'classnames'; +import { styled } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUp from '@mui/icons-material/KeyboardArrowUp'; +import { Popper as MuIPopper } from '@mui/material'; +import { DropDownItem, DropDownProps } from './Dropdown.type'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Icon from '../Icon/Icon'; +import CheckSvg from '../../assets/image/svg/check.svg'; + +const Root = styled(Box)(({ theme }) => ({ + fontFamily: '"Noto Sans TC", "Noto Sans"', + fontSize: '0.875rem', + lineHeight: 1.5, + minWidth: 220, + height: 40, + userSelect: 'none', + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + color: theme.color.secondary.$80, + backgroundColor: '#FFF', + padding: '8px 0px 8px 16px', + borderRadius: 4, + // Ellipsis for selected text + '.Dropdown-selected-text': { + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + flex: 1, + minWidth: 0, + marginRight: 8, + display: 'block', + fontFamily: '"Noto Sans TC", "Noto Sans"', + fontSize: '0.875rem', + lineHeight: 1.5, + }, + '&.dark': { + color: 'white', + backgroundColor: 'rgba(0, 0 ,0, 0.2)', + }, + '&.Dropdown-empty': { + color: theme.color.secondary.$60, + '&.dark': { + color: theme.color.secondary.$80, + }, + }, + '&.Dropdown--disabled': { + opacity: 0.3, + pointerEvents: 'none', + }, +})); + +const List = styled(Box)(({ theme }) => ({ + backgroundColor: '#FFF', + margin: '8px auto', + borderRadius: 4, + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', + '&.dark': { + color: 'white', + backgroundColor: theme.color.secondary.$100, + }, +})); + +const Popper = styled(MuIPopper)(({ theme }) => ({ + maxHeight: '200px', + overflow: 'auto', +})); + +const Item = styled(Box, { label: 'Dropdown-item' })(({ theme }) => ({ + fontFamily: '"Noto Sans TC", "Noto Sans"', + fontSize: '0.875rem', + lineHeight: 1.5, + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + + width: '100%', // Ensure item fills parent width + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, .05)', + }, + + '.Dropdown-icon': { + width: 24, + minWidth: 24, + maxWidth: 24, + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: 8, + }, + '.Dropdown-item-text': { + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + flex: 1, + minWidth: 0, + width: '100%', // Ensure text fills available space + fontFamily: '"Noto Sans TC", "Noto Sans"', + fontSize: '0.875rem', + lineHeight: 1.5, + }, +})); + +const DropdownV3: React.FC = (props) => { + const { + list, + itemProps, + placeholder, + selectedId, + disabled, + onSelect, + popperProps, + selectionId, + mode = 'light', + ...otherProps + } = props; + const selectRef = useRef(null); + const [selectedItem, setSelectedItem] = useState(null); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + if (selectedId && selectedId !== selectedItem?.id) { + for (let i = 0; i < list.length; i++) { + if (selectedId === list[i].id) { + setSelectedItem(list[i]); + break; + } + } + } else if (selectedId === undefined) { + setSelectedItem(null); + } + }, [selectedId]); + + useEffect(() => { + for (let i = 0; i < list.length; i++) { + if (selectedId === list[i].id) { + setSelectedItem(list[i]); + break; + } + } + }, [list]); + + const handleOnClickSelect = () => { + setIsOpen(true); + }; + + const handleOnClickAway = () => { + setIsOpen(false); + }; + + const handleOnClick = (item: DropDownItem) => { + setIsOpen(false); + onSelect(item.value, item); + }; + + const items = list + .filter((item) => item.id !== selectionId) + .map((item) => ( + handleOnClick(item)} + {...itemProps} + > + + {selectedItem?.id === item.id && } + + {item.name} + + )); + + return ( + <> + + + {selectedItem?.name ?? placeholder} + + + {isOpen ? : } + + + + + + {items} + + + + + ); +}; + +export default DropdownV3; diff --git a/src/components/Tooltip/Tooltip.stories.tsx b/src/components/Tooltip/Tooltip.stories.tsx new file mode 100644 index 0000000..fadd6eb --- /dev/null +++ b/src/components/Tooltip/Tooltip.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import TooltipIcon from './TooltipIcon'; + +const meta: Meta = { + title: 'Components/TooltipIcon', + component: TooltipIcon, +}; + +export default meta; + +type Story = StoryObj; +export const Default: Story = { + render: () => ( + + ), +}; diff --git a/src/components/Tooltip/TooltipIcon.tsx b/src/components/Tooltip/TooltipIcon.tsx new file mode 100644 index 0000000..3a2cdc0 --- /dev/null +++ b/src/components/Tooltip/TooltipIcon.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import Tooltip from '@mui/material/Tooltip'; +import InfoCircleSolidSvgIcon from '../../assets/SvgIcon/InfoCircleSolidSvgIcon'; +import { styled } from '@mui/material'; + +const ContainerTooltip = styled('div')({ + display: 'flex', + alignItems: 'center', + gap: '4px', + fontFamily: '"Noto Sans TC", "Noto Sans"', + fontSize: '0.875rem', + lineHeight: 1.5, +}); + +const IconWrapper = styled('span')({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', +}); + +const TooltipIcon = ({ title, text }: { title: string; text: string }) => { + return ( + + {text} + ({ + '& .MuiTooltip-tooltip': { + color: '#fff', + backgroundColor: theme.externalColor?.secondary?.$140 || '#333', + borderRadius: '4px', + boxShadow: '0px 2px 2px rgba(0, 0, 0, 0.1)', + maxWidth: '250px', + padding: '10px', + }, + }), + }} + > + + + + + + ); +}; + +export default TooltipIcon;