Skip to content

Commit 8372085

Browse files
committed
refactor: review updates
1 parent efe445f commit 8372085

File tree

9 files changed

+132
-109
lines changed

9 files changed

+132
-109
lines changed

apps/site/components/Downloads/Release/VersionDropdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import Select from '@node-core/ui-components/Common/Select';
3+
import WithNoScriptSelect from '@node-core/ui-components/Common/Select/NoScriptSelect';
44
import { useLocale, useTranslations } from 'next-intl';
55
import type { FC } from 'react';
66
import { useContext } from 'react';
@@ -51,7 +51,7 @@ const VersionDropdown: FC = () => {
5151
};
5252

5353
return (
54-
<Select
54+
<WithNoScriptSelect
5555
ariaLabel={t('layouts.download.dropdown.version')}
5656
values={releases.map(({ status, versionWithPrefix }) => ({
5757
value: versionWithPrefix,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useId } from 'react';
2+
3+
import Select from '#ui/Common/Select';
4+
import type { StatelessSelectProps } from '#ui/Common/Select/StatelessSelect';
5+
import StatelessSelect from '#ui/Common/Select/StatelessSelect';
6+
7+
const WithNoScriptSelect = <T extends string>({
8+
as,
9+
...props
10+
}: StatelessSelectProps<T>) => {
11+
const id = useId();
12+
const selectId = `select-${id.replace(/[^a-zA-Z0-9]/g, '')}`;
13+
14+
return (
15+
<>
16+
<Select {...props} fallbackClass={selectId} />
17+
<noscript>
18+
<style>{`.${selectId} { display: none!important; }`}</style>
19+
<StatelessSelect {...props} as={as} />
20+
</noscript>
21+
</>
22+
);
23+
};
24+
25+
export default WithNoScriptSelect;

packages/ui-components/src/Common/Select/StatelessSelect/index.tsx

Lines changed: 71 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import classNames from 'classnames';
33
import { useId, useMemo } from 'react';
44

55
import type { SelectGroup, SelectProps } from '#ui/Common/Select';
6-
import { isStringArray, isValuesArray } from '#ui/Common/Select';
6+
import type { LinkLike } from '#ui/types';
7+
import { isStringArray, isValuesArray } from '#ui/util/array';
78

89
import styles from '../index.module.css';
910

10-
export type StatelessSelectProps<T extends string> = Omit<
11-
SelectProps<T>,
12-
'onChange' | 'loading'
13-
> & {
14-
targetElement: string;
11+
type StatelessSelectConfig = {
12+
as?: LinkLike | 'div';
1513
};
1614

15+
export type StatelessSelectProps<T extends string> = SelectProps<T> &
16+
StatelessSelectConfig;
17+
1718
const StatelessSelect = <T extends string>({
1819
values = [],
1920
defaultValue,
@@ -24,7 +25,6 @@ const StatelessSelect = <T extends string>({
2425
ariaLabel,
2526
disabled = false,
2627
as: Component = 'div',
27-
targetElement,
2828
}: StatelessSelectProps<T>) => {
2929
const id = useId();
3030

@@ -43,7 +43,7 @@ const StatelessSelect = <T extends string>({
4343
}
4444

4545
return mappedValues as Array<SelectGroup<T>>;
46-
}, [values]);
46+
}, [values]) as Array<SelectGroup<T>>;
4747

4848
// Find the current/default item to display in summary
4949
const currentItem = useMemo(
@@ -55,75 +55,72 @@ const StatelessSelect = <T extends string>({
5555
);
5656

5757
return (
58-
<noscript>
59-
<style>{`.${targetElement} { display: none!important; }`}</style>
60-
<div
61-
className={classNames(
62-
styles.select,
63-
styles.noscript,
64-
{ [styles.inline]: inline },
65-
className
66-
)}
67-
>
68-
{label && (
69-
<label className={styles.label} htmlFor={id}>
70-
{label}
71-
</label>
72-
)}
58+
<div
59+
className={classNames(
60+
styles.select,
61+
styles.noscript,
62+
{ [styles.inline]: inline },
63+
className
64+
)}
65+
>
66+
{label && (
67+
<label className={styles.label} htmlFor={id}>
68+
{label}
69+
</label>
70+
)}
7371

74-
<details className={styles.trigger} id={id}>
75-
<summary
76-
className={styles.summary}
77-
aria-label={ariaLabel}
78-
aria-disabled={disabled}
79-
>
80-
{currentItem ? (
81-
<span className={styles.selectedValue}>
82-
{currentItem.iconImage}
83-
<span>{currentItem.label}</span>
84-
</span>
85-
) : (
86-
<span className={styles.placeholder}>{placeholder}</span>
87-
)}
88-
<ChevronDownIcon className={styles.icon} />
89-
</summary>
72+
<details className={styles.trigger} id={id}>
73+
<summary
74+
className={styles.summary}
75+
aria-label={ariaLabel}
76+
aria-disabled={disabled}
77+
>
78+
{currentItem ? (
79+
<span className={styles.selectedValue}>
80+
{currentItem.iconImage}
81+
<span>{currentItem.label}</span>
82+
</span>
83+
) : (
84+
<span className={styles.placeholder}>{placeholder}</span>
85+
)}
86+
<ChevronDownIcon className={styles.icon} />
87+
</summary>
9088

91-
<div
92-
className={classNames(styles.dropdown, { [styles.inline]: inline })}
93-
>
94-
{mappedValues.map(({ label: groupLabel, items }, groupKey) => (
95-
<div
96-
key={groupLabel?.toString() ?? groupKey}
97-
className={styles.group}
98-
>
99-
{groupLabel && (
100-
<div className={classNames(styles.item, styles.label)}>
101-
{groupLabel}
102-
</div>
103-
)}
89+
<div
90+
className={classNames(styles.dropdown, { [styles.inline]: inline })}
91+
>
92+
{mappedValues.map(({ label: groupLabel, items }, groupKey) => (
93+
<div
94+
key={groupLabel?.toString() ?? groupKey}
95+
className={styles.group}
96+
>
97+
{groupLabel && (
98+
<div className={classNames(styles.item, styles.label)}>
99+
{groupLabel}
100+
</div>
101+
)}
104102

105-
{items.map(
106-
({ value, label, iconImage, disabled: itemDisabled }) => (
107-
<Component
108-
key={value}
109-
href={value}
110-
className={classNames(styles.item, styles.text, {
111-
[styles.disabled]: itemDisabled || disabled,
112-
[styles.selected]: value === defaultValue,
113-
})}
114-
aria-disabled={itemDisabled || disabled}
115-
>
116-
{iconImage}
117-
<span>{label}</span>
118-
</Component>
119-
)
120-
)}
121-
</div>
122-
))}
123-
</div>
124-
</details>
125-
</div>
126-
</noscript>
103+
{items.map(
104+
({ value, label, iconImage, disabled: itemDisabled }) => (
105+
<Component
106+
key={value}
107+
href={value}
108+
className={classNames(styles.item, styles.text, {
109+
[styles.disabled]: itemDisabled || disabled,
110+
[styles.selected]: value === defaultValue,
111+
})}
112+
aria-disabled={itemDisabled || disabled}
113+
>
114+
{iconImage}
115+
<span>{label}</span>
116+
</Component>
117+
)
118+
)}
119+
</div>
120+
))}
121+
</div>
122+
</details>
123+
</div>
127124
);
128125
};
129126

packages/ui-components/src/Common/Select/index.module.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,22 @@
161161
}
162162

163163
.noscript {
164+
@apply relative;
165+
164166
summary {
165167
@apply flex
166168
w-full
167169
justify-between;
168170
}
169171

172+
.trigger {
173+
@apply block;
174+
}
175+
170176
.dropdown {
171177
@apply absolute
172-
left-4
173-
mt-6;
178+
left-0
179+
mt-4;
174180
}
175181

176182
.text {

packages/ui-components/src/Common/Select/index.stories.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
22

33
import Select from '#ui/Common/Select';
4+
import StatelessSelect from '#ui/Common/Select/StatelessSelect';
45
import * as OSIcons from '#ui/Icons/OperatingSystem';
56

67
type Story = StoryObj<typeof Select>;
@@ -108,4 +109,13 @@ export const InlineSelect: Story = {
108109
},
109110
};
110111

112+
export const WithNoScriptSelect: Story = {
113+
render: () => (
114+
<StatelessSelect
115+
values={Array.from({ length: 100 }, (_, i) => `Item ${i}`)}
116+
defaultValue="Item 50"
117+
/>
118+
),
119+
};
120+
111121
export default { component: Select } as Meta;

packages/ui-components/src/Common/Select/index.tsx

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import classNames from 'classnames';
66
import { useEffect, useId, useMemo, useState } from 'react';
77
import type { ReactElement, ReactNode } from 'react';
88

9-
import StatelessSelect from '#ui/Common/Select/StatelessSelect';
109
import Skeleton from '#ui/Common/Skeleton';
1110
import type { FormattedMessage, LinkLike } from '#ui/types';
11+
import { isStringArray, isValuesArray } from '#ui/util/array';
1212

1313
import styles from './index.module.css';
1414

@@ -24,16 +24,6 @@ export type SelectGroup<T extends string> = {
2424
items: Array<SelectValue<T>>;
2525
};
2626

27-
export const isStringArray = (
28-
values: Array<unknown>
29-
): values is Array<string> =>
30-
Boolean(values[0] && typeof values[0] === 'string');
31-
32-
export const isValuesArray = <T extends string>(
33-
values: Array<unknown>
34-
): values is Array<SelectValue<T>> =>
35-
Boolean(values[0] && typeof values[0] === 'object' && 'value' in values[0]);
36-
3727
export type SelectProps<T extends string> = {
3828
values: Array<SelectGroup<T>> | Array<T> | Array<SelectValue<T>>;
3929
defaultValue?: T;
@@ -45,6 +35,7 @@ export type SelectProps<T extends string> = {
4535
ariaLabel?: string;
4636
loading?: boolean;
4737
disabled?: boolean;
38+
fallbackClass?: string;
4839
as?: LinkLike | 'div';
4940
};
5041

@@ -59,10 +50,9 @@ const Select = <T extends string>({
5950
ariaLabel,
6051
loading = false,
6152
disabled = false,
62-
as,
53+
fallbackClass = '',
6354
}: SelectProps<T>): ReactNode => {
6455
const id = useId();
65-
const uniqueSelectClass = `select-${id.replace(/[^a-zA-Z0-9]/g, '')}`;
6656
const [value, setValue] = useState(defaultValue);
6757

6858
useEffect(() => setValue(defaultValue), [defaultValue]);
@@ -81,8 +71,8 @@ const Select = <T extends string>({
8171
return [{ items: mappedValues }];
8272
}
8373

84-
return mappedValues as Array<SelectGroup<T>>;
85-
}, [values]);
74+
return mappedValues;
75+
}, [values]) as Array<SelectGroup<T>>;
8676

8777
// We render the actual item slotted to fix/prevent the issue
8878
// of the tirgger flashing on the initial render
@@ -140,7 +130,7 @@ const Select = <T extends string>({
140130
styles.select,
141131
{ [styles.inline]: inline },
142132
className,
143-
uniqueSelectClass
133+
fallbackClass
144134
)}
145135
>
146136
{label && (
@@ -190,18 +180,6 @@ const Select = <T extends string>({
190180
</SelectPrimitive.Portal>
191181
</SelectPrimitive.Root>
192182
</span>
193-
<StatelessSelect
194-
values={values}
195-
defaultValue={defaultValue}
196-
placeholder={placeholder}
197-
label={label}
198-
inline={inline}
199-
className={className}
200-
ariaLabel={ariaLabel}
201-
disabled={disabled}
202-
targetElement={uniqueSelectClass}
203-
as={as}
204-
/>
205183
</Skeleton>
206184
);
207185
};

packages/ui-components/src/Containers/Sidebar/index.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
w-full
66
flex-col
77
gap-8
8-
overflow-auto
98
border-r-0
109
border-neutral-200
1110
bg-white
1211
px-4
1312
py-6
13+
sm:overflow-auto
1414
sm:border-r
1515
md:max-w-xs
1616
lg:px-6

packages/ui-components/src/Containers/Sidebar/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ComponentProps, FC, PropsWithChildren } from 'react';
22

3-
import Select from '#ui/Common/Select';
3+
import WithNoScriptSelect from '#ui/Common/Select/NoScriptSelect';
44
import SidebarGroup from '#ui/Containers/Sidebar/SidebarGroup';
55
import type { LinkLike } from '#ui/types';
66

@@ -42,7 +42,7 @@ const SideBar: FC<PropsWithChildren<SidebarProps>> = ({
4242
{children}
4343

4444
{selectItems.length > 0 && (
45-
<Select
45+
<WithNoScriptSelect
4646
label={title}
4747
values={selectItems}
4848
defaultValue={currentItem?.value}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const isStringArray = (
2+
values: Array<unknown>
3+
): values is Array<string> =>
4+
Boolean(values[0] && typeof values[0] === 'string');
5+
6+
export const isValuesArray = <T>(values: Array<unknown>): values is Array<T> =>
7+
Boolean(values[0] && typeof values[0] === 'object' && 'value' in values[0]);

0 commit comments

Comments
 (0)