diff --git a/packages/frontend/package.json b/packages/frontend/package.json index c484a38d..855c29c5 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,7 +8,9 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/packages/frontend/src/components/shared/Button/Button.tsx b/packages/frontend/src/components/shared/Button/Button.tsx index af845e0c..968cca72 100644 --- a/packages/frontend/src/components/shared/Button/Button.tsx +++ b/packages/frontend/src/components/shared/Button/Button.tsx @@ -98,19 +98,29 @@ const Button = forwardRef< href, ...baseLinkProps, }; - return {_children}; + return ( + // @ts-expect-error - ref + + {_children} + + ); } // Internal link return ( - + // @ts-expect-error - ref + {_children} ); } else { const { ...buttonProps } = _props; - // @ts-expect-error - as prop is not a valid prop for button elements - return ; + return ( + // @ts-expect-error - as prop is not a valid prop for button elements + + ); } }, [], @@ -161,8 +171,6 @@ const Button = forwardRef< return ( {cloneIcon(leftIcon, { ...iconSize })} diff --git a/packages/frontend/src/components/shared/Radio/Radio.theme.ts b/packages/frontend/src/components/shared/Radio/Radio.theme.ts new file mode 100644 index 00000000..0b84601e --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/Radio.theme.ts @@ -0,0 +1,54 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const radioTheme = tv({ + slots: { + root: ['flex', 'gap-3', 'flex-wrap'], + wrapper: ['flex', 'items-center', 'gap-2', 'group'], + label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'], + radio: [ + 'w-5', + 'h-5', + 'rounded-full', + 'border', + 'group', + 'border-border-interactive/10', + 'shadow-button', + 'group-hover:border-border-interactive/[0.14]', + 'focus-ring', + // Checked + 'data-[state=checked]:bg-controls-primary', + 'data-[state=checked]:group-hover:bg-controls-primary-hovered', + ], + indicator: [ + 'flex', + 'items-center', + 'justify-center', + 'relative', + 'w-full', + 'h-full', + 'after:content-[""]', + 'after:block', + 'after:w-2.5', + 'after:h-2.5', + 'after:rounded-full', + 'after:bg-transparent', + 'after:group-hover:bg-controls-disabled', + 'after:group-focus-visible:bg-controls-disabled', + // Checked + 'after:data-[state=checked]:bg-elements-on-primary', + 'after:data-[state=checked]:group-hover:bg-elements-on-primary', + 'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary', + ], + }, + variants: { + orientation: { + vertical: { root: ['flex-col'] }, + horizontal: { root: ['flex-row'] }, + }, + }, + defaultVariants: { + orientation: 'vertical', + }, +}); + +export type RadioTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Radio/Radio.tsx b/packages/frontend/src/components/shared/Radio/Radio.tsx new file mode 100644 index 00000000..96542493 --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/Radio.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { + Root as RadixRoot, + RadioGroupProps, +} from '@radix-ui/react-radio-group'; +import { RadioTheme, radioTheme } from './Radio.theme'; +import { RadioItem, RadioItemProps } from './RadioItem'; + +export interface RadioOption extends RadioItemProps { + /** + * The label of the radio option. + */ + label: string; + /** + * The value of the radio option. + */ + value: string; +} + +export interface RadioProps extends RadioGroupProps, RadioTheme { + /** + * The options of the radio. + * @default [] + * @example + * ```tsx + * const options = [ + * { + * label: 'Label 1', + * value: '1', + * }, + * { + * label: 'Label 2', + * value: '2', + * }, + * { + * label: 'Label 3', + * value: '3', + * }, + * ]; + * ``` + */ + options: RadioOption[]; +} + +/** + * The Radio component is used to select one option from a list of options. + */ +export const Radio = ({ + className, + options, + orientation, + ...props +}: RadioProps) => { + const { root } = radioTheme({ orientation }); + + return ( + + {options.map((option) => ( + + ))} + + ); +}; diff --git a/packages/frontend/src/components/shared/Radio/RadioItem.tsx b/packages/frontend/src/components/shared/Radio/RadioItem.tsx new file mode 100644 index 00000000..177af9db --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/RadioItem.tsx @@ -0,0 +1,74 @@ +import React, { ComponentPropsWithoutRef } from 'react'; +import { + Item as RadixRadio, + Indicator as RadixIndicator, + RadioGroupItemProps, + RadioGroupIndicatorProps, +} from '@radix-ui/react-radio-group'; +import { radioTheme } from './Radio.theme'; + +export interface RadioItemProps extends RadioGroupItemProps { + /** + * The wrapper props of the radio item. + * You can use this prop to customize the wrapper props. + */ + wrapperProps?: ComponentPropsWithoutRef<'div'>; + /** + * The label props of the radio item. + * You can use this prop to customize the label props. + */ + labelProps?: ComponentPropsWithoutRef<'label'>; + /** + * The indicator props of the radio item. + * You can use this prop to customize the indicator props. + */ + indicatorProps?: RadioGroupIndicatorProps; + /** + * The id of the radio item. + */ + id?: string; + /** + * The label of the radio item. + */ + label?: string; +} + +/** + * The RadioItem component is used to render a radio item. + */ +export const RadioItem = ({ + className, + wrapperProps, + labelProps, + indicatorProps, + label, + id, + ...props +}: RadioItemProps) => { + const { wrapper, label: labelClass, radio, indicator } = radioTheme(); + + // Generate a unique id for the radio item from the label if the id is not provided + const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-'); + const componentId = id ?? kebabCaseLabel; + + return ( +
+ + + + {label && ( + + )} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Radio/index.ts b/packages/frontend/src/components/shared/Radio/index.ts new file mode 100644 index 00000000..6d49f1a8 --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/index.ts @@ -0,0 +1,2 @@ +export * from './Radio'; +export * from './RadioItem'; diff --git a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts index 4667fe83..7c39b077 100644 --- a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts +++ b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts @@ -9,6 +9,8 @@ export const tabsTheme = tv({ // Horizontal – default 'px-1', 'pb-5', + 'cursor-default', + 'select-none', 'text-elements-low-em', 'border-b-2', 'border-transparent', diff --git a/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts b/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts new file mode 100644 index 00000000..c4c1c5e0 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts @@ -0,0 +1,14 @@ +import { tv } from 'tailwind-variants'; + +export const tooltipTheme = tv({ + slots: { + content: [ + 'z-tooltip', + 'rounded-md', + 'bg-surface-high-contrast', + 'p-2', + 'text-elements-on-high-contrast', + ], + arrow: ['fill-surface-high-contrast'], + }, +}); diff --git a/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx b/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..75d73c78 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { + TooltipContentProps, + TooltipTriggerProps, +} from '@radix-ui/react-tooltip'; +import { ReactNode, useState } from 'react'; + +import { TooltipBase, type TooltipBaseProps } from './TooltipBase'; + +export interface TooltipProps extends TooltipBaseProps { + triggerProps?: TooltipTriggerProps; + contentProps?: TooltipContentProps; + content?: ReactNode; +} + +// https://github.com/radix-ui/primitives/issues/955#issuecomment-1798201143 +// Wrap on top of Tooltip base to make tooltip open on mobile via click +export const Tooltip = ({ + children, + triggerProps, + contentProps, + content, + ...props +}: TooltipProps) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + return ( + + setIsTooltipVisible(false)} + onClick={() => setIsTooltipVisible((prevOpen) => !prevOpen)} + onFocus={() => setTimeout(() => setIsTooltipVisible(true), 0)} + {...triggerProps} + > + {triggerProps?.children ?? children} + + + {content ?? contentProps?.children ?? 'Coming soon'} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx new file mode 100644 index 00000000..273a9cbe --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { + Provider, + TooltipProps as RadixTooltipProps, + Root, + type TooltipProviderProps, +} from '@radix-ui/react-tooltip'; +import { useState, type PropsWithChildren } from 'react'; + +import { TooltipContent } from './TooltipContent'; +import { TooltipTrigger } from './TooltipTrigger'; + +export interface TooltipBaseProps extends RadixTooltipProps { + providerProps?: TooltipProviderProps; +} + +export const TooltipBase = ({ + children, + providerProps, + ...props +}: PropsWithChildren) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + return ( + + + {children} + + + ); +}; + +TooltipBase.Trigger = TooltipTrigger; +TooltipBase.Content = TooltipContent; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx new file mode 100644 index 00000000..57678206 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + Arrow, + Content, + Portal, + type TooltipArrowProps, + type TooltipContentProps, +} from '@radix-ui/react-tooltip'; + +import { tooltipTheme } from '../Tooltip.theme'; + +export interface ContentProps extends TooltipContentProps { + hasArrow?: boolean; + arrowProps?: TooltipArrowProps; +} + +export const TooltipContent = ({ + children, + arrowProps, + className, + hasArrow = true, + ...props +}: ContentProps) => { + const { content, arrow } = tooltipTheme(); + return ( + + + {hasArrow && ( + + )} + {children} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts b/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts new file mode 100644 index 00000000..1675248c --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts @@ -0,0 +1 @@ +export * from './TooltipContent'; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx new file mode 100644 index 00000000..c27993ed --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Trigger, type TooltipTriggerProps } from '@radix-ui/react-tooltip'; + +export type TriggerProps = TooltipTriggerProps; + +export const TooltipTrigger = ({ children, ...props }: TriggerProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts new file mode 100644 index 00000000..e73d66c6 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts @@ -0,0 +1 @@ +export * from './TooltipTrigger'; diff --git a/packages/frontend/src/components/shared/Tooltip/index.ts b/packages/frontend/src/components/shared/Tooltip/index.ts new file mode 100644 index 00000000..a51f2a31 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/index.ts @@ -0,0 +1,2 @@ +export * from './Tooltip'; +export * from './TooltipBase'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 28378064..a9c3674d 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -18,16 +18,20 @@ import { renderTabs, renderVerticalTabs, } from './renders/tabs'; +import { RADIO_OPTIONS } from './renders/radio'; +import { Radio } from 'components/shared/Radio'; import { renderInlineNotificationWithDescriptions, renderInlineNotifications, } from './renders/inlineNotifications'; import { renderInputs } from './renders/input'; import { DatePicker } from 'components/shared/DatePicker'; +import { renderTooltips } from './renders/tooltip'; const Page = () => { const [singleDate, setSingleDate] = useState(); const [dateRange, setDateRange] = useState(); + const [selectedRadio, setSelectedRadio] = useState(''); return (
@@ -42,13 +46,21 @@ const Page = () => {
- {/* Button */}
+

Tooltip

+
+ {renderTooltips()} +
+ +
+ + {/* Input */}

Input

{renderInputs()}
+ {/* Button */}

Button

{renderButtons()} @@ -162,6 +174,27 @@ const Page = () => {
+ {/* Radio */} +
+

Radio

+
+ + +
+
+ +
+ {/* Inline notification */}

Inline Notification

diff --git a/packages/frontend/src/pages/components/renders/radio.ts b/packages/frontend/src/pages/components/renders/radio.ts new file mode 100644 index 00000000..ff262f66 --- /dev/null +++ b/packages/frontend/src/pages/components/renders/radio.ts @@ -0,0 +1,16 @@ +import { RadioOption } from 'components/shared/Radio'; + +export const RADIO_OPTIONS: RadioOption[] = [ + { + label: 'Label 1', + value: '1', + }, + { + label: 'Label 2', + value: '2', + }, + { + label: 'Label 3', + value: '3', + }, +]; diff --git a/packages/frontend/src/pages/components/renders/tooltip.tsx b/packages/frontend/src/pages/components/renders/tooltip.tsx new file mode 100644 index 00000000..7114f4db --- /dev/null +++ b/packages/frontend/src/pages/components/renders/tooltip.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Button } from 'components/shared/Button'; +import { Tooltip } from 'components/shared/Tooltip'; +import { ContentProps } from 'components/shared/Tooltip/TooltipContent'; + +const alignments: ContentProps['align'][] = ['start', 'center', 'end']; +const sides: ContentProps['side'][] = ['left', 'top', 'bottom', 'right']; + +export const renderTooltips = () => { + const tooltips = sides.map((side) => { + return alignments.map((align) => { + return ( + + + + ); + }); + }); + return tooltips; +}; diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 4bcb0b01..9a814026 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -8,10 +8,13 @@ export default withMT({ '../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}', ], theme: { - fontFamily: { - sans: ['Inter', 'sans-serif'], - }, extend: { + zIndex: { + tooltip: '52', + }, + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, fontSize: { '2xs': '0.625rem', '3xs': '0.5rem', diff --git a/yarn.lock b/yarn.lock index dc61ca8a..5fdb09b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3478,6 +3478,23 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" +"@radix-ui/react-radio-group@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3" + integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-roving-focus@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" @@ -3517,6 +3534,25 @@ "@radix-ui/react-roving-focus" "1.0.4" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-tooltip@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e" + integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.3" + "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" @@ -3570,6 +3606,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-visually-hidden@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" + integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/rect@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"