Skip to content

Conversation

r00gm
Copy link
Contributor

@r00gm r00gm commented Oct 20, 2025

Summary

Add select component based on reka-ui for component migration

Link to figma screens: https://www.figma.com/design/8zib7Trf2D2CHYXrEGPHkg/n8n-Design-System-V3?node-id=2121-511

Related Linear tickets, Github issues, and Community forum posts

PAY-3996

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@r00gm r00gm marked this pull request as ready for review October 20, 2025 09:57
Copy link

cursor bot commented Oct 20, 2025

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on November 5.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link

bundlemon bot commented Oct 20, 2025

BundleMon

Unchanged files (2)
Status Path Size Limits
WASM Dependencies
tree-sitter-bash.wasm
181.26KB -
WASM Dependencies
tree-sitter.wasm
74.47KB -

No change in files bundle size

Groups updated (2)
Status Path Size Limits
**/*.js
5.84MB (+430.75KB +7.76%) -
**/*.css
214.82KB (+1.56KB +0.73%) -

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 8 files

Prompt for AI agents (all 6 issues)

Understand the root cause of the following 6 issues and fix them.


<file name="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.test.ts">

<violation number="1" location="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.test.ts:405">
The test labelled for the `item-label` slot still registers the `item-trailing` slot, so it just duplicates the trailing-slot check and leaves the label slot untested. Please target the `item-label` slot here so coverage matches the description.</violation>
</file>

<file name="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.vue">

<violation number="1" location="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.vue:141">
Rule violated: **Prefer Typeguards over Type casting**

Please remove the `as string` cast on `props.labelKey`. The prop is typed as `GetItemKeys&lt;T&gt; | undefined`, so this assertion hides the undefined case—callers can still omit the prop, which would make `get` receive `undefined` and throw at runtime. Adjust the typing (e.g., derive a default key or refine the prop type) so the compiler knows the value is a string without a cast, in line with our “Prefer type guards over type casting” rule.</violation>

<violation number="2" location="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.vue:172">
The selection indicator ignores the new `selectedIcon` prop, so passing a custom icon has no effect. Please bind the indicator to the prop instead of hard-coding &quot;check&quot;.</violation>
</file>

<file name="packages/frontend/@n8n/design-system/src/__tests__/setup.ts">

<violation number="1" location="packages/frontend/@n8n/design-system/src/__tests__/setup.ts:30">
Rule violated: **Prefer Typeguards over Type casting**

Avoid narrowing with `as` here; annotate the variable instead so the optional pointer APIs are reflected without a cast. The &quot;Prefer Typeguards over Type casting&quot; rule forbids this assertion scenario.</violation>

<violation number="2" location="packages/frontend/@n8n/design-system/src/__tests__/setup.ts:63">
Overriding MouseEvent.defaultPrevented to always return false hides preventDefault() effects, so tests can’t detect default-preventing logic.</violation>
</file>

<file name="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.stories.ts">

<violation number="1" location="packages/frontend/@n8n/design-system/src/v2/components/Select/Select.stories.ts:134">
Rule violated: **Prefer Typeguards over Type casting**

Remove the `(args.items as SelectItem[])` assertion and either type the story args or guard the runtime value so you don&#39;t bypass the SelectItem contract; this cast currently suppresses TS2322 and violates the &quot;Prefer Typeguards over Type casting&quot; rule.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

defaultOpen: true,
},
slots: {
'item-trailing': '<span data-test-id="custom-trailing">any</span>',
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test labelled for the item-label slot still registers the item-trailing slot, so it just duplicates the trailing-slot check and leaves the label slot untested. Please target the item-label slot here so coverage matches the description.

Prompt for AI agents
Address the following comment on packages/frontend/@n8n/design-system/src/v2/components/Select/Select.test.ts at line 405:

<comment>The test labelled for the `item-label` slot still registers the `item-trailing` slot, so it just duplicates the trailing-slot check and leaves the label slot untested. Please target the `item-label` slot here so coverage matches the description.</comment>

<file context>
@@ -0,0 +1,471 @@
+					defaultOpen: true,
+				},
+				slots: {
+					&#39;item-trailing&#39;: &#39;&lt;span data-test-id=&quot;custom-trailing&quot;&gt;any&lt;/span&gt;&#39;,
+				},
+			});
</file context>

✅ Addressed in 4d1eca3

<slot name="item-trailing" :item="item as NestedItem<T>" :index="index" />
<SelectItemIndicator as-child>
<Icon icon="check" />
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selection indicator ignores the new selectedIcon prop, so passing a custom icon has no effect. Please bind the indicator to the prop instead of hard-coding "check".

