Skip to content

Commit 26b13ee

Browse files
committed
feat(hovercard): adds a new HoverCard component
based on radix HoverCard using paperstyles and overlay annimation styles fixes #287
1 parent 983af25 commit 26b13ee

File tree

11 files changed

+239
-8
lines changed

11 files changed

+239
-8
lines changed

package-lock.json

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
"@radix-ui/react-context-menu": "^1.0.0",
156156
"@radix-ui/react-dialog": "^1.0.0",
157157
"@radix-ui/react-dropdown-menu": "^1.0.0",
158+
"@radix-ui/react-hover-card": "^1.0.0",
158159
"@radix-ui/react-label": "^1.0.0",
159160
"@radix-ui/react-navigation-menu": "^1.0.0",
160161
"@radix-ui/react-popover": "^1.0.0",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Meta, Story } from '@storybook/react'
2+
import React, { ComponentProps } from 'react'
3+
import { HoverCard, HoverCardContent, HoverCardTrigger } from '.'
4+
import { Card, CardBody, CardHeading, CardLeadIn } from '../Card'
5+
import { Link } from '../Link'
6+
import { Text } from '../Text'
7+
import image from './committed_io.png'
8+
9+
export default {
10+
title: 'Components/HoverCard',
11+
component: HoverCard,
12+
subcomponents: {
13+
HoverCardTrigger,
14+
HoverCardContent,
15+
},
16+
} as Meta
17+
18+
export const Default: Story<ComponentProps<typeof HoverCard>> = (args) => (
19+
<HoverCard {...args}>
20+
<HoverCardTrigger>
21+
<Link href="https://committed.io" target="_blank">
22+
Committed Website
23+
</Link>
24+
</HoverCardTrigger>
25+
<HoverCardContent>
26+
<img src={image} />
27+
</HoverCardContent>
28+
</HoverCard>
29+
)
30+
31+
/**
32+
* The HoverCard content can be a preview like the main example or show further information.
33+
*/
34+
export const Sticky: Story = () => (
35+
<HoverCard>
36+
<HoverCardTrigger>
37+
<Link href="https://committed.io" target="_blank">
38+
Committed Website
39+
</Link>
40+
</HoverCardTrigger>
41+
<HoverCardContent>
42+
<Card>
43+
<CardLeadIn>https://committed.io</CardLeadIn>
44+
<CardHeading as="h3">Committed Software</CardHeading>
45+
<CardBody>
46+
<Text>
47+
Bespoke software company specializing in integrating the best of
48+
breed open source technologies to allow rapid development and
49+
minimal duplication of effort.
50+
</Text>
51+
</CardBody>
52+
</Card>
53+
</HoverCardContent>
54+
</HoverCard>
55+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react'
2+
import { renderDark, renderLight } from 'test-utils'
3+
import { Card, CardBody, CardHeading, CardLeadIn } from '../Card'
4+
import { Link } from '../Link'
5+
import { Text } from '../Text'
6+
import { HoverCard, HoverCardContent, HoverCardTrigger } from './HoverCard'
7+
8+
const Default = () => (
9+
<HoverCard>
10+
<HoverCardTrigger>
11+
<Link href="https://committed.io" target="_blank">
12+
Committed Website
13+
</Link>
14+
</HoverCardTrigger>
15+
<HoverCardContent>
16+
<Card>
17+
<CardLeadIn>https://committed.io</CardLeadIn>
18+
<CardHeading as="h3">Committed Software</CardHeading>
19+
<CardBody>
20+
<Text>
21+
Bespoke software company specializing in integrating the best of
22+
breed open source technologies to allow rapid development and
23+
minimal duplication of effort.
24+
</Text>
25+
</CardBody>
26+
</Card>
27+
</HoverCardContent>
28+
</HoverCard>
29+
)
30+
31+
it('renders light without error', () => {
32+
const { asFragment } = renderLight(<Default />)
33+
expect(asFragment()).toBeDefined()
34+
})
35+
36+
it('renders dark without error', () => {
37+
const { asFragment } = renderDark(<Default />)
38+
expect(asFragment()).toBeDefined()
39+
})
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
Arrow,
3+
Content,
4+
Portal,
5+
Root,
6+
Trigger,
7+
} from '@radix-ui/react-hover-card'
8+
import React, { ComponentProps, ElementRef, FC, forwardRef } from 'react'
9+
import type { CSSProps } from '../../stitches.config'
10+
import { styled } from '../../stitches.config'
11+
import { ConditionalWrapper } from '../../utils'
12+
import { overlayAnimationStyles } from '../Overlay'
13+
import { paperStyles } from '../Paper'
14+
15+
const StyledContent = styled(
16+
Content,
17+
paperStyles,
18+
{
19+
borderRadius: '$default',
20+
21+
width: '$11',
22+
23+
'@md': { width: '$12' },
24+
'@lg': { width: '$13' },
25+
26+
display: 'flex',
27+
flexDirection: 'column',
28+
gap: '$2',
29+
30+
'&::before': {
31+
boxShadow: 'none',
32+
},
33+
filter:
34+
'drop-shadow(0px 3px 2.5px rgba(0,0,0,0.2)) drop-shadow(0px 6px 5px rgba(0,0,0,0.14)) drop-shadow(0px 1px 9px rgba(0,0,0,0.12))',
35+
},
36+
overlayAnimationStyles
37+
)
38+
39+
const StyledArrow = styled(Arrow, {
40+
fill: '$paper',
41+
})
42+
43+
type HoverCardContentProps = CSSProps &
44+
ComponentProps<typeof Content> & {
45+
/** By default, portals your content parts into the body, set false to add at dom location. */
46+
portalled?: boolean
47+
/** Specify a container element to portal the content into. */
48+
container?: ComponentProps<typeof Portal>['container']
49+
}
50+
51+
export const HoverCardContent = forwardRef<
52+
ElementRef<typeof StyledContent>,
53+
HoverCardContentProps
54+
>(({ portalled = true, container, children, ...props }, forwardedRef) => (
55+
<ConditionalWrapper
56+
condition={portalled}
57+
wrapper={(child) => <Portal container={container}>{child}</Portal>}
58+
>
59+
<StyledContent {...props} ref={forwardedRef}>
60+
<StyledArrow offset={-1} />
61+
{children}
62+
</StyledContent>
63+
</ConditionalWrapper>
64+
))
65+
HoverCardContent.toString = () => `.${StyledContent.className}`
66+
67+
const HOVER_CARD_TRIGGER_CLASS_NAME = 'c-hover-card-trigger'
68+
69+
export const HoverCardTrigger = forwardRef<
70+
ElementRef<typeof Trigger>,
71+
Omit<ComponentProps<typeof Trigger>, 'asChild'>
72+
>(({ children, ...props }, forwardedRef) => (
73+
<Trigger
74+
className={HOVER_CARD_TRIGGER_CLASS_NAME}
75+
asChild
76+
{...props}
77+
ref={forwardedRef}
78+
>
79+
{children}
80+
</Trigger>
81+
))
82+
HoverCardTrigger.toString = () => `.${HOVER_CARD_TRIGGER_CLASS_NAME}`
83+
84+
/**
85+
*
86+
* HoverCards display extra information when users hover over a link.
87+
* Intended for sighted users to preview or see additional information.
88+
*
89+
* Built using [Radix HoverCard](https://radix-ui.com/primitives/docs/components/hover-card)
90+
*/
91+
export const HoverCard: FC<React.ComponentProps<typeof Root>> = Root
150 KB
Loading

