Skip to content

Commit

Permalink
Merge pull request #10 from miksrv/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
miksrv authored Oct 4, 2024
2 parents e1eb6bd + eb90472 commit 5386fc6
Show file tree
Hide file tree
Showing 30 changed files with 1,264 additions and 168 deletions.
Binary file modified .yarn/install-state.gz
Binary file not shown.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# simple-react-ui-kit

## 1.1.0

### Minor Changes

- Added new UI Components: Popout, Dropdown, Skeleton, Table
- Added new icons: BarChart, Chart, Download, ArrowUp, ArrowDown
- Added story for UI Components: Dropdown, Popout, Skeleton, Table
- Improved Button UI Component styles
- Improved icons story for storybook
- Improved global styles variables
- Dependent libraries updated
- Renamed "GPS" icons to "Position"
- Renamed "Address" icons to "AddressSign"
- Renamed icons "Dark" -> "Moon", "Light" -> "Sun"
- Removed icons Down, Up, LeftLarge

## 1.0.3

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ export default [
// Forbid import of modules using absolute paths
'import/no-absolute-path': 'error',
// forbid default exports - we want to standardize on named exports so that imported names are consistent
'import/no-default-export': 'error',
// 'import/no-default-export': 'error',
// disallow imports from duplicate paths
'import/no-duplicates': 'error',
// Forbid the use of extraneous packages
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simple-react-ui-kit",
"version": "1.0.3",
"version": "1.1.0",
"description": "My small UI framework for projects",
"repository": "https://github.com/miksrv/simple-react-ui-kit.git",
"scripts": {
Expand Down Expand Up @@ -31,19 +31,19 @@
"author": "Misha Topchilo <[email protected]>",
"license": "ISC",
"devDependencies": {
"@changesets/cli": "^2.27.8",
"@eslint/compat": "^1.1.1",
"@changesets/cli": "^2.27.9",
"@eslint/compat": "^1.2.0",
"@eslint/js": "^9.11.1",
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.10",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"eslint": "^9.11.1",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-eslint-plugin": "^6.2.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/components/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const Button: React.FC<ButtonProps> = ({
)}
>
{loading ? <Spinner className={styles.loader} /> : icon && <Icon name={icon} />}
<div>{label?.length ? label : children}</div>
{label?.length ? label : children}
</button>
)

Expand Down
13 changes: 13 additions & 0 deletions src/components/button/styles.module.sass
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
color: var(--button-default-color)
background: var(--button-default-background)
border-radius: var(--border-radius)
text-wrap: nowrap
cursor: pointer
padding: 6px 10px
border: 0
Expand Down Expand Up @@ -82,6 +83,9 @@
background-color: var(--color-red)
color: var(--color-contrast)

svg
fill: var(--color-red)

&:hover, &:focus
background-color: var(--color-red-hover)

Expand All @@ -92,6 +96,9 @@
background-color: var(--color-green)
color: var(--color-contrast)

svg
fill: var(--color-green)

&:hover, &:focus
background-color: var(--color-green-hover)

Expand All @@ -113,6 +120,9 @@
&.negative
color: var(--color-red)

svg
fill: var(--color-red)

&:hover, &:focus
color: var(--color-red-hover)

Expand All @@ -122,6 +132,9 @@
&.positive
color: var(--color-green)

svg
fill: var(--color-green)

&:hover, &:focus
color: var(--color-green-hover)

Expand Down
184 changes: 184 additions & 0 deletions src/components/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React, { useEffect, useRef, useState } from 'react'

import { cn } from '../../utils'
import Button, { ButtonProps } from '../button'
import Icon, { IconTypes } from '../icon'

import OptionsList from './OptionsList'
import styles from './styles.module.sass'

/**
* Dropdown option type, representing a single option in the dropdown list
*/
export type DropdownOption<T> = {
/** Unique key to identify the option */
key: T
/** Display value of the option (can be text, number, or a React node) */
value: React.ReactNode | string | number
/** Optional icon to display next to the option */
icon?: IconTypes
/** Optional image to display next to the option */
image?: MediaImage
/** Disable the option from being selected */
disabled?: boolean
}

/**
* Dropdown component properties
*/
export interface DropdownProps<T> extends Pick<ButtonProps, 'size' | 'mode'> {
/** Additional class names for custom styling */
className?: string
/** Array of options to display in the dropdown */
options?: DropdownOption<T>[]
/** Mark the dropdown as required */
required?: boolean
/** Disable the dropdown */
disabled?: boolean
/** Whether the dropdown can be cleared (reset to no selection) */
clearable?: boolean
/** Placeholder text to display when no option is selected */
placeholder?: string
/** Label text for the dropdown */
label?: string
/** Error message to display when validation fails */
error?: string
/** Current selected value (key) in the dropdown */
value?: T
/** Callback function triggered when an option is selected */
onSelect?: (selectedOption: DropdownOption<T> | undefined) => void
/** Callback function triggered when the dropdown is opened */
onOpen?: () => void
}

