;
+
+ private _connectedOptions: BlSelectOption[] = [];
+
+ private _cleanUpPopover: CleanUpFunction | null = null;
+
+ get options() {
+ return this._connectedOptions;
+ }
+
+ get isPopoverOpen() {
+ return this._isPopoverOpen;
+ }
+
+ get selectedOptions() {
+ return this._selectedOptions;
+ }
+
+ get additionalSelectedOptionCount() {
+ return this._additionalSelectedOptionCount;
+ }
+
+ get isInvalid() {
+ return this._isInvalid;
+ }
+
+ open() {
+ this._isPopoverOpen = true;
+ this._setupPopover();
+ }
+
+ close() {
+ this._isPopoverOpen = false;
+ this._cleanUpPopover && this._cleanUpPopover();
+ }
+
+ private _clickOutsideHandler = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+
+ if (!this.contains(target) && this._isPopoverOpen) {
+ this.close();
+ this._checkRequired();
+ }
+ };
+
+ private _setupPopover() {
+ this._cleanUpPopover = autoUpdate(this._selectInput, this._popover, () => {
+ computePosition(this._selectInput, this._popover, {
+ placement: 'bottom',
+ strategy: 'fixed',
+ middleware: [
+ flip(),
+ offset(8),
+ size({
+ apply(args: MiddlewareArguments) {
+ Object.assign(args.elements.floating.style, {
+ width: `${args.elements.reference.getBoundingClientRect().width}px`,
+ });
+ },
+ }),
+ ],
+ }).then(({ x, y }) => {
+ this._popover.style.setProperty('--left', `${x}px`);
+ this._popover.style.setProperty('--top', `${y}px`);
+ });
+ });
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ document.addEventListener('click', this._clickOutsideHandler);
+ }
+ disconnectedCallback() {
+ super.disconnectedCallback();
+
+ document.removeEventListener('click', this._clickOutsideHandler);
+ this._cleanUpPopover && this._cleanUpPopover();
+ }
+
+ private inputTemplate() {
+ const inputSelectedOptions = html`
+ ${this._selectedOptions.map(item => html`- ${item.text}
`)}
+
`;
+ const _selectedItemCount = this._additionalSelectedOptionCount
+ ? html`+${this._additionalSelectedOptionCount}`
+ : null;
+ const removeButton = html``;
+ const placeholder = this._showPlaceHolder
+ ? html`${this.placeholder}`
+ : '';
+
+ return html``;
+ }
+
+ private menuTemplate() {
+ return html`
+
+
`;
+ }
+
+ render() {
+ const invalidMessage = this._isInvalid && this.customInvalidText
+ ? html`${this.customInvalidText}
` : ``;
+ const helpMessage = this.helpText && !invalidMessage
+ ? html`${this.helpText}
` : ``;
+ const label = this.label
+ ? html`` : '';
+
+ return html` 0,
+ 'invalid': this._isInvalid,
+ })}
+ tabindex="-1"
+ >
+ ${label} ${this.inputTemplate()} ${this.menuTemplate()} ${helpMessage} ${invalidMessage}
+
`;
+ }
+
+ private get _showPlaceHolder() {
+ if (this.label && !this.labelFixed) {
+ return !this._selectedOptions.length && this._isPopoverOpen;
+ }
+ return !this._selectedOptions.length;
+ }
+
+ private _onClickSelectInput() {
+ this._isPopoverOpen ? this.close() : this.open();
+ }
+
+ private _handleSelectEvent() {
+ this._onBlSelect(this._selectedOptions);
+ }
+
+ private _handleSingleSelect(optionItem: ISelectOption) {
+ const oldItem = this._connectedOptions.find(option => option.selected);
+
+ if (oldItem) {
+ oldItem.selected = false;
+ }
+
+ this._selectedOptions = [optionItem];
+ this._handleSelectEvent();
+ this._isPopoverOpen = false;
+ }
+
+ private _handleMultipleSelect(optionItem: ISelectOption) {
+ const { value } = optionItem;
+
+ if (!optionItem.selected) {
+ this._selectedOptions = this._selectedOptions.filter(item => item.value !== value);
+ } else {
+ this._selectedOptions = [...this._selectedOptions, optionItem];
+ }
+
+ this._handleSelectEvent();
+ }
+
+ private _handleSelectOptionEvent(e: CustomEvent) {
+ const optionItem = e.detail as ISelectOption;
+
+ if (this.multiple) {
+ this._handleMultipleSelect(optionItem);
+ } else {
+ this._handleSingleSelect(optionItem);
+ }
+ }
+
+ private _onClickRemove(e: MouseEvent) {
+ e.stopPropagation();
+
+ this._connectedOptions
+ .filter(option => option.selected)
+ .forEach(option => {
+ option.selected = false;
+ });
+
+ this._selectedOptions = [];
+ this._handleSelectEvent();
+ }
+
+ private _checkAdditionalItemCount() {
+ if (!this.multiple) return;
+
+ let visibleItems = 0;
+ for(const value of this._selectedOptionsItems) {
+ if (value.offsetLeft < this._selectedOptionsContainer.offsetWidth) {
+ visibleItems++;
+ } else {
+ break;
+ }
+ }
+
+ this._additionalSelectedOptionCount = this._selectedOptionsItems.length - visibleItems;
+ }
+
+ private _checkRequired() {
+ if (this.required) {
+ this._isInvalid = this._selectedOptions.length === 0;
+ }
+ }
+
+ protected updated(_changedProperties: PropertyValues) {
+ if (
+ _changedProperties.has('_selectedOptions') &&
+ _changedProperties.get('_selectedOptions') instanceof Array
+ ) {
+ this._checkRequired();
+ this._checkAdditionalItemCount();
+ } else if (
+ _changedProperties.has('multiple') &&
+ typeof _changedProperties.get('multiple') === 'boolean'
+ ) {
+ this._connectedOptions.forEach(option => {
+ option.multiple = this.multiple;
+ option.selected = false;
+ });
+ this._selectedOptions = [];
+ }
+ }
+
+ /**
+ * This method is used by `bl-select-option` component to register itself to bl-select.
+ * @param option BlSelectOption reference to be registered
+ */
+ registerOption(option: BlSelectOption) {
+ this._connectedOptions.push(option);
+
+ if (option.selected) {
+ const optionItem = {
+ value: option.value,
+ text: option.textContent,
+ selected: option.selected,
+ } as ISelectOption;
+
+ if (this.multiple) {
+ this._selectedOptions = [...this._selectedOptions, optionItem];
+ } else {
+ this._selectedOptions = [optionItem];
+ }
+
+ this.requestUpdate();
+ }
+ }
+
+ /**
+ * This method is used by `bl-select-option` component to unregister itself from bl-select.
+ * @param option BlSelectOption reference to be unregistered
+ */
+ unregisterOption(option: BlSelectOption) {
+ this._connectedOptions.splice(this._connectedOptions.indexOf(option), 1);
+ this._selectedOptions = this._selectedOptions.filter(item => item.value !== option.value);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'bl-select': BlSelect;
+ }
+}
diff --git a/src/components/select/option/bl-select-option.css b/src/components/select/option/bl-select-option.css
new file mode 100644
index 00000000..f7cf0114
--- /dev/null
+++ b/src/components/select/option/bl-select-option.css
@@ -0,0 +1,63 @@
+:host {
+ position: relative;
+}
+
+.option-container {
+ --option-font: var(--bl-font-title-3-regular);
+ --option-padding: var(--bl-size-xs) 0;
+ --option-selected-color: var(--bl-color-primary);
+ --option-hover-color: var(--bl-color-primary-hover);
+ --option-color: var(--bl-color-secondary);
+ --option-disabled-color: var(--bl-color-content-passive);
+ --option-seperator: 1px solid var(--bl-color-border);
+ --option-gap: var(--bl-size-2xs);
+ --option-transition: color 120ms ease-out;
+}
+
+.option-container::after {
+ position: absolute;
+ content: '';
+ width: 100%;
+ bottom: 0;
+ border-bottom: var(--option-seperator);
+}
+
+:host(:last-of-type) .option-container::after {
+ border-bottom: none;
+}
+
+.single-option {
+ width: 100%;
+ display: block;
+ cursor: pointer;
+ color: var(--option-color);
+ padding: var(--option-padding);
+ transition: var(--option-transition);
+ font: var(--option-font);
+ user-select: none;
+}
+
+:host(:hover) .single-option {
+ color: var(--option-hover-color);
+}
+
+:host([selected]) .single-option {
+ color: var(--option-selected-color);
+}
+
+:host([disabled]) {
+ cursor: not-allowed;
+}
+
+:host([disabled]) .single-option {
+ color: var(--option-disabled-color);
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.checkbox-option {
+ width: 100%;
+ display: block;
+ color: var(--option-color);
+ padding: var(--option-padding);
+}
diff --git a/src/components/select/option/bl-select-option.stories.mdx b/src/components/select/option/bl-select-option.stories.mdx
new file mode 100644
index 00000000..8b48019a
--- /dev/null
+++ b/src/components/select/option/bl-select-option.stories.mdx
@@ -0,0 +1,58 @@
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import {
+ Meta,
+ Canvas,
+ ArgsTable,
+ Story,
+} from '@storybook/addon-docs';
+
+
+
+# Select Option
+Select option component is a component should be used inside a select component. It is used to display a single option in the select component.
+
+export const Template = (args) => html`
+ baklava
+`
+
+## Basic Usage
+
+
+## Selected
+If you want to select an option by default, you can use the `selected` attribute.
+
+
+## Disabled
+If you want to disable an option, you can use the `disabled` attribute.
+
+
+
+
diff --git a/src/components/select/option/bl-select-option.test.ts b/src/components/select/option/bl-select-option.test.ts
new file mode 100644
index 00000000..755066ba
--- /dev/null
+++ b/src/components/select/option/bl-select-option.test.ts
@@ -0,0 +1,9 @@
+import BlSelectOption from './bl-select-option';
+import { assert } from '@open-wc/testing';
+
+describe('bl-select', () => {
+ it('is defined', () => {
+ const el = document.createElement('bl-select-option');
+ assert.instanceOf(el, BlSelectOption);
+ });
+});
diff --git a/src/components/select/option/bl-select-option.ts b/src/components/select/option/bl-select-option.ts
new file mode 100644
index 00000000..710e920f
--- /dev/null
+++ b/src/components/select/option/bl-select-option.ts
@@ -0,0 +1,105 @@
+import { LitElement, html, CSSResultGroup } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { event, EventDispatcher } from '../../../utilities/event';
+import BlSelect, { ISelectOption } from '../bl-select';
+import style from './bl-select-option.css';
+
+@customElement('bl-select-option')
+export default class BlSelectOption extends LitElement {
+ static get styles(): CSSResultGroup {
+ return [style];
+ }
+
+ /* Declare reactive properties */
+ /**
+ * Sets the value for the option
+ */
+ @property({})
+ value: string;
+
+ /**
+ * Sets option as disabled
+ */
+ @property({ type: Boolean })
+ disabled = false;
+
+ /**
+ * Sets option as selected state
+ */
+ @property({ type: Boolean, reflect: true })
+ selected = false;
+
+ @state()
+ multiple = false;
+
+ /**
+ * Fires when clicked on the option
+ */
+ @event('bl-select-option') private _onSelect: EventDispatcher;
+
+ private blSelect: BlSelect | null;
+
+ private singleOptionTemplate() {
+ return html`
+
+
`;
+ }
+
+ private checkboxOptionTemplate() {
+ return html`
+
+ `;
+ }
+
+ render() {
+ return html`
+ ${this.multiple ? this.checkboxOptionTemplate() : this.singleOptionTemplate()}
+
`;
+ }
+
+ private _handleEvent() {
+ this._onSelect({
+ value: this.value,
+ text: this.textContent,
+ selected: this.selected,
+ } as ISelectOption);
+ }
+
+ private _onClickOption() {
+ this.selected = !this.selected;
+ this._handleEvent();
+ }
+
+ private _onCheckboxChange(event: CustomEvent) {
+ this.selected = event.detail;
+ this._handleEvent();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ this.updateComplete.then(() => {
+ this.blSelect = this.closest('bl-select');
+ // FIXME: We should warn when parent is not bl-select
+
+ this.multiple = this.blSelect?.multiple || false;
+ this.blSelect?.registerOption(this);
+ });
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.blSelect?.unregisterOption(this);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'bl-select-option': BlSelectOption;
+ }
+}