From 14c8e8e6f98e20ff5b03517e6c3744d0385216d6 Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 01:32:27 +0200 Subject: [PATCH 01/12] feat: initial chip component implementation --- src/design-system/components/chip/Chip.scss | 119 ++++++++++ src/design-system/components/chip/Chip.tsx | 185 ++++++++++++++++ src/design-system/stories/Chip.stories.tsx | 232 ++++++++++++++++++++ 3 files changed, 536 insertions(+) create mode 100644 src/design-system/components/chip/Chip.scss create mode 100644 src/design-system/components/chip/Chip.tsx create mode 100644 src/design-system/stories/Chip.stories.tsx diff --git a/src/design-system/components/chip/Chip.scss b/src/design-system/components/chip/Chip.scss new file mode 100644 index 000000000..4f303df14 --- /dev/null +++ b/src/design-system/components/chip/Chip.scss @@ -0,0 +1,119 @@ +@use '~/scss/utilities' as *; + +.chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--#{$prefix}space-0-5) var(--#{$prefix}space-1); + border: none; + border-radius: var(--#{$prefix}border-radius-md); + font-family: var(--#{$prefix}font-family-rubik); + cursor: pointer; + border-width: var(--#{$prefix}border-width-md); + + &--sm { + font-size: var(--#{$prefix}font-size-sm); + line-height: var(--#{$prefix}line-height-sm); + } + + &--md { + font-size: var(--#{$prefix}font-size-md); + line-height: var(--#{$prefix}line-height-md); + } + + &--lg { + font-size: var(--#{$prefix}font-size-lg); + line-height: var(--#{$prefix}line-height-lg); + } + + &.disabled { + opacity: 0.7; + cursor: not-allowed; + } + + &--filter, + &--input { + &.filled { + background-color: var(--#{$prefix}blue-gray-50); + color: var(--#{$prefix}blue-gray-800); + + &:hover { + box-shadow: var(--#{$prefix}box-shadow-xs); + } + + &:focus { + background-color: var(--#{$prefix}blue-gray-100); + } + + &.disabled { + opacity: 1; + color: var(--#{$prefix}blue-gray-500); + } + } + + &.outlined, + &.filled-outlined { + background-color: var(--#{$prefix}neutral-0); + color: var(--#{$prefix}blue-gray-800); + border: solid var(--#{$prefix}border-width-md) + var(--#{$prefix}blue-gray-400); + + &.disabled { + opacity: 1; + color: var(--#{$prefix}blue-gray-500); + border-color: var(--#{$prefix}blue-gray-200); + } + } + + &.filled-outlined { + background-color: var(--#{$prefix}blue-gray-50); + } + } + + &--input { + font-weight: var(--#{$prefix}font-weight-medium); + } + + &--categories { + display: inline-flex; + gap: var(--#{$prefix}space-0-5); + } + + &--category { + color: var(--chip-text-color); + background-color: var(--chip-bg-color); + font-weight: var(--#{$prefix}font-weight-regular); + letter-spacing: var(--#{$prefix}$letter-spacing-xl); + text-transform: uppercase; + } + + &--state { + background-color: var(--chip-bg-color); + border-style: solid; + border-color: var(--chip-border-color); + color: var(--chip-text-color); + font-weight: var(--#{$prefix}font-weight-regular); + letter-spacing: var(--#{$prefix}letter-spacing-xl); + text-transform: uppercase; + } + + .startIcon, + .endIcon { + display: flex; + align-items: center; + justify-content: start; + font-size: inherit; + } + + .startIcon { + margin-right: var(--#{$prefix}space-1); + } + + .endIcon { + margin-left: var(--#{$prefix}space-1); + } + + .label { + padding: var(--#{$prefix}space-0-5); + } +} diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx new file mode 100644 index 000000000..1549be586 --- /dev/null +++ b/src/design-system/components/chip/Chip.tsx @@ -0,0 +1,185 @@ +import React, { CSSProperties } from 'react' +import classNames from 'classnames' +import CircleIcon from '@mui/icons-material/Circle' +import CloseRoundedIcon from '@mui/icons-material/CloseRounded' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' + +import './Chip.scss' + +type ChipType = 'filter' | 'input' | 'category' | 'state' + +type BaseChipProps = { + type: ChipType + size?: 'sm' | 'md' | 'lg' + color?: string + disabled?: boolean +} + +type FilterChipProps = BaseChipProps & { + type: 'filter' + label: string + variant?: 'filled' | 'minimal' + startIcon?: React.ReactNode + endIcon?: React.ReactNode + disabled?: boolean +} + +type InputChipProps = BaseChipProps & { + type: 'input' + label: string + variant?: 'filled' | 'outlined' | 'filled-outlined' + startIcon?: React.ReactNode + endIcon?: React.ReactNode + disabled?: boolean +} + +type CategoryChipProps = BaseChipProps & { + type: 'category' + subject: string + level: string +} + +type StateChipProps = BaseChipProps & { + type: 'state' + label: string + startIcon?: React.ReactNode +} + +export type ChipProps = + | FilterChipProps + | InputChipProps + | CategoryChipProps + | StateChipProps + +type CustomStyle = CSSProperties & { + [key: `--chip-${string}`]: string | undefined +} + +const FilterChip: React.FC = ({ + label, + variant = 'filled', + startIcon = , + endIcon = , + disabled = false, + size = 'md' +}) => { + const classes = classNames('chip', `chip--${size}`, `chip--filter`, variant, { + disabled + }) + return ( +
+ {startIcon && {startIcon}} + {label} + {endIcon && {endIcon}} +
+ ) +} + +const InputChip: React.FC = ({ + label, + variant = 'outlined', + startIcon = , + endIcon = , + disabled = false, + size = 'md' +}) => { + const classes = classNames('chip', `chip--${size}`, `chip--input`, variant, { + disabled + }) + return ( +
+ {startIcon && {startIcon}} + {label} + {endIcon && {endIcon}} +
+ ) +} + +const CategoryChip: React.FC = ({ + subject, + level, + size = 'md', + color = 'blue-gray', + disabled = false +}) => { + const subjectStyle: CustomStyle = { + '--chip-text-color': `var(--s2s-${color}-900)`, + '--chip-bg-color': `var(--s2s-${color}-300)` + } + + const levelStyle: CustomStyle = { + '--chip-text-color': `var(--s2s-${color}-900)`, + '--chip-bg-color': `var(--s2s-${color}-100)` + } + + return ( +
+
+ {subject} +
+
+ {level} +
+
+ ) +} + +const StateChip: React.FC = ({ + label, + startIcon = , + size = 'md', + color = 'blue-gray', + disabled = false +}) => { + const style: CustomStyle = { + '--chip-bg-color': `var(--s2s-${color}-100)`, + '--chip-text-color': `var(--s2s-${color}-700)`, + '--chip-border-color': `var(--s2s-${color}-700)` + } + return ( +
+ {startIcon && {startIcon}} + {label} +
+ ) +} + +const Chip: React.FC = (props) => { + switch (props.type) { + case 'filter': + return + case 'input': + return + case 'category': + return + case 'state': + return + default: + return null + } +} + +export default Chip diff --git a/src/design-system/stories/Chip.stories.tsx b/src/design-system/stories/Chip.stories.tsx new file mode 100644 index 000000000..1e9a7f417 --- /dev/null +++ b/src/design-system/stories/Chip.stories.tsx @@ -0,0 +1,232 @@ +import type { Meta } from '@storybook/react' +import Chip, { type ChipProps } from '~/design-system/components/chip/Chip' +export {} + +const meta: Meta = { + title: 'Components/Chip', + component: Chip, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: ` +The \`Chip\` component is a versatile and stylish UI element designed to display contextual information, represent actions, or categorize content in your application. +With multiple types, sizes, and visual states, it offers flexibility to suit various design and functional requirements while ensuring a consistent user experience. + +#### Key Features: +- **Variants:** Choose between different types of chips, such as \`filter\`, \`input\`, \`category\`, or \`state\`, each tailored to specific use cases. +- **Chip Types:** + - **\`filter\`:** Used for filtering actions, with options like \`filled\` or \`minimal\` variants. + - **\`input\`:** Ideal for user input scenarios, supporting \`filled\`, \`outlined\`, and \`filled-outlined\` variants. + - **\`category\`:** Designed to display categories with a \`subject\` and \`level\`, allowing for clear categorization of content. + - **\`state\`:** Represents status or state, typically used to indicate the condition of an item (e.g., active, pending). +- **Sizes:** Select from \`sm\`, \`md\`, or \`lg\` sizes to match the chip to the context—whether you need a compact tag or a larger, more prominent element. +- **Icons:** Enhance the chip’s appearance with customizable \`startIcon\` and \`endIcon\` options to provide additional visual context or interactivity. +- **Colors:** Use color options (\`red\`, \`yellow\` etc.) to align the chip -with your application's theme, or customize the color based on your design needs. +- **Disabled State:** The chip can be disabled to prevent user interaction, providing clear feedback to the user when an action is unavailable. + +### Chip Types and Props: + +0. **Shared Props**: +- **type**: Defines the type of chip. Available values: \`filter\`, \`input\`, \`category\`, \`state\`. +- **size**: Controls the size of the chip (\`sm\`, \`md\`, \`lg\`). +- **color**: Customizes the chip's color (\`string\`). +- **disabled**: Makes the chip non-interactive (\`boolean\`). +1. **Filter Chip** (\`type: 'filter'\`): + - **label**: The text displayed inside the chip (\`string\`). + - **variant**: Defines the visual style (\`filled\` or \`minimal\`). + - **startIcon**: An optional icon before the label (\`ReactNode\`). + - **endIcon**: An optional icon after the label (\`ReactNode\`). + - **disabled**: Makes the chip non-interactive (\`boolean\`). +2. **Input Chip** (\`type: 'input'\`): + - **label**: The text displayed inside the chip (\`string\`). + - **variant**: Supports \`filled\`, \`outlined\`, and \`filled-outlined\` styles. + - **startIcon**: An optional icon before the label (\`ReactNode\`). + - **endIcon**: An optional icon after the label (\`ReactNode\`). + - **disabled**: Makes the chip non-interactive. +3. **Category Chip** (\`type: 'category'\`): + - **subject**: Represents the main category name (\`string\`). + - **level**: Indicates the category level or subcategory (\`string\`). +4. **State Chip** (\`type: 'state'\`): + - **label**: The text displayed inside the chip (\`string\`). + - **startIcon**: An optional icon before the label (\`ReactNode\`). + ` + } + } + } +} + +export default meta + +export const FilterChip = (args: ChipProps) => +FilterChip.args = { + type: 'filter', + size: 'md', + // color: 'blue-gray', + label: 'Filter Chip', + variant: 'filled', + disabled: false +} +FilterChip.argTypes = { + type: { + control: { type: 'select' }, + options: ['filter'], + description: 'Filter type.' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md', 'lg'], + description: 'Size of the chip.', + table: { defaultValue: { summary: 'md' } } + }, + // color: { + // control: { type: 'text' }, + // description: 'Color of the chip.', + // table: { defaultValue: { summary: 'blue-gray' } } + // }, + label: { + control: { type: 'text' }, + description: 'Label text displayed on the chip.' + }, + variant: { + control: { type: 'const' }, + options: ['filled', 'minimal'], + description: 'Visual style of the chip.', + table: { defaultValue: { summary: 'filled' } } + }, + startIcon: { + control: { type: 'ReactNode' }, + description: 'Icon displayed at the start of the chip.' + }, + endIcon: { + control: { type: 'ReactNode' }, + description: 'Icon displayed at the end of the chip.' + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disables the chip if true.', + table: { defaultValue: { summary: false } } + } +} + +export const InputChip = (args: ChipProps) => +InputChip.args = { + type: 'input', + size: 'lg', + // color: 'blue-gray', + label: 'Input Chip', + variant: 'outlined', + disabled: false +} +InputChip.argTypes = { + type: { + control: { type: 'select' }, + options: ['input'], + description: 'Input type.' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md', 'lg'], + description: 'Size of the chip.' + }, + // color: { + // control: { type: 'text' }, + // description: 'Color of the chip.' + // }, + label: { + control: { type: 'text' }, + description: 'Label text displayed on the chip.' + }, + variant: { + control: { type: 'select' }, + options: ['filled', 'outlined', 'filled-outlined'], + description: 'Visual style of the chip.' + }, + startIcon: { + control: { type: 'object' }, + description: 'Icon displayed at the start of the chip.' + }, + endIcon: { + control: { type: 'object' }, + description: 'Icon displayed at the end of the chip.' + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disables the chip if true.' + } +} + +export const CategoryChip = (args: ChipProps) => +CategoryChip.args = { + type: 'category', + size: 'md', + color: 'purple', + subject: 'Astronomy', + level: 'Advanced', + disabled: false +} +CategoryChip.argTypes = { + type: { + control: { type: 'select' }, + options: ['category'], + description: 'Category type.' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md', 'lg'], + description: 'Size of the chip.' + }, + color: { + control: { type: 'text' }, + description: 'Color of the chip.' + }, + subject: { + control: { type: 'text' }, + description: 'Primary text displayed in the subject field.' + }, + level: { + control: { type: 'text' }, + description: 'Secondary text indicating the level or category.' + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disables the chip if true.' + } +} + +export const StateChip = (args: ChipProps) => +StateChip.args = { + type: 'state', + size: 'sm', + color: 'green', + label: 'Active', + disabled: false +} +StateChip.argTypes = { + type: { + control: { type: 'select' }, + options: ['state'], + description: 'State type.' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md', 'lg'], + description: 'Size of the chip.' + }, + color: { + control: { type: 'text' }, + description: 'Color of the chip.' + }, + label: { + control: { type: 'text' }, + description: 'Label text displayed on the chip.' + }, + startIcon: { + control: { type: 'object' }, + description: 'Icon displayed at the start of the chip.' + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disables the chip if true.' + } +} From a1ff4b2320fc577b9c052060f77f3c9e82413fbf Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 18:06:26 +0200 Subject: [PATCH 02/12] fix: edit filter chip and refactor --- src/design-system/components/chip/Chip.scss | 109 ++++++++++++--- src/design-system/components/chip/Chip.tsx | 146 +++++++++++++------- src/design-system/stories/Chip.stories.tsx | 85 +++++++----- 3 files changed, 230 insertions(+), 110 deletions(-) diff --git a/src/design-system/components/chip/Chip.scss b/src/design-system/components/chip/Chip.scss index 4f303df14..58e9ee5e8 100644 --- a/src/design-system/components/chip/Chip.scss +++ b/src/design-system/components/chip/Chip.scss @@ -1,6 +1,7 @@ @use '~/scss/utilities' as *; .chip { + margin: 5px; display: inline-flex; align-items: center; justify-content: center; @@ -31,25 +32,97 @@ cursor: not-allowed; } - &--filter, - &--input { + &--filter { + position: relative; + &.filled { - background-color: var(--#{$prefix}blue-gray-50); - color: var(--#{$prefix}blue-gray-800); + &.selected { + background-color: var(--#{$prefix}blue-gray-100); + } + + &.unselected { + background-color: var(--#{$prefix}blue-gray-50); + } + } + + &.minimal { + color: var(--#{$prefix}blue-gray-600); &:hover { - box-shadow: var(--#{$prefix}box-shadow-xs); + color: var(--#{$prefix}blue-gray-800); } - &:focus { - background-color: var(--#{$prefix}blue-gray-100); + &.selected, + &.unselected { + background-color: var(--#{$prefix}neutral-0); + } + } + + &.minimal, + &.filled { + &:hover { + box-shadow: var(--#{$prefix}box-shadow-xs); } &.disabled { opacity: 1; color: var(--#{$prefix}blue-gray-500); } + + &.selected { + color: var(--#{$prefix}blue-gray-800); + + &:hover { + background-color: var(--#{$prefix}blue-gray-100); + } + + &:active { + background-color: var(--#{$prefix}blue-gray-200); + transition: background-color 0.2s; + } + } + + &.unselected { + &:hover { + background-color: var(--#{$prefix}blue-gray-50); + color: var(--#{$prefix}blue-gray-800); + } + + &:active { + background-color: var(--#{$prefix}blue-gray-100); + transition: background-color 0.2s; + } + } + } + + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + background-color: white; + border: 1px solid var(--#{$prefix}blue-gray-300); + border-radius: var(--#{$prefix}space-1); + padding: var(--#{$prefix}space-1) 0; + list-style: none; + margin: 0; + width: 100%; + z-index: 10; + box-shadow: var(--#{$prefix}box-shadow-xs); + + .dropdown-item { + padding: var(--#{$prefix}space-1) var(--#{$prefix}space-2); + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: var(--#{$prefix}blue-gray-100); + } + } } + } + + &--input { + font-weight: var(--#{$prefix}font-weight-medium); &.outlined, &.filled-outlined { @@ -70,38 +143,28 @@ } } - &--input { - font-weight: var(--#{$prefix}font-weight-medium); - } - &--categories { display: inline-flex; gap: var(--#{$prefix}space-0-5); } - &--category { - color: var(--chip-text-color); - background-color: var(--chip-bg-color); + &--category, + &--state { font-weight: var(--#{$prefix}font-weight-regular); - letter-spacing: var(--#{$prefix}$letter-spacing-xl); + letter-spacing: var(--#{$prefix}letter-spacing-xl); text-transform: uppercase; + background-color: var(--chip-bg-color); + color: var(--chip-text-color); } &--state { - background-color: var(--chip-bg-color); - border-style: solid; - border-color: var(--chip-border-color); - color: var(--chip-text-color); - font-weight: var(--#{$prefix}font-weight-regular); - letter-spacing: var(--#{$prefix}letter-spacing-xl); - text-transform: uppercase; + border: solid 1px var(--chip-border-color); } .startIcon, .endIcon { display: flex; align-items: center; - justify-content: start; font-size: inherit; } diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index 1549be586..011db231b 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties } from 'react' +import React, { CSSProperties, useState } from 'react' import classNames from 'classnames' import CircleIcon from '@mui/icons-material/Circle' import CloseRoundedIcon from '@mui/icons-material/CloseRounded' @@ -6,43 +6,50 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import './Chip.scss' +type ChipContentProps = { + label: string + startIcon?: React.ReactNode + endIcon?: React.ReactNode +} + type ChipType = 'filter' | 'input' | 'category' | 'state' -type BaseChipProps = { +type BaseChipProps = ChipContentProps & { type: ChipType size?: 'sm' | 'md' | 'lg' - color?: string disabled?: boolean } type FilterChipProps = BaseChipProps & { type: 'filter' - label: string + options: string[] variant?: 'filled' | 'minimal' - startIcon?: React.ReactNode - endIcon?: React.ReactNode - disabled?: boolean } type InputChipProps = BaseChipProps & { type: 'input' - label: string variant?: 'filled' | 'outlined' | 'filled-outlined' - startIcon?: React.ReactNode - endIcon?: React.ReactNode - disabled?: boolean } +type ChipColor = + | 'blue-gray' + | 'turquoise' + | 'blue' + | 'green' + | 'yellow' + | 'purple' + | 'red' + | 'neutral' + type CategoryChipProps = BaseChipProps & { type: 'category' - subject: string - level: string + detail: string + color?: ChipColor } type StateChipProps = BaseChipProps & { type: 'state' - label: string - startIcon?: React.ReactNode + color?: ChipColor } export type ChipProps = @@ -55,22 +62,74 @@ type CustomStyle = CSSProperties & { [key: `--chip-${string}`]: string | undefined } +const ChipContent: React.FC = ({ + label, + startIcon, + endIcon +}) => ( + <> + {startIcon && {startIcon}} + {label} + {endIcon && {endIcon}} + +) + const FilterChip: React.FC = ({ label, + options = [], variant = 'filled', startIcon = , endIcon = , disabled = false, size = 'md' }) => { - const classes = classNames('chip', `chip--${size}`, `chip--filter`, variant, { - disabled - }) + const [isOpen, setIsOpen] = useState(false) + const [selectedOption, setSelectedOption] = useState(null) + + const handleSelect = (option: string) => { + setSelectedOption(option) + setIsOpen(false) + } + + const isSelected = !!selectedOption + + const classes = classNames( + 'chip', + `chip--${size}`, + `chip--filter`, + variant, + isSelected ? 'selected' : 'unselected', + { + disabled + } + ) return ( -
- {startIcon && {startIcon}} - {label} - {endIcon && {endIcon}} +
!disabled && setIsOpen((prev) => !prev)} + tabIndex={0} + > + + {isOpen && ( +
    + {options.map((option) => ( +
  • { + handleSelect(option) + setIsOpen((prev) => !prev) + }} + > + {option} +
  • + ))} +
+ )}
) } @@ -88,26 +147,24 @@ const InputChip: React.FC = ({ }) return (
- {startIcon && {startIcon}} - {label} - {endIcon && {endIcon}} +
) } const CategoryChip: React.FC = ({ - subject, - level, + label, + detail, size = 'md', color = 'blue-gray', disabled = false }) => { - const subjectStyle: CustomStyle = { + const labelStyle: CustomStyle = { '--chip-text-color': `var(--s2s-${color}-900)`, '--chip-bg-color': `var(--s2s-${color}-300)` } - const levelStyle: CustomStyle = { + const detailStyle: CustomStyle = { '--chip-text-color': `var(--s2s-${color}-900)`, '--chip-bg-color': `var(--s2s-${color}-100)` } @@ -115,28 +172,20 @@ const CategoryChip: React.FC = ({ return (
- {subject} +
- {level} +
) @@ -161,8 +210,7 @@ const StateChip: React.FC = ({ })} style={style} > - {startIcon && {startIcon}} - {label} +
) } diff --git a/src/design-system/stories/Chip.stories.tsx b/src/design-system/stories/Chip.stories.tsx index 1e9a7f417..abfd74222 100644 --- a/src/design-system/stories/Chip.stories.tsx +++ b/src/design-system/stories/Chip.stories.tsx @@ -16,13 +16,12 @@ With multiple types, sizes, and visual states, it offers flexibility to suit var #### Key Features: - **Variants:** Choose between different types of chips, such as \`filter\`, \`input\`, \`category\`, or \`state\`, each tailored to specific use cases. - **Chip Types:** - - **\`filter\`:** Used for filtering actions, with options like \`filled\` or \`minimal\` variants. + - **\`filter\`:** Used for filtering actions, where users can select from a dropdown list of options, with \`filled\` or \`minimal\` styles. - **\`input\`:** Ideal for user input scenarios, supporting \`filled\`, \`outlined\`, and \`filled-outlined\` variants. - - **\`category\`:** Designed to display categories with a \`subject\` and \`level\`, allowing for clear categorization of content. + - **\`category\`:** Displays categorized items, each with a descriptive label and detailed information. - **\`state\`:** Represents status or state, typically used to indicate the condition of an item (e.g., active, pending). - **Sizes:** Select from \`sm\`, \`md\`, or \`lg\` sizes to match the chip to the context—whether you need a compact tag or a larger, more prominent element. - **Icons:** Enhance the chip’s appearance with customizable \`startIcon\` and \`endIcon\` options to provide additional visual context or interactivity. -- **Colors:** Use color options (\`red\`, \`yellow\` etc.) to align the chip -with your application's theme, or customize the color based on your design needs. - **Disabled State:** The chip can be disabled to prevent user interaction, providing clear feedback to the user when an action is unavailable. ### Chip Types and Props: @@ -30,26 +29,19 @@ With multiple types, sizes, and visual states, it offers flexibility to suit var 0. **Shared Props**: - **type**: Defines the type of chip. Available values: \`filter\`, \`input\`, \`category\`, \`state\`. - **size**: Controls the size of the chip (\`sm\`, \`md\`, \`lg\`). -- **color**: Customizes the chip's color (\`string\`). +- **label**: The text displayed inside the chip (\`string\`). +- **startIcon**: An optional icon before the label (\`ReactNode\`). +- **endIcon**: An optional icon after the label (\`ReactNode\`). - **disabled**: Makes the chip non-interactive (\`boolean\`). 1. **Filter Chip** (\`type: 'filter'\`): - - **label**: The text displayed inside the chip (\`string\`). - **variant**: Defines the visual style (\`filled\` or \`minimal\`). - - **startIcon**: An optional icon before the label (\`ReactNode\`). - - **endIcon**: An optional icon after the label (\`ReactNode\`). - - **disabled**: Makes the chip non-interactive (\`boolean\`). 2. **Input Chip** (\`type: 'input'\`): - - **label**: The text displayed inside the chip (\`string\`). - **variant**: Supports \`filled\`, \`outlined\`, and \`filled-outlined\` styles. - - **startIcon**: An optional icon before the label (\`ReactNode\`). - - **endIcon**: An optional icon after the label (\`ReactNode\`). - - **disabled**: Makes the chip non-interactive. 3. **Category Chip** (\`type: 'category'\`): - - **subject**: Represents the main category name (\`string\`). - - **level**: Indicates the category level or subcategory (\`string\`). + - **detail**: Indicates certain detail about label (\`string\`). + - **color**: Customizes the chip's color (\`string\`). 4. **State Chip** (\`type: 'state'\`): - - **label**: The text displayed inside the chip (\`string\`). - - **startIcon**: An optional icon before the label (\`ReactNode\`). + - **color**: Customizes the chip's color (\`string\`). ` } } @@ -62,8 +54,8 @@ export const FilterChip = (args: ChipProps) => FilterChip.args = { type: 'filter', size: 'md', - // color: 'blue-gray', label: 'Filter Chip', + options: ['Option 1', 'Option 2', 'Option 3'], variant: 'filled', disabled: false } @@ -79,17 +71,17 @@ FilterChip.argTypes = { description: 'Size of the chip.', table: { defaultValue: { summary: 'md' } } }, - // color: { - // control: { type: 'text' }, - // description: 'Color of the chip.', - // table: { defaultValue: { summary: 'blue-gray' } } - // }, + options: { + control: { type: 'array' }, + description: 'List of options for the filter chip', + defaultValue: ['Option 1', 'Option 2', 'Option 3'] + }, label: { control: { type: 'text' }, description: 'Label text displayed on the chip.' }, variant: { - control: { type: 'const' }, + control: { type: 'select' }, options: ['filled', 'minimal'], description: 'Visual style of the chip.', table: { defaultValue: { summary: 'filled' } } @@ -113,7 +105,6 @@ export const InputChip = (args: ChipProps) => InputChip.args = { type: 'input', size: 'lg', - // color: 'blue-gray', label: 'Input Chip', variant: 'outlined', disabled: false @@ -129,10 +120,6 @@ InputChip.argTypes = { options: ['sm', 'md', 'lg'], description: 'Size of the chip.' }, - // color: { - // control: { type: 'text' }, - // description: 'Color of the chip.' - // }, label: { control: { type: 'text' }, description: 'Label text displayed on the chip.' @@ -161,8 +148,8 @@ CategoryChip.args = { type: 'category', size: 'md', color: 'purple', - subject: 'Astronomy', - level: 'Advanced', + label: 'Astronomy', + detail: 'Advanced', disabled: false } CategoryChip.argTypes = { @@ -177,16 +164,27 @@ CategoryChip.argTypes = { description: 'Size of the chip.' }, color: { - control: { type: 'text' }, - description: 'Color of the chip.' + control: { type: 'select' }, + options: [ + 'blue-gray', + 'turquoise', + 'blue', + 'green', + 'yellow', + 'purple', + 'red', + 'neutral' + ], + description: 'Color of the chip.', + table: { defaultValue: { summary: 'blue-gray' } } }, - subject: { + label: { control: { type: 'text' }, - description: 'Primary text displayed in the subject field.' + description: 'Label text displayed on the first chip.' }, - level: { + detail: { control: { type: 'text' }, - description: 'Secondary text indicating the level or category.' + description: 'Secondary text displayed on the second chip.' }, disabled: { control: { type: 'boolean' }, @@ -214,8 +212,19 @@ StateChip.argTypes = { description: 'Size of the chip.' }, color: { - control: { type: 'text' }, - description: 'Color of the chip.' + control: { type: 'select' }, + options: [ + 'blue-gray', + 'turquoise', + 'blue', + 'green', + 'yellow', + 'purple', + 'red', + 'neutral' + ], + description: 'Color of the chip.', + table: { defaultValue: { summary: 'blue-gray' } } }, label: { control: { type: 'text' }, From b261d8b27b1fc0be0c32644253b5707732a347b4 Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 22:04:05 +0200 Subject: [PATCH 03/12] fix: border values --- src/design-system/components/chip/Chip.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/design-system/components/chip/Chip.scss b/src/design-system/components/chip/Chip.scss index 58e9ee5e8..a5cb46136 100644 --- a/src/design-system/components/chip/Chip.scss +++ b/src/design-system/components/chip/Chip.scss @@ -10,7 +10,6 @@ border-radius: var(--#{$prefix}border-radius-md); font-family: var(--#{$prefix}font-family-rubik); cursor: pointer; - border-width: var(--#{$prefix}border-width-md); &--sm { font-size: var(--#{$prefix}font-size-sm); @@ -100,7 +99,8 @@ top: 100%; left: 0; background-color: white; - border: 1px solid var(--#{$prefix}blue-gray-300); + border: var(--#{$prefix}border-width-xs) solid + var(--#{$prefix}blue-gray-300); border-radius: var(--#{$prefix}space-1); padding: var(--#{$prefix}space-1) 0; list-style: none; @@ -128,7 +128,7 @@ &.filled-outlined { background-color: var(--#{$prefix}neutral-0); color: var(--#{$prefix}blue-gray-800); - border: solid var(--#{$prefix}border-width-md) + border: var(--#{$prefix}border-width-sm) solid var(--#{$prefix}blue-gray-400); &.disabled { @@ -158,7 +158,7 @@ } &--state { - border: solid 1px var(--chip-border-color); + border: var(--#{$prefix}border-width-sm) solid var(--chip-border-color); } .startIcon, From ab5dae73753ab122c9daea98874b8dd8a6fae43c Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 22:05:25 +0200 Subject: [PATCH 04/12] feat: add chip tests --- .../design-system/components/Chip.spec.jsx | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/unit/design-system/components/Chip.spec.jsx diff --git a/tests/unit/design-system/components/Chip.spec.jsx b/tests/unit/design-system/components/Chip.spec.jsx new file mode 100644 index 000000000..0a4d4a56b --- /dev/null +++ b/tests/unit/design-system/components/Chip.spec.jsx @@ -0,0 +1,137 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import Chip from '~/design-system/components/chip/Chip' + +// Test for Basic Chip +describe('Chip', () => { + it('checks the basic properties of chip', () => { + render() + + const chip = screen.getByText('Chip').parentElement + expect(chip).toHaveClass('chip--input') + expect(chip).toHaveClass('chip--sm') + expect(chip).toHaveClass('outlined') + }) +}) + +// Test for FilterChip +describe('FilterChip', () => { + it('renders the filter chip with a label', () => { + render( + + ) + + expect(screen.getByText('Filter Chip')).toBeInTheDocument() + }) + + it('opens the dropdown menu when clicked', () => { + render( + + ) + + fireEvent.click(screen.getByText('Filter Chip')) + expect(screen.getByText('Option 1')).toBeInTheDocument() + expect(screen.getByText('Option 2')).toBeInTheDocument() + }) + + it('selects an option when clicked and closes dropdown menu after', () => { + render( + + ) + + fireEvent.click(screen.getByText('Filter Chip')) + fireEvent.click(screen.getByText('Option 1')) + + expect(screen.getByText('Option 1')).toBeInTheDocument() + expect(screen.queryByText('Filter Chip')).not.toBeInTheDocument() + }) +}) + +// Test for InputChip +describe('InputChip', () => { + it('renders the input chip with a label', () => { + render() + + expect(screen.getByText('Input Chip')).toBeInTheDocument() + }) + + it('renders with the correct variant', () => { + render() + + const chip = screen.getByText('Input Chip').parentElement + expect(chip).toHaveClass('outlined') + }) +}) + +// Test for CategoryChip +describe('CategoryChip', () => { + it('renders the category chip with a label and detail', () => { + render( + + ) + + expect(screen.getByText('Category Chip')).toBeInTheDocument() + expect(screen.getByText('Detail')).toBeInTheDocument() + }) + + it('applies the correct style based on the color prop', () => { + render( + + ) + + const labelChip = screen.getByText('Category Chip').parentElement + const detailChip = screen.getByText('Detail').parentElement + + expect(labelChip.style.getPropertyValue('--chip-bg-color')).toBe( + 'var(--s2s-blue-300)' + ) + expect(detailChip.style.getPropertyValue('--chip-bg-color')).toBe( + 'var(--s2s-blue-100)' + ) + }) +}) + +// Test for StateChip +describe('StateChip', () => { + it('renders the state chip with a label', () => { + render() + + expect(screen.getByText('State Chip')).toBeInTheDocument() + }) + + it('applies the correct style based on the color prop', () => { + render() + + const chip = screen.getByText('State Chip').parentElement + expect(chip.style.getPropertyValue('--chip-bg-color')).toBe( + 'var(--s2s-green-100)' + ) + expect(chip.style.getPropertyValue('--chip-text-color')).toBe( + 'var(--s2s-green-700)' + ) + expect(chip.style.getPropertyValue('--chip-border-color')).toBe( + 'var(--s2s-green-700)' + ) + }) +}) From 071af7d464a8b9c502ed4c6257df9626acd60c30 Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 22:16:15 +0200 Subject: [PATCH 05/12] fix: remove unneeded margin --- src/design-system/components/chip/Chip.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/design-system/components/chip/Chip.scss b/src/design-system/components/chip/Chip.scss index a5cb46136..3dec2d74a 100644 --- a/src/design-system/components/chip/Chip.scss +++ b/src/design-system/components/chip/Chip.scss @@ -1,7 +1,6 @@ @use '~/scss/utilities' as *; .chip { - margin: 5px; display: inline-flex; align-items: center; justify-content: center; From 5295ce8349ea8cdd21894fd5754ca4d282b91f73 Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 22:51:52 +0200 Subject: [PATCH 06/12] fix: some sonar issues --- src/design-system/components/chip/Chip.tsx | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index 011db231b..3c50520a5 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -75,6 +75,7 @@ const ChipContent: React.FC = ({ ) const FilterChip: React.FC = ({ + type, label, options = [], variant = 'filled', @@ -96,7 +97,7 @@ const FilterChip: React.FC = ({ const classes = classNames( 'chip', `chip--${size}`, - `chip--filter`, + `chip--${type}`, variant, isSelected ? 'selected' : 'unselected', { @@ -107,7 +108,7 @@ const FilterChip: React.FC = ({
!disabled && setIsOpen((prev) => !prev)} - tabIndex={0} + role='button' > = ({ } const InputChip: React.FC = ({ + type, label, variant = 'outlined', startIcon = , @@ -142,9 +144,15 @@ const InputChip: React.FC = ({ disabled = false, size = 'md' }) => { - const classes = classNames('chip', `chip--${size}`, `chip--input`, variant, { - disabled - }) + const classes = classNames( + 'chip', + `chip--${size}`, + `chip--${type}`, + variant, + { + disabled + } + ) return (
@@ -153,6 +161,7 @@ const InputChip: React.FC = ({ } const CategoryChip: React.FC = ({ + type, label, detail, size = 'md', @@ -172,7 +181,7 @@ const CategoryChip: React.FC = ({ return (
= ({
= ({ } const StateChip: React.FC = ({ + type, label, startIcon = , size = 'md', @@ -205,7 +215,7 @@ const StateChip: React.FC = ({ } return (
Date: Wed, 27 Nov 2024 23:09:23 +0200 Subject: [PATCH 07/12] fix: rewrite with cn utils function --- src/design-system/components/chip/Chip.tsx | 25 ++++++++----------- .../design-system/components/Chip.spec.jsx | 5 ---- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index 3c50520a5..9ac7cd822 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -1,5 +1,6 @@ import React, { CSSProperties, useState } from 'react' -import classNames from 'classnames' +import { cn } from '~/utils/cn' +// import classNames from 'classnames' import CircleIcon from '@mui/icons-material/Circle' import CloseRoundedIcon from '@mui/icons-material/CloseRounded' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' @@ -92,9 +93,9 @@ const FilterChip: React.FC = ({ setIsOpen(false) } - const isSelected = !!selectedOption + const isSelected = Boolean(selectedOption) - const classes = classNames( + const classes = cn( 'chip', `chip--${size}`, `chip--${type}`, @@ -144,15 +145,9 @@ const InputChip: React.FC = ({ disabled = false, size = 'md' }) => { - const classes = classNames( - 'chip', - `chip--${size}`, - `chip--${type}`, - variant, - { - disabled - } - ) + const classes = cn('chip', `chip--${size}`, `chip--${type}`, variant, { + disabled + }) return (
@@ -181,7 +176,7 @@ const CategoryChip: React.FC = ({ return (
= ({
= ({ } return (
{ it('checks the basic properties of chip', () => { render() @@ -13,7 +12,6 @@ describe('Chip', () => { }) }) -// Test for FilterChip describe('FilterChip', () => { it('renders the filter chip with a label', () => { render( @@ -58,7 +56,6 @@ describe('FilterChip', () => { }) }) -// Test for InputChip describe('InputChip', () => { it('renders the input chip with a label', () => { render() @@ -74,7 +71,6 @@ describe('InputChip', () => { }) }) -// Test for CategoryChip describe('CategoryChip', () => { it('renders the category chip with a label and detail', () => { render( @@ -112,7 +108,6 @@ describe('CategoryChip', () => { }) }) -// Test for StateChip describe('StateChip', () => { it('renders the state chip with a label', () => { render() From 7468446c28802e8dae1d10a9921bd28a57c084ba Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Wed, 27 Nov 2024 23:21:13 +0200 Subject: [PATCH 08/12] fix: change div to button --- src/design-system/components/chip/Chip.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index 9ac7cd822..e7d39fcec 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -106,10 +106,9 @@ const FilterChip: React.FC = ({ } ) return ( -
!disabled && setIsOpen((prev) => !prev)} - role='button' > = ({ ))} )} -
+ ) } From d982443093238b99b94f0474a85f8447acd33e2e Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Thu, 28 Nov 2024 01:08:05 +0200 Subject: [PATCH 09/12] fix --- src/design-system/components/chip/Chip.scss | 16 ++++++++-------- src/design-system/components/chip/Chip.tsx | 6 +++--- .../design-system/components/Chip.spec.jsx | 19 +++++++++++++------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/design-system/components/chip/Chip.scss b/src/design-system/components/chip/Chip.scss index 3dec2d74a..ae3717f0b 100644 --- a/src/design-system/components/chip/Chip.scss +++ b/src/design-system/components/chip/Chip.scss @@ -27,7 +27,7 @@ &.disabled { opacity: 0.7; - cursor: not-allowed; + pointer-events: none; } &--filter { @@ -62,12 +62,7 @@ box-shadow: var(--#{$prefix}box-shadow-xs); } - &.disabled { - opacity: 1; - color: var(--#{$prefix}blue-gray-500); - } - - &.selected { + &.selected:not(.disabled) { color: var(--#{$prefix}blue-gray-800); &:hover { @@ -80,7 +75,7 @@ } } - &.unselected { + &.unselected:not(.disabled) { &:hover { background-color: var(--#{$prefix}blue-gray-50); color: var(--#{$prefix}blue-gray-800); @@ -93,6 +88,11 @@ } } + &.disabled { + opacity: 1; + color: var(--#{$prefix}blue-gray-500); + } + .dropdown-menu { position: absolute; top: 100%; diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index e7d39fcec..7e6989b6c 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -90,7 +90,7 @@ const FilterChip: React.FC = ({ const handleSelect = (option: string) => { setSelectedOption(option) - setIsOpen(false) + setIsOpen((prev) => !prev) } const isSelected = Boolean(selectedOption) @@ -119,11 +119,11 @@ const FilterChip: React.FC = ({
    {options.map((option) => (
  • { + onMouseDown={() => { handleSelect(option) - setIsOpen((prev) => !prev) }} > {option} diff --git a/tests/unit/design-system/components/Chip.spec.jsx b/tests/unit/design-system/components/Chip.spec.jsx index c30fb1299..045d9e7aa 100644 --- a/tests/unit/design-system/components/Chip.spec.jsx +++ b/tests/unit/design-system/components/Chip.spec.jsx @@ -25,7 +25,7 @@ describe('FilterChip', () => { expect(screen.getByText('Filter Chip')).toBeInTheDocument() }) - it('opens the dropdown menu when clicked', () => { + it('opens the dropdown menu when clicked', async () => { render( { ) fireEvent.click(screen.getByText('Filter Chip')) - expect(screen.getByText('Option 1')).toBeInTheDocument() - expect(screen.getByText('Option 2')).toBeInTheDocument() + + const option1 = await screen.findByText('Option 1') + const option2 = await screen.findByText('Option 2') + + expect(option1).toBeInTheDocument() + expect(option2).toBeInTheDocument() }) - it('selects an option when clicked and closes dropdown menu after', () => { + it('selects an option when clicked and closes dropdown menu after', async () => { render( { ) fireEvent.click(screen.getByText('Filter Chip')) - fireEvent.click(screen.getByText('Option 1')) - + + const option1 = await screen.findByText('Option 1') + + fireEvent.mouseDown(option1) + expect(screen.getByText('Option 1')).toBeInTheDocument() expect(screen.queryByText('Filter Chip')).not.toBeInTheDocument() }) From b981e4c222230067d3fff1c75563b07d3cd7de29 Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Thu, 28 Nov 2024 11:57:12 +0200 Subject: [PATCH 10/12] fix: cleanup --- src/design-system/components/chip/Chip.tsx | 3 +-- src/design-system/stories/Chip.stories.tsx | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index 7e6989b6c..1ac1f0981 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -1,6 +1,5 @@ import React, { CSSProperties, useState } from 'react' import { cn } from '~/utils/cn' -// import classNames from 'classnames' import CircleIcon from '@mui/icons-material/Circle' import CloseRoundedIcon from '@mui/icons-material/CloseRounded' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' @@ -112,7 +111,7 @@ const FilterChip: React.FC = ({ > {isOpen && ( diff --git a/src/design-system/stories/Chip.stories.tsx b/src/design-system/stories/Chip.stories.tsx index abfd74222..33da6a272 100644 --- a/src/design-system/stories/Chip.stories.tsx +++ b/src/design-system/stories/Chip.stories.tsx @@ -1,6 +1,5 @@ import type { Meta } from '@storybook/react' import Chip, { type ChipProps } from '~/design-system/components/chip/Chip' -export {} const meta: Meta = { title: 'Components/Chip', From deadd9c9a6fa5277e4f1a5cb6a1249fee98aae59 Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Mon, 2 Dec 2024 18:06:49 +0200 Subject: [PATCH 11/12] fix: refactoring --- src/design-system/components/chip/Chip.scss | 58 +++-- src/design-system/components/chip/Chip.tsx | 227 +---------------- .../components/chip/ChipTypes.tsx | 241 ++++++++++++++++++ src/design-system/stories/Chip.stories.tsx | 3 +- .../design-system/components/Chip.spec.jsx | 15 +- 5 files changed, 291 insertions(+), 253 deletions(-) create mode 100644 src/design-system/components/chip/ChipTypes.tsx diff --git a/src/design-system/components/chip/Chip.scss b/src/design-system/components/chip/Chip.scss index ae3717f0b..15f109ed4 100644 --- a/src/design-system/components/chip/Chip.scss +++ b/src/design-system/components/chip/Chip.scss @@ -1,6 +1,6 @@ @use '~/scss/utilities' as *; -.chip { +.#{$prefix}chip { display: inline-flex; align-items: center; justify-content: center; @@ -25,7 +25,7 @@ line-height: var(--#{$prefix}line-height-lg); } - &.disabled { + &.#{$prefix}disabled { opacity: 0.7; pointer-events: none; } @@ -33,36 +33,36 @@ &--filter { position: relative; - &.filled { - &.selected { + &.#{$prefix}filled { + &.#{$prefix}selected { background-color: var(--#{$prefix}blue-gray-100); } - &.unselected { + &.#{$prefix}unselected { background-color: var(--#{$prefix}blue-gray-50); } } - &.minimal { + &.#{$prefix}minimal { color: var(--#{$prefix}blue-gray-600); &:hover { color: var(--#{$prefix}blue-gray-800); } - &.selected, - &.unselected { + &.#{$prefix}selected, + &.#{$prefix}unselected { background-color: var(--#{$prefix}neutral-0); } } - &.minimal, - &.filled { + &.#{$prefix}minimal, + &.#{$prefix}filled { &:hover { box-shadow: var(--#{$prefix}box-shadow-xs); } - &.selected:not(.disabled) { + &.#{$prefix}selected { color: var(--#{$prefix}blue-gray-800); &:hover { @@ -75,7 +75,7 @@ } } - &.unselected:not(.disabled) { + &.#{$prefix}unselected { &:hover { background-color: var(--#{$prefix}blue-gray-50); color: var(--#{$prefix}blue-gray-800); @@ -88,16 +88,22 @@ } } - &.disabled { + &.#{$prefix}disabled { opacity: 1; color: var(--#{$prefix}blue-gray-500); } - .dropdown-menu { + .#{$prefix}btn { + display: flex; + align-items: center; + cursor: pointer; + } + + .#{$prefix}dropdown-menu { position: absolute; top: 100%; left: 0; - background-color: white; + background-color: var(--#{$prefix}neutral-0); border: var(--#{$prefix}border-width-xs) solid var(--#{$prefix}blue-gray-300); border-radius: var(--#{$prefix}space-1); @@ -108,7 +114,7 @@ z-index: 10; box-shadow: var(--#{$prefix}box-shadow-xs); - .dropdown-item { + .#{$prefix}dropdown-item { padding: var(--#{$prefix}space-1) var(--#{$prefix}space-2); cursor: pointer; transition: background-color 0.2s; @@ -123,26 +129,26 @@ &--input { font-weight: var(--#{$prefix}font-weight-medium); - &.outlined, - &.filled-outlined { + &.#{$prefix}outlined, + &.#{$prefix}filled-outlined { background-color: var(--#{$prefix}neutral-0); color: var(--#{$prefix}blue-gray-800); border: var(--#{$prefix}border-width-sm) solid var(--#{$prefix}blue-gray-400); - &.disabled { + &.#{$prefix}disabled { opacity: 1; color: var(--#{$prefix}blue-gray-500); border-color: var(--#{$prefix}blue-gray-200); } } - &.filled-outlined { + &.#{$prefix}filled-outlined { background-color: var(--#{$prefix}blue-gray-50); } } - &--categories { + &-categories { display: inline-flex; gap: var(--#{$prefix}space-0-5); } @@ -160,22 +166,22 @@ border: var(--#{$prefix}border-width-sm) solid var(--chip-border-color); } - .startIcon, - .endIcon { + .#{$prefix}startIcon, + .#{$prefix}endIcon { display: flex; align-items: center; font-size: inherit; } - .startIcon { + .#{$prefix}startIcon { margin-right: var(--#{$prefix}space-1); } - .endIcon { + .#{$prefix}endIcon { margin-left: var(--#{$prefix}space-1); } - .label { + .#{$prefix}label { padding: var(--#{$prefix}space-0-5); } } diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index 1ac1f0981..d45068890 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -1,222 +1,11 @@ -import React, { CSSProperties, useState } from 'react' -import { cn } from '~/utils/cn' -import CircleIcon from '@mui/icons-material/Circle' -import CloseRoundedIcon from '@mui/icons-material/CloseRounded' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' - -import './Chip.scss' - -type ChipContentProps = { - label: string - startIcon?: React.ReactNode - endIcon?: React.ReactNode -} - -type ChipType = 'filter' | 'input' | 'category' | 'state' - -type BaseChipProps = ChipContentProps & { - type: ChipType - size?: 'sm' | 'md' | 'lg' - disabled?: boolean -} - -type FilterChipProps = BaseChipProps & { - type: 'filter' - options: string[] - variant?: 'filled' | 'minimal' -} - -type InputChipProps = BaseChipProps & { - type: 'input' - variant?: 'filled' | 'outlined' | 'filled-outlined' -} - -type ChipColor = - | 'blue-gray' - | 'turquoise' - | 'blue' - | 'green' - | 'yellow' - | 'purple' - | 'red' - | 'neutral' - -type CategoryChipProps = BaseChipProps & { - type: 'category' - detail: string - color?: ChipColor -} - -type StateChipProps = BaseChipProps & { - type: 'state' - color?: ChipColor -} - -export type ChipProps = - | FilterChipProps - | InputChipProps - | CategoryChipProps - | StateChipProps - -type CustomStyle = CSSProperties & { - [key: `--chip-${string}`]: string | undefined -} - -const ChipContent: React.FC = ({ - label, - startIcon, - endIcon -}) => ( - <> - {startIcon && {startIcon}} - {label} - {endIcon && {endIcon}} - -) - -const FilterChip: React.FC = ({ - type, - label, - options = [], - variant = 'filled', - startIcon = , - endIcon = , - disabled = false, - size = 'md' -}) => { - const [isOpen, setIsOpen] = useState(false) - const [selectedOption, setSelectedOption] = useState(null) - - const handleSelect = (option: string) => { - setSelectedOption(option) - setIsOpen((prev) => !prev) - } - - const isSelected = Boolean(selectedOption) - - const classes = cn( - 'chip', - `chip--${size}`, - `chip--${type}`, - variant, - isSelected ? 'selected' : 'unselected', - { - disabled - } - ) - return ( - - ) -} - -const InputChip: React.FC = ({ - type, - label, - variant = 'outlined', - startIcon = , - endIcon = , - disabled = false, - size = 'md' -}) => { - const classes = cn('chip', `chip--${size}`, `chip--${type}`, variant, { - disabled - }) - return ( -
    - -
    - ) -} - -const CategoryChip: React.FC = ({ - type, - label, - detail, - size = 'md', - color = 'blue-gray', - disabled = false -}) => { - const labelStyle: CustomStyle = { - '--chip-text-color': `var(--s2s-${color}-900)`, - '--chip-bg-color': `var(--s2s-${color}-300)` - } - - const detailStyle: CustomStyle = { - '--chip-text-color': `var(--s2s-${color}-900)`, - '--chip-bg-color': `var(--s2s-${color}-100)` - } - - return ( -
    -
    - -
    -
    - -
    -
    - ) -} - -const StateChip: React.FC = ({ - type, - label, - startIcon = , - size = 'md', - color = 'blue-gray', - disabled = false -}) => { - const style: CustomStyle = { - '--chip-bg-color': `var(--s2s-${color}-100)`, - '--chip-text-color': `var(--s2s-${color}-700)`, - '--chip-border-color': `var(--s2s-${color}-700)` - } - return ( -
    - -
    - ) -} +import React from 'react' +import { + ChipProps, + FilterChip, + InputChip, + CategoryChip, + StateChip +} from './ChipTypes' const Chip: React.FC = (props) => { switch (props.type) { diff --git a/src/design-system/components/chip/ChipTypes.tsx b/src/design-system/components/chip/ChipTypes.tsx new file mode 100644 index 000000000..c4676e99a --- /dev/null +++ b/src/design-system/components/chip/ChipTypes.tsx @@ -0,0 +1,241 @@ +import React, { CSSProperties, useState } from 'react' +import { cn } from '~/utils/cn' +import CircleIcon from '@mui/icons-material/Circle' +import CloseRoundedIcon from '@mui/icons-material/CloseRounded' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' + +import './Chip.scss' + +type ChipContentProps = { + label: string + startIcon?: React.ReactNode + endIcon?: React.ReactNode +} + +type ChipType = 'filter' | 'input' | 'category' | 'state' + +type BaseChipProps = ChipContentProps & { + type: ChipType + size?: 'sm' | 'md' | 'lg' + disabled?: boolean +} + +type FilterChipProps = BaseChipProps & { + type: 'filter' + options: string[] + variant?: 'filled' | 'minimal' +} + +type InputChipProps = BaseChipProps & { + type: 'input' + variant?: 'filled' | 'outlined' | 'filled-outlined' +} + +type ChipColor = + | 'blue-gray' + | 'turquoise' + | 'blue' + | 'green' + | 'yellow' + | 'purple' + | 'red' + | 'neutral' + +type CategoryChipProps = BaseChipProps & { + type: 'category' + detail: string + color?: ChipColor +} + +type StateChipProps = BaseChipProps & { + type: 'state' + color?: ChipColor +} + +export type ChipProps = + | FilterChipProps + | InputChipProps + | CategoryChipProps + | StateChipProps + +type CustomStyle = CSSProperties & { + [key: `--chip-${string}`]: string | undefined +} + +const ChipContent: React.FC = ({ + label, + startIcon, + endIcon +}) => ( + <> + {startIcon && {startIcon}} + {label} + {endIcon && {endIcon}} + +) + +const BaseChip: React.FC< + Omit & { + children: React.ReactNode + className?: string + style?: CustomStyle + } +> = ({ type, size, disabled, children, className, style }) => { + return ( +
    + {children} +
    + ) +} + +export const FilterChip: React.FC = ({ + type, + label, + options = [], + variant = 'filled', + startIcon = , + endIcon = , + disabled = false, + size = 'md' +}) => { + const [isOpen, setIsOpen] = useState(false) + const [selectedOption, setSelectedOption] = useState(null) + + const handleSelect = (option: string) => { + setSelectedOption(option) + setIsOpen((prev) => !prev) + } + + const isSelected = Boolean(selectedOption) + + return ( + +
    !disabled && setIsOpen(!isOpen)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setIsOpen(!isOpen) + } + }} + role='button' + tabIndex={0} + > + +
    + {isOpen && ( +
      + {options.map((option) => ( +
    • { + handleSelect(option) + }} + > + {option} +
    • + ))} +
    + )} +
    + ) +} + +export const InputChip: React.FC = ({ + type, + label, + variant = 'outlined', + startIcon = , + endIcon = , + disabled = false, + size = 'md' +}) => { + return ( + + + + ) +} + +export const CategoryChip: React.FC = ({ + type, + label, + detail, + size = 'md', + color = 'blue-gray', + disabled = false +}) => { + const labelStyle: CustomStyle = { + '--chip-text-color': `var(--s2s-${color}-900)`, + '--chip-bg-color': `var(--s2s-${color}-300)` + } + + const detailStyle: CustomStyle = { + '--chip-text-color': `var(--s2s-${color}-900)`, + '--chip-bg-color': `var(--s2s-${color}-100)` + } + + return ( +
    + + + + + + +
    + ) +} + +export const StateChip: React.FC = ({ + type, + label, + startIcon = , + size = 'md', + color = 'blue-gray', + disabled = false +}) => { + const style: CustomStyle = { + '--chip-bg-color': `var(--s2s-${color}-100)`, + '--chip-text-color': `var(--s2s-${color}-700)`, + '--chip-border-color': `var(--s2s-${color}-700)` + } + + return ( + + + + ) +} diff --git a/src/design-system/stories/Chip.stories.tsx b/src/design-system/stories/Chip.stories.tsx index 33da6a272..a7881f749 100644 --- a/src/design-system/stories/Chip.stories.tsx +++ b/src/design-system/stories/Chip.stories.tsx @@ -1,5 +1,6 @@ import type { Meta } from '@storybook/react' -import Chip, { type ChipProps } from '~/design-system/components/chip/Chip' +import Chip from '~/design-system/components/chip/Chip' +import { type ChipProps } from '~/design-system/components/chip/ChipTypes' const meta: Meta = { title: 'Components/Chip', diff --git a/tests/unit/design-system/components/Chip.spec.jsx b/tests/unit/design-system/components/Chip.spec.jsx index 045d9e7aa..56ee38a24 100644 --- a/tests/unit/design-system/components/Chip.spec.jsx +++ b/tests/unit/design-system/components/Chip.spec.jsx @@ -6,9 +6,10 @@ describe('Chip', () => { render() const chip = screen.getByText('Chip').parentElement - expect(chip).toHaveClass('chip--input') - expect(chip).toHaveClass('chip--sm') - expect(chip).toHaveClass('outlined') + expect(chip).toHaveClass('s2s-chip--input') + expect(chip).toHaveClass('s2s-chip--sm') + expect(chip).toHaveClass('s2s-outlined') + expect(chip).toHaveClass('s2s-disabled') }) }) @@ -53,11 +54,11 @@ describe('FilterChip', () => { ) fireEvent.click(screen.getByText('Filter Chip')) - + const option1 = await screen.findByText('Option 1') - + fireEvent.mouseDown(option1) - + expect(screen.getByText('Option 1')).toBeInTheDocument() expect(screen.queryByText('Filter Chip')).not.toBeInTheDocument() }) @@ -74,7 +75,7 @@ describe('InputChip', () => { render() const chip = screen.getByText('Input Chip').parentElement - expect(chip).toHaveClass('outlined') + expect(chip).toHaveClass('s2s-outlined') }) }) From 50b9f1d8283e362dbd889331758882aca9da054b Mon Sep 17 00:00:00 2001 From: sandrvvu Date: Tue, 3 Dec 2024 18:39:31 +0200 Subject: [PATCH 12/12] fix: state in child component --- src/design-system/components/chip/Chip.tsx | 19 +++++++++++++-- .../components/chip/ChipTypes.tsx | 24 ++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/design-system/components/chip/Chip.tsx b/src/design-system/components/chip/Chip.tsx index d45068890..9f5a5e0f5 100644 --- a/src/design-system/components/chip/Chip.tsx +++ b/src/design-system/components/chip/Chip.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { ChipProps, FilterChip, @@ -8,9 +8,24 @@ import { } from './ChipTypes' const Chip: React.FC = (props) => { + const [isOpen, setIsOpen] = useState(false) + const [selectedOption, setSelectedOption] = useState(null) + + const handleSelectChange = (option: string) => { + setSelectedOption(option) + } + switch (props.type) { case 'filter': - return + return ( + + ) case 'input': return case 'category': diff --git a/src/design-system/components/chip/ChipTypes.tsx b/src/design-system/components/chip/ChipTypes.tsx index c4676e99a..7b70dd89e 100644 --- a/src/design-system/components/chip/ChipTypes.tsx +++ b/src/design-system/components/chip/ChipTypes.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, useState } from 'react' +import React, { CSSProperties } from 'react' import { cn } from '~/utils/cn' import CircleIcon from '@mui/icons-material/Circle' import CloseRoundedIcon from '@mui/icons-material/CloseRounded' @@ -99,7 +99,14 @@ const BaseChip: React.FC< ) } -export const FilterChip: React.FC = ({ +export const FilterChip: React.FC< + FilterChipProps & { + isOpen: boolean + selectedOption: string | null + setIsOpen: (isOpen: boolean) => void + onSelectChange: (option: string) => void + } +> = ({ type, label, options = [], @@ -107,14 +114,15 @@ export const FilterChip: React.FC = ({ startIcon = , endIcon = , disabled = false, - size = 'md' + size = 'md', + isOpen, + setIsOpen, + selectedOption, + onSelectChange }) => { - const [isOpen, setIsOpen] = useState(false) - const [selectedOption, setSelectedOption] = useState(null) - const handleSelect = (option: string) => { - setSelectedOption(option) - setIsOpen((prev) => !prev) + onSelectChange(option) + setIsOpen(false) } const isSelected = Boolean(selectedOption)