const Dropdown = <T,>({
className,
required,
options,
disabled,
clearable,
value,
placeholder,
label,
error,
onSelect,
onOpen,
...props
}: DropdownProps<T>) => {
const dropdownRef = useRef<HTMLDivElement>(null)
const [optionsListTop, setOptionsListTop] = useState<number>(30)
const [isOpen, setIsOpen] = useState<boolean>(false)
const [selectedOption, setSelectedOption] = useState<DropdownOption<T> | undefined>(undefined)

const toggleDropdown = () => {
if (onOpen) {
onOpen()
} else {
setIsOpen(!isOpen)
}
}

const handleSelect = (option: DropdownOption<T> | undefined) => {
if (selectedOption?.key !== option?.key) {
setSelectedOption(option)
onSelect?.(option ?? undefined)
}

setIsOpen(false)
}

const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}

const handleClearClick = (event: React.MouseEvent) => {
event.stopPropagation()
handleSelect(undefined)
}

useEffect(() => {
document.addEventListener('mousedown', handleClickOutside)

setOptionsListTop(dropdownRef?.current?.clientHeight ?? 0)

return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])

useEffect(() => {
if (!value) {
setSelectedOption(undefined)
} else {
const selected = options?.find((opt) => opt.key === value)
setSelectedOption(selected)
}
}, [value, options])

return (
<div className={cn(className, styles.dropdown, required && styles.required, disabled && styles.disabled)}>
{label && <label className={styles.label}>{label}</label>}
<div
ref={dropdownRef}
className={cn(styles.container, isOpen && styles.open, disabled && styles.disabled)}
>
<Button
size={props?.size ?? 'small'}
mode={props?.mode ?? 'secondary'}
variant={error ? 'negative' : undefined}
disabled={disabled}
onClick={toggleDropdown}
className={cn(styles.dropdownButton, selectedOption && styles.selected, isOpen && styles.open)}
>
<span className={styles.value}>
{selectedOption?.icon && <Icon name={selectedOption.icon} />}

{selectedOption?.image && (
<img
className={styles.categoryIcon}
src={selectedOption.image.src}
alt={''}
width={22}
height={26}
/>
)}

{selectedOption?.value ? (
selectedOption?.value
) : (
<span className={styles.placeholder}>{placeholder ?? ''}</span>
)}
</span>

<span className={styles.buttonContainer}>
{clearable && selectedOption?.key && (
<button
type={'button'}
className={styles.clearButton}
onClick={handleClearClick}
>
<Icon name={'Close'} />
</button>
)}
{isOpen ? <Icon name={'KeyboardUp'} /> : <Icon name={'KeyboardDown'} />}
</span>
</Button>

{!!error?.length && <div className={styles.error}>{error}</div>}

{isOpen && (
<OptionsList<T>
style={{ top: optionsListTop }}
options={options}
selectedOption={selectedOption}
onOptionSelect={handleSelect}
/>
)}
</div>
</div>
)
}

export default Dropdown
44 changes: 44 additions & 0 deletions src/components/dropdown/OptionsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'

import { cn } from '../../utils'
import Icon from '../icon'

import type { DropdownOption } from './Dropdown'
import styles from './styles.module.sass'

interface DropdownProps<T> extends React.HTMLAttributes<HTMLUListElement> {
options?: DropdownOption<T>[]
selectedOption?: DropdownOption<T>
onOptionSelect?: (selectedOption: DropdownOption<T>) => void
}

const OptionsList = <T,>({ selectedOption, options, onOptionSelect, ...props }: DropdownProps<T>) => (
<ul
className={styles.optionsList}
style={props.style}
>
{options?.map((option, i) => (
<li
key={`option-${option.key ?? i}`}
className={cn(option.key === selectedOption?.key && styles.active, option.disabled && styles.disabled)}
>
<button onClick={() => (!option.disabled ? onOptionSelect?.(option) : undefined)}>
{option?.icon && <Icon name={option.icon} />}

{option?.image && (
<img
src={option.image.src}
alt={''}
width={22}
height={26}
/>
)}

<span>{option.value}</span>
</button>
</li>
))}
</ul>
)

export default OptionsList
2 changes: 2 additions & 0 deletions src/components/dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './Dropdown'
export type { DropdownOption, DropdownProps } from './Dropdown'
Loading

0 comments on commit 5386fc6

Please sign in to comment.