From f248e579b4e711df3863a24a7736e17d709c22ba Mon Sep 17 00:00:00 2001 From: Mauro Reis Vieira Date: Wed, 1 Oct 2025 16:59:44 +0100 Subject: [PATCH 1/4] feat(select): support for select group and loading status --- .../base/src/select/SelectGroup.tsx | 46 +++++++++++ packages/components/base/src/select/index.tsx | 77 +++++++++++++++---- .../base/src/select/styles/index.module.scss | 43 ++++++++--- packages/components/base/src/select/types.ts | 20 +++++ packages/components/base/src/select/utils.ts | 77 ++++++++++++------- 5 files changed, 209 insertions(+), 54 deletions(-) create mode 100644 packages/components/base/src/select/SelectGroup.tsx diff --git a/packages/components/base/src/select/SelectGroup.tsx b/packages/components/base/src/select/SelectGroup.tsx new file mode 100644 index 00000000..42f86621 --- /dev/null +++ b/packages/components/base/src/select/SelectGroup.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { DISPLAY_NAME_ATTRIBUTE } from "@react-ck/react-utils"; +import { type SelectGroupProps } from "./types"; +import { Text } from "../text"; +import styles from "./styles/index.module.scss"; + +/** + * A group component for organizing Select options with a label. + * Provides visual separation and organization for related options. + * + * @example + * ```tsx + * + * ``` + * + * @param props - Component props {@link SelectGroupProps} + * @returns React element + */ +const SelectGroup = ({ + name, + children, +}: Readonly>): React.ReactElement => { + return ( +
+
+ + {name} + +
+ {children} +
+ ); +}; + +SelectGroup[DISPLAY_NAME_ATTRIBUTE] = "SelectGroup"; + +export { SelectGroup }; diff --git a/packages/components/base/src/select/index.tsx b/packages/components/base/src/select/index.tsx index be8422b2..7e8d973e 100644 --- a/packages/components/base/src/select/index.tsx +++ b/packages/components/base/src/select/index.tsx @@ -1,6 +1,7 @@ import styles from "./styles/index.module.scss"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { SelectOption } from "./SelectOption"; +import { SelectGroup } from "./SelectGroup"; import { Menu } from "../menu"; import { Dropdown } from "../dropdown"; import { Input } from "../input"; @@ -11,6 +12,9 @@ import { getChildrenData, simplifyString, valueAsArray } from "./utils"; import { type SelectProps, type ChangeHandler } from "./types"; import { SelectContext, type SelectContextProps } from "./context"; import { useFormFieldContext } from "../form-field"; +import { Spinner } from "../spinner"; +import { Icon } from "@react-ck/icon"; +import { IconChevronDown } from "@react-ck/icon/icons/IconChevronDown"; /** Default positions to exclude from auto-positioning */ const defaultExclude: SelectProps["excludeAutoPosition"] = [ @@ -75,6 +79,7 @@ const Select = ({ allowDeselect = true, required, disabled, + loading, displayValueDivider = ",", fullWidth, position, @@ -105,17 +110,47 @@ const Select = ({ const childrenData = useMemo(() => getChildrenData(children), [children]); /** Options list children filtered by user search */ - const filteredOptions = useMemo( - () => - childrenData - .filter( - (i) => - !search.length || - (i.textContent && simplifyString(i.textContent).includes(simplifyString(search))), - ) - .map((i) => i.element), - [childrenData, search], - ); + const filteredOptions = useMemo(() => { + const filtered = childrenData.filter( + (i) => + !search.length || + (i.textContent && simplifyString(i.textContent).includes(simplifyString(search))), + ); + + // Group options by groupName + const groupedOptions: { [key: string]: React.ReactNode[] } = {}; + const ungroupedOptions: React.ReactNode[] = []; + + filtered.forEach((item) => { + if (item.isSelectOption) { + if (item.groupName) { + if (!(item.groupName in groupedOptions)) { + groupedOptions[item.groupName] = []; + } + groupedOptions[item.groupName]?.push(item.element); + } else { + ungroupedOptions.push(item.element); + } + } + }); + + // Render grouped options + const result: React.ReactNode[] = []; + + // Add ungrouped options first + ungroupedOptions.forEach((option) => result.push(option)); + + // Add grouped options using SelectGroup component + Object.entries(groupedOptions).forEach(([groupName, options]) => { + result.push( + + {options} + , + ); + }); + + return result; + }, [childrenData, search]); /** Returns the internal value always as an array to facilitate operations */ const selectedValuesList = useMemo( @@ -256,7 +291,9 @@ const Select = ({ const resizeObserver = new ResizeObserver(() => { if (!valueSlotRefCurrent || !sizeSetterRef.current) return; - valueSlotRefCurrent.style.width = `${sizeSetterRef.current.clientWidth + 10}px`; + if (valueSlotRefCurrent.clientWidth < sizeSetterRef.current.clientWidth) { + valueSlotRefCurrent.style.width = `${sizeSetterRef.current.clientWidth + 10}px`; + } }); resizeObserver.observe(sizeSetterRef.current); @@ -278,7 +315,7 @@ const Select = ({ styles[`skin_${computedSkin}`], formFieldContext === undefined && styles.standalone, (disabled || formFieldContext?.disabled) && styles.disabled, - + loading && styles.loading, (fullWidth ?? formFieldContext?.fullWidth) && styles.full_width, className, )} @@ -291,7 +328,13 @@ const Select = ({ onBlur?.(e); }}>
- {displayValue || {placeholder}} +
+ {displayValue || {placeholder}} +
+ {loading && } + + +
{ - megeRefs(ref, containerRef)(el); + mergeRefs(ref, containerRef)(el); setFocusWrapperElement(el || undefined); }} tabIndex={0} diff --git a/packages/components/base/src/file-uploader/FileUploader.tsx b/packages/components/base/src/file-uploader/FileUploader.tsx index 40715c71..83e5cbce 100644 --- a/packages/components/base/src/file-uploader/FileUploader.tsx +++ b/packages/components/base/src/file-uploader/FileUploader.tsx @@ -4,7 +4,7 @@ import classNames from "classnames"; import { Text } from "../text"; import { Button, type ButtonProps } from "../button"; import { readFileList } from "./utils/read-file"; -import { megeRefs } from "@react-ck/react-utils"; +import { mergeRefs } from "@react-ck/react-utils"; // TODO: check https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file // TODO: add size limitation: https://stackoverflow.com/questions/5697605/limit-the-size-of-a-file-upload-html-input-element @@ -106,7 +106,7 @@ export const FileUploader = ({ )}>