Skip to content

Commit

Permalink
feat(combobox, combobox-item): add description, shortHeading prop…
Browse files Browse the repository at this point in the history
…s and `content-end` slot (#9771)

**Related Issue:** #3695

## Summary

This adds the following enhancements to `combobox`/`combobox-item`:

* `description` prop - displays description below label
* `shortHeading` prop - displays short version of the heading (label) in
selection
* `content-end` slot - enables slotting non-interactive elements after
the item's content

**Note**: the new props are filterable and also participate in visual
matching
  • Loading branch information
jcfranco authored Jul 16, 2024
1 parent 5a6a68f commit 25a4778
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 55 deletions.
16 changes: 16 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,10 @@ export namespace Components {
* Specifies the parent and grandparent items, which are set on `calcite-combobox`.
*/
"ancestors": ComboboxChildElement[];
/**
* A description for the component, which displays below the label.
*/
"description": string;
/**
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
Expand Down Expand Up @@ -1306,6 +1310,10 @@ export namespace Components {
"single" | "single-persist" | "ancestors" | "multiple",
SelectionMode
>;
/**
* The component's short heading. When provided, the short heading will be displayed in the component's selection. It is recommended to use 5 characters or fewer.
*/
"shortHeading": string;
/**
* The component's text.
*/
Expand Down Expand Up @@ -9110,6 +9118,10 @@ declare namespace LocalJSX {
* Specifies the parent and grandparent items, which are set on `calcite-combobox`.
*/
"ancestors"?: ComboboxChildElement[];
/**
* A description for the component, which displays below the label.
*/
"description"?: string;
/**
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
Expand Down Expand Up @@ -9153,6 +9165,10 @@ declare namespace LocalJSX {
"single" | "single-persist" | "ancestors" | "multiple",
SelectionMode
>;
/**
* The component's short heading. When provided, the short heading will be displayed in the component's selection. It is recommended to use 5 characters or fewer.
*/
"shortHeading"?: string;
/**
* The component's text.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
--calcite-combobox-item-spacing-unit-s: theme("spacing.1");
--calcite-combobox-item-spacing-indent: theme("spacing.2");
--calcite-combobox-item-selector-icon-size: theme("spacing.4");
--calcite-combobox-item-description-font-size: var(--calcite-font-size-xs);
}

.scale--m {
Expand All @@ -14,6 +15,7 @@
--calcite-combobox-item-spacing-unit-s: theme("spacing.2");
--calcite-combobox-item-spacing-indent: theme("spacing.3");
--calcite-combobox-item-selector-icon-size: theme("spacing.4");
--calcite-combobox-item-description-font-size: var(--calcite-font-size-sm);
}

.scale--l {
Expand All @@ -22,6 +24,7 @@
--calcite-combobox-item-spacing-unit-s: theme("spacing[2.5]");
--calcite-combobox-item-spacing-indent: theme("spacing.4");
--calcite-combobox-item-selector-icon-size: theme("spacing.6");
--calcite-combobox-item-description-font-size: var(--calcite-font-size);
}

.container {
Expand All @@ -48,20 +51,22 @@ ul:focus {

.label {
@apply text-color-3
focus-base
relative
box-border
flex
w-full
min-w-full
cursor-pointer
items-center
no-underline
duration-150
ease-in-out;
focus-base
relative
box-border
flex
w-full
min-w-full
cursor-pointer
items-center
no-underline
duration-150
ease-in-out;
@include word-break();
justify-content: space-around;
gap: var(--calcite-combobox-item-spacing-unit-l);
padding-block: var(--calcite-combobox-item-spacing-unit-s);
padding-inline: var(--calcite-combobox-item-spacing-unit-l);
padding-inline: var(--calcite-combobox-item-indent-value);
}

:host([disabled]) .label {
Expand All @@ -85,11 +90,6 @@ ul:focus {
shadow-none;
}

.title {
padding-block: 0;
padding-inline: var(--calcite-combobox-item-spacing-unit-l);
}

.icon {
@apply inline-flex
opacity-0
Expand All @@ -98,13 +98,8 @@ ul:focus {
color: theme("borderColor.color.1");
}

.icon--indent {
padding-inline-start: var(--calcite-combobox-item-indent-value);
}

.icon--custom {
margin-block-start: -1px;
padding-inline-start: var(--calcite-combobox-item-spacing-unit-l);
@apply text-color-3;
}

Expand Down Expand Up @@ -140,3 +135,26 @@ ul:focus {
color: var(--calcite-color-text-1);
background-color: var(--calcite-color-foreground-current);
}

.center-content {
display: flex;
flex-direction: column;
flex-grow: 1;
padding-block: 0;
}

.description {
font-size: var(--calcite-combobox-item-description-font-size);
font-weight: var(--calcite-font-weight-normal);
}

:host([selected]),
:host(:hover) {
.description {
color: var(--calcite-color-text-2);
}
}

.short-text {
color: var(--calcite-color-text-3);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import { getAncestors, getDepth, isSingleLike } from "../combobox/utils";
import { Scale, SelectionMode } from "../interfaces";
import { getIconScale } from "../../utils/component";
import { IconName } from "../icon/interfaces";
import { CSS } from "./resources";
import { CSS, SLOTS } from "./resources";

/**
* @slot - A slot for adding nested `calcite-combobox-item`s.
* @slot content-end - A slot for adding non-actionable elements after the component's content.
*/
@Component({
tag: "calcite-combobox-item",
Expand Down Expand Up @@ -59,6 +60,11 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
/** Specifies the parent and grandparent items, which are set on `calcite-combobox`. */
@Prop({ mutable: true }) ancestors: ComboboxChildElement[];

/**
* A description for the component, which displays below the label.
*/
@Prop() description: string;

/** The `id` attribute of the component. When omitted, a globally unique identifier is used. */
@Prop({ reflect: true }) guid = guid();

Expand All @@ -83,14 +89,18 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
*/
@Prop({ reflect: true }) filterTextMatchPattern: RegExp;

/** The component's value. */
@Prop() value!: any;

/**
* When `true`, omits the component from the `calcite-combobox` filtered search results.
*/
@Prop({ reflect: true }) filterDisabled: boolean;

/**
* Specifies the size of the component inherited from the `calcite-combobox`, defaults to `m`.
*
* @internal
*/
@Prop() scale: Scale = "m";

/**
* Specifies the selection mode of the component, where:
*
Expand All @@ -110,11 +120,16 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
> = "multiple";

/**
* Specifies the size of the component inherited from the `calcite-combobox`, defaults to `m`.
* The component's short heading.
*
* @internal
* When provided, the short heading will be displayed in the component's selection.
*
* It is recommended to use 5 characters or fewer.
*/
@Prop() scale: Scale = "m";
@Prop({ reflect: true }) shortHeading: string;

/** The component's value. */
@Prop() value!: any;

// --------------------------------------------------------------------------
//
Expand Down Expand Up @@ -189,7 +204,6 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
class={{
[CSS.custom]: !!this.icon,
[CSS.iconActive]: this.icon && this.selected,
[CSS.iconIndent]: true,
}}
flipRtl={this.iconFlipRtl}
icon={this.icon || iconPath}
Expand All @@ -207,15 +221,13 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
class={{
[CSS.icon]: true,
[CSS.dot]: true,
[CSS.iconIndent]: true,
}}
/>
) : (
<calcite-icon
class={{
[CSS.icon]: true,
[CSS.iconActive]: this.selected,
[CSS.iconIndent]: true,
}}
flipRtl={this.iconFlipRtl}
icon={iconPath}
Expand Down Expand Up @@ -250,19 +262,31 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
[CSS.active]: this.active,
[CSS.single]: isSingleSelect,
};
const depth = getDepth(this.el);
const depth = getDepth(this.el) + 1;

return (
<Host aria-hidden="true">
<InteractiveContainer disabled={disabled}>
<div
class={`container scale--${this.scale}`}
class={{
[CSS.container]: true,
[CSS.scale(this.scale)]: true,
}}
style={{ "--calcite-combobox-item-spacing-indent-multiplier": `${depth}` }}
>
<li class={classes} id={this.guid} onClick={this.itemClickHandler}>
{this.renderSelectIndicator(showDot, iconPath)}
{this.renderIcon(iconPath)}
<span class="title">{this.renderTextContent()}</span>
<div class={CSS.centerContent}>
<div class={CSS.title}>{this.renderTextContent(this.textLabel)}</div>
{this.description ? (
<div class={CSS.description}>{this.renderTextContent(this.description)}</div>
) : null}
</div>
{this.shortHeading ? (
<div class={CSS.shortText}>{this.renderTextContent(this.shortHeading)}</div>
) : null}
<slot name={SLOTS.contentEnd} />
</li>
{this.renderChildren()}
</div>
Expand All @@ -271,12 +295,14 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
);
}

private renderTextContent(): string | (string | VNode)[] {
if (!this.filterTextMatchPattern) {
return this.textLabel;
private renderTextContent(text: string): string | (string | VNode)[] {
const pattern = this.filterTextMatchPattern;

if (!pattern || !text) {
return text;
}

const parts: (string | VNode)[] = this.textLabel.split(this.filterTextMatchPattern);
const parts: (string | VNode)[] = text.split(pattern);

if (parts.length > 1) {
// we only highlight the first match
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { Scale } from "../interfaces";

export const CSS = {
icon: "icon",
iconActive: "icon--active",
iconIndent: "icon--indent",
active: "label--active",
centerContent: "center-content",
container: "container",
custom: "icon--custom",
description: "description",
dot: "icon--dot",
single: "label--single",
filterMatch: "filter-match",
icon: "icon",
iconActive: "icon--active",
label: "label",
active: "label--active",
scale: (scale: Scale) => `scale--${scale}` as const,
selected: "label--selected",
title: "title",
shortText: "short-text",
single: "label--single",
textContainer: "text-container",
filterMatch: "filter-match",
title: "title",
};

export const SLOTS = {
contentEnd: "content-end",
};
Loading

0 comments on commit 25a4778

Please sign in to comment.