src/components/HoverCard/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './HoverCard'

src/components/Link/Link.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ export function isExternalUrl(url: string | undefined): boolean {
2727
type InternalLinkProps = React.ComponentPropsWithRef<typeof DEFAULT_TAG> &
2828
AsChildProps
2929

30-
const InternalLink: React.FC<InternalLinkProps> = ({ asChild, ...props }) => {
31-
const Comp = asChild ? Slot : DEFAULT_TAG
32-
return <Comp {...props} />
33-
}
30+
const InternalLink: React.FC<InternalLinkProps> = forwardRef(
31+
({ asChild, ...props }, forwardedRef) => {
32+
const Comp = asChild ? Slot : DEFAULT_TAG
33+
return <Comp {...props} ref={forwardedRef} />
34+
}
35+
)
3436

3537
/**
3638
* Used to provide link props derived from the href

src/components/Popover/Popover.stories.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mdiAlertCircleOutline } from '@mdi/js'
22
import { Meta, Story } from '@storybook/react'
3-
import React from 'react'
3+
import React, { ComponentProps } from 'react'
44
import {
55
Popover,
66
PopoverAnchor,
@@ -21,8 +21,8 @@ export default {
2121
},
2222
} as Meta
2323

24-
export const Default: Story = (args) => (
25-
<Popover>
24+
export const Default: Story<ComponentProps<typeof Popover>> = (args) => (
25+
<Popover {...args}>
2626
<PopoverTrigger>
2727
<Button>Trigger</Button>
2828
</PopoverTrigger>

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export * from './FormControl'
2222
export * from './Grid'
2323
export * from './Heading'
2424
export * from './Hidden'
25+
export * from './HoverCard'
2526
export * from './IconButton'
2627
export * from './Icons'
2728
export * from './Input'

0 commit comments

Comments
 (0)