Skip to content

Feat/trail 4 dropdown component#24

Merged
Hannahbird merged 10 commits intomainfrom
feat/TRAIL-4_dropdown_component
Mar 23, 2026
Merged

Feat/trail 4 dropdown component#24
Hannahbird merged 10 commits intomainfrom
feat/TRAIL-4_dropdown_component

Conversation

@Hannahbird
Copy link
Copy Markdown
Collaborator

@Hannahbird Hannahbird commented Mar 18, 2026

Summary

Fixes: #

Related Tickets: TRAIL-4 https://jira.atlassian.krum.io/browse/TRAIL-4?atlOrigin=eyJpIjoiNDRlZTg3NjA2ODZkNDBkMjkxNzE2OWVkODJhZGFkMTMiLCJwIjoiaiJ9

Adds a new trailhand-dropdown web component supporting single and multiselect modes with
built-in search filtering, keyboard navigation, form association, and full dark mode support.
Also integrates the dropdown into the existing dev examples and form modal mockups.


Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📚 Documentation update
  • 🎨 Style/UI change
  • ♻️ Refactor (no functional changes)
  • ⚡ Performance improvement
  • 🧪 Test update
  • 🔧 Build/CI configuration
  • 🧹 Chore (dependency updates, cleanup, etc.)

Changes Made

Primary Changes

  • Added trailhand-dropdown Lit web component with single and multiselect support
  • Multiselect selections render as trailhand-tag (info, sm, dismissible)
  • Built-in search/filter input inside the dropdown panel with a clear button
  • clearOthers: boolean flag on DropdownOption designates an option that deselects all others when selected (e.g. "All Namespaces"), and is itself deselected when any other option is chosen
  • Full keyboard navigation: Arrow up/down between options, Escape to close, Enter/Space to select
  • Form-Associated Custom Elements spec implemented, works natively in <form> with FormData, including formResetCallback and formDisableCallback; multiselect uses FormData to submit multiple values under the same name
  • size prop (small / medium / large), disabled, required, invalid, label, placeholder
  • min-width: var(--th-dropdown-min-width, 220px) prevents the trigger from collapsing to fit its selected value
  • Outside-click detection using composedPath() for correct shadow DOM handling

Secondary/Collateral Changes

  • Added --th-dropdown-* CSS custom properties to colors.css for both light and dark themes (background, option text, selected/hover states, shadow)
  • Fixed --th-input-bg in colors.css was set to 'transparent' (quoted string, invalid CSS); corrected to var(--th-color-grey-200) so disabled inputs and the disabled dropdown trigger both show a visible grey background in light mode
  • Applied background: var(--th-input-bg) to the disabled trigger to match text-input disabled treatment
  • Added dropdown import, a dedicated Dropdowns showcase section, and updated all three form modal mockups in dev/main.ts (Namespace, Catalog Service, Bind to Application fields replaced with dropdowns)
  • Exported component from src/index.ts

Technical Notes

Files Modified

New files:

  • src/components/dropdown/dropdown.ts component implementation
  • src/components/dropdown/dropdown.stories.ts Storybook stories with formatted code snippets
  • src/components/dropdown/dropdown.test.ts Vitest unit tests

Modified files:

  • src/components/dropdown/index.ts export wired up (was empty placeholder)
  • src/index.ts added export * from './components/dropdown'
  • src/styles/colors.css new --th-dropdown-* variables (light + dark), fixed --th-input-bg
  • dev/main.ts dropdown import, showcase section, modal field updates

Implementation Details

clearOthers design: The "All" behavior is a per-option flag rather than a component-level prop. This keeps the component generic the consumer controls which option(s) carry the behavior through their data, with no hardcoded concept of "all" in the component itself. Multiple options can carry the flag if needed.

Outside click handling: Uses e.composedPath().includes(this) instead of e.target to correctly detect clicks inside shadow DOM children without requiring stopPropagation on every internal interaction.

Dark mode panel background: The dropdown panel uses var(--th-dropdown-bg) which is defined per-theme in colors.css. Previously the fallback was hardcoded #ffffff, making the panel appear white in dark mode. The fix required adding explicit variables to colors.css rather than relying on inheritance, since the panel sits in shadow DOM and doesn't automatically inherit data-theme styles from the document.