Prompt for AI agents
Address the following comment on packages/frontend/@n8n/design-system/src/v2/components/Select/Select.vue at line 172:

<comment>The selection indicator ignores the new `selectedIcon` prop, so passing a custom icon has no effect. Please bind the indicator to the prop instead of hard-coding &quot;check&quot;.</comment>

<file context>
@@ -0,0 +1,361 @@
+										&lt;slot name=&quot;item-trailing&quot; :item=&quot;item as NestedItem&lt;T&gt;&quot; :index=&quot;index&quot; /&gt;
+
+										&lt;SelectItemIndicator as-child&gt;
+											&lt;Icon icon=&quot;check&quot; /&gt;
+										&lt;/SelectItemIndicator&gt;
+									&lt;/span&gt;
</file context>
Suggested change
<Icon icon="check" />
<Icon :icon="selectedIcon" />
Fix with Cubic

constructor(type: string, eventInit?: MouseEventInit) {
super(type, eventInit);
Object.defineProperty(this, 'defaultPrevented', {
get: () => false,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding MouseEvent.defaultPrevented to always return false hides preventDefault() effects, so tests can’t detect default-preventing logic.

Prompt for AI agents
Address the following comment on packages/frontend/@n8n/design-system/src/__tests__/setup.ts at line 63:

<comment>Overriding MouseEvent.defaultPrevented to always return false hides preventDefault() effects, so tests can’t detect default-preventing logic.</comment>

<file context>
@@ -20,3 +21,60 @@ window.ResizeObserver =
+	constructor(type: string, eventInit?: MouseEventInit) {
+		super(type, eventInit);
+		Object.defineProperty(this, &#39;defaultPrevented&#39;, {
+			get: () =&gt; false,
+		});
+	}
</file context>
Fix with Cubic

const value = ref(args.modelValue);
const icon = computed(
// @ts-expect-error TS2322
() => (args.items as SelectItem[])?.find((item) => item.value === value.value)?.icon,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Remove the (args.items as SelectItem[]) assertion and either type the story args or guard the runtime value so you don't bypass the SelectItem contract; this cast currently suppresses TS2322 and violates the "Prefer Typeguards over Type casting" rule.

Prompt for AI agents
Address the following comment on packages/frontend/@n8n/design-system/src/v2/components/Select/Select.stories.ts at line 134:

<comment>Remove the `(args.items as SelectItem[])` assertion and either type the story args or guard the runtime value so you don&#39;t bypass the SelectItem contract; this cast currently suppresses TS2322 and violates the &quot;Prefer Typeguards over Type casting&quot; rule.</comment>

<file context>
@@ -0,0 +1,263 @@
+			const value = ref(args.modelValue);
+			const icon = computed(
+				// @ts-expect-error TS2322
+				() =&gt; (args.items as SelectItem[])?.find((item) =&gt; item.value === value.value)?.icon,
+			);
+			return { args, value, icon };
</file context>
Fix with Cubic

v-if="isSelectItem(item) && item.type === 'label'"
:class="$style.SelectLabel"
>
{{ get(item, props.labelKey as string) }}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Please remove the as string cast on props.labelKey. The prop is typed as GetItemKeys<T> | undefined, so this assertion hides the undefined case—callers can still omit the prop, which would make get receive undefined and throw at runtime. Adjust the typing (e.g., derive a default key or refine the prop type) so the compiler knows the value is a string without a cast, in line with our “Prefer type guards over type casting” rule.

Prompt for AI agents
Address the following comment on packages/frontend/@n8n/design-system/src/v2/components/Select/Select.vue at line 141:

<comment>Please remove the `as string` cast on `props.labelKey`. The prop is typed as `GetItemKeys&lt;T&gt; | undefined`, so this assertion hides the undefined case—callers can still omit the prop, which would make `get` receive `undefined` and throw at runtime. Adjust the typing (e.g., derive a default key or refine the prop type) so the compiler knows the value is a string without a cast, in line with our “Prefer type guards over type casting” rule.</comment>

<file context>
@@ -0,0 +1,361 @@
+								v-if=&quot;isSelectItem(item) &amp;&amp; item.type === &#39;label&#39;&quot;
+								:class=&quot;$style.SelectLabel&quot;
+							&gt;
+								{{ get(item, props.labelKey as string) }}
+							&lt;/SelectLabel&gt;
+
</file context>
Fix with Cubic

*/
beforeAll(() => {
// Patch missing pointer APIs
const elementProto = HTMLElement.prototype as HTMLElement & {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Avoid narrowing with as here; annotate the variable instead so the optional pointer APIs are reflected without a cast. The "Prefer Typeguards over Type casting" rule forbids this assertion scenario.

Prompt for AI agents
Address the following comment on packages/frontend/@n8n/design-system/src/__tests__/setup.ts at line 30:

<comment>Avoid narrowing with `as` here; annotate the variable instead so the optional pointer APIs are reflected without a cast. The &quot;Prefer Typeguards over Type casting&quot; rule forbids this assertion scenario.</comment>

<file context>
@@ -20,3 +21,60 @@ window.ResizeObserver =
+ */
+beforeAll(() =&gt; {
+	// Patch missing pointer APIs
+	const elementProto = HTMLElement.prototype as HTMLElement &amp; {
+		hasPointerCapture?: (pointerId: number) =&gt; boolean;
+		setPointerCapture?: (pointerId: number) =&gt; void;
</file context>
Fix with Cubic

Copy link

codecov bot commented Oct 20, 2025

const variant = computed(() => variants[props.variant]);
const sizes = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding a constraint here to maintain consistency between props and defined values.

Suggested change
const sizes = {
const sizes: Record<SelectProps['size'], string> = {

triggerRef,
});
const variants = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding a constraint here to maintain consistency between props and defined values.

Suggested change
const variants = {
const sizes: Record<SelectProps['variant'], string> = {

<RSelectItem
v-else
:disabled="isSelectItem(item) && item.disabled"
:value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely to cause a performance issue. The get will get called on every single re-render for every single item. I'd add a wrapper component for SelectItem and have this live within a computed property.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on having a dedicated SelectItem component that is to be consumed by this new Select.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in terms of performance for the amount of data that this component is expect to handle, there is no significant difference(if any). For the combobox that is expected to handle lots of data then yes.

i think the SelectedItem is not exported on purpose, why?

we have the item slot - that allows to override the content of the item - which also makes sure that no random component can be fed to the list

item-leading, item-label, item-trailing allow for chaging only specific parts

<SelectItemText>
<slot name="item-label" :item="item as NestedItem<T>" :index="index">
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. This should probably get extracted alongside SelectItem (maybe in the same component).

line-height: var(--line-height--md);
border: 1px solid transparent;
background-color: var(--color--background--light-2);
height: 24px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please discuss with design team whether we want to have consistent component heights across sizes and extract into CSS variables. I'd say yes, since we want the select to have the same height as inputs/buttons at this size.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tuukkaa this one is for you :)

background-color: var(--color--background--light-2);
height: 24px;
position: relative;
gap: 6px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use spacing variable here 👀

margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use spacing variable here

}
.SelectItem {
font-size: 13px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use font size variable

position: relative;
user-select: none;
color: var(--text--color);
gap: 6px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use spacing variable

display: flex;
align-items: center;
height: 24px;
padding: 0 8px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use spacing variable

}
.SelectLabel {
padding: 6px 8px 4px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use spacing variables

:disabled="disabled"
:default-value="castToSelectItemValue(defaultValue)"
:model-value="castToSelectItemValue(modelValue)"
style="z-index: 1000"
Copy link
Member

@alexgrozav alexgrozav Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create and use z-index variable and say no to inline styles 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this must have slipped from my test, nice catch!

@konstantintieber
Copy link
Contributor

Visual comments:
Link to figma
Link to local storybook story with 20 options for easy testing

I don't see the drop shadow for the up and down carets on scrollable options in the design:
image

Also: the options list should also show a scrollbar

}
&[data-placeholder] {
color: var(--color--text--tint-1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As raised by the accessibility plugin in figma (and also noticed by my own eyes when testing manual): this color results in a low contrast of 3.03:1, when it should be at least 4.5:1 (for reference: white on black would be 21:1 contrast ratio).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Color contrasts have been a problem for a long while. We'll have to discuss with the design team.

height: 24px;
position: relative;
gap: 6px;
color: var(--text--color);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inspected the element in devtools to see what value this variable resolves to, and it turns out it's not defined. Is this just a sideeffect of the storybook dev server?
I also don't see any reference to --text-color anywhere else in our code though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the figma file doesn't have the right color token so i've done best estimates but we need the definitive ones, specially when it comes to accesibility

@r00gm
Copy link
Contributor Author

r00gm commented Oct 21, 2025

Visual comments: Link to figma Link to local storybook story with 20 options for easy testing

I don't see the drop shadow for the up and down carets on scrollable options in the design: image

Also: the options list should also show a scrollbar

about the shadow and the scrollbar.

this was already agreed with @Tuukkaa. even if it's not 1:1 with the designs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants