diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 18def2c4..b0bb526d 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -47,6 +47,7 @@
- [Buttons](/components/button)
- [Button Group](/components/button-group)
- [Checkbox](/components/checkbox)
+ - [Combobox](/components/combobox)
- [Dropdown](/components/dropdown)
- [Input](/components/input)
- [Icon Button](/components/icon-button)
@@ -99,6 +100,7 @@
- [Format Date](/utilities/format-date)
- [Format Number](/utilities/format-number)
- [Include](/utilities/include)
+ - [Intersection Observer](/utilities/intersection-observer)
- [Mutation Observer](/utilities/mutation-observer)
- [Popup](/utilities/popup)
- [Relative Time](/utilities/relative-time)
diff --git a/docs/components/combobox.md b/docs/components/combobox.md
new file mode 100644
index 00000000..145268f8
--- /dev/null
+++ b/docs/components/combobox.md
@@ -0,0 +1,169 @@
+# Combobox
+
+[component-header:lynk-combobox]
+
+A combobox consist of a text input trigger and a popup containing a listbox. By default, interacting with the combobox via focus or input will expose the popup and interacting outside of the combobox will close it.
+
+```html preview
+
+
+ Alabama
+ Alaska
+ Arizona
+ Arkansas
+ California
+ Colorado
+ Connecticut
+ Delaware
+ District of Columbia
+ Florida
+ Georgia
+ Hawaii
+ Idaho
+ Illinois
+ Indiana
+ Iowa
+ Kansas
+ Kentucky
+ Louisiana
+ Maine
+ Montana
+ Nebraska
+ Nevada
+ New Hampshire
+ New Jersey
+ New Mexico
+ New York
+ North Carolina
+ North Dakota
+ Ohio
+ Oklahoma
+ Oregon
+ Maryland
+ Massachusetts
+ Michigan
+ Minnesota
+ Missippi
+ Missouri
+ Pennsylvania
+ Rhode Island
+ South Carolina
+ South Dakota
+ Tennessee
+ Texas
+ Utah
+ Vermont
+ Virginia
+ Washington
+ West Viginia
+ Wisconsin
+ Wyoming
+
+```
+
+## Trigger
+
+By default, the ComboBox's popup is opened when the user types into the input field `trigger="input"`. There are two other supported modes: one where the menu opens when the ComboBox is focused `trigger="focus"` and the other `trigger="manual"` where the listbox can be opened by clicking on the expand icon, using the `open` attribute or by calling the `show()` method.
+
+```html preview
+
+ Item One
+ Item Two
+ Item Three
+
+
+
+
+
+ Item One
+ Item Two
+ Item Three
+
+
+
+
+
+ Item One
+ Item Two
+ Item Three
+
+```
+
+
+## Autocomplete
+
+Determines if the value in the input changes or not as the user navigates with the keyboard. If true, the value changes automatically, if false the value will only change when a selection is made.
+
+Set this to false when you don't really need the value from the input but want to populate some other state (like the recipient selector in Gmail). But if your input is more like a normal text input, then leave the true default.
+
+### List Autocomplete
+
+This example illustrates the autocomplete behavior known as list autocomplete with manual selection. If the user types one or more characters in the combobox and the typed characters match the beginning of the name of one or more options, a listbox popup appears containing the matching names or values. When the listbox appears, a suggested option is not automatically selected. Thus, after typing, if the user tabs or clicks out of the combobox without choosing a value from the listbox, the typed string becomes the value of the combobox. Note that this implementation enables users to input the name or value of an option, but it does not prevent input of any other arbitrary value.
+
+```html preview
+
+
+ Alabama
+ Alaska
+ Arizona
+ Arkansas
+ California
+ Colorado
+ Connecticut
+ Delaware
+ District of Columbia
+ Florida
+ Georgia
+ Hawaii
+ Idaho
+ Illinois
+ Indiana
+ Iowa
+ Kansas
+ Kentucky
+ Louisiana
+ Maine
+ Montana
+ Nebraska
+ Nevada
+ New Hampshire
+ New Jersey
+ New Mexico
+ New York
+ North Carolina
+ North Dakota
+ Ohio
+ Oklahoma
+ Oregon
+ Maryland
+ Massachusetts
+ Michigan
+ Minnesota
+ Missippi
+ Missouri
+ Pennsylvania
+ Rhode Island
+ South Carolina
+ South Dakota
+ Tennessee
+ Texas
+ Utah
+ Vermont
+ Virginia
+ Washington
+ West Viginia
+ Wisconsin
+ Wyoming
+
+```
+
+## Allow Custom Values
+
+By default on blur, a ComboBox will either reset its input value to match the selected option's text or clear its input value if an option has not been selected. If you would like to allow the end user to provide a custom input value to the ComboBox, the `allow-custom` property can be used to override the default behavior.
+
+## Multiple Selections
+
+Allow more than one option to be selected by adding the `multiple` attribute. By default, each additional option will be appended to the text input as a `lynk-tag` element. You can change this default behavior and append the options as a csv by using the `separator` attribute set to an string value like 'separator=", "'.
+
+
+[component-metadata:lynk-combobox]
diff --git a/docs/components/dropdown.md b/docs/components/dropdown.md
index 88d343a6..dcb4f41c 100644
--- a/docs/components/dropdown.md
+++ b/docs/components/dropdown.md
@@ -84,6 +84,35 @@ Alternatively, you can listen for the `click` event on individual menu items. No
```
+### Using a lynk-input as the trigger
+
+```html preview
+
+
+
+
+
+
+ Cut
+ Copy
+ Paste
+
+
+
+
+
+```
+
+
### Placement
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
diff --git a/docs/components/nav-item.md b/docs/components/nav-item.md
index efec14c5..45379cf9 100644
--- a/docs/components/nav-item.md
+++ b/docs/components/nav-item.md
@@ -7,7 +7,7 @@
Getting Started
Usage
Contributing
-
+
Components
Button
Checkbox
diff --git a/docs/components/option.md b/docs/components/option.md
index 3f7dde56..8d1fbff9 100644
--- a/docs/components/option.md
+++ b/docs/components/option.md
@@ -21,6 +21,7 @@ Use the `disabled` attribute to disable an option and prevent it from being sele
Option 1
Option 2
Option 3
+ Option 4
```
diff --git a/docs/getting-started/changelog.md b/docs/getting-started/changelog.md
index acc88fac..bc99fe5b 100644
--- a/docs/getting-started/changelog.md
+++ b/docs/getting-started/changelog.md
@@ -6,6 +6,12 @@ Components with the Experimental ba
During the beta period, these restrictions may be relaxed in the event of a mission-critical bug. 🐛
+## 0.6.5
+
+- 🎉 NEW: Added experimental `` component
+- Improved `` so it converts non-string values to strings for convenience
+- Improved styles for `` and `` slotted into a ``
+
## 0.6.4
- Added `href` property to `` to support router navigation
diff --git a/docs/getting-started/contributing.md b/docs/getting-started/contributing.md
index 991d13c4..cc3078bb 100644
--- a/docs/getting-started/contributing.md
+++ b/docs/getting-started/contributing.md
@@ -41,7 +41,7 @@ After the initial build, a browser will open automatically to a local version of
To scaffold a new component, run the following command, replacing `lynk-tag-name` with the desired tag name.
```bash
-yarn create lynk-tag-name
+yarn run create lynk-tag-name
```
This will generate a source file, a stylesheet, and a docs page for you. When you start the dev server, you'll find the new component in the "Components" section of the sidebar.
diff --git a/docs/layout/app-layout-sidebar.md b/docs/layout/app-layout-sidebar.md
index b9cd05e6..29617b02 100644
--- a/docs/layout/app-layout-sidebar.md
+++ b/docs/layout/app-layout-sidebar.md
@@ -25,7 +25,28 @@
-
+
+
+
+ All Video Content
+
+
+ Static Graphics
+
+
+ Playlists
+
+
+ My Library
+
+ Messages
+
+
+
+
+
+
+
diff --git a/docs/utilities/intersection-observer.md b/docs/utilities/intersection-observer.md
new file mode 100644
index 00000000..07b55b39
--- /dev/null
+++ b/docs/utilities/intersection-observer.md
@@ -0,0 +1,43 @@
+# Intersection Observer
+
+[component-header:lynk-intersection-observer]
+
+The intersection observer will report the elements it wraps enters and exits the viewport through the `on:enter` and `on:exit` events.
+
+```html preview
+
+ Scroll down and watch the console...
+
+ Hello, I'm a div!
+ Hello, I'm another div!
+
+
+
+
+
+
+```
+
+[component-metadata:lynk-intersection-observer]
\ No newline at end of file
diff --git a/docs/utilities/resize-observer.md b/docs/utilities/resize-observer.md
index 8c42f108..1d9cbf33 100644
--- a/docs/utilities/resize-observer.md
+++ b/docs/utilities/resize-observer.md
@@ -15,7 +15,7 @@ The resize observer will report changes to the dimensions of the elements it wra
const container = document.querySelector('.resize-observer-overview');
const resizeObserver = container.querySelector('lynk-resize-observer');
- resizeObserver.addEventListener('lynk-resize', event => {
+ resizeObserver.addEventListener('on:resize', event => {
console.log(event.detail);
});
diff --git a/src/components/button-group/button-group.ts b/src/components/button-group/button-group.ts
index 2802d3f9..1dea75a3 100644
--- a/src/components/button-group/button-group.ts
+++ b/src/components/button-group/button-group.ts
@@ -25,22 +25,22 @@ export default class LynkButtonGroup extends LynkElement {
/** A label to use for the button group's `aria-label` attribute. */
@property() label = '';
- handleFocus(event: CustomEvent) {
+ handleFocus(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.add('lynk-button-group__button--focus');
}
- handleBlur(event: CustomEvent) {
+ handleBlur(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.remove('lynk-button-group__button--focus');
}
- handleMouseOver(event: CustomEvent) {
+ handleMouseOver(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.add('lynk-button-group__button--hover');
}
- handleMouseOut(event: CustomEvent) {
+ handleMouseOut(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.remove('lynk-button-group__button--hover');
}
diff --git a/src/components/button/button.ts b/src/components/button/button.ts
index 4314af0e..9f1b4360 100644
--- a/src/components/button/button.ts
+++ b/src/components/button/button.ts
@@ -281,6 +281,11 @@ export default class LynkButton extends LynkElement implements LynkFormControl {
return true;
}
+ /** Gets the associated form, if one exists. */
+ getForm(): HTMLFormElement | null {
+ return this.formControlController.getForm();
+ }
+
/** Checks for validity and shows the browser's validation message if the control is invalid. */
reportValidity() {
if (this.isButton()) {
diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts
index d4914e22..aaa4fc21 100644
--- a/src/components/checkbox/checkbox.ts
+++ b/src/components/checkbox/checkbox.ts
@@ -199,6 +199,11 @@ export default class LynkCheckbox extends LynkElement implements LynkFormControl
return this.input.checkValidity();
}
+ /** Gets the associated form, if one exists. */
+ getForm(): HTMLFormElement | null {
+ return this.formControlController.getForm();
+ }
+
/** Checks for validity and shows a validation message if the control is invalid. */
reportValidity() {
return this.input.reportValidity();
diff --git a/src/components/combobox/combobox-alt.styles.ts b/src/components/combobox/combobox-alt.styles.ts
new file mode 100644
index 00000000..db310134
--- /dev/null
+++ b/src/components/combobox/combobox-alt.styles.ts
@@ -0,0 +1,34 @@
+import { css } from 'lit';
+import componentStyles from '../../styles/component.styles';
+import formControlStyles from '../../styles/form-control.styles';
+
+export default css`
+ [hidden] {
+ display: none;
+ }
+
+ :host([loading]) lynk-input lynk-icon {
+ animation-name: spin;
+ animation-duration: 500ms;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+ }
+
+ @keyframes spin {
+ from {
+ transform:rotate(0deg);
+ }
+ to {
+ transform:rotate(360deg);
+ }
+ }
+
+ lynk-dropdown {
+ display: block;
+ }
+
+ lynk-input lynk-icon[slot=suffix] {
+ padding-right: 0;
+ margin-right: var(--lynk-input-spacing-medium);
+ }
+`;
diff --git a/src/components/combobox/combobox-alt.ts b/src/components/combobox/combobox-alt.ts
new file mode 100644
index 00000000..e086c977
--- /dev/null
+++ b/src/components/combobox/combobox-alt.ts
@@ -0,0 +1,227 @@
+import '../dropdown/dropdown';
+import '../input/input';
+import '../menu/menu';
+import '../menu-item/menu-item';
+import { classMap } from 'lit/directives/class-map.js';
+import { customElement, property, query } from 'lit/decorators.js';
+import { HasSlotController } from '../../internal/slot';
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { LocalizeController } from '../../utilities/localize';
+import { waitForEvent } from '../../internal/event';
+import { watch } from '../../internal/watch';
+import LynkElement from '../../internal/lynk-element';
+import styles from './combobox.styles';
+import type { CSSResultGroup } from 'lit';
+import type LynkDropdown from '../dropdown/dropdown';
+import type LynkInput from '../input/input';
+import type LynkMenu from '../menu/menu';
+import type LynkMenuItem from '../menu-item/menu-item';
+
+/**
+ * @summary A combobox is a text input with an associated popup that enables users to select a value from a collection of possible values or optionally enter a custom value.
+ * @documentation https:/lynk.design/components/combobox
+ * @since 1.0
+ * @status experimental
+ *
+ * @dependency lynk-input
+ * @dependency lynk-dropdown
+ * @dependency lynk-menu
+ * @dependency lynk-menu-item
+ *
+ * @slot - The combobox's content.
+ * @slot trigger - The combobox's trigger, usually a `` element.
+ *
+ * @event on:show - Emitted when the combobox opens.
+ * @event after:show - Emitted after the combobox opens and all animations are complete.
+ * @event on:hide - Emitted when the combobox closes.
+ * @event after:hide - Emitted after the combobox closes and all animations are complete.
+ *
+ * @csspart base - The component's internal wrapper.
+ * @csspart trigger - The container that wraps the trigger.
+ * @csspart panel - The panel that gets shown when the combobox is open.
+ *
+ * @animation combobox.show - The animation to use when showing the combobox.
+ * @animation combobox.hide - The animation to use when hiding the combobox.
+ */
+@customElement('lynk-combobox')
+export default class LynkCombobox extends LynkElement {
+ static styles: CSSResultGroup = styles;
+
+ private readonly hasSlotController = new HasSlotController(this, 'help-text', 'help-tip', 'label');
+
+ @query('lynk-dropdown') _dropdown: LynkDropdown;
+ @query('lynk-input') _input: LynkDropdown;
+
+ @property({ type: String })
+ public selected?: string;
+
+ @property({ type: String })
+ public value = '';
+
+ /** The combobox input's validity state when using manual validation or set automatically to `error` when field uses Contraint Validation */
+ @property({ reflect: true })
+ public state: 'error' | 'warning' | 'success' | 'default' = 'default';
+
+ /** The combobox input's size. */
+ @property({ reflect: true })
+ public size: 'small' | 'medium' | 'large' = 'medium';
+
+ /** The input's label. Alternatively, you can use the label slot. */
+ @property({ type: String })
+ public label = '';
+
+ /**
+ * Specifies what permission the browser has to provide assistance in filling out form field values. Refer to
+ * [this page on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for available values.
+ */
+ @property() autocomplete: string;
+
+ /** The help text below the input. Alternatively, you can use the help-text slot. */
+ @property({ attribute: 'help-text', type: String })
+ public helpText = '';
+
+ /** The help tooltip appended to the label. Alternatively, you can use the help-tip slot. */
+ @property({ attribute: 'help-tip', type: String })
+ public helpTip = '';
+
+ /** The help tooltip appended to the label. Alternatively, you can use the help-tip slot. */
+ @property({ attribute: 'placeholder', type: String })
+ public placeholder = '';
+
+ @property({ type: Boolean })
+ public caret = false;
+
+ /** Adds a clear button when the input is populated. */
+ @property({ type: Boolean })
+ public clearable = false;
+
+ @property({ type: Boolean })
+ public empty = true;
+
+ @property({ type: Boolean })
+ public expanded = false;
+
+ @property({ type: Boolean })
+ public hoist = false;
+
+ @property({ type: Boolean })
+ public disabled = false;
+
+ @property({ type: Number, attribute: 'debounce-timeout' })
+ public debounceTimeout = 350;
+
+ @property({ type: Boolean, reflect: true })
+ public loading?: boolean;
+
+ protected updated(_changedProperties: PropertyValues) {
+ if (_changedProperties.has('loading')) {
+ if (this.loading) {
+ this._dropdown.hide();
+ } else {
+ this._dropdown.show();
+ }
+ }
+ }
+
+ updateEmpty(e: Event) {
+ const slot = e.target as HTMLSlotElement;
+ this.empty = slot.assignedElements().length === 0;
+ }
+
+ dispatchSearch() {
+ this.dispatchEvent(new CustomEvent('search', {
+ detail: {
+ value: this._input.value,
+ },
+ }))
+ }
+
+ dispatchItemSelected(e: CustomEvent) {
+ this.value = e.detail.item.getTextLabel()
+ this.selected = e.detail.item.value
+ this.dispatchEvent(new CustomEvent('itemSelected', {
+ detail: {
+ value: e.detail.item.value,
+ },
+ }))
+ }
+
+ handleTriggerKeyDown(e: KeyboardEvent) {
+ if (e.key === ' ') {
+ e.stopPropagation()
+ }
+ }
+
+ handleMenuKeyDown(e: KeyboardEvent) {
+ if (['ArrowRight', 'ArrowLeft'].includes(event.key)) {
+ this._input.focus();
+ }
+
+ // All other "printable" keys trigger type to select
+ if (event.key.length === 1 || event.key === 'Backspace') {
+
+ // Don't block important key combos like CMD+R
+ if (event.metaKey || event.ctrlKey || event.altKey) {
+ return;
+ }
+
+ this._input.focus();
+ }
+ }
+
+ render() {
+ return html`
+
+
+
+
+
+ ${this.caret
+ ? html`
+
+ `
+ : ''}
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'lynk-combobox': LynkDropdown;
+ }
+}
+
diff --git a/src/components/combobox/combobox.styles.ts b/src/components/combobox/combobox.styles.ts
new file mode 100644
index 00000000..5790a6f3
--- /dev/null
+++ b/src/components/combobox/combobox.styles.ts
@@ -0,0 +1,406 @@
+import { css } from 'lit';
+import componentStyles from '../../styles/component.styles';
+import formControlStyles from '../../styles/form-control.styles';
+
+export default css`
+ ${componentStyles}
+ ${formControlStyles}
+
+ :host {
+ display: block;
+ }
+
+ /** The popup */
+ .lynk-combobox {
+ flex: 1 1 auto;
+ display: inline-flex;
+ width: 100%;
+ position: relative;
+ vertical-align: middle;
+ }
+
+ .lynk-combobox::part(popup) {
+ z-index: var(--lynk-z-index-dropdown);
+ }
+
+ .lynk-combobox[data-current-placement^='top']::part(popup) {
+ transform-origin: bottom;
+ }
+
+ .lynk-combobox[data-current-placement^='bottom']::part(popup) {
+ transform-origin: top;
+ }
+
+ /* Combobox */
+ .lynk-combobox__control {
+ flex: 1;
+ display: flex;
+ width: 100%;
+ min-width: 0;
+ position: relative;
+ align-items: center;
+ justify-content: start;
+ font-family: var(--lynk-input-font-family);
+ font-weight: var(--lynk-input-font-weight);
+ letter-spacing: var(--lynk-input-letter-spacing);
+ vertical-align: middle;
+ overflow: hidden;
+ cursor: pointer;
+ transition: var(--lynk-transition-fast) color, var(--lynk-transition-fast) border,
+ var(--lynk-transition-fast) box-shadow, var(--lynk-transition-fast) background-color;
+ }
+
+ .lynk-combobox__display-input {
+ position: relative;
+ width: 100%;
+ font: inherit;
+ border: none;
+ background: none;
+ color: var(--lynk-input-color);
+ cursor: inherit;
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ -webkit-appearance: none;
+ }
+
+ .lynk-combobox:not(.lynk-combobox--disabled):hover .lynk-combobox__display-input {
+ color: var(--lynk-input-color-hover);
+ }
+
+ .lynk-combobox__display-input:focus {
+ outline: none;
+ }
+
+ /* Visually hide the display input when multiple is enabled */
+ .lynk-combobox--multiple:not(.lynk-combobox--placeholder-visible) .lynk-combobox__display-input {
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ }
+
+ .lynk-combobox__value-input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ margin: 0;
+ opacity: 0;
+ z-index: -1;
+ }
+
+ .lynk-combobox__tags {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ flex-wrap: wrap;
+ margin-inline-start: var(--lynk-spacing-2x-small);
+ }
+
+ .lynk-combobox__tags::slotted(lynk-tag) {
+ cursor: pointer !important;
+ }
+
+ .lynk-combobox--disabled .lynk-combobox__tags,
+ .lynk-combobox--disabled .lynk-combobox__tags::slotted(lynk-tag) {
+ cursor: not-allowed !important;
+ }
+
+ /* Standard selects */
+ .lynk-combobox--standard .lynk-combobox__control {
+ background-color: var(--lynk-input-background-color);
+ border: solid var(--lynk-input-border-width) var(--lynk-input-border-color);
+ }
+
+ .lynk-combobox--standard.lynk-combobox--disabled .lynk-combobox__control {
+ background-color: var(--lynk-input-background-color-disabled);
+ border-color: var(--lynk-input-border-color-disabled);
+ color: var(--lynk-input-color-disabled);
+ opacity: 0.75;
+ cursor: not-allowed;
+ outline: none;
+ }
+
+ .lynk-combobox--standard:not(.lynk-combobox--disabled).lynk-combobox--open .lynk-combobox__control,
+ .lynk-combobox--standard:not(.lynk-combobox--disabled).lynk-combobox--focused .lynk-combobox__control {
+ background-color: var(--lynk-input-background-color-focus);
+ border-color: var(--lynk-input-border-color-focus);
+ box-shadow: 0 0 0 var(--lynk-focus-ring-width) var(--lynk-input-focus-ring-color);
+ }
+
+ /* Filled selects */
+ .lynk-combobox--filled .lynk-combobox__control {
+ border: none;
+ background-color: var(--lynk-input-filled-background-color);
+ color: var(--lynk-input-color);
+ }
+
+ .lynk-combobox--filled:hover:not(.lynk-combobox--disabled) .lynk-combobox__control {
+ background-color: var(--lynk-input-filled-background-color-hover);
+ }
+
+ .lynk-combobox--filled.lynk-combobox--disabled .lynk-combobox__control {
+ background-color: var(--lynk-input-filled-background-color-disabled);
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .lynk-combobox--filled:not(.lynk-combobox--disabled).lynk-combobox--open .lynk-combobox__control,
+ .lynk-combobox--filled:not(.lynk-combobox--disabled).lynk-combobox--focused .lynk-combobox__control {
+ background-color: var(--lynk-input-filled-background-color-focus);
+ outline: var(--lynk-focus-ring);
+ }
+
+ /* Sizes */
+ .lynk-combobox--small .lynk-combobox__control {
+ border-radius: var(--lynk-input-border-radius-small);
+ font-size: var(--lynk-input-font-size-small);
+ min-height: var(--lynk-input-height-small);
+ padding-block: 0;
+ padding-inline: var(--lynk-input-spacing-small);
+ }
+
+ .lynk-combobox--small .lynk-combobox__clear-btn,
+ .lynk-combobox--small .lynk-combobox__expand-btn, {
+ margin-inline-start: var(--lynk-input-spacing-small);
+ }
+
+ .lynk-combobox--small .lynk-combobox__prefix::slotted(*) {
+ margin-inline-end: var(--lynk-input-spacing-small);
+ }
+
+ .lynk-combobox--small.lynk-combobox--multiple:not(.lynk-combobox--placeholder-visible) .lynk-combobox__control {
+ padding-block: 2px;
+ padding-inline-start: 0;
+ }
+
+ .lynk-combobox--small .lynk-combobox__tags {
+ gap: 2px;
+ }
+
+ .lynk-combobox--medium .lynk-combobox__control {
+ border-radius: var(--lynk-input-border-radius-medium);
+ font-size: var(--lynk-input-font-size-medium);
+ min-height: var(--lynk-input-height-medium);
+ padding-block: 0;
+ padding-inline: var(--lynk-input-spacing-medium);
+ }
+
+ .lynk-combobox--medium .lynk-combobox__clear-btn,
+ .lynk-combobox--medium .lynk-combobox__expand-btn {
+ margin-inline-start: var(--lynk-input-spacing-medium);
+ }
+
+ .lynk-combobox--medium .lynk-combobox__prefix::slotted(*) {
+ margin-inline-end: var(--lynk-input-spacing-medium);
+ }
+
+ .lynk-combobox--medium.lynk-combobox--multiple:not(.lynk-combobox--placeholder-visible) .lynk-combobox__control {
+ padding-inline-start: 0;
+ padding-block: 3px;
+ }
+
+ .lynk-combobox--medium .lynk-combobox__tags {
+ gap: 3px;
+ }
+
+ .lynk-combobox--large .lynk-combobox__control {
+ border-radius: var(--lynk-input-border-radius-large);
+ font-size: var(--lynk-input-font-size-large);
+ min-height: var(--lynk-input-height-large);
+ padding-block: 0;
+ padding-inline: var(--lynk-input-spacing-large);
+ }
+
+ .lynk-combobox--large .lynk-combobox__clear-btn,
+ .lynk-combobox--large .lynk-combobox__expand-btn, {
+ margin-inline-start: var(--lynk-input-spacing-large);
+ }
+
+ .lynk-combobox--large .lynk-combobox__prefix::slotted(*) {
+ margin-inline-end: var(--lynk-input-spacing-large);
+ }
+
+ .lynk-combobox--large.lynk-combobox--multiple:not(.lynk-combobox--placeholder-visible) .lynk-combobox__control {
+ padding-inline-start: 0;
+ padding-block: 4px;
+ }
+
+ .lynk-combobox--large .lynk-combobox__tags {
+ gap: 4px;
+ }
+
+ /* Pills */
+ .lynk-combobox--pill.lynk-combobox--small .lynk-combobox__control {
+ border-radius: var(--lynk-input-height-small);
+ }
+
+ .lynk-combobox--pill.lynk-combobox--medium .lynk-combobox__control {
+ border-radius: var(--lynk-input-height-medium);
+ }
+
+ .lynk-combobox--pill.lynk-combobox--large .lynk-combobox__control {
+ border-radius: var(--lynk-input-height-large);
+ }
+
+ /*
+ * Error & Warning States
+ */
+
+ .lynk-combobox--has-error .lynk-combobox__control,
+ .lynk-combobox--has-error:hover:not(.lynk-combobox--disabled) .lynk-combobox__control {
+ border-color: var(--lynk-color-danger-500);
+ }
+
+ .lynk-combobox--has-error:not(.lynk-combobox--disabled).lynk-combobox--open .lynk-combobox__control,
+ .lynk-combobox--has-error:not(.lynk-combobox--disabled).lynk-combobox--focused .lynk-combobox__control {
+ border-color: var(--lynk-color-danger-500);
+ box-shadow: 0 0 2px var(--lynk-focus-ring-width) var(--lynk-color-danger-a50);
+ }
+
+ .lynk-combobox--has-warning .lynk-combobox__control,
+ .lynk-combobox--has-warning:hover:not(.lynk-combobox--disabled) .lynk-combobox__control {
+ border-color: var(--lynk-color-warning-500);
+ }
+
+ .lynk-combobox--has-warning:not(.lynk-combobox--disabled).lynk-combobox--open .lynk-combobox__control,
+ .lynk-combobox--has-warning:not(.lynk-combobox--disabled).lynk-combobox--focused .lynk-combobox__control {
+ border-color: var(--lynk-color-warning-500);
+ box-shadow: 0 0 2px var(--lynk-focus-ring-width) var(--lynk-color-warning-a50);
+ }
+
+ .lynk-combobox--has-success .lynk-combobox__control,
+ .lynk-combobox--has-success:hover:not(.lynk-combobox--disabled) .lynk-combobox__control {
+ border-color: var(--lynk-color-success-500);
+ }
+
+ .lynk-combobox--has-success:not(.lynk-combobox--disabled).lynk-combobox--open .lynk-combobox__control,
+ .lynk-combobox--has-success:not(.lynk-combobox--disabled).lynk-combobox--focused .lynk-combobox__control {
+ border-color: var(--lynk-color-success-500);
+ box-shadow: 0 0 2px var(--lynk-focus-ring-width) var(--lynk-color-success-a50);
+ }
+
+ /* Prefix */
+ .lynk-combobox__prefix {
+ flex: 0;
+ display: inline-flex;
+ align-items: center;
+ color: var(--lynk-input-placeholder-color);
+ }
+
+ /* Clear/Expand button */
+ .lynk-combobox__clear-btn,
+ .lynk-combobox__expand-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: inherit;
+ color: var(--lynk-input-icon-color);
+ border: none;
+ background: none;
+ padding: 0;
+ transition: var(--lynk-transition-fast) color;
+ cursor: pointer;
+ }
+
+ .lynk-combobox__clear-btn:hover,
+ .lynk-combobox__expand-btn:hover {
+ color: var(--lynk-input-icon-color-hover);
+ }
+
+ .lynk-combobox__clear-btn:focus,
+ .lynk-combobox__expand-btn:focus {
+ outline: none;
+ }
+
+ /* Expand icon */
+ .lynk-combobox__expand-icon {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ transition: var(--lynk-transition-medium) rotate ease;
+ rotate: 0;
+ margin-inline-start: var(--lynk-spacing-small);
+ }
+
+ .lynk-combobox--open .lynk-combobox__expand-icon {
+ rotate: -180deg;
+ }
+
+ /* Listbox */
+ .lynk-combobox__listbox {
+ display: block;
+ position: relative;
+ font-family: var(--lynk-font-sans);
+ font-size: var(--lynk-font-size-medium);
+ font-weight: var(--lynk-font-weight-normal);
+ box-shadow: var(--lynk-shadow-large);
+ background: var(--lynk-panel-background-color);
+ border: solid var(--lynk-panel-border-width) var(--lynk-panel-border-color);
+ border-radius: var(--lynk-border-radius-medium);
+ padding-block: var(--lynk-spacing-x-small);
+ padding-inline: 0;
+ overflow: auto;
+ overscroll-behavior: none;
+
+ /* Make sure it adheres to the popup's auto size */
+ max-width: var(--auto-size-available-width);
+ max-height: var(--auto-size-available-height);
+ }
+
+ .lynk-combobox__listbox::slotted(lynk-divider) {
+ --spacing: var(--lynk-spacing-x-small);
+ }
+
+ .lynk-combobox__listbox::slotted(small) {
+ font-size: var(--lynk-font-size-small);
+ font-weight: var(--lynk-font-weight-semibold);
+ color: var(--lynk-color-neutral-500);
+ padding-block: var(--lynk-spacing-x-small);
+ padding-inline: var(--lynk-spacing-x-large);
+ }
+
+ .lynk-combobox__listbox-empty {
+ display: block;
+ font-family: var(--lynk-font-sans);
+ font-size: var(--lynk-font-size-medium);
+ color: var(--lynk-color-neutral-500);
+ padding: var(--lynk-spacing-x-small) var(--lynk-spacing-medium);
+ }
+
+ /*
+ * Restricted
+ */
+ .lynk-combobox--restricted {
+ opacity: 1 !important;
+ }
+
+ .lynk-combobox--restricted .lynk-combobox__control {
+ border-color: transparent;
+ cursor: initial;
+ gap: 0;
+ }
+
+ .lynk-combobox--restricted .lynk-combobox__control,
+ .lynk-combobox--restricted .lynk-combobox__tags {
+ padding: 0;
+ margin-inline-start: 0;
+ user-select: text;
+ }
+
+ .lynk-combobox--restricted .lynk-combobox__clear-btn,
+ .lynk-combobox--restricted .lynk-combobox__expand-btn, {
+ display: none;
+ }
+
+ .lynk-combobox--restricted lynk-tag::part(base) {
+ cursor: text;
+ user-select: text;
+ }
+`;
diff --git a/src/components/combobox/combobox.test.ts b/src/components/combobox/combobox.test.ts
new file mode 100644
index 00000000..ea98fe5f
--- /dev/null
+++ b/src/components/combobox/combobox.test.ts
@@ -0,0 +1,9 @@
+import { expect, fixture, html } from '@open-wc/testing';
+
+describe('', () => {
+ it('should render a component', async () => {
+ const el = await fixture(html` `);
+
+ expect(el).to.exist;
+ });
+});
diff --git a/src/components/combobox/combobox.ts b/src/components/combobox/combobox.ts
new file mode 100644
index 00000000..673e667d
--- /dev/null
+++ b/src/components/combobox/combobox.ts
@@ -0,0 +1,1096 @@
+import '../icon/icon';
+import '../popup/popup';
+import { animateTo, stopAnimations } from '../../internal/animate';
+import { classMap } from 'lit/directives/class-map.js';
+import { customElement, property, query, state } from 'lit/decorators.js';
+import { defaultValue } from '../../internal/default-value';
+import { FormControlController } from '../../internal/form';
+import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
+import { HasSlotController } from '../../internal/slot';
+import { html } from 'lit';
+import { LocalizeController } from '../../utilities/localize';
+import { scrollIntoView } from 'src/internal/scroll';
+import { waitForEvent } from '../../internal/event';
+import { watch } from '../../internal/watch';
+import LynkElement from '../../internal/lynk-element';
+import styles from './combobox.styles';
+import type { CSSResultGroup } from 'lit';
+import type { LynkFormControl } from '../../internal/lynk-element';
+import type LynkOption from '../option/option';
+import type LynkPopup from '../popup/popup';
+import type OnRemoveEvent from '../../events/on-remove';
+
+/**
+ * @summary A combobox is a text input with an associated popup that enables users to select a value from a collection of possible values.
+ * @documentation https:/lynk.design/components/combobox
+ * @since 1.0
+ * @status experimental
+ *
+ * @dependency lynk-icon
+ * @dependency lynk-popup
+ *
+ * @slot - The listbox options. Must be `` elements. You can use `` to group items visually.
+ * @slot label - The input's label. Alternatively, you can use the `label` attribute.
+ * @slot prefix - Used to prepend a presentational icon or similar element to the combobox.
+ * @slot clear-icon - An icon to use in lieu of the default clear icon.
+ * @slot expand-icon - The icon to show when the control is expanded and collapsed. Rotates on open and close.
+ * @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
+ *
+ * @event on:change - Emitted when the combobox's value changes.
+ * @event on:clear - Emitted when the combobox's value is cleared.
+ * @event on:input - Emitted when the combobox receives input.
+ * @event on:focus - Emitted when the combobox gains focus.
+ * @event on:blur - Emitted when the combobox loses focus.
+ * @event on:show - Emitted when the combobox's menu opens.
+ * @event after:show - Emitted after the combobox's menu opens and all animations are complete.
+ * @event on:hide - Emitted when the combobox's menu closes.
+ * @event after:hide - Emitted after the combobox's menu closes and all animations are complete.
+ * @event on:invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
+ */
+@customElement('lynk-combobox')
+export default class LynkCombobox extends LynkElement implements LynkFormControl {
+ static styles: CSSResultGroup = styles;
+
+ private readonly formControlController = new FormControlController(this, {
+ assumeInteractionOn: ['on:blur', 'on:input']
+ });
+ private readonly hasSlotController = new HasSlotController(this, 'help-text', 'help-tip', 'label');
+ private readonly localize = new LocalizeController(this);
+ private typeToSelectString = '';
+ private typeToSelectTimeout: number;
+
+ @query('.lynk-combobox') popup: LynkPopup;
+ @query('.lynk-combobox__control') combobox: HTMLSlotElement;
+ @query('.lynk-combobox__display-input') displayInput: HTMLInputElement;
+ @query('.lynk-combobox__value-input') valueInput: HTMLInputElement;
+ @query('.lynk-combobox__listbox') listbox: HTMLSlotElement;
+
+
+ @state() private hasFocus = false;
+ @state() private comboboxHasFocus = false;
+ @state() private listboxHasFocus = false;
+
+ @state() displayValue = '';
+ @state() currentOption: LynkOption;
+ @state() selectedOptions: LynkOption[] = [];
+ @state() filteredOptions: LynkOption[] = [];
+
+ /** The name of the combobox, submitted as a name/value pair with form data. */
+ @property() name = '';
+
+ /**
+ * The current value of the combobox, submitted as a name/value pair with form data. When `multiple` is enabled, the
+ * value will be a space-delimited list of values based on the options selected.
+ */
+ @property({
+ converter: {
+ fromAttribute: (value: string) => value.split(' '),
+ toAttribute: (value: string[]) => value.join(' ')
+ }
+ })
+ value: string | string[] = '';
+
+ /** The default value of the form control. Primarily used for resetting the form control. */
+ @defaultValue() defaultValue: string | string[] = '';
+
+ /** The combobox's size. */
+ @property() size: 'small' | 'medium' | 'large' = 'medium';
+
+ /** Placeholder text to show as a hint when the combobox is empty. */
+ @property() placeholder = '';
+
+ /** Allow custom values to be entered */
+ @property({ attribute: 'autocomplete', reflect: true }) autocomplete: 'none' | 'list' | 'inline' | 'both' = 'none';
+
+ /** Allow custom values to be entered */
+ @property({ attribute: 'allow-custom-value', type: Boolean, reflect: true }) allowCustomValue = false;
+
+ /** Determine when the listbox popup is triggered */
+ @property({ attribute: 'trigger', reflect: true }) trigger: 'input' | 'focus' | 'manual' = 'input';
+
+ /** Allows more than one option to be selected. */
+ @property({ type: Boolean, reflect: true }) multiple = false;
+
+ /**
+ * 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({ attribute: 'max-options-visible', type: Number }) maxOptionsVisible = 3;
+
+ /** Disables the combobox control. */
+ @property({ type: Boolean, reflect: true }) disabled = false;
+
+ /** Adds a clear button when the combobox is not empty. */
+ @property({ type: Boolean }) clearable = false;
+
+ /**
+ * Indicates whether or not the combobox is open. You can toggle this attribute to show and hide the menu, or you can
+ * use the `show()` and `hide()` methods and this attribute will reflect the combobox's open state.
+ */
+ @property({ type: Boolean, reflect: true }) open = false;
+
+ /**
+ * Enable this option to prevent the listbox from being clipped when the component is placed inside a container with
+ * `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
+ */
+ @property({ type: Boolean }) hoist = false;
+
+ /** Draws a filled combobox. */
+ @property({ type: Boolean, reflect: true }) filled = false;
+
+ /** Draws a pill-style combobox with rounded edges. */
+ @property({ type: Boolean, reflect: true }) pill = false;
+
+ /** The combobox's label. If you need to display HTML, use the `label` slot instead. */
+ @property() label = '';
+
+ /**
+ * The preferred placement of the combobox's menu. Note that the actual placement may vary as needed to keep the listbox
+ * inside of the viewport.
+ */
+ @property({ reflect: true }) placement: 'top' | 'bottom' = 'bottom';
+
+ /** The combobox's help text. If you need to display HTML, use the `help-text` slot instead. */
+ @property({ attribute: 'help-text' }) helpText = '';
+
+ /** The combobox's help text. If you need to display HTML, use the `help-text` slot instead. */
+ @property({ attribute: 'help-tip' }) helpTip = '';
+
+ /**
+ * By default, form controls are associated with the nearest containing `