Multiselect form values: Uses internals.setFormValue(FormData) to submit multiple values under the same field name, which is the correct approach per the FACE spec for multi-value fields.


Testing

How to Test

  1. Run npm run dev and navigate to the Dropdowns section
  2. Verify single select: click to open, type to filter, select an option, confirm trigger updates and dropdown closes
  3. Verify multiselect: select multiple options, confirm tags appear in the trigger, dismiss a tag via ×, confirm the value is removed
  4. Select "All Namespaces" in a multiselect confirm all other selections clear; then select a specific option, confirm "All Namespaces" is deselected
  5. Open any of the three form modal examples (Configuration, Instances – Catalog Service, Instances – Config Data) and confirm Namespace/Catalog Service/Application fields render as dropdowns
  6. Toggle dark mode: confirm dropdown panel, options, search input, and tags all render correctly with dark backgrounds
  7. Set a dropdown to disabled confirm trigger shows grey background and cannot be opened
  8. Run npm run test:unit confirm all dropdown tests pass

Test Coverage

  • Unit tests added/updated
  • Integration tests added/updated
  • E2E tests added/updated
  • Manual testing completed

Browsers Tested

  • Chrome
  • Firefox
  • Safari
  • Edge

Potential Regressions

  • --th-input-bg fix in colors.css now applies a grey background to disabled trailhand-text-input fields in light mode (previously transparent due to the invalid quoted value). This is the intended behavior but worth a visual check on any forms with disabled inputs.

Screenshots / Videos

Before After
No dropdown component Single select, multiselect, dark mode, disabled state

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code in hard-to-understand areas (clearOthers JSDoc with example)
  • I have updated documentation as needed
  • My changes generate no new warnings or errors
  • I have tested my changes locally
  • Any dependent changes have been merged and published

…yling and gathering similar logic from other dropdown components from different libraries as well as epinios use case
@Hannahbird Hannahbird requested a review from johnlcos March 18, 2026 19:34
return html`
<div class="dropdown-panel" role="listbox" aria-multiselectable=${this.multiselect}>
<div class="search-wrapper">
<input
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should add a prop that enables the filter input. I can see places where it may not be needed.

label = '';

@property({ type: Boolean })
required = false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Right now required doesnt behave as you would expect in a form. If the user tries to submit a form without selecting from the dropdown, we want the submit to be blocked and the user to be notified. I think we can probably handle this similar to the validity methods added to the text input component

johnlcos and others added 7 commits March 19, 2026 16:16
- Add  boolean prop to opt-in to a search input at the top
  of the panel (works for both single and multiselect)
- Remove inline search from multiselect triggersearch now lives
  consistently in the panel for all modes when filterable is enabled
- Implement  via ElementInternals so required dropdowns
  block form submission and surface a native validation message
- Expose , , and  as public
  methods delegating to internals (standard FACE pattern)
- Use  lifecycle to re-validate when required/value/values
  change without a user interaction
- Add WithFilter and MultiselectWithFilter Storybook stories
- Update tests: replace inline-search assertion, add filterable
  beforeEach to Filtering suite, add Required validation test group
…railhand-ui into feat/TRAIL-4_dropdown_component
…railhand-ui into feat/TRAIL-4_dropdown_component
@Hannahbird Hannahbird requested a review from johnlcos March 20, 2026 15:30
this._updateValidity();
}

private _updateValidity() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Right now when the form renders, the dropdown looks invalid by default. It should only be invalid if the user tries to submit if possible, this way it behaves like an input

Image

- Defer invalid visual state until user interacts or submit is attempted
- Fix required border showing blue on submit — add invalid event listener,
  outline: none on trigger, and strengthen invalid CSS
Copy link
Copy Markdown
Member

@johnlcos johnlcos left a comment

Choose a reason for hiding this comment

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

LGTM

@Hannahbird Hannahbird merged commit 5df4e66 into main Mar 23, 2026
3 of 4 checks passed
@Hannahbird Hannahbird deleted the feat/TRAIL-4_dropdown_component branch March 23, 2026 14:58
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.

2 participants