From efe445ffdb199896c792f5e64cfe89413909c30d Mon Sep 17 00:00:00 2001 From: Caner Akdas Date: Sun, 13 Jul 2025 16:07:25 +0300 Subject: [PATCH 1/2] feat: introduce StatelessSelect --- .../Common/Select/StatelessSelect/index.tsx | 130 ++++++++++++++++++ .../src/Common/Select/index.module.css | 37 +++++ .../ui-components/src/Common/Select/index.tsx | 29 +++- .../src/Containers/Sidebar/index.tsx | 1 + 4 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 packages/ui-components/src/Common/Select/StatelessSelect/index.tsx diff --git a/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx b/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx new file mode 100644 index 0000000000000..609f883a94aaf --- /dev/null +++ b/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx @@ -0,0 +1,130 @@ +import { ChevronDownIcon } from '@heroicons/react/24/solid'; +import classNames from 'classnames'; +import { useId, useMemo } from 'react'; + +import type { SelectGroup, SelectProps } from '#ui/Common/Select'; +import { isStringArray, isValuesArray } from '#ui/Common/Select'; + +import styles from '../index.module.css'; + +export type StatelessSelectProps = Omit< + SelectProps, + 'onChange' | 'loading' +> & { + targetElement: string; +}; + +const StatelessSelect = ({ + values = [], + defaultValue, + placeholder, + label, + inline, + className, + ariaLabel, + disabled = false, + as: Component = 'div', + targetElement, +}: StatelessSelectProps) => { + const id = useId(); + + const mappedValues = useMemo(() => { + let mappedValues = values; + + if (isStringArray(mappedValues)) { + mappedValues = mappedValues.map(value => ({ + label: value, + value: value, + })); + } + + if (isValuesArray(mappedValues)) { + return [{ items: mappedValues }]; + } + + return mappedValues as Array>; + }, [values]); + + // Find the current/default item to display in summary + const currentItem = useMemo( + () => + mappedValues + .flatMap(({ items }) => items) + .find(item => item.value === defaultValue), + [mappedValues, defaultValue] + ); + + return ( + + ); +}; + +export default StatelessSelect; diff --git a/packages/ui-components/src/Common/Select/index.module.css b/packages/ui-components/src/Common/Select/index.module.css index ed4f05e834bdb..4e5fbb082c2fd 100644 --- a/packages/ui-components/src/Common/Select/index.module.css +++ b/packages/ui-components/src/Common/Select/index.module.css @@ -159,3 +159,40 @@ text-neutral-700 dark:text-neutral-200; } + +.noscript { + summary { + @apply flex + w-full + justify-between; + } + + .dropdown { + @apply absolute + left-4 + mt-6; + } + + .text { + @apply hover:outline-hidden + block + whitespace-normal + pl-4 + text-neutral-800 + hover:bg-green-500 + hover:text-white + dark:text-neutral-200 + dark:hover:bg-green-600 + dark:hover:text-white; + + span { + @apply h-auto; + } + } + + .inline { + .text { + @apply pl-2.5; + } + } +} diff --git a/packages/ui-components/src/Common/Select/index.tsx b/packages/ui-components/src/Common/Select/index.tsx index bf534c014eee3..ea5c23d325c66 100644 --- a/packages/ui-components/src/Common/Select/index.tsx +++ b/packages/ui-components/src/Common/Select/index.tsx @@ -6,8 +6,9 @@ import classNames from 'classnames'; import { useEffect, useId, useMemo, useState } from 'react'; import type { ReactElement, ReactNode } from 'react'; +import StatelessSelect from '#ui/Common/Select/StatelessSelect'; import Skeleton from '#ui/Common/Skeleton'; -import type { FormattedMessage } from '#ui/types'; +import type { FormattedMessage, LinkLike } from '#ui/types'; import styles from './index.module.css'; @@ -23,15 +24,17 @@ export type SelectGroup = { items: Array>; }; -const isStringArray = (values: Array): values is Array => +export const isStringArray = ( + values: Array +): values is Array => Boolean(values[0] && typeof values[0] === 'string'); -const isValuesArray = ( +export const isValuesArray = ( values: Array ): values is Array> => Boolean(values[0] && typeof values[0] === 'object' && 'value' in values[0]); -type SelectProps = { +export type SelectProps = { values: Array> | Array | Array>; defaultValue?: T; placeholder?: string; @@ -42,6 +45,7 @@ type SelectProps = { ariaLabel?: string; loading?: boolean; disabled?: boolean; + as?: LinkLike | 'div'; }; const Select = ({ @@ -55,8 +59,10 @@ const Select = ({ ariaLabel, loading = false, disabled = false, + as, }: SelectProps): ReactNode => { const id = useId(); + const uniqueSelectClass = `select-${id.replace(/[^a-zA-Z0-9]/g, '')}`; const [value, setValue] = useState(defaultValue); useEffect(() => setValue(defaultValue), [defaultValue]); @@ -133,7 +139,8 @@ const Select = ({ className={classNames( styles.select, { [styles.inline]: inline }, - className + className, + uniqueSelectClass )} > {label && ( @@ -183,6 +190,18 @@ const Select = ({ + ); }; diff --git a/packages/ui-components/src/Containers/Sidebar/index.tsx b/packages/ui-components/src/Containers/Sidebar/index.tsx index 70c71f1ff03bd..ebaff23a635fe 100644 --- a/packages/ui-components/src/Containers/Sidebar/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/index.tsx @@ -49,6 +49,7 @@ const SideBar: FC> = ({ placeholder={placeholder} onChange={onSelect} className={styles.mobileSelect} + as={as} /> )} From 8372085b5c0af1ecb89d7e71d5cf08a6fe3f5897 Mon Sep 17 00:00:00 2001 From: Caner Akdas Date: Sun, 13 Jul 2025 19:15:36 +0300 Subject: [PATCH 2/2] refactor: review updates --- .../Downloads/Release/VersionDropdown.tsx | 4 +- .../Common/Select/NoScriptSelect/index.tsx | 25 +++ .../Common/Select/StatelessSelect/index.tsx | 145 +++++++++--------- .../src/Common/Select/index.module.css | 10 +- .../src/Common/Select/index.stories.tsx | 10 ++ .../ui-components/src/Common/Select/index.tsx | 34 +--- .../src/Containers/Sidebar/index.module.css | 2 +- .../src/Containers/Sidebar/index.tsx | 4 +- packages/ui-components/src/util/array.ts | 7 + 9 files changed, 132 insertions(+), 109 deletions(-) create mode 100644 packages/ui-components/src/Common/Select/NoScriptSelect/index.tsx create mode 100644 packages/ui-components/src/util/array.ts diff --git a/apps/site/components/Downloads/Release/VersionDropdown.tsx b/apps/site/components/Downloads/Release/VersionDropdown.tsx index 2a56458666ebf..c22ed830b5dc2 100644 --- a/apps/site/components/Downloads/Release/VersionDropdown.tsx +++ b/apps/site/components/Downloads/Release/VersionDropdown.tsx @@ -1,6 +1,6 @@ 'use client'; -import Select from '@node-core/ui-components/Common/Select'; +import WithNoScriptSelect from '@node-core/ui-components/Common/Select/NoScriptSelect'; import { useLocale, useTranslations } from 'next-intl'; import type { FC } from 'react'; import { useContext } from 'react'; @@ -51,7 +51,7 @@ const VersionDropdown: FC = () => { }; return ( - + + + ); +}; + +export default WithNoScriptSelect; diff --git a/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx b/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx index 609f883a94aaf..556332d391fad 100644 --- a/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx +++ b/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx @@ -3,17 +3,18 @@ import classNames from 'classnames'; import { useId, useMemo } from 'react'; import type { SelectGroup, SelectProps } from '#ui/Common/Select'; -import { isStringArray, isValuesArray } from '#ui/Common/Select'; +import type { LinkLike } from '#ui/types'; +import { isStringArray, isValuesArray } from '#ui/util/array'; import styles from '../index.module.css'; -export type StatelessSelectProps = Omit< - SelectProps, - 'onChange' | 'loading' -> & { - targetElement: string; +type StatelessSelectConfig = { + as?: LinkLike | 'div'; }; +export type StatelessSelectProps = SelectProps & + StatelessSelectConfig; + const StatelessSelect = ({ values = [], defaultValue, @@ -24,7 +25,6 @@ const StatelessSelect = ({ ariaLabel, disabled = false, as: Component = 'div', - targetElement, }: StatelessSelectProps) => { const id = useId(); @@ -43,7 +43,7 @@ const StatelessSelect = ({ } return mappedValues as Array>; - }, [values]); + }, [values]) as Array>; // Find the current/default item to display in summary const currentItem = useMemo( @@ -55,75 +55,72 @@ const StatelessSelect = ({ ); return ( -