Skip to content

feat: add <ep-select> form select (Etherpad nice-select look)#5

Open
JohnMcLear wants to merge 1 commit into
mainfrom
feat/ep-select
Open

feat: add <ep-select> form select (Etherpad nice-select look)#5
JohnMcLear wants to merge 1 commit into
mainfrom
feat/ep-select

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Why

The library had no native-<select>-style component. The closest were <ep-toolbar-select> (the editbar font/heading picker) and <ep-dropdown> (a menu) — neither is a form select with a bound value. The demo's own theme switcher had to fall back to a bare, unstyled native <select>.

Etherpad skins its <select>s with nice-select (jquery-nice-select), so this component reproduces that look against the colibris palette.

What

<ep-select> — a form select matching nice-select:

  • Visuals: .nice-select box with rotating chevron + .list popup; bg-soft box, bg-color option hover/selected, bold selected option, greyed disabled option — ported from css/pad/form.css (base geometry) + skins/colibris/src/components/form.css (colours).
  • API: options ([{value,label,disabled?}]), value (reflected), placeholder, disabled, name; fires ep-select:change with { value, label }.
  • Keyboard: Enter/Space toggle & select, ArrowUp/Down (skips disabled), Escape; closes on outside click.

Tests (built test-first)

6 Storybook play-function tests: render+placeholder+option count, opens on click, selects option (value + event + closes), preselected value, disabled box doesn't open, disabled option not selectable. Full suite now 88 tests / 16 files, typecheck clean.

Verified the rendered look matches nice-select in light + dark (bold selected row, normal others, greyed disabled, rotating chevron) with no console errors.

(Carries the pnpm dlxpnpm exec Playwright CI fix so this branch's browser tests pass independently.)

🤖 Generated with Claude Code

The library had no native-<select>-style component — the closest were the
toolbar picker (ep-toolbar-select) and the menu (ep-dropdown). This adds a
proper form select styled to match Etherpad colibris's nice-select look
(the plugin Etherpad uses to skin native <select>s):

- nice-select DOM/visuals: .nice-select box + rotating chevron, .list popup
  with the colibris palette (bg-soft box, bg-color option hover/selected,
  bold selected, greyed disabled).
- API: options[{value,label,disabled?}], value (reflected), placeholder,
  disabled, name; fires ep-select:change {value,label}.
- Keyboard: Enter/Space toggle+select, Arrow up/down (skips disabled),
  Escape; closes on outside click.

Built test-first; 6 Storybook play-function tests cover render, open,
select+event, preselected value, disabled box, disabled option. Suite is
now 88 tests across 16 files. Carries the dlx→exec Playwright CI fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add form select component with nice-select styling

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add `` form select component matching Etherpad nice-select styling
• Implements full keyboard navigation (Enter/Space/Arrow keys/Escape)
• Includes 6 comprehensive Storybook play-function tests covering all features
• Fix Playwright CI by replacing pnpm dlx with pnpm exec
Diagram
flowchart LR
  A["EpSelect Component"] --> B["DOM & Styling"]
  A --> C["API & Events"]
  A --> D["Keyboard Interaction"]
  B --> E["nice-select box + chevron"]
  B --> F["Dropdown list with colibris palette"]
  C --> G["options, value, placeholder, disabled"]
  C --> H["ep-select:change event"]
  D --> I["Enter/Space/Arrow/Escape keys"]
  D --> J["Outside click closes"]

Loading

Grey Divider

File Changes

1. src/EpSelect.ts ✨ Enhancement +249/-0

New styled form select component with full keyboard support

• New LitElement component implementing styled form select matching Etherpad's nice-select
• Comprehensive CSS styling with colibris palette variables (bg-soft, bg-color, text colors)
• Full keyboard support: Enter/Space to toggle/select, Arrow keys to navigate (skipping disabled),
 Escape to close
• API includes options array, reflected value property, placeholder, disabled, name, and
 fires ep-select:change event with {value, label} detail
• Outside click detection to close dropdown, focus management for keyboard navigation

src/EpSelect.ts


2. src/index.ts ✨ Enhancement +2/-0

Export new EpSelect component and types

• Export new EpSelect component class
• Export EpSelectOption interface for type safety

src/index.ts


3. stories/EpSelect.stories.ts 🧪 Tests +112/-0

Comprehensive Storybook tests for EpSelect component

• 6 Storybook play-function tests covering render, placeholder display, option count
• Tests for open-on-click, option selection with event firing, preselected values
• Tests for disabled state (box doesn't open, disabled options not selectable)
• Uses Storybook test utilities (expect, userEvent, fn, waitFor)

stories/EpSelect.stories.ts


View more (2)
4. .github/workflows/ci.yml 🐞 Bug fix +1/-1

Fix Playwright CI command for browser installation

• Replace pnpm dlx playwright install with pnpm exec playwright install
• Fixes Playwright browser installation in CI pipeline

.github/workflows/ci.yml


5. package.json ⚙️ Configuration changes +1/-0

Add EpSelect to package exports

• Add ./EpSelect.js export entry mapping to ./dist/EpSelect.js
• Maintains consistent export pattern with other components

package.json


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Action required

1. Stale focusIndex crash 🐞 Bug ≡ Correctness
Description
EpSelect._onKeydown calls _select(this.options[this._focusIndex]) without checking the upper bound,
so if options changes while open, Enter/Space can pass undefined into _select() and throw when
reading opt.disabled. This can crash the component during keyboard interaction in dynamic UIs.
Code

src/EpSelect.ts[R192-200]

Evidence
_onKeydown() only checks _focusIndex >= 0 before passing this.options[this._focusIndex] to
_select(), and _select() immediately dereferences opt.disabled, which will throw if the option
is undefined. EpDropdown shows the expected defensive bound-check pattern before dereferencing the
focused item.

src/EpSelect.ts[171-181]
src/EpSelect.ts[192-200]
src/EpDropdown.ts[238-245]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`EpSelect._onKeydown()` can call `_select()` with an out-of-bounds option (undefined) if `options` changes while the menu is open, leading to a runtime exception when `_select()` dereferences `opt.disabled`.

## Issue Context
`options` is a public reactive property and can be updated by consumers at any time. The repo's `EpDropdown` guards focus index bounds before dereferencing.

## Fix Focus Areas
- src/EpSelect.ts[171-181]
- src/EpSelect.ts[192-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Always-on document click 🐞 Bug ➹ Performance
Description
EpSelect installs a document click listener for the lifetime of every instance (even while closed),
creating unnecessary global event overhead at scale. EpDropdown only attaches/removes global
listeners when open changes, which avoids this cost.
Code

src/EpSelect.ts[R140-152]

Evidence
EpSelect registers the document listener unconditionally on connect and removes it only on
disconnect. EpDropdown shows an in-repo pattern that adds global listeners on open and removes them
on close via updated() and _onOpened/_onClosed().

src/EpSelect.ts[140-152]
src/EpDropdown.ts[89-95]
src/EpDropdown.ts[156-175]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`EpSelect` adds a `document.click` listener in `connectedCallback()` and keeps it registered for the entire lifetime of the element, even though it only matters when `open === true`.

## Issue Context
`EpDropdown` in this repo demonstrates a pattern of attaching global listeners only while open and removing them on close.

## Fix Focus Areas
- src/EpSelect.ts[140-152]
- src/EpDropdown.ts[89-95]
- src/EpDropdown.ts[156-175]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/EpSelect.ts
Comment on lines +192 to +200
private _onKeydown(e: KeyboardEvent) {
if (this.disabled) return;
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
if (this.open && this._focusIndex >= 0) this._select(this.options[this._focusIndex]);
else this._toggle();
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Stale focusindex crash 🐞 Bug ≡ Correctness

EpSelect._onKeydown calls _select(this.options[this._focusIndex]) without checking the upper bound,
so if options changes while open, Enter/Space can pass undefined into _select() and throw when
reading opt.disabled. This can crash the component during keyboard interaction in dynamic UIs.
Agent Prompt
## Issue description
`EpSelect._onKeydown()` can call `_select()` with an out-of-bounds option (undefined) if `options` changes while the menu is open, leading to a runtime exception when `_select()` dereferences `opt.disabled`.

## Issue Context
`options` is a public reactive property and can be updated by consumers at any time. The repo's `EpDropdown` guards focus index bounds before dereferencing.

## Fix Focus Areas
- src/EpSelect.ts[171-181]
- src/EpSelect.ts[192-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@JohnMcLear JohnMcLear requested a review from SamTV12345 June 2, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant