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 new file mode 100644 index 0000000000000..556332d391fad --- /dev/null +++ b/packages/ui-components/src/Common/Select/StatelessSelect/index.tsx @@ -0,0 +1,127 @@ +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 type { LinkLike } from '#ui/types'; +import { isStringArray, isValuesArray } from '#ui/util/array'; + +import styles from '../index.module.css'; + +type StatelessSelectConfig = { + as?: LinkLike | 'div'; +}; + +export type StatelessSelectProps = SelectProps & + StatelessSelectConfig; + +const StatelessSelect = ({ + values = [], + defaultValue, + placeholder, + label, + inline, + className, + ariaLabel, + disabled = false, + as: Component = 'div', +}: 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]) as Array>; + + // Find the current/default item to display in summary + const currentItem = useMemo( + () => + mappedValues + .flatMap(({ items }) => items) + .find(item => item.value === defaultValue), + [mappedValues, defaultValue] + ); + + return ( +
+ {label && ( + + )} + +
+ + {currentItem ? ( + + {currentItem.iconImage} + {currentItem.label} + + ) : ( + {placeholder} + )} + + + +
+ {mappedValues.map(({ label: groupLabel, items }, groupKey) => ( +
+ {groupLabel && ( +
+ {groupLabel} +
+ )} + + {items.map( + ({ value, label, iconImage, disabled: itemDisabled }) => ( + + {iconImage} + {label} + + ) + )} +
+ ))} +
+
+
+ ); +}; + +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..5e048f9b4be71 100644 --- a/packages/ui-components/src/Common/Select/index.module.css +++ b/packages/ui-components/src/Common/Select/index.module.css @@ -159,3 +159,46 @@ text-neutral-700 dark:text-neutral-200; } + +.noscript { + @apply relative; + + summary { + @apply flex + w-full + justify-between; + } + + .trigger { + @apply block; + } + + .dropdown { + @apply absolute + left-0 + mt-4; + } + + .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.stories.tsx b/packages/ui-components/src/Common/Select/index.stories.tsx index c3230f6857dde..e924d0e1a05ee 100644 --- a/packages/ui-components/src/Common/Select/index.stories.tsx +++ b/packages/ui-components/src/Common/Select/index.stories.tsx @@ -1,6 +1,7 @@ import type { Meta as MetaObj, StoryObj } from '@storybook/react'; import Select from '#ui/Common/Select'; +import StatelessSelect from '#ui/Common/Select/StatelessSelect'; import * as OSIcons from '#ui/Icons/OperatingSystem'; type Story = StoryObj; @@ -108,4 +109,13 @@ export const InlineSelect: Story = { }, }; +export const WithNoScriptSelect: Story = { + render: () => ( + `Item ${i}`)} + defaultValue="Item 50" + /> + ), +}; + export default { component: Select } as Meta; diff --git a/packages/ui-components/src/Common/Select/index.tsx b/packages/ui-components/src/Common/Select/index.tsx index bf534c014eee3..d195e33e5acb9 100644 --- a/packages/ui-components/src/Common/Select/index.tsx +++ b/packages/ui-components/src/Common/Select/index.tsx @@ -7,7 +7,8 @@ import { useEffect, useId, useMemo, useState } from 'react'; import type { ReactElement, ReactNode } from 'react'; import Skeleton from '#ui/Common/Skeleton'; -import type { FormattedMessage } from '#ui/types'; +import type { FormattedMessage, LinkLike } from '#ui/types'; +import { isStringArray, isValuesArray } from '#ui/util/array'; import styles from './index.module.css'; @@ -23,15 +24,7 @@ export type SelectGroup = { items: Array>; }; -const isStringArray = (values: Array): values is Array => - Boolean(values[0] && typeof values[0] === 'string'); - -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 +35,8 @@ type SelectProps = { ariaLabel?: string; loading?: boolean; disabled?: boolean; + fallbackClass?: string; + as?: LinkLike | 'div'; }; const Select = ({ @@ -55,6 +50,7 @@ const Select = ({ ariaLabel, loading = false, disabled = false, + fallbackClass = '', }: SelectProps): ReactNode => { const id = useId(); const [value, setValue] = useState(defaultValue); @@ -75,8 +71,8 @@ const Select = ({ return [{ items: mappedValues }]; } - return mappedValues as Array>; - }, [values]); + return mappedValues; + }, [values]) as Array>; // We render the actual item slotted to fix/prevent the issue // of the tirgger flashing on the initial render @@ -133,7 +129,8 @@ const Select = ({ className={classNames( styles.select, { [styles.inline]: inline }, - className + className, + fallbackClass )} > {label && ( diff --git a/packages/ui-components/src/Containers/Sidebar/index.module.css b/packages/ui-components/src/Containers/Sidebar/index.module.css index 902b1305f708e..67873b65391f5 100644 --- a/packages/ui-components/src/Containers/Sidebar/index.module.css +++ b/packages/ui-components/src/Containers/Sidebar/index.module.css @@ -5,12 +5,12 @@ w-full flex-col gap-8 - overflow-auto border-r-0 border-neutral-200 bg-white px-4 py-6 + sm:overflow-auto sm:border-r md:max-w-xs lg:px-6 diff --git a/packages/ui-components/src/Containers/Sidebar/index.tsx b/packages/ui-components/src/Containers/Sidebar/index.tsx index 70c71f1ff03bd..51f98f8714a45 100644 --- a/packages/ui-components/src/Containers/Sidebar/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/index.tsx @@ -1,6 +1,6 @@ import type { ComponentProps, FC, PropsWithChildren } from 'react'; -import Select from '#ui/Common/Select'; +import WithNoScriptSelect from '#ui/Common/Select/NoScriptSelect'; import SidebarGroup from '#ui/Containers/Sidebar/SidebarGroup'; import type { LinkLike } from '#ui/types'; @@ -42,13 +42,14 @@ const SideBar: FC> = ({ {children} {selectItems.length > 0 && ( -