From 3d993ab3d2c69cd8ce55298adfefdfb3ae25c1a7 Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Tue, 18 Feb 2025 00:33:28 +0530 Subject: [PATCH 01/41] added upload preview in input field --- src/components/input/input.stories.tsx | 8 ++ src/components/input/input.tsx | 138 ++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/components/input/input.stories.tsx b/src/components/input/input.stories.tsx index 4fee7533..2962b119 100644 --- a/src/components/input/input.stories.tsx +++ b/src/components/input/input.stories.tsx @@ -89,3 +89,11 @@ Required.args = { label: 'Required Input', defaultValue: 'Required Input', }; + +export const FileInputWithPreview = Template.bind( {} ); +FileInputWithPreview.args = { + type: 'file', + size: 'md', + disabled: false, + error: false, +}; diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 9de9b45d..af099758 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -8,8 +8,8 @@ import React, { LabelHTMLAttributes, } from 'react'; import { nanoid } from 'nanoid'; -import { cn } from '@/utilities/functions'; -import { Upload, X } from 'lucide-react'; +import { cn, formatFileSize } from '@/utilities/functions'; +import { Upload, X, File, ImageOff, Trash } from 'lucide-react'; import Label from '../label'; import { mergeRefs } from '@/components/toaster/utils'; @@ -58,8 +58,122 @@ export declare interface InputProps { /** Indicates whether the input is required. */ required?: boolean; + + /** Function called when file is removed */ + onFileRemove?: () => void; } +const commonFilePreviewClasses = { + sm: { + image: 'w-8 h-8', + name: 'text-xs', + fileIcon: 'h-8', + uploadText: 'text-xs', + }, + md: { + image: 'w-10 h-10', + name: 'text-sm', + fileIcon: 'h-10', + uploadText: 'text-xs', + }, + lg: { + image: 'w-10 h-10', + name: 'text-sm', + fileIcon: 'h-10', + uploadText: 'text-xs', + }, +}; + +const FilePreview = ( { + file, + onRemove, + error, + disabled, + size = 'sm', +}: { + file: File; + onRemove: () => void; + error?: boolean; + disabled?: boolean; + size?: 'sm' | 'md' | 'lg'; +} ) => { + const renderFileIcon = useMemo( () => { + return ( + + + + ); + }, [] ); + + return ( +
+
+ { file.type.startsWith( 'image/' ) ? ( +
+ { error ? ( + + ) : ( + Preview + ) } +
+ ) : ( + renderFileIcon + ) } + +
+ + { file.name } + + + { formatFileSize( file.size ) } + +
+ { ! disabled && ( + + ) } +
+
+ ); +}; + export const InputComponent = ( { id, @@ -75,6 +189,7 @@ export const InputComponent = ( prefix = null, suffix = null, label = '', + onFileRemove, ...props }: InputProps & Omit, 'size' | 'prefix'>, @@ -127,6 +242,13 @@ export const InputComponent = ( onChange( null ); }; + const selectedFileObject = useMemo( () => { + if ( ! inputRef.current?.files?.length ) { + return null; + } + return inputRef.current.files[ 0 ]; + }, [ selectedFile ] ); + const baseClasses = 'bg-field-secondary-background font-normal placeholder-text-tertiary text-text-primary w-full outline outline-1 outline-border-subtle border-none transition-[color,box-shadow,outline] duration-200'; const sizeClasses = { @@ -304,6 +426,18 @@ export const InputComponent = ( + { selectedFileObject && ( + { + handleReset(); + onFileRemove?.(); + } } + error={ error } + disabled={ disabled } + size={ size } + /> + ) } ); } From 5b28567f493f11c7a658bd1e12934284273114d1 Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Tue, 18 Feb 2025 10:19:01 +0530 Subject: [PATCH 02/41] fixes related to WP media uploader --- src/components/input/input.stories.tsx | 1 + src/components/input/input.tsx | 85 ++++++++++++++++++-------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/components/input/input.stories.tsx b/src/components/input/input.stories.tsx index 2962b119..23736789 100644 --- a/src/components/input/input.stories.tsx +++ b/src/components/input/input.stories.tsx @@ -96,4 +96,5 @@ FileInputWithPreview.args = { size: 'md', disabled: false, error: false, + variant: 'preview', }; diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index af099758..4d98296e 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -20,6 +20,9 @@ export declare interface InputProps { /** Specifies the type of the input element (e.g., text, file). */ type?: 'text' | 'password' | 'email' | 'file'; + /** Determines the input variant (default or file preview). */ + variant?: 'default' | 'preview'; + /** Initial value of the input element. */ defaultValue?: string; @@ -61,6 +64,14 @@ export declare interface InputProps { /** Function called when file is removed */ onFileRemove?: () => void; + + /** Data of the selected file */ + selectedFileData?: { + name: string; + url: string; + type: string; + size?: number; + }; } const commonFilePreviewClasses = { @@ -91,24 +102,22 @@ const FilePreview = ( { disabled, size = 'sm', }: { - file: File; + file: File | { name: string; url: string; type: string; size: number }; onRemove: () => void; error?: boolean; disabled?: boolean; size?: 'sm' | 'md' | 'lg'; } ) => { - const renderFileIcon = useMemo( () => { - return ( - - - - ); - }, [] ); + const renderFileIcon = () => ( + + + + ); return (
- { file.type.startsWith( 'image/' ) ? ( + { file.type.startsWith( 'image' ) ? (
) : ( Preview ) : ( - renderFileIcon + renderFileIcon() ) }
@@ -151,15 +164,17 @@ const FilePreview = ( { > { file.name } - - { formatFileSize( file.size ) } - + { file.size && file.size > 0 && ( + + { formatFileSize( file.size ) } + + ) }
{ ! disabled && (
- { selectedFileObject && ( + { selectedFileObject && + variant === 'preview' && + selectedFileObject.size > 0 && ( { From 910c26a4d8302a0be34c6657f4ed4715da485a2b Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Tue, 18 Feb 2025 12:46:24 +0530 Subject: [PATCH 03/41] updated changelog.txt --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 535edfba..b96e7f51 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,5 @@ +Version 1.4.2 - xxth February, 2025 +- New - Added a variant in Input component to show uploaded file preview. Version 1.4.2 - xxth February, 2025 - Improvement - Adjusted the font size of the help text in the Switch and Checkbox component. From e04977377ff3c74af895e0433096acf45b511741 Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Tue, 18 Feb 2025 16:00:04 +0530 Subject: [PATCH 04/41] Update changelog.txt Co-authored-by: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> --- changelog.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index b96e7f51..42db9a1c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,6 @@ -Version 1.4.2 - xxth February, 2025 -- New - Added a variant in Input component to show uploaded file preview. - Version 1.4.2 - xxth February, 2025 - Improvement - Adjusted the font size of the help text in the Switch and Checkbox component. +- New - Added a variant in Input component to show uploaded file preview. Version 1.4.1 - 10th February, 2025 - Improvement - Show active preset with background color in the DatePicker component. From 3bc72811358cf7dc051082ef4aab0cd7d8bdb022 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:28:16 +0600 Subject: [PATCH 05/41] Remove file preview from the input --- src/components/input/input.stories.tsx | 9 -- src/components/input/input.tsx | 177 +------------------------ 2 files changed, 6 insertions(+), 180 deletions(-) diff --git a/src/components/input/input.stories.tsx b/src/components/input/input.stories.tsx index 23736789..4fee7533 100644 --- a/src/components/input/input.stories.tsx +++ b/src/components/input/input.stories.tsx @@ -89,12 +89,3 @@ Required.args = { label: 'Required Input', defaultValue: 'Required Input', }; - -export const FileInputWithPreview = Template.bind( {} ); -FileInputWithPreview.args = { - type: 'file', - size: 'md', - disabled: false, - error: false, - variant: 'preview', -}; diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 4d98296e..7a04c9e2 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -8,8 +8,8 @@ import React, { LabelHTMLAttributes, } from 'react'; import { nanoid } from 'nanoid'; -import { cn, formatFileSize } from '@/utilities/functions'; -import { Upload, X, File, ImageOff, Trash } from 'lucide-react'; +import { cn } from '@/utilities/functions'; +import { Upload, X } from 'lucide-react'; import Label from '../label'; import { mergeRefs } from '@/components/toaster/utils'; @@ -20,9 +20,6 @@ export declare interface InputProps { /** Specifies the type of the input element (e.g., text, file). */ type?: 'text' | 'password' | 'email' | 'file'; - /** Determines the input variant (default or file preview). */ - variant?: 'default' | 'preview'; - /** Initial value of the input element. */ defaultValue?: string; @@ -39,7 +36,7 @@ export declare interface InputProps { disabled?: boolean; /** Function called when the input value changes. */ - onChange?: ( value: string | null ) => void; + onChange?: ( value: string | FileList | null ) => void; /** Indicates whether the input has an error state. */ error?: boolean; @@ -61,134 +58,8 @@ export declare interface InputProps { /** Indicates whether the input is required. */ required?: boolean; - - /** Function called when file is removed */ - onFileRemove?: () => void; - - /** Data of the selected file */ - selectedFileData?: { - name: string; - url: string; - type: string; - size?: number; - }; } -const commonFilePreviewClasses = { - sm: { - image: 'w-8 h-8', - name: 'text-xs', - fileIcon: 'h-8', - uploadText: 'text-xs', - }, - md: { - image: 'w-10 h-10', - name: 'text-sm', - fileIcon: 'h-10', - uploadText: 'text-xs', - }, - lg: { - image: 'w-10 h-10', - name: 'text-sm', - fileIcon: 'h-10', - uploadText: 'text-xs', - }, -}; - -const FilePreview = ( { - file, - onRemove, - error, - disabled, - size = 'sm', -}: { - file: File | { name: string; url: string; type: string; size: number }; - onRemove: () => void; - error?: boolean; - disabled?: boolean; - size?: 'sm' | 'md' | 'lg'; -} ) => { - const renderFileIcon = () => ( - - - - ); - - return ( -
-
- { file.type.startsWith( 'image' ) ? ( -
- { error ? ( - - ) : ( - Preview - ) } -
- ) : ( - renderFileIcon() - ) } - -
- - { file.name } - - { file.size && file.size > 0 && ( - - { formatFileSize( file.size ) } - - ) } -
- { ! disabled && ( - - ) } -
-
- ); -}; - export const InputComponent = ( { id, @@ -204,12 +75,9 @@ export const InputComponent = ( prefix = null, suffix = null, label = '', - onFileRemove, - selectedFileData = { name: '', url: '', type: '', size: 0 }, - variant = 'default', ...props }: InputProps & - Omit, 'size' | 'prefix'>, + Omit, 'size' | 'prefix' | 'onChange'>, ref: React.ForwardedRef ) => { const inputRef = useRef( null ); @@ -228,7 +96,7 @@ export const InputComponent = ( return; } - let newValue: string | FileList | null; + let newValue: FileList | string | null; if ( type === 'file' ) { newValue = event.target.files; if ( newValue && newValue.length > 0 ) { @@ -247,7 +115,7 @@ export const InputComponent = ( if ( typeof onChange !== 'function' ) { return; } - onChange( newValue as string ); + onChange( newValue ); }; const handleReset = () => { @@ -259,25 +127,6 @@ export const InputComponent = ( onChange( null ); }; - const selectedFileObject = useMemo( () => { - if ( - selectedFileData && - selectedFileData.size && - selectedFileData.size > 0 - ) { - return { - name: selectedFileData.name, - url: selectedFileData.url, - type: selectedFileData.type, - size: selectedFileData.size ?? 0, // Default size if not provided - }; - } - if ( ! inputRef.current?.files?.length ) { - return null; - } - return inputRef.current.files[ 0 ]; - }, [ selectedFileData ] ); - const baseClasses = 'bg-field-secondary-background font-normal placeholder-text-tertiary text-text-primary w-full outline outline-1 outline-border-subtle border-none transition-[color,box-shadow,outline] duration-200'; const sizeClasses = { @@ -455,20 +304,6 @@ export const InputComponent = (
- { selectedFileObject && - variant === 'preview' && - selectedFileObject.size > 0 && ( - { - handleReset(); - onFileRemove?.(); - } } - error={ error } - disabled={ disabled } - size={ size } - /> - ) } ); } From fa8324af2cddda11301b93447207c56564815145 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:28:47 +0600 Subject: [PATCH 06/41] Create a separate File Preview component --- src/components/file-preview/file-preview.tsx | 132 +++++++++++++++++++ src/components/file-preview/index.ts | 1 + src/components/index.ts | 1 + 3 files changed, 134 insertions(+) create mode 100644 src/components/file-preview/file-preview.tsx create mode 100644 src/components/file-preview/index.ts diff --git a/src/components/file-preview/file-preview.tsx b/src/components/file-preview/file-preview.tsx new file mode 100644 index 00000000..8e50506c --- /dev/null +++ b/src/components/file-preview/file-preview.tsx @@ -0,0 +1,132 @@ +import { cn, formatFileSize } from '@/utilities/functions'; +import { File, ImageOff, Trash } from 'lucide-react'; + +export type FilePreviewFile = File | { name: string; url: string; type: string; size: number }; + +export interface FilePreviewProps { + /** The file to display. */ + file: FilePreviewFile; + + /** Function called when the file is removed. */ + onRemove: ( selectedFile: FilePreviewFile ) => void; + + /** Indicates whether the file preview is disabled. */ + disabled?: boolean; + + /** The size of the file preview. */ + size?: 'sm' | 'md' | 'lg'; + + /** Indicates whether the file preview has an error. */ + error?: boolean; +} + +const commonFilePreviewClasses = { + sm: { + image: 'w-8 h-8', + name: 'text-xs', + fileIcon: 'h-8', + uploadText: 'text-xs', + }, + md: { + image: 'w-10 h-10', + name: 'text-sm', + fileIcon: 'h-10', + uploadText: 'text-xs', + }, + lg: { + image: 'w-10 h-10', + name: 'text-sm', + fileIcon: 'h-10', + uploadText: 'text-xs', + }, +}; + +export const FilePreview = ( { + file, + onRemove, + error, + disabled, + size = 'sm', +}: FilePreviewProps ) => { + const renderFileIcon = () => ( + + + + ); + + return ( +
+
+ { file.type.startsWith( 'image' ) ? ( +
+ { error ? ( + + ) : ( + Preview + ) } +
+ ) : ( + renderFileIcon() + ) } + +
+ + { file.name } + + { file.size && file.size > 0 && ( + + { formatFileSize( file.size ) } + + ) } +
+ { ! disabled && ( + + ) } +
+
+ ); +}; + +export default FilePreview; diff --git a/src/components/file-preview/index.ts b/src/components/file-preview/index.ts new file mode 100644 index 00000000..ff864038 --- /dev/null +++ b/src/components/file-preview/index.ts @@ -0,0 +1 @@ +export { default } from './file-preview'; diff --git a/src/components/index.ts b/src/components/index.ts index f3a5bdef..43ff2878 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -40,3 +40,4 @@ export { default as PieChart } from './pie-chart'; export { default as AreaChart } from './area-chart'; export { default as Dropzone } from './dropzone'; export { default as Table } from './table'; +export { default as FilePreview } from './file-preview'; From 7e6869ba37cd0d33a7c6c3759e97bfee92e85406 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:29:02 +0600 Subject: [PATCH 07/41] Create a story for the file preview component --- .../file-preview/file-preview.stories.tsx | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/components/file-preview/file-preview.stories.tsx diff --git a/src/components/file-preview/file-preview.stories.tsx b/src/components/file-preview/file-preview.stories.tsx new file mode 100644 index 00000000..730380cb --- /dev/null +++ b/src/components/file-preview/file-preview.stories.tsx @@ -0,0 +1,95 @@ +import { Meta, StoryFn } from '@storybook/react'; +import { FilePreview, FilePreviewFile, FilePreviewProps } from './file-preview'; +import { Input } from '@/index'; +import { useState } from 'react'; + +type InputValue = string | FileList | null; + +const meta = { + title: 'Components/FilePreview', + component: FilePreview, + parameters: { + layout: 'centered', + }, + tags: [ 'autodocs' ], + argTypes: { + onRemove: { action: 'removed' }, + }, + decorators: [ + ( Story: React.FC ) => ( +
+ +
+ ), + ], +} satisfies Meta< typeof FilePreview >; + +export default meta; + +type Story = StoryFn< typeof FilePreview >; + +const Template: Story = ( args: FilePreviewProps ) => ; + +const placeholderFile = { + name: 'example-file.png', + url: 'https://placehold.co/600x400.png', + type: 'image/png', + size: 102400, +} as FilePreviewFile; + +export const Default: Story = Template.bind( {} ); +Default.args = { + file: placeholderFile, + onRemove: () => {}, + disabled: false, + size: 'md', + error: false, +}; + +export const ErrorState = Template.bind( {} ); +ErrorState.args = { + file: placeholderFile, + onRemove: () => {}, + disabled: false, + size: 'md', + error: true, +}; + +export const DisabledState = Template.bind( {} ); +DisabledState.args = { + file: placeholderFile, + onRemove: () => {}, + disabled: true, + size: 'md', + error: false, +}; + +export const FileInputWithPreview: Story = ( args ) => { + const [ selectedFile, setSelectedFile ] = useState( null ); + + const handleFileChange = ( value: InputValue ) => { + if ( ! value ) { + setSelectedFile( null ); + return; + } + + let files: FileList | null = null; + + if ( value instanceof FileList ) { + files = value; + } + + setSelectedFile( files?.[ 0 ] || null ); + }; + + return ( + <> + + { selectedFile && ( +
+ setSelectedFile( null ) } /> +
+ ) } + + ); +}; From e28bc9e2f64895d08d69454cb8fe125c0016fb42 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:30:00 +0600 Subject: [PATCH 08/41] chore: Lint --- .../file-preview/file-preview.stories.tsx | 18 ++++++++++++++---- src/components/file-preview/file-preview.tsx | 4 +++- src/components/input/input.tsx | 5 ++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/file-preview/file-preview.stories.tsx b/src/components/file-preview/file-preview.stories.tsx index 730380cb..3779bfc5 100644 --- a/src/components/file-preview/file-preview.stories.tsx +++ b/src/components/file-preview/file-preview.stories.tsx @@ -22,11 +22,11 @@ const meta = { ), ], -} satisfies Meta< typeof FilePreview >; +} satisfies Meta; export default meta; -type Story = StoryFn< typeof FilePreview >; +type Story = StoryFn; const Template: Story = ( args: FilePreviewProps ) => ; @@ -84,10 +84,20 @@ export const FileInputWithPreview: Story = ( args ) => { return ( <> - + { selectedFile && (
- setSelectedFile( null ) } /> + setSelectedFile( null ) } + />
) } diff --git a/src/components/file-preview/file-preview.tsx b/src/components/file-preview/file-preview.tsx index 8e50506c..59ca31f6 100644 --- a/src/components/file-preview/file-preview.tsx +++ b/src/components/file-preview/file-preview.tsx @@ -1,7 +1,9 @@ import { cn, formatFileSize } from '@/utilities/functions'; import { File, ImageOff, Trash } from 'lucide-react'; -export type FilePreviewFile = File | { name: string; url: string; type: string; size: number }; +export type FilePreviewFile = + | File + | { name: string; url: string; type: string; size: number }; export interface FilePreviewProps { /** The file to display. */ diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 7a04c9e2..8357b7fb 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -77,7 +77,10 @@ export const InputComponent = ( label = '', ...props }: InputProps & - Omit, 'size' | 'prefix' | 'onChange'>, + Omit< + React.InputHTMLAttributes, + 'size' | 'prefix' | 'onChange' + >, ref: React.ForwardedRef ) => { const inputRef = useRef( null ); From 15ed544e0edb014b77f295d05e693a488cec8c6d Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:51:39 +0600 Subject: [PATCH 09/41] Update description for better clarity about the prop --- src/components/file-preview/file-preview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/file-preview/file-preview.tsx b/src/components/file-preview/file-preview.tsx index 59ca31f6..e8b34f62 100644 --- a/src/components/file-preview/file-preview.tsx +++ b/src/components/file-preview/file-preview.tsx @@ -6,10 +6,10 @@ export type FilePreviewFile = | { name: string; url: string; type: string; size: number }; export interface FilePreviewProps { - /** The file to display. */ + /** The file to display. It can be a File object or an object with name, url, type, and size properties. */ file: FilePreviewFile; - /** Function called when the file is removed. */ + /** Function called when the file is removed. The parameter is the selected file object, which can be a File object or an object with name, url, type, and size properties or null. */ onRemove: ( selectedFile: FilePreviewFile ) => void; /** Indicates whether the file preview is disabled. */ From 03039254016a89fd1664c6ee1373223fac61ccf7 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 7 Mar 2025 21:07:03 +0600 Subject: [PATCH 10/41] Update right side padding --- src/components/radio-button/radio-button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/radio-button/radio-button.tsx b/src/components/radio-button/radio-button.tsx index 24945136..96b39b0f 100644 --- a/src/components/radio-button/radio-button.tsx +++ b/src/components/radio-button/radio-button.tsx @@ -425,6 +425,7 @@ export const RadioButtonComponent = ( checkedValue && 'outline-border-interactive', paddingClasses, + 'pr-12', isDisabled && 'cursor-not-allowed opacity-40', buttonWrapperClasses ) } From 85e6ed9666c4c75699a1bd8bfb435d2136035465 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 15:35:41 +0600 Subject: [PATCH 11/41] Extract and create a separate portal component - Update story. --- src/components/search/search.stories.tsx | 57 +++++++++++++----------- src/components/search/search.tsx | 36 +++++++++++++-- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/components/search/search.stories.tsx b/src/components/search/search.stories.tsx index a23061bf..daafcb83 100644 --- a/src/components/search/search.stories.tsx +++ b/src/components/search/search.stories.tsx @@ -10,6 +10,7 @@ const meta: Meta = { 'SearchBox.Input': SearchBox.Input, 'SearchBox.Loading': SearchBox.Loading, 'SearchBox.Separator': SearchBox.Separator, + 'SearchBox.Portal': SearchBox.Portal, 'SearchBox.Content': SearchBox.Content, 'SearchBox.List': SearchBox.List, 'SearchBox.Empty': SearchBox.Empty, @@ -51,33 +52,35 @@ const Template: StoryFn = ( args ) => { onOpenChange={ handleOpenChange } > - - - - }> - Calendar - - }> - Document - - }> - Attendance - - - - - }> - Calendar Folder - - }> - Document Folder - - }> - Attendance Folder - - - - + + + + + }> + Calendar + + }> + Document + + }> + Attendance + + + + + }> + Calendar Folder + + }> + Document Folder + + }> + Attendance Folder + + + + + ); }; diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 906a733f..78661582 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -79,6 +79,20 @@ export interface BaseSearchBoxProps { children?: ReactNode; } +type SearchBoxPortalProps = { + /** Additional class names for styling. */ + className?: string; + + /** Child components to be rendered. */ + children: ReactNode; + + /** Unique identifier for the portal, which determines where the dropdown will be rendered in the DOM. */ + id?: string; + + /** The HTML element that serves as the root for the portal, defining the location in the DOM where the dropdown will be displayed. This can be null if no specific root is provided. */ + root?: HTMLElement | null; +}; + // Extend the type to allow assigning subcomponents to SearchBox type SearchBoxComponent = React.ForwardRefExoticComponent< BaseSearchBoxProps & React.RefAttributes @@ -91,6 +105,7 @@ type SearchBoxComponent = React.ForwardRefExoticComponent< Empty: typeof SearchBoxEmpty; Group: typeof SearchBoxGroup; Item: typeof SearchBoxItem; + Portal: typeof SearchBoxPortal; }; export const SearchBox = forwardRef( @@ -330,8 +345,8 @@ export interface SearchBoxContentProps { export const SearchBoxContent = ( { className, - dropdownPortalRoot = null, // Root element where the dropdown will be rendered. - dropdownPortalId = '', // Id of the dropdown portal where the dropdown will be rendered. + dropdownPortalRoot, // Root element where the dropdown will be rendered. + dropdownPortalId, // Id of the dropdown portal where the dropdown will be rendered. children, ...props }: SearchBoxContentProps ) => { @@ -364,6 +379,19 @@ export const SearchBoxContent = ( { }; SearchBoxContent.displayName = 'SearchBox.Content'; +export const SearchBoxPortal = ( { + children, + id, + root, +}: SearchBoxPortalProps ) => { + return ( + + { children } + + ); +}; +SearchBoxPortal.displayName = 'SearchBox.Portal'; + // Define props for SearchBoxList export interface SearchBoxListProps { /** Whether to filter children based on the search term. */ @@ -470,7 +498,7 @@ export const SearchBoxGroup = ( { heading, children }: SearchBoxGroupProps ) =>
{ heading } @@ -589,5 +617,5 @@ SearchBox.List = SearchBoxList; SearchBox.Empty = SearchBoxEmpty; SearchBox.Group = SearchBoxGroup; SearchBox.Item = SearchBoxItem; - +SearchBox.Portal = SearchBoxPortal; export default SearchBox; From 2986bcf7358428b686f5ea31c457805a8a2d8719 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:26:56 +0600 Subject: [PATCH 12/41] style: Enhance search input focus and disabled states - Add focus-within styles for primary and secondary variants - Update disabled state to use outline instead of border - Improve visual feedback for different input states --- src/components/search/styles.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/search/styles.ts b/src/components/search/styles.ts index 02b48208..103568f6 100644 --- a/src/components/search/styles.ts +++ b/src/components/search/styles.ts @@ -43,9 +43,9 @@ export const sizeClassNames = { }; export const variantClassNames = { primary: - 'bg-field-primary-background outline outline-1 outline-field-border hover:outline-border-strong', + 'bg-field-primary-background outline outline-1 outline-field-border hover:outline-border-strong focus-within:outline-focus-border focus-within:hover:outline-focus-border', secondary: - 'bg-field-secondary-background outline outline-1 outline-field-border hover:outline-border-strong', + 'bg-field-secondary-background outline outline-1 outline-field-border hover:outline-border-strong focus-within:outline-focus-border focus-within:hover:outline-focus-border', ghost: 'bg-field-secondary-background outline outline-1 outline-transparent', }; @@ -55,7 +55,7 @@ export const iconClasses = export const disabledClassNames = { ghost: 'cursor-not-allowed text-text-disabled placeholder:text-text-disabled', primary: - 'border-border-disabled hover:border-border-disabled bg-field-background-disabled cursor-not-allowed text-text-disabled placeholder:text-text-disabled', + 'outline-border-disabled hover:outline-border-disabled bg-field-background-disabled cursor-not-allowed text-text-disabled placeholder:text-text-disabled', secondary: - 'border-border-disabled hover:border-border-disabled cursor-not-allowed text-text-disabled placeholder:text-text-disabled', + 'outline-border-disabled hover:outline-border-disabled cursor-not-allowed text-text-disabled placeholder:text-text-disabled', }; From 97d40a6f626536aafd47adc752de4969eae28f93 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:31:51 +0600 Subject: [PATCH 13/41] feat: Improve SearchBox component with new features and interactions - Add `clearSearchOnClick` prop to optionally clear search term - Implement focus handling to open dropdown when input is not empty - Enhance SearchBoxItem with click and accessibility improvements - Add tabindex and role for better keyboard navigation - Utilize `callAll` utility for combining multiple click handlers --- src/components/search/search.tsx | 59 +++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 78661582..e8df1e69 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -7,9 +7,10 @@ import React, { useContext, Children, cloneElement, + type MouseEventHandler, } from 'react'; import { omit } from 'lodash'; // or define your own omit function -import { cn, getOperatingSystem } from '@/utilities/functions'; +import { callAll, cn, getOperatingSystem } from '@/utilities/functions'; import { Search } from 'lucide-react'; import Loader from '../loader'; import Badge from '../badge'; @@ -46,6 +47,7 @@ type TSearchContentValue = Partial<{ open: boolean; context: UseFloatingReturn['context']; setIsLoading: ( loading: boolean ) => void; + clearSearchOnClick: boolean; }>; // Define a context for the SearchBox @@ -77,12 +79,12 @@ export interface BaseSearchBoxProps { /** Child components to be rendered. */ children?: ReactNode; + + /** Clear search on clicking result item. */ + clearSearchOnClick?: boolean; } type SearchBoxPortalProps = { - /** Additional class names for styling. */ - className?: string; - /** Child components to be rendered. */ children: ReactNode; @@ -116,6 +118,7 @@ export const SearchBox = forwardRef( open = false, onOpenChange = () => {}, loading = false, + clearSearchOnClick = false, ...props }, ref @@ -195,6 +198,7 @@ export const SearchBox = forwardRef( setSearchTerm, isLoading, setIsLoading, + clearSearchOnClick, } } >
( setSearchTerm, } = useSearchContext(); const badgeSize = size === 'lg' ? 'sm' : 'xs'; + const handleChange = ( event: React.ChangeEvent ) => { const newValue = event.target.value; setSearchTerm!( newValue ); @@ -268,18 +273,28 @@ export const SearchBoxInput = forwardRef( } }; + const handleFocus = () => { + if ( disabled || typeof onOpenChange !== 'function' ) { + return; + } + if ( searchTerm?.trim() ) { + onOpenChange( true ); // Open the dropdown on focus if input is not empty + } + }; + return (
( className={ cn( textSizeClassNames[ size! ], 'flex-grow font-medium bg-transparent border-none outline-none border-transparent focus:ring-0 py-0', - disabled - ? disabledClassNames[ variant ] - : [ - 'text-field-placeholder focus-within:text-field-input group-hover:text-field-input', - 'placeholder:text-field-placeholder', - ], - className + disabled && + 'text-field-placeholder focus-within:text-field-input group-hover:text-field-input placeholder:text-field-placeholder', ) } disabled={ disabled } value={ searchTerm } onChange={ handleChange } + onFocus={ handleFocus } // Set open state on focus placeholder={ placeholder } // Omit custom props that are not valid for input { ...omit( props, [ @@ -520,11 +531,20 @@ export interface SearchBoxItemProps { /** Child components to be rendered. */ children: ReactNode; + + /** On click handler. */ + onClick?: () => void; } export const SearchBoxItem = forwardRef( - ( { className, icon, children, ...props }, ref ) => { - const { size } = useSearchContext(); + ( { className, icon, children, onClick, ...props }, ref ) => { + const { size, setSearchTerm, clearSearchOnClick } = useSearchContext(); + + const handleClick = () => { + if ( typeof onClick === 'function' ) { + onClick(); + } + }; return (
( 'flex items-center justify-start gap-1 p-1 hover:bg-background-secondary focus:bg-background-secondary cursor-pointer', sizeClassNames.item[ size! ] ) } + onClick={ callAll( handleClick, () => { + if ( clearSearchOnClick ) { + setSearchTerm!( '' ); + } + } ) as MouseEventHandler } { ...props } + tabIndex={ 0 } + role="button" > { icon && ( Date: Mon, 10 Mar 2025 16:38:35 +0600 Subject: [PATCH 14/41] Update changelog.txt --- changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.txt b/changelog.txt index 6f5311e3..b67f6f49 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +Version 1.4.3 - xth xxxx, 2025 +- Improvement - Enhanced the UI and functionality of the Searchbox component for better flexibility and user experience. + + Version 1.4.2 - 6th March, 2025 - New - Added new size 'xs' to the Switch component. - Improvement - Adjusted the ring width and padding of the Radio Button component. From cd1d4a5b6caf62518876b4a35a2cdfb2ce99b779 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:01:43 +0600 Subject: [PATCH 15/41] style: Refine SearchBox input styling with minimal padding and height --- src/components/search/search.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index e8df1e69..76c8f487 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -310,7 +310,7 @@ export const SearchBoxInput = forwardRef( ref={ ref } className={ cn( textSizeClassNames[ size! ], - 'flex-grow font-medium bg-transparent border-none outline-none border-transparent focus:ring-0 py-0', + 'flex-grow font-medium bg-transparent border-none outline-none border-transparent focus:ring-0 p-0 min-h-fit', disabled && 'text-field-placeholder focus-within:text-field-input group-hover:text-field-input placeholder:text-field-placeholder', ) } From 6a4196cb3b96f3cef2718cc4134982450c4444d7 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:54:35 +0600 Subject: [PATCH 16/41] feat: Enhance SearchBox with advanced keyboard navigation and accessibility - Implement list navigation using Floating UI's useListNavigation - Add keyboard interactions for dropdown opening and item selection - Convert SearchBoxItem to a button for better accessibility - Integrate FloatingFocusManager for improved focus management - Add active state highlighting for list items - Remove unused imports and simplify component logic --- src/components/search/search.tsx | 147 +++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 38 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 76c8f487..4194d1d1 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -7,10 +7,9 @@ import React, { useContext, Children, cloneElement, - type MouseEventHandler, } from 'react'; import { omit } from 'lodash'; // or define your own omit function -import { callAll, cn, getOperatingSystem } from '@/utilities/functions'; +import { cn, getOperatingSystem } from '@/utilities/functions'; import { Search } from 'lucide-react'; import Loader from '../loader'; import Badge from '../badge'; @@ -32,6 +31,10 @@ import { useInteractions, type UseFloatingReturn, type UseInteractionsReturn, + useListNavigation, + FloatingFocusManager, + FloatingList, + useListItem, } from '@floating-ui/react'; type TSearchContentValue = Partial<{ @@ -43,6 +46,10 @@ type TSearchContentValue = Partial<{ floatingStyles: UseFloatingReturn['floatingStyles']; getReferenceProps: UseInteractionsReturn['getReferenceProps']; getFloatingProps: UseInteractionsReturn['getFloatingProps']; + getItemProps: ( userProps?: React.HTMLProps ) => Record; + activeIndex: number | null; + setActiveIndex: React.Dispatch>; + listRef: React.MutableRefObject<( HTMLElement | null )[]>; setSearchTerm: React.Dispatch>; open: boolean; context: UseFloatingReturn['context']; @@ -125,6 +132,8 @@ export const SearchBox = forwardRef( ) => { const [ searchTerm, setSearchTerm ] = useState( '' ); const [ isLoading, setIsLoading ] = useState( loading ?? false ); + const [ activeIndex, setActiveIndex ] = useState( null ); + const listRef = React.useRef<( HTMLElement | null )[]>( [] ); const { refs, floatingStyles, context } = useFloating( { open, @@ -146,10 +155,19 @@ export const SearchBox = forwardRef( } ), ], } ); + + const listNavigation = useListNavigation( context, { + listRef, + activeIndex, + onNavigate: setActiveIndex, + loop: true, + } ); + const dismiss = useDismiss( context ); - const { getReferenceProps, getFloatingProps } = useInteractions( [ + const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions( [ dismiss, + listNavigation, ] ); useEffect( () => { @@ -183,6 +201,13 @@ export const SearchBox = forwardRef( }; }, [ refs.reference ] ); + // Reset active index when closing dropdown + useEffect( () => { + if ( ! open ) { + setActiveIndex( null ); + } + }, [ open ] ); + return ( ( context, getReferenceProps, getFloatingProps, + getItemProps, + activeIndex, + setActiveIndex, + listRef, searchTerm, setSearchTerm, isLoading, @@ -256,6 +285,8 @@ export const SearchBoxInput = forwardRef( getReferenceProps, searchTerm, setSearchTerm, + setActiveIndex, + open, } = useSearchContext(); const badgeSize = size === 'lg' ? 'sm' : 'xs'; @@ -282,6 +313,22 @@ export const SearchBoxInput = forwardRef( } }; + const handleKeyDown = ( event: React.KeyboardEvent ) => { + if ( disabled ) { + return; + } + + if ( event.key === 'ArrowDown' ) { + event.preventDefault(); + if ( ! open && searchTerm?.trim() ) { + onOpenChange!( true ); + } + setActiveIndex!( 0 ); + } else if ( event.key === 'Escape' ) { + onOpenChange!( false ); + } + }; + return (
( disabled={ disabled } value={ searchTerm } onChange={ handleChange } - onFocus={ handleFocus } // Set open state on focus + onFocus={ handleFocus } + onKeyDown={ handleKeyDown } placeholder={ placeholder } // Omit custom props that are not valid for input { ...omit( props, [ @@ -356,12 +404,10 @@ export interface SearchBoxContentProps { export const SearchBoxContent = ( { className, - dropdownPortalRoot, // Root element where the dropdown will be rendered. - dropdownPortalId, // Id of the dropdown portal where the dropdown will be rendered. children, ...props }: SearchBoxContentProps ) => { - const { size, open, refs, floatingStyles, getFloatingProps } = + const { size, open, refs, floatingStyles, getFloatingProps, context } = useSearchContext(); if ( ! open ) { @@ -369,14 +415,14 @@ export const SearchBoxContent = ( { } return ( - +
{ children }
-
+ ); }; SearchBoxContent.displayName = 'SearchBox.Content'; @@ -416,10 +462,14 @@ export const SearchBoxList = ( { filter = true, children, }: SearchBoxListProps ) => { - const { searchTerm, isLoading } = useSearchContext(); + const { searchTerm, isLoading, listRef } = useSearchContext(); if ( ! filter ) { - return
{ children }
; + return ( + +
{ children }
+
+ ); } const filteredChildren = Children.toArray( children ) .map( ( child ) => { @@ -448,17 +498,19 @@ export const SearchBoxList = ( { return ; } return ( -
- { filteredChildren.some( - ( child ) => - React.isValidElement( child ) && - child.type !== SearchBoxSeparator - ) ? ( - filteredChildren - ) : ( - - ) } -
+ +
+ { filteredChildren.some( + ( child ) => + React.isValidElement( child ) && + child.type !== SearchBoxSeparator + ) ? ( + filteredChildren + ) : ( + + ) } +
+
); }; SearchBoxList.displayName = 'SearchBox.List'; @@ -536,30 +588,49 @@ export interface SearchBoxItemProps { onClick?: () => void; } -export const SearchBoxItem = forwardRef( +export const SearchBoxItem = forwardRef( ( { className, icon, children, onClick, ...props }, ref ) => { - const { size, setSearchTerm, clearSearchOnClick } = useSearchContext(); + const { size, setSearchTerm, clearSearchOnClick, getItemProps, activeIndex } = useSearchContext(); + const { ref: itemRef, index } = useListItem(); + + // Combine the refs + const combinedRef = ( node: HTMLButtonElement | null ) => { + if ( typeof ref === 'function' ) { + ref( node ); + } else if ( ref ) { + ref.current = node; + } + itemRef( node ); + }; + + const isActive = activeIndex === index; const handleClick = () => { if ( typeof onClick === 'function' ) { onClick(); } + + if ( clearSearchOnClick ) { + setSearchTerm!( '' ); + } }; + return ( -
{ - if ( clearSearchOnClick ) { - setSearchTerm!( '' ); - } - } ) as MouseEventHandler } - { ...props } - tabIndex={ 0 } - role="button" + { ...getItemProps?.( { + role: 'option', + 'aria-selected': isActive, + onClick: handleClick, + ...props, + } ) } > { icon && ( ( ) } { children } -
+ ); } ); From 0ac45560cffc70b3ea92976fc32de59d4c86deb3 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:02:13 +0600 Subject: [PATCH 17/41] feat: Add `closeOnClick` option to SearchBox for improved interaction --- src/components/search/search.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 4194d1d1..2b384d1f 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -55,6 +55,7 @@ type TSearchContentValue = Partial<{ context: UseFloatingReturn['context']; setIsLoading: ( loading: boolean ) => void; clearSearchOnClick: boolean; + closeOnClick: boolean; }>; // Define a context for the SearchBox @@ -89,6 +90,9 @@ export interface BaseSearchBoxProps { /** Clear search on clicking result item. */ clearSearchOnClick?: boolean; + + /** Close on clicking result item. */ + closeOnClick?: boolean; } type SearchBoxPortalProps = { @@ -126,6 +130,7 @@ export const SearchBox = forwardRef( onOpenChange = () => {}, loading = false, clearSearchOnClick = false, + closeOnClick = false, ...props }, ref @@ -228,6 +233,7 @@ export const SearchBox = forwardRef( isLoading, setIsLoading, clearSearchOnClick, + closeOnClick, } } >
( ( { className, icon, children, onClick, ...props }, ref ) => { - const { size, setSearchTerm, clearSearchOnClick, getItemProps, activeIndex } = useSearchContext(); + const { + size, + setSearchTerm, + clearSearchOnClick, + getItemProps, + activeIndex, + onOpenChange, + closeOnClick, + } = useSearchContext(); const { ref: itemRef, index } = useListItem(); // Combine the refs @@ -613,6 +627,10 @@ export const SearchBoxItem = forwardRef( if ( clearSearchOnClick ) { setSearchTerm!( '' ); } + + if ( closeOnClick ) { + onOpenChange!( false ); + } }; return ( From 42b444db03e0706e4b81ad78d3b06621e1845035 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 10 Mar 2025 19:15:31 +0600 Subject: [PATCH 18/41] Prevent opening the dropdown - After pressing arrow down - After pressing arrow up --- src/components/search/search.tsx | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 2b384d1f..df737d2d 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -166,6 +166,8 @@ export const SearchBox = forwardRef( activeIndex, onNavigate: setActiveIndex, loop: true, + // Prevent opening the dropdown with arrow keys + openOnArrowKeyDown: false, } ); const dismiss = useDismiss( context ); @@ -291,8 +293,9 @@ export const SearchBoxInput = forwardRef( getReferenceProps, searchTerm, setSearchTerm, - setActiveIndex, open, + setActiveIndex, + listRef, } = useSearchContext(); const badgeSize = size === 'lg' ? 'sm' : 'xs'; @@ -324,13 +327,28 @@ export const SearchBoxInput = forwardRef( return; } - if ( event.key === 'ArrowDown' ) { - event.preventDefault(); - if ( ! open && searchTerm?.trim() ) { - onOpenChange!( true ); + // Do not open dropdown on arrow keys + if ( event.key === 'ArrowDown' || event.key === 'ArrowUp' ) { + // Only navigate if dropdown is already open + if ( open ) { + event.preventDefault(); + if ( event.key === 'ArrowDown' ) { + // Navigate to first item if none selected, otherwise listNavigation will handle it + setActiveIndex!( ( prev ) => ( prev === null ? 0 : prev ) ); + } else if ( event.key === 'ArrowUp' ) { + // Navigate to last item if none selected, otherwise listNavigation will handle it + setActiveIndex!( ( prev ) => { + // Get the length of the list to select the last item + const listLength = listRef?.current?.length || 0; + return prev === null && listLength > 0 ? listLength - 1 : prev; + } ); + } } - setActiveIndex!( 0 ); - } else if ( event.key === 'Escape' ) { + // Do not open the dropdown + return; + } + + if ( event.key === 'Escape' ) { onOpenChange!( false ); } }; From 73e4237c3b5c5a61313b08eed06e8bd942d124f7 Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Tue, 11 Mar 2025 17:50:31 +0530 Subject: [PATCH 19/41] added number and icon variant for completed step --- .../progress-steps/progress-steps.stories.tsx | 34 ++++++++++++++- .../progress-steps/progress-steps.tsx | 43 +++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/components/progress-steps/progress-steps.stories.tsx b/src/components/progress-steps/progress-steps.stories.tsx index b3544815..00022eeb 100644 --- a/src/components/progress-steps/progress-steps.stories.tsx +++ b/src/components/progress-steps/progress-steps.stories.tsx @@ -1,5 +1,5 @@ import ProgressSteps, { ProgressStepsProps } from './progress-steps'; -import { Check, Home } from 'lucide-react'; +import { Check, Home, BadgeCheck } from 'lucide-react'; import type { Meta, StoryFn } from '@storybook/react'; // ProgressSteps.Step display name for better documentation in Storybook @@ -108,3 +108,35 @@ export const StackType = { }, render: Template, }; + +// Numbered Completed Steps Variant +export const NumberedCompletedSteps = { + args: { + variant: 'dot', + size: 'md', + type: 'inline', + currentStep: 3, + completedVariant: 'numbered', + }, + parameters: { + docs: { + description: { + story: 'Shows completed steps with step numbers in colored circles', + }, + }, + }, + render: Template, +}; + +// custom icon in completed steps +export const CustomIconInCompletedSteps = { + args: { + variant: 'icon', + size: 'md', + type: 'inline', + currentStep: 3, + completedVariant: 'icon', + completedIcon: , + }, + render: Template, +}; diff --git a/src/components/progress-steps/progress-steps.tsx b/src/components/progress-steps/progress-steps.tsx index 2976da09..0985596a 100644 --- a/src/components/progress-steps/progress-steps.tsx +++ b/src/components/progress-steps/progress-steps.tsx @@ -33,6 +33,9 @@ const sizeClassnames = { type StepSizeClasses = typeof sizeClassnames; +// Enhanced to include completed step variants +export type CompletedVariant = 'icon' | 'numbered'; + // Common props interface export interface ProgressCommonProps { /** Defines the children of the progress steps. */ @@ -53,6 +56,10 @@ export interface ProgressStepsProps extends ProgressCommonProps { currentStep?: number; /** Additional props for the connecting line. */ lineClassName?: string; + /** Defines how completed steps should be displayed */ + completedVariant?: CompletedVariant; + /** Custom icon for completed steps when completedVariant is 'icon' */ + completedIcon?: ReactNode; } // Progress Step props interface @@ -89,6 +96,12 @@ export interface ProgressStepProps extends ProgressCommonProps { /** Additional class names for the connecting line. */ lineClassName?: string; + + /** How to display completed steps */ + completedVariant?: CompletedVariant; + + /** Custom icon for completed steps */ + completedIcon?: ReactNode; } export const ProgressSteps = ( { @@ -99,6 +112,8 @@ export const ProgressSteps = ( { children, className, lineClassName = 'min-w-10', + completedVariant = 'icon', + completedIcon = , ...rest }: ProgressStepsProps ) => { const totalSteps = React.Children.count( children ); @@ -120,6 +135,8 @@ export const ProgressSteps = ( { isLast, index, lineClassName, + completedVariant, + completedIcon, }; return ( @@ -159,6 +176,8 @@ export const ProgressStep = ( { isLast, index, lineClassName, + completedVariant = 'icon', + completedIcon = , ...rest }: ProgressStepProps ) => { const stepContent = createStepContent( @@ -168,7 +187,9 @@ export const ProgressStep = ( { sizeClasses!, size, icon, - index as number + index as number, + completedVariant, + completedIcon ); const stackSizeOffset = { @@ -277,11 +298,27 @@ export const createStepContent = ( sizeClasses: StepSizeClasses, size: 'sm' | 'md' | 'lg', icon: ReactNode, - index: number + index: number, + completedVariant: CompletedVariant = 'icon', + completedIcon: ReactNode = ) => { if ( isCompleted ) { + if ( completedVariant === 'numbered' ) { + return ( + + { index + 1 } + + ); + } return ( - + + { completedIcon } + ); } From 9fb11a5a1c7a540107da1b90fc9db47631f0c42c Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Wed, 12 Mar 2025 12:12:36 +0530 Subject: [PATCH 20/41] story and text fix --- src/components/progress-steps/progress-steps.stories.tsx | 2 +- src/components/progress-steps/progress-steps.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/progress-steps/progress-steps.stories.tsx b/src/components/progress-steps/progress-steps.stories.tsx index 00022eeb..8bd08a09 100644 --- a/src/components/progress-steps/progress-steps.stories.tsx +++ b/src/components/progress-steps/progress-steps.stories.tsx @@ -112,7 +112,7 @@ export const StackType = { // Numbered Completed Steps Variant export const NumberedCompletedSteps = { args: { - variant: 'dot', + variant: 'number', size: 'md', type: 'inline', currentStep: 3, diff --git a/src/components/progress-steps/progress-steps.tsx b/src/components/progress-steps/progress-steps.tsx index 0985596a..7f80f6a4 100644 --- a/src/components/progress-steps/progress-steps.tsx +++ b/src/components/progress-steps/progress-steps.tsx @@ -308,7 +308,7 @@ export const createStepContent = ( { index + 1 } From a54d2ae551d00d37941960e22384f2cc9e927271 Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Wed, 12 Mar 2025 14:33:15 +0530 Subject: [PATCH 21/41] string changed --- src/components/progress-steps/progress-steps.stories.tsx | 2 +- src/components/progress-steps/progress-steps.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/progress-steps/progress-steps.stories.tsx b/src/components/progress-steps/progress-steps.stories.tsx index 8bd08a09..18c41b98 100644 --- a/src/components/progress-steps/progress-steps.stories.tsx +++ b/src/components/progress-steps/progress-steps.stories.tsx @@ -116,7 +116,7 @@ export const NumberedCompletedSteps = { size: 'md', type: 'inline', currentStep: 3, - completedVariant: 'numbered', + completedVariant: 'number', }, parameters: { docs: { diff --git a/src/components/progress-steps/progress-steps.tsx b/src/components/progress-steps/progress-steps.tsx index 7f80f6a4..885c678a 100644 --- a/src/components/progress-steps/progress-steps.tsx +++ b/src/components/progress-steps/progress-steps.tsx @@ -34,7 +34,7 @@ const sizeClassnames = { type StepSizeClasses = typeof sizeClassnames; // Enhanced to include completed step variants -export type CompletedVariant = 'icon' | 'numbered'; +export type CompletedVariant = 'icon' | 'number'; // Common props interface export interface ProgressCommonProps { @@ -303,7 +303,7 @@ export const createStepContent = ( completedIcon: ReactNode = ) => { if ( isCompleted ) { - if ( completedVariant === 'numbered' ) { + if ( completedVariant === 'number' ) { return ( Date: Wed, 12 Mar 2025 15:30:44 +0600 Subject: [PATCH 22/41] Removed props that are not required --- src/components/search/search.tsx | 52 ++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index df737d2d..feeff6e7 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -46,7 +46,9 @@ type TSearchContentValue = Partial<{ floatingStyles: UseFloatingReturn['floatingStyles']; getReferenceProps: UseInteractionsReturn['getReferenceProps']; getFloatingProps: UseInteractionsReturn['getFloatingProps']; - getItemProps: ( userProps?: React.HTMLProps ) => Record; + getItemProps: ( + userProps?: React.HTMLProps + ) => Record; activeIndex: number | null; setActiveIndex: React.Dispatch>; listRef: React.MutableRefObject<( HTMLElement | null )[]>; @@ -65,6 +67,11 @@ const useSearchContext = () => { return useContext( SearchContext ); }; +export interface CommonSearchBoxProps { + /** Additional class names for styling. */ + className?: string; +} + // Define the Size type type Size = 'sm' | 'md' | 'lg'; @@ -76,6 +83,9 @@ export interface BaseSearchBoxProps { /** Size of the SearchBox. */ size?: 'sm' | 'md' | 'lg'; + /** Style variant of the input. */ + variant?: 'primary' | 'secondary' | 'ghost'; + /** Whether the dropdown is open. */ open?: boolean; @@ -131,6 +141,7 @@ export const SearchBox = forwardRef( loading = false, clearSearchOnClick = false, closeOnClick = false, + variant = 'primary', ...props }, ref @@ -172,10 +183,8 @@ export const SearchBox = forwardRef( const dismiss = useDismiss( context ); - const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions( [ - dismiss, - listNavigation, - ] ); + const { getReferenceProps, getFloatingProps, getItemProps } = + useInteractions( [ dismiss, listNavigation ] ); useEffect( () => { const operatingSystem = getOperatingSystem(); @@ -236,6 +245,7 @@ export const SearchBox = forwardRef( setIsLoading, clearSearchOnClick, closeOnClick, + variant, } } >
( SearchBox.displayName = 'SearchBox'; // Define props for SearchBoxInput -export interface SearchBoxInputProps extends BaseSearchBoxProps { +export interface SearchBoxInputProps extends CommonSearchBoxProps { /** Type of the input (e.g., text, search). */ type?: string; /** Placeholder text for the input. */ placeholder?: string; - /** Style variant of the input. */ - variant?: 'primary' | 'secondary' | 'ghost'; - /** Whether the input is disabled. */ disabled?: boolean; @@ -279,7 +286,6 @@ export const SearchBoxInput = forwardRef( className, type = 'text', placeholder = 'Search...', - variant = 'primary', disabled = false, onChange = () => {}, ...props @@ -288,7 +294,6 @@ export const SearchBoxInput = forwardRef( ) => { const { size, - onOpenChange, refs, getReferenceProps, searchTerm, @@ -296,6 +301,8 @@ export const SearchBoxInput = forwardRef( open, setActiveIndex, listRef, + onOpenChange, + variant, } = useSearchContext(); const badgeSize = size === 'lg' ? 'sm' : 'xs'; @@ -318,7 +325,7 @@ export const SearchBoxInput = forwardRef( return; } if ( searchTerm?.trim() ) { - onOpenChange( true ); // Open the dropdown on focus if input is not empty + onOpenChange!( true ); // Open the dropdown on focus if input is not empty } }; @@ -340,7 +347,9 @@ export const SearchBoxInput = forwardRef( setActiveIndex!( ( prev ) => { // Get the length of the list to select the last item const listLength = listRef?.current?.length || 0; - return prev === null && listLength > 0 ? listLength - 1 : prev; + return prev === null && listLength > 0 + ? listLength - 1 + : prev; } ); } } @@ -383,7 +392,7 @@ export const SearchBoxInput = forwardRef( textSizeClassNames[ size! ], 'flex-grow font-medium bg-transparent border-none outline-none border-transparent focus:ring-0 p-0 min-h-fit', disabled && - 'text-field-placeholder focus-within:text-field-input group-hover:text-field-input placeholder:text-field-placeholder', + 'text-field-placeholder focus-within:text-field-input group-hover:text-field-input placeholder:text-field-placeholder' ) } disabled={ disabled } value={ searchTerm } @@ -416,12 +425,6 @@ export interface SearchBoxContentProps { /** Additional class names for styling. */ className?: string; - /** Root element where the dropdown will be rendered. */ - dropdownPortalRoot?: HTMLElement | null; - - /** Id of the dropdown portal where the dropdown will be rendered. */ - dropdownPortalId?: string; - /** Child components to be rendered inside the dropdown. */ children: ReactNode; } @@ -439,7 +442,11 @@ export const SearchBoxContent = ( { } return ( - +
( className={ cn( 'flex w-full items-center justify-start gap-1 p-1 cursor-pointer border-none bg-transparent text-left focus:outline-none', isActive && 'bg-background-secondary', - ! isActive && 'hover:bg-background-secondary focus:bg-background-secondary', + ! isActive && + 'hover:bg-background-secondary focus:bg-background-secondary', sizeClassNames.item[ size! ] ) } { ...getItemProps?.( { From e83a3719f04037bdfe4a763a1a352f316974b573 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:36:06 +0600 Subject: [PATCH 23/41] fix: TS errors --- src/components/search/search.tsx | 101 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index feeff6e7..ab6dd984 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -37,36 +37,6 @@ import { useListItem, } from '@floating-ui/react'; -type TSearchContentValue = Partial<{ - size: 'sm' | 'md' | 'lg'; - searchTerm: string; - isLoading: boolean; - onOpenChange: ( open: boolean ) => void; - refs: UseFloatingReturn['refs']; - floatingStyles: UseFloatingReturn['floatingStyles']; - getReferenceProps: UseInteractionsReturn['getReferenceProps']; - getFloatingProps: UseInteractionsReturn['getFloatingProps']; - getItemProps: ( - userProps?: React.HTMLProps - ) => Record; - activeIndex: number | null; - setActiveIndex: React.Dispatch>; - listRef: React.MutableRefObject<( HTMLElement | null )[]>; - setSearchTerm: React.Dispatch>; - open: boolean; - context: UseFloatingReturn['context']; - setIsLoading: ( loading: boolean ) => void; - clearSearchOnClick: boolean; - closeOnClick: boolean; -}>; - -// Define a context for the SearchBox -const SearchContext = createContext( {} ); - -const useSearchContext = () => { - return useContext( SearchContext ); -}; - export interface CommonSearchBoxProps { /** Additional class names for styling. */ className?: string; @@ -116,6 +86,24 @@ type SearchBoxPortalProps = { root?: HTMLElement | null; }; +// Define props for SearchBoxInput +export interface SearchBoxInputProps extends CommonSearchBoxProps { + /** Type of the input (e.g., text, search). */ + type?: string; + + /** Placeholder text for the input. */ + placeholder?: string; + + /** Whether the input is disabled. */ + disabled?: boolean; + + /** Callback for input changes. */ + onChange?: ( value: string ) => void; + + /** Child components to be rendered. */ + children?: ReactNode; +} + // Extend the type to allow assigning subcomponents to SearchBox type SearchBoxComponent = React.ForwardRefExoticComponent< BaseSearchBoxProps & React.RefAttributes @@ -131,6 +119,37 @@ type SearchBoxComponent = React.ForwardRefExoticComponent< Portal: typeof SearchBoxPortal; }; +type TSearchContentValue = Partial<{ + size: 'sm' | 'md' | 'lg'; + searchTerm: string; + isLoading: boolean; + onOpenChange: ( open: boolean ) => void; + refs: UseFloatingReturn['refs']; + floatingStyles: UseFloatingReturn['floatingStyles']; + getReferenceProps: UseInteractionsReturn['getReferenceProps']; + getFloatingProps: UseInteractionsReturn['getFloatingProps']; + getItemProps: ( + userProps?: React.HTMLProps + ) => Record; + activeIndex: number | null; + setActiveIndex: React.Dispatch>; + listRef: React.MutableRefObject<( HTMLElement | null )[]>; + setSearchTerm: React.Dispatch>; + open: boolean; + context: UseFloatingReturn['context']; + setIsLoading: ( loading: boolean ) => void; + clearSearchOnClick: boolean; + closeOnClick: boolean; + variant: BaseSearchBoxProps['variant']; +}>; + +// Define a context for the SearchBox +const SearchContext = createContext( {} ); + +const useSearchContext = () => { + return useContext( SearchContext ); +}; + export const SearchBox = forwardRef( ( { @@ -262,24 +281,6 @@ export const SearchBox = forwardRef( ) as SearchBoxComponent; SearchBox.displayName = 'SearchBox'; -// Define props for SearchBoxInput -export interface SearchBoxInputProps extends CommonSearchBoxProps { - /** Type of the input (e.g., text, search). */ - type?: string; - - /** Placeholder text for the input. */ - placeholder?: string; - - /** Whether the input is disabled. */ - disabled?: boolean; - - /** Callback for input changes. */ - onChange?: ( value: string ) => void; - - /** Child components to be rendered. */ - children?: ReactNode; -} - export const SearchBoxInput = forwardRef( ( { @@ -367,10 +368,10 @@ export const SearchBoxInput = forwardRef( ref={ refs!.setReference } className={ cn( 'w-full group relative flex justify-center items-center gap-1.5 focus-within:z-10 transition-all ease-in-out duration-200', - variantClassNames[ variant ], + variantClassNames[ variant! ], sizeClassNames.input[ size! ], disabled - ? disabledClassNames[ variant ] + ? disabledClassNames[ variant! ] : 'focus-within:ring-2 focus-within:ring-focus focus-within:ring-offset-2 focus-within:border-focus-border focus-within:hover:border-focus-border', className ) } From b65063d6500f464dad634a0ad9436af8ea212594 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:39:42 +0600 Subject: [PATCH 24/41] Update search.stories.tsx --- src/components/search/search.stories.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/search/search.stories.tsx b/src/components/search/search.stories.tsx index daafcb83..d8d19074 100644 --- a/src/components/search/search.stories.tsx +++ b/src/components/search/search.stories.tsx @@ -94,8 +94,8 @@ export const SecondarySearchBox = Template.bind( {} ); SecondarySearchBox.args = {}; SecondarySearchBox.decorators = [ () => ( - - + + ), ]; @@ -104,8 +104,8 @@ export const GhostSearchBox = Template.bind( {} ); GhostSearchBox.args = {}; GhostSearchBox.decorators = [ () => ( - - + + ), ]; From c4f8675c4122fdecd4df89c1b0ac4558802813fb Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:06:12 +0600 Subject: [PATCH 25/41] Moved filter prop to the root component --- src/components/search/search.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index ab6dd984..fe71afe5 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -62,6 +62,9 @@ export interface BaseSearchBoxProps { /** Callback when dropdown state changes. */ onOpenChange?: ( open: boolean ) => void; + /** Whether to filter children based on the search term. Turn off when you want to filter children manually. */ + filter?: boolean; + /** Whether to show loading state. */ loading?: boolean; @@ -141,6 +144,7 @@ type TSearchContentValue = Partial<{ clearSearchOnClick: boolean; closeOnClick: boolean; variant: BaseSearchBoxProps['variant']; + filter: boolean; }>; // Define a context for the SearchBox @@ -161,6 +165,7 @@ export const SearchBox = forwardRef( clearSearchOnClick = false, closeOnClick = false, variant = 'primary', + filter = true, ...props }, ref @@ -265,6 +270,7 @@ export const SearchBox = forwardRef( clearSearchOnClick, closeOnClick, variant, + filter, } } >
{ - const { searchTerm, isLoading, listRef } = useSearchContext(); + const { searchTerm, isLoading, listRef, filter = true } = useSearchContext(); if ( ! filter ) { return ( @@ -531,7 +534,7 @@ export const SearchBoxList = ( { } return ( -
+
{ filteredChildren.some( ( child ) => React.isValidElement( child ) && From df1cf628712680a809388b56e30030dcb149ed4c Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Wed, 12 Mar 2025 17:56:04 +0600 Subject: [PATCH 26/41] Apply className to the parent element --- src/components/search/search.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index fe71afe5..654836a7 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -671,7 +671,8 @@ export const SearchBoxItem = forwardRef( isActive && 'bg-background-secondary', ! isActive && 'hover:bg-background-secondary focus:bg-background-secondary', - sizeClassNames.item[ size! ] + sizeClassNames.item[ size! ], + className ) } { ...getItemProps?.( { role: 'option', @@ -694,7 +695,6 @@ export const SearchBoxItem = forwardRef( className={ cn( 'flex-grow p-1 font-normal', sizeClassNames.item[ size! ], - className ) } > { children } From d627b3bdea9b9fe03b709762ab441aee8ae05715 Mon Sep 17 00:00:00 2001 From: Ravindra Kele Date: Wed, 12 Mar 2025 20:52:03 +0530 Subject: [PATCH 27/41] added changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index f173db3c..82df81b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ Version 1.4.3 - xth x, 2025 - New - Added a variant in Input component to show uploaded file preview. +- New - Added a variant in Progress Steps component to show icon and number in the completed step. Version 1.4.2 - 6th March, 2025 - New - Added new size 'xs' to the Switch component. From 9152210d67e789e33e3693025f35a58ac253eb00 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Thu, 13 Mar 2025 13:37:28 +0600 Subject: [PATCH 28/41] Update prop name --- src/components/search/search.stories.tsx | 2 +- src/components/search/search.tsx | 63 +++++++++++++++++------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/components/search/search.stories.tsx b/src/components/search/search.stories.tsx index d8d19074..fd16c7f2 100644 --- a/src/components/search/search.stories.tsx +++ b/src/components/search/search.stories.tsx @@ -49,7 +49,7 @@ const Template: StoryFn = ( args ) => { diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 654836a7..065b31f9 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -7,6 +7,7 @@ import React, { useContext, Children, cloneElement, + useMemo, } from 'react'; import { omit } from 'lodash'; // or define your own omit function import { cn, getOperatingSystem } from '@/utilities/functions'; @@ -59,7 +60,16 @@ export interface BaseSearchBoxProps { /** Whether the dropdown is open. */ open?: boolean; - /** Callback when dropdown state changes. */ + /** + * Call back function to handle the open state of the dropdown. + */ + setOpen?: ( open: boolean ) => void; + + /** + * Callback when dropdown state changes. + * + * @deprecated Use `setOpen` instead. + */ onOpenChange?: ( open: boolean ) => void; /** Whether to filter children based on the search term. Turn off when you want to filter children manually. */ @@ -71,11 +81,19 @@ export interface BaseSearchBoxProps { /** Child components to be rendered. */ children?: ReactNode; - /** Clear search on clicking result item. */ - clearSearchOnClick?: boolean; - - /** Close on clicking result item. */ - closeOnClick?: boolean; + /** + * Clear search term after selecting a result. + * + * @default true + */ + clearAfterSelect?: boolean; + + /** + * Close dropdown after selecting a result. + * + * @default true + */ + closeAfterSelect?: boolean; } type SearchBoxPortalProps = { @@ -141,8 +159,8 @@ type TSearchContentValue = Partial<{ open: boolean; context: UseFloatingReturn['context']; setIsLoading: ( loading: boolean ) => void; - clearSearchOnClick: boolean; - closeOnClick: boolean; + clearAfterSelect: boolean; + closeAfterSelect: boolean; variant: BaseSearchBoxProps['variant']; filter: boolean; }>; @@ -160,10 +178,11 @@ export const SearchBox = forwardRef( className, size = 'sm' as Size, open = false, - onOpenChange = () => {}, + setOpen = () => {}, + onOpenChange: _onOpenChange = () => {}, loading = false, - clearSearchOnClick = false, - closeOnClick = false, + clearAfterSelect = true, + closeAfterSelect = true, variant = 'primary', filter = true, ...props @@ -175,6 +194,16 @@ export const SearchBox = forwardRef( const [ activeIndex, setActiveIndex ] = useState( null ); const listRef = React.useRef<( HTMLElement | null )[]>( [] ); + /** + * Memoized function to handle the open state of the dropdown. + */ + const onOpenChange = useMemo( () => { + if ( typeof setOpen === 'function' ) { + return setOpen; + } + return _onOpenChange; + }, [ setOpen, _onOpenChange ] ); + const { refs, floatingStyles, context } = useFloating( { open, onOpenChange, @@ -267,8 +296,8 @@ export const SearchBox = forwardRef( setSearchTerm, isLoading, setIsLoading, - clearSearchOnClick, - closeOnClick, + clearAfterSelect, + closeAfterSelect, variant, filter, } } @@ -628,11 +657,11 @@ export const SearchBoxItem = forwardRef( const { size, setSearchTerm, - clearSearchOnClick, + clearAfterSelect, getItemProps, activeIndex, onOpenChange, - closeOnClick, + closeAfterSelect, } = useSearchContext(); const { ref: itemRef, index } = useListItem(); @@ -653,11 +682,11 @@ export const SearchBoxItem = forwardRef( onClick(); } - if ( clearSearchOnClick ) { + if ( clearAfterSelect ) { setSearchTerm!( '' ); } - if ( closeOnClick ) { + if ( closeAfterSelect ) { onOpenChange!( false ); } }; From 36fc5f718ba1f272aff36c8742fa0cb73727082d Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:07:39 +0600 Subject: [PATCH 29/41] Set default value to undefined --- src/components/search/search.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 065b31f9..22aa201d 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -178,8 +178,8 @@ export const SearchBox = forwardRef( className, size = 'sm' as Size, open = false, - setOpen = () => {}, - onOpenChange: _onOpenChange = () => {}, + setOpen, + onOpenChange: _onOpenChange, loading = false, clearAfterSelect = true, closeAfterSelect = true, From ecff26354c1780eb99adf424f34490c6ddaa4bc9 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:16:32 +0600 Subject: [PATCH 30/41] Add className to the wrapper - Add className prop to the Empty component --- src/components/search/search.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 22aa201d..1d02c2cf 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -531,7 +531,7 @@ export const SearchBoxList = ( { if ( ! filter ) { return ( -
{ children }
+
{ children }
); } @@ -583,10 +583,14 @@ SearchBoxList.displayName = 'SearchBox.List'; export interface SearchBoxEmptyProps { /** Content to display when there are no results. */ children?: ReactNode; + + /** Additional class names for styling. */ + className?: string; } export const SearchBoxEmpty = ( { children = 'No results found.', + className, }: SearchBoxEmptyProps ) => { const { size } = useSearchContext(); return ( @@ -594,7 +598,8 @@ export const SearchBoxEmpty = ( { className={ cn( 'flex justify-center items-center', sizeClassNames.item[ size! ], - 'text-text-tertiary p-4' + 'text-text-tertiary p-4', + className ) } > { children } From 0fa3fa35fe19c9369e6c1621c9e3302bc3146598 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:52:09 +0600 Subject: [PATCH 31/41] fix: FilePreview component accessibility issues --- src/components/file-preview/file-preview.stories.tsx | 3 ++- src/components/file-preview/file-preview.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/file-preview/file-preview.stories.tsx b/src/components/file-preview/file-preview.stories.tsx index 3779bfc5..d7901548 100644 --- a/src/components/file-preview/file-preview.stories.tsx +++ b/src/components/file-preview/file-preview.stories.tsx @@ -6,7 +6,7 @@ import { useState } from 'react'; type InputValue = string | FileList | null; const meta = { - title: 'Components/FilePreview', + title: 'Atoms/FilePreview', component: FilePreview, parameters: { layout: 'centered', @@ -85,6 +85,7 @@ export const FileInputWithPreview: Story = ( args ) => { return ( <> onRemove( file ) } className="inline-flex cursor-pointer bg-transparent border-0 p-1 my-0 ml-auto mr-0 ring-0 focus:outline-none self-start" + aria-label="Remove file" > From b9e1115715977ab807dc340260b116eca7bc8a2c Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:12:43 +0600 Subject: [PATCH 32/41] Show inner outline on focus for ghost variant --- src/components/search/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/styles.ts b/src/components/search/styles.ts index 103568f6..191a0b59 100644 --- a/src/components/search/styles.ts +++ b/src/components/search/styles.ts @@ -46,7 +46,7 @@ export const variantClassNames = { 'bg-field-primary-background outline outline-1 outline-field-border hover:outline-border-strong focus-within:outline-focus-border focus-within:hover:outline-focus-border', secondary: 'bg-field-secondary-background outline outline-1 outline-field-border hover:outline-border-strong focus-within:outline-focus-border focus-within:hover:outline-focus-border', - ghost: 'bg-field-secondary-background outline outline-1 outline-transparent', + ghost: 'bg-field-secondary-background outline outline-1 outline-transparent focus-within:outline-focus-border', }; export const iconClasses = From ee98d5e7c48dd2c90f7a58e48f0ed53be1c34859 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:15:54 +0600 Subject: [PATCH 33/41] Update search result item padding --- src/components/search/search.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 1d02c2cf..467cb541 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -727,7 +727,7 @@ export const SearchBoxItem = forwardRef( ) } From f05731b90755a00c8c5c4ef58b90a412e620cc12 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:23:23 +0600 Subject: [PATCH 34/41] Update dropdown offset value --- src/components/search/search.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 467cb541..6e00b555 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -204,13 +204,27 @@ export const SearchBox = forwardRef( return _onOpenChange; }, [ setOpen, _onOpenChange ] ); + // Offset value for the dropdown based on the size. + const offsetValue = useMemo( () => { + switch ( size ) { + case 'sm': + return 4; + case 'md': + return 6; + case 'lg': + return 8; + default: + return 6; + } + }, [ size ] ); + const { refs, floatingStyles, context } = useFloating( { open, onOpenChange, placement: 'bottom-start', whileElementsMounted: autoUpdate, middleware: [ - offset( size === 'sm' ? 4 : 6 ), + offset( offsetValue ), flip( { padding: 10 } ), floatingSize( { apply( { rects, elements, availableHeight } ) { From 6fa2bafdad13f40f232eeffc82ded6e7bb88c75c Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:41:48 +0600 Subject: [PATCH 35/41] Update offset to match the Figma mockup --- src/components/search/search.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 6e00b555..696f9d98 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -204,27 +204,13 @@ export const SearchBox = forwardRef( return _onOpenChange; }, [ setOpen, _onOpenChange ] ); - // Offset value for the dropdown based on the size. - const offsetValue = useMemo( () => { - switch ( size ) { - case 'sm': - return 4; - case 'md': - return 6; - case 'lg': - return 8; - default: - return 6; - } - }, [ size ] ); - const { refs, floatingStyles, context } = useFloating( { open, onOpenChange, placement: 'bottom-start', whileElementsMounted: autoUpdate, middleware: [ - offset( offsetValue ), + offset( 2 ), flip( { padding: 10 } ), floatingSize( { apply( { rects, elements, availableHeight } ) { From 6e8447266ec6567d83f0467ab923190029819745 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 16:02:34 +0600 Subject: [PATCH 36/41] Show keyboard shortcut based on OS --- src/components/search/search.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 696f9d98..3ec676a6 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -445,10 +445,11 @@ export const SearchBoxInput = forwardRef( ] ) } />
); From 2ca5303afd956ee77f4051ab495a282046ee5069 Mon Sep 17 00:00:00 2001 From: Vrunda Kansara Date: Fri, 14 Mar 2025 16:24:26 +0530 Subject: [PATCH 37/41] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index a329c76b..d36f6da2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,5 @@ Version 1.4.3 - xth xxxx, 2025 -- New - Added new Atom, File Uploader - to show uploaded file preview. +- New - Added new Atom, File Preview - to show uploaded file preview. - New - Added new variants in Progress Steps component to show icon and number in the completed step. - Improvement - Enhanced the UI and functionality of the Searchbox component for better flexibility and user experience. From c5f75503a32a19584b015db6e3d92cb5e5efcf20 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:01:34 +0600 Subject: [PATCH 38/41] Update search.stories.tsx --- src/components/search/search.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/search.stories.tsx b/src/components/search/search.stories.tsx index fd16c7f2..f6806613 100644 --- a/src/components/search/search.stories.tsx +++ b/src/components/search/search.stories.tsx @@ -122,6 +122,6 @@ DisabledSearchBox.decorators = [ export const LoadingSearchBox = Template.bind( {} ); LoadingSearchBox.args = { - open: true, + open: false, loading: true, }; From 323e0e6fe1dc8ce6c830210e00944be7ad3c0a1e Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:14:44 +0600 Subject: [PATCH 39/41] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d36f6da2..51acbb05 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,4 @@ -Version 1.4.3 - xth xxxx, 2025 +Version 1.5.0 - 14th March, 2025 - New - Added new Atom, File Preview - to show uploaded file preview. - New - Added new variants in Progress Steps component to show icon and number in the completed step. - Improvement - Enhanced the UI and functionality of the Searchbox component for better flexibility and user experience. From d1c2d36a7585b5fd9f5362b2c4835ef1482fb369 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:15:42 +0600 Subject: [PATCH 40/41] Bump version to `1.5.0` --- README.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- version.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 99059028..dfd305e1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Using Force UI as a dependency in package.json - ```json "dependencies": { - "@bsf/force-ui": "git+https://github.com/brainstormforce/force-ui#1.4.2" + "@bsf/force-ui": "git+https://github.com/brainstormforce/force-ui#1.5.0" } ``` @@ -28,7 +28,7 @@ npm install Or you can directly run the following command to install the package - ```bash -npm i -S @bsf/force-ui@git+https://github.com/brainstormforce/force-ui.git#1.4.2 +npm i -S @bsf/force-ui@git+https://github.com/brainstormforce/force-ui.git#1.5.0 ```
diff --git a/package-lock.json b/package-lock.json index 5cf010f5..cbdaf511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bsf/force-ui", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bsf/force-ui", - "version": "1.4.2", + "version": "1.5.0", "license": "ISC", "dependencies": { "@emotion/is-prop-valid": "^1.3.0", diff --git a/package.json b/package.json index f5610754..5192e615 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bsf/force-ui", - "version": "1.4.2", + "version": "1.5.0", "description": "Library of components for the BSF project", "main": "./dist/force-ui.cjs.js", "module": "./dist/force-ui.es.js", diff --git a/version.json b/version.json index 991287a5..3b7733d0 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "force-ui": "1.4.2" + "force-ui": "1.5.0" } From 2a82fd68011352019eebb352795a8ba874827b2f Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:11:52 +0600 Subject: [PATCH 41/41] Enhance FilePreview story by adding file input reference and resetting value on file removal --- src/components/file-preview/file-preview.stories.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/file-preview/file-preview.stories.tsx b/src/components/file-preview/file-preview.stories.tsx index d7901548..e1e022b2 100644 --- a/src/components/file-preview/file-preview.stories.tsx +++ b/src/components/file-preview/file-preview.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { FilePreview, FilePreviewFile, FilePreviewProps } from './file-preview'; import { Input } from '@/index'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; type InputValue = string | FileList | null; @@ -65,6 +65,7 @@ DisabledState.args = { }; export const FileInputWithPreview: Story = ( args ) => { + const fileInputRef = useRef( null ); const [ selectedFile, setSelectedFile ] = useState( null ); const handleFileChange = ( value: InputValue ) => { @@ -85,6 +86,7 @@ export const FileInputWithPreview: Story = ( args ) => { return ( <> { setSelectedFile( null ) } + onRemove={ () => { + setSelectedFile( null ); + if ( fileInputRef.current ) { + fileInputRef.current.value = ''; + } + } } />
) }