diff --git a/cspell.json b/cspell.json index 1ae873361..028a62671 100644 --- a/cspell.json +++ b/cspell.json @@ -160,6 +160,7 @@ "unbundles", "unbundling", "unicons", + "unsanitized", "unsupportive", "valpha", "valuenow", diff --git a/docs/pages/components/select.md b/docs/pages/components/select.md index 6fd05cbe1..4705dc07b 100644 --- a/docs/pages/components/select.md +++ b/docs/pages/components/select.md @@ -454,3 +454,53 @@ const App = () => ( ); ``` + +### Custom Tags + +When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a Lit Template, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `` element and the second argument is the tag's index (its position in the tag list). + +Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/getting-started/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector. + +```html:preview + + + + Email + + + + Phone + + + + Chat + + + + +``` + +:::warning +Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities. +::: diff --git a/src/components/select/select.component.ts b/src/components/select/select.component.ts index 54d809854..9bc5a0cd3 100644 --- a/src/components/select/select.component.ts +++ b/src/components/select/select.component.ts @@ -8,6 +8,7 @@ import { html } from 'lit'; import { LocalizeController } from '../../utilities/localize.js'; import { property, query, state } from 'lit/decorators.js'; import { scrollIntoView } from '../../internal/scroll.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { waitForEvent } from '../../internal/event.js'; import { watch } from '../../internal/watch.js'; import ShoelaceElement from '../../internal/shoelace-element.js'; @@ -15,7 +16,7 @@ import SlIcon from '../icon/icon.component.js'; import SlPopup from '../popup/popup.component.js'; import SlTag from '../tag/tag.component.js'; import styles from './select.styles.js'; -import type { CSSResultGroup } from 'lit'; +import type { CSSResultGroup, TemplateResult } from 'lit'; import type { ShoelaceFormControl } from '../../internal/shoelace-element.js'; import type SlOption from '../option/option.component.js'; import type SlRemoveEvent from '../../events/sl-remove.js'; @@ -172,6 +173,31 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon /** The select's required attribute. */ @property({ type: Boolean, reflect: true }) required = false; + /** + * 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() getTag: (option: SlOption, index: number) => TemplateResult | string | HTMLElement = option => { + return html` + this.handleTagRemove(event, option)} + > + ${option.getTextLabel()} + + `; + }; + /** Gets the validity state object */ get validity() { return this.valueInput.validity; @@ -547,6 +573,21 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon this.formControlController.updateValidity(); }); } + protected get tags() { + return this.selectedOptions.map((option, index) => { + if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) { + const tag = this.getTag(option, index); + // Wrap so we can handle the remove + return html`
this.handleTagRemove(e, option)}> + ${typeof tag === 'string' ? unsafeHTML(tag) : tag} +
`; + } else if (index === this.maxOptionsVisible) { + // Hit tag limit + return html`+${this.selectedOptions.length - index}`; + } + return html``; + }); + } private handleInvalid(event: Event) { this.formControlController.setValidity(false); @@ -755,37 +796,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon @blur=${this.handleBlur} /> - ${this.multiple - ? html` -
- ${this.selectedOptions.map((option, index) => { - if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) { - return html` - this.handleTagRemove(event, option)} - > - ${option.getTextLabel()} - - `; - } else if (index === this.maxOptionsVisible) { - return html` +${this.selectedOptions.length - index} `; - } else { - return null; - } - })} -
- ` - : ''} + ${this.multiple ? html`
${this.tags}
` : ''}