spectrum-web-components icon indicating copy to clipboard operation
spectrum-web-components copied to clipboard

RFC: feat(picker): now leverages multiple selection

Open Rajdeepc opened this issue 7 months ago • 3 comments
trafficstars

RFC: Enhancing Picker Component with Multiple Selection

Summary

This RFC proposes enhancing the sp-picker component with improved multiple selection capabilities. The changes focus on providing a better user experience by keeping the overlay open during selection and implementing proper tag-based display of selected items.

Basic Multilple Selection:

<sp-picker multiple label="Select options">
  <sp-menu-item value="option1">Option 1</sp-menu-item>
  <sp-menu-item value="option2">Option 2</sp-menu-item>
  <sp-menu-item value="option3">Option 3</sp-menu-item>
</sp-picker>

With Icon Support

<sp-picker multiple label="Select actions">
  <sp-menu-item value="edit">
    <sp-icon-edit slot="icon"></sp-icon-edit>
    Edit
  </sp-menu-item>
  <sp-menu-item value="copy">
    <sp-icon-copy slot="icon"></sp-icon-copy>
    Copy
  </sp-menu-item>
  <sp-menu-item value="delete">
    <sp-icon-delete slot="icon"></sp-icon-delete>
    Delete
  </sp-menu-item>
</sp-picker>

Motivation

The sp-picker component currently supports single selection operation, but lacks support for selecting multiple items - a common use case in form interfaces. Users frequently need to choose several options from a dropdown list without the friction of repeated interactions.

Implementing multiple selection in the sp-picker component addresses several key user needs:

  1. Efficient Selection Process: Users can select multiple items in a single interaction flow, without needing to reopen the dropdown for each selection.
  2. Consistent Visual Representation: Selected items should appear as proper sp-tag components, providing a cohesive visual experience with the rest of the Spectrum design system.
  3. Rich Visual Feedback: The component should preserve visual attributes (such as icons) from the original menu items in their selected representation.
  4. Space Efficiency: When many items are selected, the interface should intelligently manage space constraints through techniques like showing "+n more" indicators.

Detailed Design

1. Keep Overlay Open During Multiple Selection

Modify the setValueFromItem method to check if the component is in multiple selection mode (this.multiple === true) and avoid closing the overlay when making selections:

protected async setValueFromItem(
    item: MenuItem,
    menuChangeEvent?: Event
): Promise<void> {
    // ...existing code...
    
    if (this.multiple) {
        // Logic for multiple selection
        
        // Do NOT close the overlay for multiple selection
        return;
    }
    
    // For single selection, close the overlay as before
    await this.close();
}

2. Use sp-tag Component for Selected Items

Replace the current HTML-based rendering of selected items with proper sp-tag components:

private renderSelectedTags(): TemplateResult {
    if (!this.multiple || this._selectedItems.length === 0) {
        return html``;
    }
    
    const visibleCount = this.maxOptionsVisible > 0 
        ? Math.min(this._selectedItems.length, this.maxOptionsVisible)
        : this._selectedItems.length;
        
    const hiddenCount = this._selectedItems.length - visibleCount;
    
    return html`
        <div class="tags">
            ${this._selectedItems.slice(0, visibleCount).map((item, index) => {
                if (this.renderTag) {
                    const customTag = this.renderTag(item, index);
                    if (typeof customTag === 'string') {
                        return html`${customTag}`;
                    }
                    return customTag;
                }
                
                // Get icon from menu item's children
                const itemChildren = item.itemChildren || {};
                const hasIcon = 'icon' in itemChildren && !!itemChildren.icon;
                
                return html`
                    <sp-tag 
                        size=${this.size || 'm'} 
                        ?deletable=${!this.readonly}
                        ?disabled=${this.disabled}
                        ?readonly=${this.readonly}
                        @delete=${(event: Event) => {
                            event.stopPropagation();
                            this.setValueFromItem(item);
                        }}
                    >
                        ${hasIcon ? html`
                            <slot name="icon" slot="icon">${itemChildren.icon}</slot>
                        ` : nothing}
                        ${item.textContent}
                    </sp-tag>
                `;
            })}
            ${hiddenCount > 0 ? html`
                <sp-tag size=${this.size || 'm'}>+${hiddenCount}</sp-tag>
            ` : nothing}
        </div>
    `;
}

3. Add Customizable Tag Rendering

Provide an API to customize the rendering of selected tags:

/**
 * A function that customizes the tags to be rendered when multiple=true.
 * The first argument is the option, the second is the current tag's index.
 * The function should return either a Lit TemplateResult or a string containing
 * trusted HTML of the symbol to render at the specified value.
 */
@property({ attribute: false })
public renderTag?: (
    option: MenuItem,
    index: number
) => TemplateResult | string;

4. Customize Default Max Visible Options

You can customize the default maxOptionsVisible value for a better user experience:

/**
 * The maximum number of selected options to show when `multiple` is true.
 * After the maximum, "+n" will be shown to indicate the number of additional items that are selected.
 * Set to 0 to remove the limit.
 */
@property({ type: Number, attribute: 'max-options-visible' })
public maxOptionsVisible = 3; // Changed from 2 to 3

Adoption Strategy

This change introduces new functionality through additional API properties that don't affect existing behavior. The new multiple property and its related APIs (maxOptionsVisible and renderTag) are entirely opt-in and won't impact components using the standard single-selection behavior. The new multiple selection API is designed with the following principles:

  1. Opt-in functionality: The default behavior of sp-picker remains unchanged; multiple selection is only activated when the multiple property is explicitly set to true.
  2. Additive API: The new API properties (multiple, maxOptionsVisible, and renderTag) extend the component's capabilities without modifying or deprecating any existing properties or methods.

Implementation Plan

  1. Modify the setValueFromItem method to keep the overlay open during multiple selection
  2. Implement tag-based rendering for selected items
  3. Add support for preserving icons from menu items
  4. Add the renderTag API for custom tag rendering
  5. Update CSS styles for tag layout
  6. Add stories demonstrating the new capabilities
  7. Update documentation
  8. Add tests for the new functionality

This implementation can be completed in a single pull request as the changes are isolated to the Picker component.

Rajdeepc avatar Apr 19 '25 10:04 Rajdeepc