From 8a900b634f2fbd8e36f3da1fa7a747c6a435c4be Mon Sep 17 00:00:00 2001 From: "Nichols, Kieran" Date: Wed, 27 Sep 2023 14:29:56 -0400 Subject: [PATCH] feat(list-dropdown): added configuration for setting secondary labels and for providing additional configuration to leading/trailing icon component elements --- .../list-dropdown/list-dropdown-constants.ts | 4 +++ src/lib/list-dropdown/list-dropdown-utils.ts | 19 ++++++++-- src/lib/select/core/base-select-adapter.ts | 3 ++ src/lib/select/option/option-constants.ts | 1 + src/lib/select/option/option-foundation.ts | 35 +++++++++++++++++++ src/lib/select/option/option.ts | 17 +++++++++ .../components/autocomplete/autocomplete.mdx | 3 ++ .../spec/list-dropdown/list-dropdown.spec.ts | 13 +++++++ 8 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/lib/list-dropdown/list-dropdown-constants.ts b/src/lib/list-dropdown/list-dropdown-constants.ts index c3bc216f1..dd771e1f4 100644 --- a/src/lib/list-dropdown/list-dropdown-constants.ts +++ b/src/lib/list-dropdown/list-dropdown-constants.ts @@ -1,3 +1,4 @@ +import { IconExternalType, IIconComponent } from '../icon'; import { IPopupPosition, PopupPlacement } from '../popup'; const attributes = { @@ -33,15 +34,18 @@ export type ListDropdownIconType = 'font' | 'component'; export interface IBaseListDropdownOption { value: T; label: string; + secondaryLabel?: string; disabled?: boolean; divider?: boolean; optionClass?: string | string[]; leadingIcon?: string; leadingIconClass?: string; leadingIconType?: ListDropdownIconType; + leadingIconComponentProps?: Partial; trailingIcon?: string; trailingIconClass?: string; trailingIconType?: ListDropdownIconType; + trailingIconComponentProps?: Partial; leadingBuilder?: () => HTMLElement; trailingBuilder?: () => HTMLElement; } diff --git a/src/lib/list-dropdown/list-dropdown-utils.ts b/src/lib/list-dropdown/list-dropdown-utils.ts index b94f3257e..0095f8f0b 100644 --- a/src/lib/list-dropdown/list-dropdown-utils.ts +++ b/src/lib/list-dropdown/list-dropdown-utils.ts @@ -1,6 +1,7 @@ import { addClass, getEventPath, isDeepEqual, isDefined, isObject } from '@tylertech/forge-core'; import { tylIconCheckBox, tylIconCheckBoxOutlineBlank } from '@tylertech/tyler-icons/standard'; import { ICON_CLASS_NAME } from '../constants'; +import { IIconComponent } from '../icon'; import { ILinearProgressComponent, LINEAR_PROGRESS_CONSTANTS } from '../linear-progress'; import { IListComponent, LIST_CONSTANTS } from '../list/list'; import { IPopupComponent, PopupAnimationType, POPUP_CONSTANTS } from '../popup'; @@ -221,6 +222,15 @@ export function createListItems(config: IListDropdownOpenConfig, listElement: IL } } + // Check for secondary (subtitle) text + if (option.secondaryLabel) { + const secondaryLabelElement = document.createElement('span'); + secondaryLabelElement.slot = 'subtitle'; + secondaryLabelElement.textContent = option.secondaryLabel; + listItemElement.twoLine = true; + listItemElement.appendChild(secondaryLabelElement); + } + // If multiple selections are enabled then we need to create and append a leading checkbox element if (config.multiple) { const checkboxElement = createCheckboxElement(isSelected); @@ -243,7 +253,7 @@ export function createListItems(config: IListDropdownOpenConfig, listElement: IL listItemElement.appendChild(element); } } else if (option.leadingIcon) { - const leadingIconElement = createIconElement(option.leadingIconType, option.leadingIcon, option.leadingIconClass || config.iconClass); + const leadingIconElement = createIconElement(option.leadingIconType, option.leadingIcon, option.leadingIconClass || config.iconClass, option.leadingIconComponentProps); leadingIconElement.slot = 'leading'; listItemElement.appendChild(leadingIconElement); } @@ -256,7 +266,7 @@ export function createListItems(config: IListDropdownOpenConfig, listElement: IL listItemElement.appendChild(element); } } else if (option.trailingIcon) { - const trailingIconElement = createIconElement(option.trailingIconType, option.trailingIcon, option.trailingIconClass || config.iconClass); + const trailingIconElement = createIconElement(option.trailingIconType, option.trailingIcon, option.trailingIconClass || config.iconClass, option.trailingIconComponentProps); trailingIconElement.slot = 'trailing'; listItemElement.appendChild(trailingIconElement); } @@ -318,7 +328,7 @@ function createDivider(): HTMLElement { return divider; } -function createIconElement(type: ListDropdownIconType = 'font', iconName: string, iconClass?: string): HTMLElement { +function createIconElement(type: ListDropdownIconType = 'font', iconName: string, iconClass?: string, componentProps?: Partial): HTMLElement { if (type === 'component') { const icon = document.createElement('forge-icon'); if (iconClass) { @@ -326,6 +336,9 @@ function createIconElement(type: ListDropdownIconType = 'font', iconName: string } icon.setAttribute('aria-hidden', 'true'); icon.name = iconName; + if (componentProps) { + Object.assign(icon, componentProps); + } return icon; } diff --git a/src/lib/select/core/base-select-adapter.ts b/src/lib/select/core/base-select-adapter.ts index 512edcbf6..71c36e53b 100644 --- a/src/lib/select/core/base-select-adapter.ts +++ b/src/lib/select/core/base-select-adapter.ts @@ -86,6 +86,7 @@ export abstract class BaseSelectAdapter extends BaseAdapter; private _trailingIcon: string; private _trailingIconClass: string; private _trailingIconType: ListDropdownIconType; + private _trailingIconComponentProps: Partial; private _leadingBuilder: () => HTMLElement; private _trailingBuilder: () => HTMLElement; @@ -45,6 +49,17 @@ export class OptionFoundation implements IOptionFoundation { } } + /** Gets/sets the secondary label of this option. */ + public get secondaryLabel(): string { + return this._secondaryLabel; + } + public set secondaryLabel(value: string) { + if (this._secondaryLabel !== value) { + this._secondaryLabel = value; + this._adapter.toggleHostAttribute(OPTION_CONSTANTS.attributes.SECONDARY_LABEL, !!this._secondaryLabel, this._secondaryLabel); + } + } + /** Gets/sets the disabled status of this option. */ public get disabled(): boolean { return this._disabled; @@ -119,6 +134,16 @@ export class OptionFoundation implements IOptionFoundation { } } + /** Gets/sets the props on the leading icon component. */ + public get leadingIconComponentProps(): Partial { + return this._leadingIconComponentProps; + } + public set leadingIconComponentProps(value: Partial) { + if (this._leadingIconComponentProps !== value) { + this._leadingIconComponentProps = value; + } + } + /** Gets/sets the trailing icon of this option. */ public get trailingIcon(): string { return this._trailingIcon; @@ -152,6 +177,16 @@ export class OptionFoundation implements IOptionFoundation { } } + /** Gets/sets the props on the trailing icon component. */ + public get trailingIconComponentProps(): Partial { + return this._trailingIconComponentProps; + } + public set trailingIconComponentProps(value: Partial) { + if (this._trailingIconComponentProps !== value) { + this._trailingIconComponentProps = value; + } + } + /** Gets/sets the leading builder of this option. */ public get leadingBuilder(): (() => HTMLElement) { return this._leadingBuilder; diff --git a/src/lib/select/option/option.ts b/src/lib/select/option/option.ts index 16fbebc02..fdbdb9ef0 100644 --- a/src/lib/select/option/option.ts +++ b/src/lib/select/option/option.ts @@ -1,4 +1,5 @@ import { coerceBoolean, CustomElement, FoundationProperty, ICustomElement } from '@tylertech/forge-core'; +import { IIconComponent } from '../../icon'; import { BaseComponent } from '../../core/base/base-component'; import { IBaseListDropdownOption, ListDropdownIconType } from '../../list-dropdown/list-dropdown-constants'; import { OptionAdapter } from './option-adapter'; @@ -26,6 +27,7 @@ export class OptionComponent extends BaseComponent implements IOptionComponent { return [ OPTION_CONSTANTS.attributes.VALUE, OPTION_CONSTANTS.attributes.LABEL, + OPTION_CONSTANTS.attributes.SECONDARY_LABEL, OPTION_CONSTANTS.attributes.DISABLED, OPTION_CONSTANTS.attributes.DIVIDER, OPTION_CONSTANTS.attributes.OPTION_CLASS, @@ -59,6 +61,9 @@ export class OptionComponent extends BaseComponent implements IOptionComponent { case OPTION_CONSTANTS.attributes.LABEL: this.label = newValue; break; + case OPTION_CONSTANTS.attributes.SECONDARY_LABEL: + this.secondaryLabel = newValue; + break; case OPTION_CONSTANTS.attributes.DISABLED: this.disabled = coerceBoolean(newValue); break; @@ -97,6 +102,10 @@ export class OptionComponent extends BaseComponent implements IOptionComponent { @FoundationProperty() public declare label: string; + /** Gets/sets the secondary label of this option. */ + @FoundationProperty() + public declare secondaryLabel: string; + /** Gets/sets the disabled status of this option. */ @FoundationProperty() public declare disabled: boolean; @@ -121,6 +130,10 @@ export class OptionComponent extends BaseComponent implements IOptionComponent { @FoundationProperty() public declare leadingIconType: ListDropdownIconType; + /** Gets/sets properties on leading icon component. */ + @FoundationProperty() + public declare leadingIconComponentProps: Partial; + /** Gets/sets the trailing icon of this option. */ @FoundationProperty() public declare trailingIcon: string; @@ -133,6 +146,10 @@ export class OptionComponent extends BaseComponent implements IOptionComponent { @FoundationProperty() public declare trailingIconType: ListDropdownIconType; + /** Gets/sets properties on trailing icon component. */ + @FoundationProperty() + public declare trailingIconComponentProps: Partial; + /** Gets/sets the leading builder of this option. */ @FoundationProperty() public declare leadingBuilder: () => HTMLElement; diff --git a/src/stories/src/components/autocomplete/autocomplete.mdx b/src/stories/src/components/autocomplete/autocomplete.mdx index 01ebf14cb..b61c2266b 100644 --- a/src/stories/src/components/autocomplete/autocomplete.mdx +++ b/src/stories/src/components/autocomplete/autocomplete.mdx @@ -287,15 +287,18 @@ enum AutocompleteMode { interface IAutocompleteOption { value: T; label: string; + secondaryLabel?: string; disabled?: boolean; divider?: boolean; optionClass?: string | string[]; leadingIcon?: string; leadingIconClass?: string; leadingIconType?: ListDropdownIconType; + leadingIconComponentProps?: Partial; trailingIcon?: string; trailingIconClass?: string; trailingIconType?: ListDropdownIconType; + trailingIconComponentProps?: Partial; leadingBuilder?: () => HTMLElement; trailingBuilder?: () => HTMLElement; options?: Array; diff --git a/src/test/spec/list-dropdown/list-dropdown.spec.ts b/src/test/spec/list-dropdown/list-dropdown.spec.ts index a42ddca3d..ad6b87626 100644 --- a/src/test/spec/list-dropdown/list-dropdown.spec.ts +++ b/src/test/spec/list-dropdown/list-dropdown.spec.ts @@ -953,4 +953,17 @@ describe('ListDropdown', function(this: ITestContext) { expect(attrValue).toBeTruthy(); expect(attrValue).toBe('test-value'); }); + + it('should display options with secondary label', async function(this: ITestContext) { + const opts: IListDropdownOption[] = [ + { label: 'Label', secondaryLabel: 'Secondary label', value: 'value' }, + ]; + this.context = createListDropdown({ ...DEFAULT_CONFIG, options: opts }); + this.context.listDropdown.open(); + await timer(POPUP_CONSTANTS.numbers.ANIMATION_DURATION); + await tick(); + + const listItems = getListItems(); + expect(listItems[0].querySelector('span[slot=subtitle]')?.textContent).toBe('Secondary label'); + }); });