feat(design): add HoverCard component (#5211)

Co-authored-by: GitHub Actions <actions@github.com>
This commit is contained in:
白熱
2025-05-16 12:15:03 +08:00
committed by GitHub
co-authored by GitHub Actions
parent bc85cd7a2f
commit 7441d00fef
15 changed files with 260 additions and 43 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

+1 -1
View File
@@ -63,7 +63,7 @@
"eslint": "9.26.0",
"eslint-plugin-format": "^1.0.1",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-jsdoc": "^50.6.14",
"eslint-plugin-jsdoc": "^50.6.17",
"eslint-plugin-no-barrel-import": "^0.0.2",
"eslint-plugin-no-penetrating-import": "^0.0.1",
"eslint-plugin-react": "^7.37.5",
+1
View File
@@ -70,6 +70,7 @@
"dependencies": {
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2",
@@ -16,7 +16,7 @@
import { ChevronSortSingle } from '@univerjs/icons';
import React, { useEffect, useMemo, useState } from 'react';
import { Dropdown } from '../dropdown';
import { Dropdown } from '../dropdown/Dropdown';
import { hexToHsv, hslToHsv, hsvToHex, hsvToHsl, hsvToRgb, rgbToHsv } from './color-conversion';
interface IColorInputProps {
@@ -14,12 +14,11 @@
* limitations under the License.
*/
import type { Content } from '@radix-ui/react-popover';
import type { ComponentProps, ReactNode } from 'react';
import { useState } from 'react';
import { PopoverContent, PopoverPrimitive, PopoverTrigger } from './PopoverPrimitive';
export interface IDropdownProps extends ComponentProps<typeof Content> {
export interface IDropdownProps extends ComponentProps<typeof PopoverContent> {
children: ReactNode;
overlay: ReactNode;
disabled?: boolean;
@@ -47,7 +47,7 @@ function PopoverContent({
`
univer-z-[1080] univer-max-h-[var(--radix-popper-available-height)] univer-overflow-y-auto
univer-rounded-md univer-bg-white univer-text-gray-900 univer-shadow-md univer-outline-hidden
dark:!univer-bg-gray-900 dark:!univer-text-white
dark:!univer-bg-gray-900 dark:!univer-text-white dark:focus-visible:univer-border-gray-600
data-[side=bottom]:univer-slide-in-from-top-2
data-[side=left]:univer-slide-in-from-right-2
data-[side=right]:univer-slide-in-from-left-2
@@ -1,17 +0,0 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Dropdown, type IDropdownProps } from './Dropdown';
@@ -0,0 +1,70 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Meta } from '@storybook/react';
import { borderClassName } from '../../helper/class-utilities';
import { clsx } from '../../helper/clsx';
import { HoverCard } from './HoverCard';
const meta: Meta<typeof HoverCard> = {
title: 'Components / HoverCard',
component: HoverCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};
export default meta;
export const Playground = {
render() {
return (
<div className="univer-relative">
<HoverCard
overlay={(
<div className="univer-text-primary-500">
Hello Univer
<HoverCard
openDelay={0}
overlay={(
<div className="univer-text-cyan-700">
<div>Nested Content</div>
<div>Nested Content</div>
<div>Nested Content</div>
<div>Nested Content</div>
</div>
)}
>
<a>Nested Hover Card</a>
</HoverCard>
</div>
)}
>
<a
className={clsx(`
univer-cursor-pointer univer-rounded-lg univer-px-4 univer-py-2 univer-transition-all
hover:univer-bg-gray-100
`, borderClassName)}
>
Click me
</a>
</HoverCard>
</div>
);
},
};
@@ -0,0 +1,68 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ComponentProps, ReactNode } from 'react';
import { useState } from 'react';
import { HoverCardContent, HoverCardPrimitive, HoverCardTrigger } from './HoverCardPrimitive';
export interface IHoverCardProps extends ComponentProps<typeof HoverCardContent> {
children: ReactNode;
overlay: ReactNode;
disabled?: boolean;
/**
* Delay in milliseconds before the dropdown opens.
* @default 200
*/
openDelay?: number;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function HoverCard(props: IHoverCardProps) {
const {
children,
overlay,
disabled,
openDelay = 200,
open: controlledOpen,
onOpenChange: controlledOnOpenChange,
...restProps
} = props;
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
function handleChangeOpen(newOpen: boolean) {
if (disabled) return;
if (!isControlled) {
setUncontrolledOpen(newOpen);
}
controlledOnOpenChange?.(newOpen);
}
return (
<HoverCardPrimitive open={open} onOpenChange={handleChangeOpen} openDelay={openDelay}>
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
<HoverCardContent {...restProps}>
{overlay}
</HoverCardContent>
</HoverCardPrimitive>
);
}
@@ -0,0 +1,57 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ComponentPropsWithoutRef, ElementRef } from 'react';
import { Content, Root, Trigger } from '@radix-ui/react-hover-card';
import { forwardRef } from 'react';
import { borderClassName } from '../../helper/class-utilities';
import { clsx } from '../../helper/clsx';
const HoverCardPrimitive = Root;
const HoverCardTrigger = Trigger;
const HoverCardContent = forwardRef<
ElementRef<typeof Content>,
ComponentPropsWithoutRef<typeof Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={clsx(
`
univer-z-[1080] univer-w-64 univer-origin-[--radix-hover-card-content-transform-origin] univer-rounded-md
univer-bg-white univer-text-gray-900 univer-shadow-md univer-outline-none
dark:univer-bg-gray-900 dark:univer-text-white
data-[side=bottom]:univer-slide-in-from-top-2
data-[side=left]:univer-slide-in-from-right-2
data-[side=right]:univer-slide-in-from-left-2
data-[side=top]:univer-slide-in-from-bottom-2
data-[state=closed]:univer-animate-out data-[state=closed]:univer-fade-out-0
data-[state=closed]:univer-zoom-out-95
data-[state=open]:univer-animate-in data-[state=open]:univer-fade-in-0 data-[state=open]:univer-zoom-in-95
`,
borderClassName,
className
)}
{...props}
/>
));
HoverCardContent.displayName = Content.displayName;
export { HoverCardContent, HoverCardPrimitive, HoverCardTrigger };
+2 -1
View File
@@ -26,7 +26,8 @@ export { ColorPicker, type IColorPickerProps } from './components/color-picker/C
export { ConfigContext, ConfigProvider, type IConfigProviderProps } from './components/config-provider/ConfigProvider';
export { Confirm, type IConfirmProps } from './components/confirm/Confirm';
export { Dialog, type IDialogProps } from './components/dialog/Dialog';
export { Dropdown, type IDropdownProps } from './components/dropdown';
export { Dropdown, type IDropdownProps } from './components/dropdown/Dropdown';
export { HoverCard, type IHoverCardProps } from './components/hover-card/HoverCard';
export { type IInputProps, Input } from './components/input/Input';
export { type IInputNumberProps, InputNumber } from './components/input-number/InputNumber';
export { type IMessageProps, message, Messager, MessageType, removeMessage } from './components/message/Message';
@@ -46,8 +46,8 @@ export const FontSize = (props: IFontSizeProps) => {
className={`
univer-block univer-h-6 univer-border-none univer-bg-transparent univer-leading-6
[&_input:focus]:!univer-ring-0
[&_input]:univer-h-6 [&_input]:univer-w-7 [&_input]:univer-border-none [&_input]:univer-bg-transparent
[&_input]:univer-p-0 [&_input]:univer-text-sm
[&_input]:univer-h-6 [&_input]:univer-w-7 [&_input]:univer-border-none
[&_input]:!univer-bg-transparent [&_input]:univer-p-0 [&_input]:univer-text-sm
`}
value={_value}
controls={false}
@@ -18,7 +18,7 @@ import type { ComponentType } from 'react';
import type { Observable } from 'rxjs';
import type { IMenuSchema } from '../../../services/menu/menu-manager.service';
import { LocaleService } from '@univerjs/core';
import { borderBottomClassName, borderClassName, clsx, divideXClassName, Dropdown } from '@univerjs/design';
import { borderBottomClassName, borderClassName, clsx, divideXClassName, Dropdown, HoverCard } from '@univerjs/design';
import { DatabaseSingle, EyeSingle, FunctionSingle, HomeSingle, InsertSingle, MoreDownSingle, MoreFunctionSingle } from '@univerjs/icons';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { combineLatest } from 'rxjs';
@@ -253,8 +253,8 @@ export function Ribbon(props: IRibbonProps) {
</search> */}
{ribbon.length > 1 && (
<Dropdown
className="univer-max-w-96 !univer-rounded-xl univer-animate-in univer-fade-in"
<HoverCard
className="univer-max-w-96 !univer-rounded-xl"
align="start"
open={groupSelectorVisible}
overlay={(
@@ -322,7 +322,7 @@ export function Ribbon(props: IRibbonProps) {
`}
/>
</a>
</Dropdown>
</HoverCard>
)}
<div ref={containerRef} className={clsx('univer-flex univer-overflow-hidden', divideXClassName)}>
@@ -336,9 +336,11 @@ export function Ribbon(props: IRibbonProps) {
</Fragment>
))}
{/* More functions dropdown */}
{collapsedIds.length > 0 && (
<div className="univer-pl-2">
<Dropdown
collisionPadding={{ right: 12 }}
overlay={(
<div
className={`
@@ -95,7 +95,7 @@ export function ToolbarButton(props: IBaseToolbarButtonProps) {
hover:univer-bg-gray-100
`, {
'univer-px-2': noIcon,
'!univer-bg-gray-200 dark:!!univer-bg-gray-500': active,
'!univer-bg-gray-200 dark:!univer-bg-gray-500': active,
})}
style={style}
disabled={disabled}
+49 -13
View File
@@ -69,8 +69,8 @@ importers:
specifier: ^3.1.1
version: 3.1.1(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-jsdoc:
specifier: ^50.6.14
version: 50.6.14(eslint@9.26.0(jiti@2.4.2))
specifier: ^50.6.17
version: 50.6.17(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-no-barrel-import:
specifier: ^0.0.2
version: 0.0.2(eslint@9.26.0(jiti@2.4.2))
@@ -1121,6 +1121,9 @@ importers:
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.14
version: 2.1.14(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-hover-card':
specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-popover':
specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -4011,14 +4014,14 @@ packages:
'@emnapi/wasi-threads@1.0.2':
resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==}
'@es-joy/jsdoccomment@0.49.0':
resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==}
engines: {node: '>=16'}
'@es-joy/jsdoccomment@0.50.0':
resolution: {integrity: sha512-+zZymuVLH6zVwXPtCAtC+bDymxmEwEqDftdAK+f407IF1bnX49anIxvBhCA1AqUIfD6egj1jM1vUnSuijjNyYg==}
engines: {node: '>=18'}
'@es-joy/jsdoccomment@0.50.1':
resolution: {integrity: sha512-fas3qe1hw38JJgU/0m5sDpcrbZGysBeZcMwW5Ws9brYxY64MJyWLXRZCj18keTycT1LFTrFXdSNMS+GRVaU6Hw==}
engines: {node: '>=18'}
'@esbuild/aix-ppc64@0.25.4':
resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
engines: {node: '>=18'}
@@ -4727,6 +4730,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-hover-card@1.1.13':
resolution: {integrity: sha512-Wtjvx0d/6Bgd/jAYS1mW6IPSUQ25y0hkUSOS1z5/4+U8+DJPwKroqJlM/AlVFl3LywGoruiPmcvB9Aks9mSOQw==}
peerDependencies:
'@types/react': ^19.1.3
'@types/react-dom': ^19.1.5
react: ^19.1.0
react-dom: ^19.1.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
@@ -7397,8 +7413,8 @@ packages:
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
eslint-plugin-jsdoc@50.6.14:
resolution: {integrity: sha512-JUudvooQbUx3iB8n/MzXMOV/VtaXq7xL4CeXhYryinr8osck7nV6fE2/xUXTiH3epPXcvq6TE3HQfGQuRHErTQ==}
eslint-plugin-jsdoc@50.6.17:
resolution: {integrity: sha512-hq+VQylhd12l8qjexyriDsejZhqiP33WgMTy2AmaGZ9+MrMWVqPECsM87GPxgHfQn0zw+YTuhqjUfk1f+q67aQ==}
engines: {node: '>=18'}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
@@ -11344,7 +11360,7 @@ snapshots:
eslint-plugin-antfu: 3.1.1(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-command: 3.2.0(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-import-x: 4.11.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
eslint-plugin-jsdoc: 50.6.14(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-jsdoc: 50.6.17(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-jsonc: 2.20.0(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-n: 17.18.0(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-no-only-tests: 3.3.0
@@ -11918,13 +11934,16 @@ snapshots:
tslib: 2.8.1
optional: true
'@es-joy/jsdoccomment@0.49.0':
'@es-joy/jsdoccomment@0.50.0':
dependencies:
'@types/eslint': 9.6.1
'@types/estree': 1.0.7
'@typescript-eslint/types': 8.32.0
comment-parser: 1.4.1
esquery: 1.6.0
jsdoc-type-pratt-parser: 4.1.0
'@es-joy/jsdoccomment@0.50.0':
'@es-joy/jsdoccomment@0.50.1':
dependencies:
'@types/eslint': 9.6.1
'@types/estree': 1.0.7
@@ -12632,6 +12651,23 @@ snapshots:
'@types/react': 19.1.3
'@types/react-dom': 19.1.5(@types/react@19.1.3)
'@radix-ui/react-hover-card@1.1.13(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.5(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@types/react-dom': 19.1.5(@types/react@19.1.3)
'@radix-ui/react-id@1.1.1(@types/react@19.1.3)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
@@ -15512,9 +15548,9 @@ snapshots:
- supports-color
- typescript
eslint-plugin-jsdoc@50.6.14(eslint@9.26.0(jiti@2.4.2)):
eslint-plugin-jsdoc@50.6.17(eslint@9.26.0(jiti@2.4.2)):
dependencies:
'@es-joy/jsdoccomment': 0.49.0
'@es-joy/jsdoccomment': 0.50.1
are-docs-informative: 0.0.2
comment-parser: 1.4.1
debug: 4.4.0