Skip to content

Commit 8c25246

Browse files
authored
feat: added floating badge functionality (#81)
* feat: added badge attached functionality * chore: improve positioning & badge * chore: added attached parent styles in theme file * chore: improved positioning * chore: added support for empty dot in attached mode * chore: removed empty string fallback * chore: rename isAttached to floating * chore: jsdoc comments * refactor: added merge ref hook & review updates
1 parent f5382b7 commit 8c25246

File tree

4 files changed

+141
-7
lines changed

4 files changed

+141
-7
lines changed

src/badge/Badge.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { cx } from "@renderlesskit/react";
44
import { useTheme } from "../theme";
55
import { Box, BoxProps } from "../box";
66
import { forwardRefWithAs } from "../utils/types";
7+
import { useMergeRefs } from "../hooks/useMergeRefs";
78

89
export type BadgeProps = BoxProps & {
910
/**
@@ -18,21 +19,64 @@ export type BadgeProps = BoxProps & {
1819
* @default "primary"
1920
*/
2021
variant?: keyof Renderlesskit.GetThemeValue<"badge", "variant">;
22+
/**
23+
* floats the badge on parent element's corners
24+
*
25+
* @default false
26+
*/
27+
floating?: boolean;
28+
/**
29+
* floating position
30+
*
31+
* @default "top-right"
32+
*/
33+
position?: keyof Renderlesskit.GetThemeValue<"badge", "position">;
2134
};
2235

2336
export const Badge = forwardRefWithAs<BadgeProps, HTMLSpanElement, "span">(
2437
(props, ref) => {
25-
const { variant = "primary", size = "md", className, ...rest } = props;
38+
const htmlref = React.useRef<HTMLSpanElement>();
39+
const {
40+
position = "top-right",
41+
variant = "primary",
42+
size = "md",
43+
floating = false,
44+
className,
45+
...rest
46+
} = props;
2647

2748
const theme = useTheme();
2849
const badgeStyles = cx(
2950
theme.badge.base,
3051
theme.badge.size[size],
3152
theme.badge.variant[variant],
53+
floating
54+
? cx(
55+
theme.badge.attached,
56+
theme.badge.position[position],
57+
!props.children ? theme.badge.dot[size] : "",
58+
)
59+
: "",
3260
className,
3361
);
3462

35-
return <Box as="span" ref={ref} className={badgeStyles} {...rest} />;
63+
React.useEffect(() => {
64+
if (!floating) return;
65+
if (htmlref && htmlref.current) {
66+
const parentElement = htmlref.current?.parentElement;
67+
parentElement!.classList.add(theme.badge.attachedParent);
68+
}
69+
// eslint-disable-next-line react-hooks/exhaustive-deps
70+
}, []);
71+
72+
return (
73+
<Box
74+
as="span"
75+
ref={useMergeRefs(htmlref, ref)}
76+
className={badgeStyles}
77+
{...rest}
78+
/>
79+
);
3680
},
3781
);
3882

src/badge/stories/Badge.stories.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@ import React from "react";
22
import { Meta } from "@storybook/react/types-6-0";
33

44
import {
5-
createControls,
65
storyTemplate,
6+
createUnionControl,
77
} from "../../../.storybook/storybookUtils";
8+
9+
import { Button } from "../../button";
810
import { Badge, BadgeProps } from "../Badge";
911

1012
export default {
1113
title: "Badge",
1214
component: Badge,
13-
argTypes: createControls("badge", {
14-
unions: ["size", "variant"],
15-
}),
15+
argTypes: {
16+
size: createUnionControl(["sm", "md", "lg"]),
17+
variant: createUnionControl(["primary", "secondary", "outline", "ghosts"]),
18+
position: createUnionControl([
19+
"top-left",
20+
"top-right",
21+
"bottom-left",
22+
"bottom-right",
23+
]),
24+
floating: { control: "boolean" },
25+
},
1626
} as Meta;
1727

1828
const base = storyTemplate<BadgeProps>(
@@ -35,3 +45,23 @@ export const Secondary = base({ variant: "secondary" });
3545
export const Outline = base({ variant: "outline" });
3646

3747
export const Ghost = base({ variant: "ghost" });
48+
49+
const floating = storyTemplate<BadgeProps & { badgeValue?: string }>(
50+
args => {
51+
return (
52+
<Button variant="outline">
53+
Hello world
54+
<Badge {...args}>{args.badgeValue}</Badge>
55+
</Button>
56+
);
57+
},
58+
{
59+
variant: "primary",
60+
position: "top-right",
61+
floating: true,
62+
size: "sm",
63+
badgeValue: "22",
64+
},
65+
);
66+
67+
export const Floating = floating({});

src/hooks/useMergeRefs.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// credits to: https://github.com/chakra-ui/chakra-ui/blob/main/packages/hooks/src/use-merge-refs.ts
2+
/* eslint-disable react-hooks/exhaustive-deps */
3+
import * as React from "react";
4+
5+
type ReactRef<T> = React.Ref<T> | React.MutableRefObject<T>;
6+
7+
export function assignRef<T = any>(ref: ReactRef<T> | undefined, value: T) {
8+
if (ref == null) return;
9+
10+
if (typeof ref === "function") {
11+
ref(value);
12+
return;
13+
}
14+
15+
try {
16+
// @ts-ignore
17+
ref.current = value;
18+
} catch (error) {
19+
throw new Error(`Cannot assign value '${value}' to ref '${ref}'`);
20+
}
21+
}
22+
23+
/**
24+
* React hook that merges react refs into a single memoized function
25+
* @example
26+
* import React from "react";
27+
* import { useMergeRefs } from `@chakra-ui/hooks`;
28+
*
29+
* const Component = React.forwardRef((props, ref) => {
30+
* const internalRef = React.useRef();
31+
* return <div {...props} ref={useMergeRefs(internalRef, ref)} />;
32+
* });
33+
*/
34+
export function useMergeRefs<T>(...refs: (ReactRef<T> | undefined)[]) {
35+
return React.useMemo(() => {
36+
if (refs.every(ref => ref == null)) {
37+
return null;
38+
}
39+
return (node: T) => {
40+
refs.forEach(ref => {
41+
if (ref) assignRef(ref, node);
42+
});
43+
};
44+
}, refs);
45+
}

src/theme/defaultTheme/badge.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
export const badge = {
22
base:
33
"lib:inline-block lib:whitespace-nowrap lib:align-middle lib:rounded-full lib:transition-all",
4+
attached: "absolute",
5+
attachedParent: "relative",
6+
position: {
7+
"top-right": "-top-0 -right-0 transform translate-x-1/2 -translate-y-1/2",
8+
"top-left": "-top-0 -left-0 transform -translate-x-1/2 -translate-y-1/2",
9+
"bottom-right":
10+
"-bottom-0 -right-0 transform translate-x-1/2 translate-y-1/2",
11+
"bottom-left":
12+
"-bottom-0 -left-0 transform -translate-x-1/2 translate-y-1/2",
13+
},
14+
dot: {
15+
sm: "py-1.5",
16+
md: "py-2",
17+
lg: "py-2",
18+
},
419
size: {
520
sm: "lib:px-1.5 lib:text-xs lib:font-medium",
621
md: "lib:px-2 lib:text-sm lib:font-medium",
@@ -9,7 +24,7 @@ export const badge = {
924
variant: {
1025
primary: "lib:bg-gray-800 lib:text-white",
1126
secondary: "lib:bg-gray-100 lib:text-gray-800",
12-
outline: "lib:text-gray-800 lib:border lib:border-gray-300",
27+
outline: "lib:text-gray-800 lib:border lib:border-gray-300 lib:bg-white",
1328
ghost: "lib:text-gray-800 lib:hover:bg-gray-100",
1429
},
1530
};

0 commit comments

Comments
 (0)