-
-
Notifications
You must be signed in to change notification settings - Fork 501
feat(alert): add dismiss transition, timing and duration props #1683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| "use client"; | ||
|
|
||
| import { Alert } from "flowbite-react"; | ||
| import { useState } from "react"; | ||
| import type { CodeData } from "~/components/code-demo"; | ||
|
|
||
| const code = ` | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { Alert } from "flowbite-react"; | ||
|
|
||
| export function Component() { | ||
| const [isDismissed, setIsDismissed] = useState(false); | ||
|
|
||
| if (isDismissed) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Alert | ||
| color="success" | ||
| duration={1000} | ||
| onDismiss={() => setIsDismissed(true)} | ||
| timing="ease-in-out" | ||
| transition="transition-opacity" | ||
| > | ||
| <span className="font-medium">Success alert!</span> Dismiss me to see the fade transition. | ||
| </Alert> | ||
| ); | ||
| } | ||
| `; | ||
|
|
||
| export function Component() { | ||
| const [isDismissed, setIsDismissed] = useState(false); | ||
|
|
||
| if (isDismissed) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Alert | ||
| color="success" | ||
| duration={1000} | ||
| onDismiss={() => setIsDismissed(true)} | ||
| timing="ease-in-out" | ||
| transition="transition-opacity" | ||
| > | ||
| <span className="font-medium">Success alert!</span> Dismiss me to see the fade transition. | ||
| </Alert> | ||
| ); | ||
| } | ||
|
|
||
| export const transition: CodeData = { | ||
| type: "single", | ||
| code: { | ||
| fileName: "index", | ||
| language: "tsx", | ||
| code, | ||
| }, | ||
| githubSlug: "alert/alert.transition.tsx", | ||
| component: <Component />, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| "use client"; | ||
|
|
||
| import { forwardRef, type ComponentProps, type FC, type ReactNode } from "react"; | ||
| import { forwardRef, useEffect, useState, type ComponentProps, type FC, type ReactNode } from "react"; | ||
| import { get } from "../../helpers/get"; | ||
| import { resolveProps } from "../../helpers/resolve-props"; | ||
| import { useResolveTheme } from "../../helpers/resolve-theme"; | ||
|
|
@@ -17,6 +17,7 @@ | |
| color: FlowbiteColors; | ||
| icon: string; | ||
| rounded: string; | ||
| transition?: Partial<AlertTransitionTheme>; | ||
| wrapper: string; | ||
| } | ||
|
|
||
|
|
@@ -26,12 +27,21 @@ | |
| icon: string; | ||
| } | ||
|
|
||
| export interface AlertTransitionTheme { | ||
| base: string; | ||
| duration: string; | ||
| timing: string; | ||
| } | ||
|
|
||
| export interface AlertProps extends Omit<ComponentProps<"div">, "color">, ThemingProps<AlertTheme> { | ||
| additionalContent?: ReactNode; | ||
| color?: DynamicStringEnumKeysOf<FlowbiteColors>; | ||
| duration?: number; | ||
| icon?: FC<ComponentProps<"svg">>; | ||
| onDismiss?: ComponentProps<"button">["onClick"]; | ||
| rounded?: boolean; | ||
| timing?: string; | ||
| transition?: string; | ||
| withBorderAccent?: boolean; | ||
| } | ||
|
|
||
|
|
@@ -48,13 +58,36 @@ | |
| children, | ||
| className, | ||
| color = "info", | ||
| duration = 300, | ||
| icon: Icon, | ||
| onDismiss, | ||
| rounded = true, | ||
| timing = "ease-out", | ||
| transition = "transition-opacity", | ||
| withBorderAccent, | ||
| ...restProps | ||
| } = resolveProps(props, provider.props?.alert); | ||
|
|
||
| const [isDismissing, setIsDismissing] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| if (!isDismissing) { | ||
| return; | ||
| } | ||
| const timeoutId = setTimeout(() => { | ||
| onDismiss?.(new MouseEvent("click") as unknown as React.MouseEvent<HTMLButtonElement>); | ||
| }, duration); | ||
| return () => clearTimeout(timeoutId); | ||
| }, [isDismissing, duration, onDismiss]); | ||
|
Comment on lines
+73
to
+81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate Alert files =="
fd -a 'Alert\.(tsx|ts|jsx|js)$' . | sed 's#^\./##'
echo "== target file outline/size =="
wc -l packages/ui/src/components/Alert/Alert.tsx 2>/dev/null || true
ast-grep outline packages/ui/src/components/Alert/Alert.tsx 2>/dev/null || true
echo "== target file relevant contents =="
cat -n packages/ui/src/components/Alert/Alert.tsx | sed -n '1,140p'
echo "== usages of Alert =="
rg -n "<Alert|Alert " packages -g '*.{ts,tsx,js,jsx}' | head -120Repository: themesberg/flowbite-react Length of output: 8034 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Alert tests relevant contents =="
cat -n packages/ui/src/components/Alert/Alert.test.tsx | sed -n '1,260p'
echo "== package config for jsdom timers/react =="
for f in package.json packages/ui/package.json; do
if [ -f "$f" ]; then
echo "-- $f --"
node -e "const p=require('./$f'); console.log(JSON.stringify({scripts:p.scripts,dependencies:p.dependencies,devDependencies:p.devDependencies}, null, 2))" | sed -n '1,220p'
fi
done
echo "== focused searches for tests around dismissing/transition/inline callbacks =="
rg -n "onDismiss|dismiss|transition|duration|setInterval|setTimeout|re-render|rerender|render\(\\\(" packages/ui/src/components/Alert packages/ui/src/components -g '!**/*.js' -g '!**/*.map' | sed -n '1,240p'Repository: themesberg/flowbite-react Length of output: 13891 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== vitest config =="
fd -a '^(vitest|vite|rollup|rslib)\.(config|configuration)\.(ts|js|mjs|cjs)$' packages/ui | sort | while read -r f; do
echo "-- $f --"
cat -n "$f" | sed -n '1,220p'
done
echo "== focused Alert searches =="
rg -n "onDismiss|dismiss|transition|duration|setInterval|setTimeout|re-render|rerender|render\\(" packages/ui/src/components/Alert packages/ui/src/components -g '!**/*.js' -g '!**/*.map' | sed -n '1,260p'
echo "== React dependency versions =="
node - <<'JS'
const fs = require('fs');
for (const f of ['package.json','packages/ui/package.json']) {
if (!fs.existsSync(f)) continue;
const p = JSON.parse(fs.readFileSync(f,'utf8'));
const deps = {...(p.dependencies||{}), ...(p.devDependencies||{})};
console.log(f, deps.react, deps['react-dom']);
}
JSRepository: themesberg/flowbite-react Length of output: 31536 🌐 Web query:
💡 Result: In React, the useEffect cleanup function plays a critical role in managing side effects that require stopping, cancelling, or undoing operations [1][2]. When using setTimeout (or similar async operations like subscriptions or intervals) within useEffect, the cleanup function is essential to ensure that pending operations from a previous render do not interfere with subsequent ones [2][3]. React guarantees the following behavior regarding cleanup execution [2][4]: 1. Timing of Cleanup: React always executes the cleanup function from the previous render before running the effect function for the next render [2][3]. If a component unmounts, the cleanup function is executed one final time [1][2]. 2. Dependency Changes: If you provide a dependency array, React only re-synchronizes the effect (running the cleanup, then the new effect) when one of the values in that array changes [5][4]. If the dependency array is empty ([]), the effect runs only once after the initial mount, and the cleanup runs only when the component unmounts [2][4]. 3. Interaction with setTimeout: When you use setTimeout within an effect, returning clearTimeout(timeoutId) in the cleanup function is the standard pattern to prevent the timeout callback from executing if the component updates or unmounts before the timer expires [2]. Because React cleans up the previous render's effect before starting the next one, this mechanism ensures that at most one timeout is active for that effect at any given time [2]. If you find that your effect is running too often—such as on every render—it is typically because the dependency array is missing or includes values that change on every render [5][6]. You can avoid this by either ensuring the dependencies are stable or by using functional state updates (e.g., setCount(c => c + 1)) to remove the state variable from the effect's dependency list entirely [7][8]. In summary, the cleanup function does not run on every render by default; it only runs when the dependencies change or the component unmounts [2][6]. By cleaning up the previous timeout before establishing a new one, you maintain predictable behavior and avoid race conditions or memory leaks [2][3]. Citations:
Keep the dismissal deadline fixed at click time. The dismiss effect depends on 🤖 Prompt for AI Agents |
||
|
|
||
| const handleDismissClick: ComponentProps<"button">["onClick"] = (event) => { | ||
| if (transition || duration > 0) { | ||
| setIsDismissing(true); | ||
| return; | ||
| } | ||
| onDismiss?.(event); | ||
|
Comment on lines
+99
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Make
Use 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| ref={ref} | ||
|
|
@@ -63,6 +96,16 @@ | |
| theme.color[color], | ||
| rounded && theme.rounded, | ||
| withBorderAccent && theme.borderAccent, | ||
| isDismissing && | ||
| twMerge( | ||
| transition, | ||
| `duration-${duration}`, | ||
| timing, | ||
|
Comment on lines
+117
to
+120
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)(Alert\.tsx|tailwind|postcss|package\.json|\.lint|eslint|stylelint|.*\.config\.(js|ts|mjs|cjs))$|Alert' || true
echo "== target snippet =="
if [ -f packages/ui/src/components/Alert/Alert.tsx ]; then
nl -ba packages/ui/src/components/Alert/Alert.tsx | sed -n '1,180p'
fi
echo "== duration/static Tailwind config candidates =="
rg -n "duration-|transitionDuration| safelist|content|preset|plugins|tailwindcss" -S . -g '!node_modules' -g '!dist' -g '!build' -g '!coverage' | head -200
echo "== lint configs mentioning duration =="
rg -n "duration|transition|valid|invalid" -S . -g '*lnt*' -g '!node_modules' -g '!dist' -g '!build' | head -200 || trueRepository: themesberg/flowbite-react Length of output: 1043 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Alert.tsx relevant lines =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ui/src/components/Alert/Alert.tsx | sed -n '105,130p'
echo "== Alert.tsx duration prop/types =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ui/src/components/Alert/Alert.tsx | sed -n '1,120p'
echo "== tailwind config duration/content =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ui/tailwind.config.cjs
echo "---"
awk '{printf "%6d\t%s\n", NR, $0}' apps/web/tailwind.config.cjs | sed -n '1,260p'
echo "== runtime duration values in Alert.tsx/usages =="
rg -n "duration|transitionDuration|twMerge\\(|duration-" packages/ui/src/components/Alert packages/ui/tailwind.config.cjs -S
echo "== lint rules mentioning invalid transition/duration =="
rg -n "transitionDuration|duration-|valid|Invalid|invalid|CSS variable|arbitrary" -S . -g '!node_modules' | head -200Repository: themesberg/flowbite-react Length of output: 40429 🌐 Web query:
💡 Result: To apply an arbitrary transition duration in Tailwind CSS, use the square bracket syntax directly within the class name [1][2]. For example: If you are using a CSS variable, you can use the same syntax [1]: Alternatively, Tailwind provides a shorthand syntax for CSS variables using parentheses, which automatically wraps your value in the var function [1]: This functionality allows you to generate one-off transition durations on the fly without needing to extend your theme configuration [2][3]. As with all arbitrary values, this syntax integrates seamlessly with responsive modifiers (e.g., md:duration-[500ms]) and interactive states (e.g., hover:duration-[1s]) [1][4][3].Citations:
Use arbitrary transition duration values.
🧰 Tools🪛 GitHub Check: 🕵 Lint[warning] 117-117: 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| theme.transition?.base, | ||
| theme.transition?.duration, | ||
| theme.transition?.timing, | ||
|
Comment on lines
+121
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Keep the callback delay aligned with themed duration. A theme can override the rendered class to 🤖 Prompt for AI Agents |
||
| "opacity-0", | ||
| ), | ||
| className, | ||
| )} | ||
| role="alert" | ||
|
|
@@ -75,7 +118,7 @@ | |
| <button | ||
| aria-label="Dismiss" | ||
| className={twMerge(theme.closeButton.base, theme.closeButton.color[color])} | ||
| onClick={onDismiss} | ||
| onClick={handleDismissClick} | ||
| type="button" | ||
| > | ||
| <XIcon aria-hidden className={theme.closeButton.icon} /> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: themesberg/flowbite-react
Length of output: 8677
🏁 Script executed:
Repository: themesberg/flowbite-react
Length of output: 4767
Pass the original dismiss click event to the deferred
onDismisscallback.The timed callback currently receives a fabricated native
MouseEvent, so callers won’t get a React synthetic click event with the correct target/currentTarget. Keep the event fromhandleDismissClickand pass it through when the timeout runs.🤖 Prompt for AI Agents