@snapdragonsnursery/react-components 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// FloatingActionButton.jsx
|
|
2
|
+
// ---------------------------------
|
|
3
|
+
// A shadcn-styled Floating Action Button (FAB).
|
|
4
|
+
// - Single fluid button that contains both icon and (optional) label
|
|
5
|
+
// - Label reveal is controlled by showLabel: 'hover' | 'always' | 'never'
|
|
6
|
+
// - Subtle icon and padding animation on hover when showLabel='hover'
|
|
7
|
+
// - Supports fixed positioning to screen corners
|
|
8
|
+
//
|
|
9
|
+
// Example usage:
|
|
10
|
+
//
|
|
11
|
+
// import { FloatingActionButton } from "@snapdragonsnursery/react-components";
|
|
12
|
+
// import { Plus } from "lucide-react";
|
|
13
|
+
//
|
|
14
|
+
// export default function Example() {
|
|
15
|
+
// return (
|
|
16
|
+
// <FloatingActionButton
|
|
17
|
+
// icon={Plus}
|
|
18
|
+
// label="Create"
|
|
19
|
+
// onClick={() => console.log("Create clicked")}
|
|
20
|
+
// showLabel="hover"
|
|
21
|
+
// position="bottom-right"
|
|
22
|
+
// />
|
|
23
|
+
// );
|
|
24
|
+
// }
|
|
25
|
+
//
|
|
26
|
+
// Notes:
|
|
27
|
+
// - Provide ariaLabel if label is omitted.
|
|
28
|
+
// - Uses Tailwind CSS + shadcn design tokens (bg-primary, text-primary-foreground, etc.).
|
|
29
|
+
|
|
30
|
+
import * as React from "react";
|
|
31
|
+
import { cn } from "../../lib/utils";
|
|
32
|
+
import { Button } from "./button.jsx";
|
|
33
|
+
|
|
34
|
+
const POSITION_CLASSES = {
|
|
35
|
+
"bottom-right": "fixed bottom-5 right-5",
|
|
36
|
+
"bottom-left": "fixed bottom-5 left-5",
|
|
37
|
+
"top-right": "fixed top-5 right-5",
|
|
38
|
+
"top-left": "fixed top-5 left-5",
|
|
39
|
+
none: "",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function FloatingActionButton({
|
|
43
|
+
icon: Icon,
|
|
44
|
+
label = "",
|
|
45
|
+
onClick,
|
|
46
|
+
showLabel,
|
|
47
|
+
// Deprecated: expandOnHover kept for backwards compatibility. If provided and showLabel is undefined,
|
|
48
|
+
// we map: true -> 'hover', false -> 'never'.
|
|
49
|
+
expandOnHover,
|
|
50
|
+
position = "bottom-right",
|
|
51
|
+
className,
|
|
52
|
+
disabled = false,
|
|
53
|
+
ariaLabel,
|
|
54
|
+
positionClassName,
|
|
55
|
+
colorClassName,
|
|
56
|
+
...props
|
|
57
|
+
}) {
|
|
58
|
+
const resolvedShowLabel = React.useMemo(() => {
|
|
59
|
+
if (showLabel) return showLabel;
|
|
60
|
+
if (typeof expandOnHover === "boolean") {
|
|
61
|
+
return expandOnHover ? "hover" : "never";
|
|
62
|
+
}
|
|
63
|
+
return label ? "hover" : "never";
|
|
64
|
+
}, [showLabel, expandOnHover, label]);
|
|
65
|
+
|
|
66
|
+
const containerClasses = cn(
|
|
67
|
+
positionClassName || (POSITION_CLASSES[position ?? "none"] ?? POSITION_CLASSES.none),
|
|
68
|
+
className
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const baseButton = cn(
|
|
72
|
+
"rounded-full shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 leading-none",
|
|
73
|
+
colorClassName || "bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const shapeClasses =
|
|
77
|
+
resolvedShowLabel === "never"
|
|
78
|
+
? "h-14 w-14 inline-flex items-center justify-center gap-0 p-0"
|
|
79
|
+
: resolvedShowLabel === "always"
|
|
80
|
+
? "h-14 inline-flex items-center justify-center gap-0 px-4"
|
|
81
|
+
: // hover mode
|
|
82
|
+
"group h-14 inline-flex items-center gap-0 justify-center min-w-[3.5rem] px-0 transition-all duration-300 ease-out hover:px-4";
|
|
83
|
+
|
|
84
|
+
const buttonClasses = cn(baseButton, shapeClasses);
|
|
85
|
+
|
|
86
|
+
const iconClasses = cn(
|
|
87
|
+
"h-6 w-6 block shrink-0 pointer-events-none transition-transform duration-200 ease-out",
|
|
88
|
+
resolvedShowLabel === "hover" ? "group-hover:scale-110 group-hover:rotate-3" : ""
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const labelClasses = cn(
|
|
92
|
+
"leading-none whitespace-nowrap",
|
|
93
|
+
resolvedShowLabel === "always"
|
|
94
|
+
? "ml-2"
|
|
95
|
+
: resolvedShowLabel === "hover"
|
|
96
|
+
? "max-w-0 overflow-hidden opacity-0 scale-95 group-hover:max-w-[14rem] group-hover:opacity-100 group-hover:scale-100 group-hover:ml-2 transition-all duration-300 ease-out"
|
|
97
|
+
: "sr-only"
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={containerClasses} data-testid="fab-container">
|
|
102
|
+
<Button
|
|
103
|
+
type="button"
|
|
104
|
+
data-slot="floating-action-button"
|
|
105
|
+
className={buttonClasses}
|
|
106
|
+
size={resolvedShowLabel === "never" ? "icon" : undefined}
|
|
107
|
+
aria-label={ariaLabel || label || "Action"}
|
|
108
|
+
title={ariaLabel || label || "Action"}
|
|
109
|
+
onClick={onClick}
|
|
110
|
+
disabled={disabled}
|
|
111
|
+
{...props}
|
|
112
|
+
>
|
|
113
|
+
{Icon ? <Icon className={iconClasses} aria-hidden="true" /> : null}
|
|
114
|
+
{label ? (
|
|
115
|
+
<span className={labelClasses} data-testid={resolvedShowLabel !== "never" ? "fab-label" : undefined}>
|
|
116
|
+
{label}
|
|
117
|
+
</span>
|
|
118
|
+
) : null}
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default FloatingActionButton;
|
|
125
|
+
|
|
126
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Basic tests for FloatingActionButton
|
|
2
|
+
// - Renders icon and label behaviour depending on expandOnHover
|
|
3
|
+
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
6
|
+
import FloatingActionButton from './floating-action-button'
|
|
7
|
+
|
|
8
|
+
function DummyIcon(props) {
|
|
9
|
+
return <svg aria-hidden="true" data-testid="dummy-icon" {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('FloatingActionButton (library)', () => {
|
|
13
|
+
it('renders with icon', () => {
|
|
14
|
+
render(<FloatingActionButton icon={DummyIcon} ariaLabel="Action" />)
|
|
15
|
+
expect(screen.getByTestId('dummy-icon')).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('expands label on hover when showLabel="hover"', () => {
|
|
19
|
+
render(<FloatingActionButton icon={DummyIcon} label="Create" showLabel="hover" />)
|
|
20
|
+
// Label exists in DOM (hidden initially via CSS)
|
|
21
|
+
const label = screen.getByTestId('fab-label')
|
|
22
|
+
expect(label).toBeInTheDocument()
|
|
23
|
+
// Simulate hover on container
|
|
24
|
+
const container = screen.getByTestId('fab-container')
|
|
25
|
+
fireEvent.mouseOver(container)
|
|
26
|
+
// Ensure label text is present
|
|
27
|
+
expect(label).toHaveTextContent('Create')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('hides label visually when showLabel="never"', () => {
|
|
31
|
+
render(<FloatingActionButton icon={DummyIcon} label="Search" showLabel="never" ariaLabel="Search" />)
|
|
32
|
+
// No test id for label since not rendered
|
|
33
|
+
const button = screen.getByRole('button', { name: /search/i })
|
|
34
|
+
expect(button).toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('calls onClick when clicked', () => {
|
|
38
|
+
const onClick = jest.fn()
|
|
39
|
+
render(<FloatingActionButton icon={DummyIcon} ariaLabel="Action" onClick={onClick} />)
|
|
40
|
+
const button = screen.getByRole('button', { name: /action/i })
|
|
41
|
+
fireEvent.click(button)
|
|
42
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
|
package/src/index.d.ts
CHANGED
|
@@ -116,6 +116,23 @@ export const BreadcrumbPage: React.ComponentType<any>
|
|
|
116
116
|
export const BreadcrumbSeparator: React.ComponentType<any>
|
|
117
117
|
export const BreadcrumbEllipsis: React.ComponentType<any>
|
|
118
118
|
|
|
119
|
+
// Floating Action Button
|
|
120
|
+
export interface FloatingActionButtonProps {
|
|
121
|
+
icon?: React.ComponentType<any>
|
|
122
|
+
label?: string
|
|
123
|
+
onClick?: () => void
|
|
124
|
+
showLabel?: 'hover' | 'always' | 'never'
|
|
125
|
+
// @deprecated use showLabel instead. If provided: true => 'hover', false => 'never'
|
|
126
|
+
expandOnHover?: boolean
|
|
127
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'none'
|
|
128
|
+
className?: string
|
|
129
|
+
positionClassName?: string
|
|
130
|
+
colorClassName?: string
|
|
131
|
+
disabled?: boolean
|
|
132
|
+
ariaLabel?: string
|
|
133
|
+
}
|
|
134
|
+
export const FloatingActionButton: React.ComponentType<FloatingActionButtonProps>
|
|
135
|
+
|
|
119
136
|
// Switchers
|
|
120
137
|
export interface SwitcherItem {
|
|
121
138
|
id?: string | number
|